KARTIKrocks/sqlguard
GitHub: KARTIKrocks/sqlguard
面向 Go 应用的生产级 SQL 查询分析器,支持运行时和 CI 双模式,自动检测慢查询、N+1 问题和危险 SQL 模式。
Stars: 0 | Forks: 0
# sqlguard
[](https://pkg.go.dev/github.com/KARTIKrocks/sqlguard)
[](https://goreportcard.com/report/github.com/KARTIKrocks/sqlguard)
[](go.mod)
[](https://github.com/KARTIKrocks/sqlguard/actions/workflows/ci.yml)
[](https://github.com/KARTIKrocks/sqlguard/actions/workflows/codeql.yml)
[](https://github.com/KARTIKrocks/sqlguard/releases)
[](LICENSE)
[](https://codecov.io/gh/KARTIKrocks/sqlguard)
适用于 Go 应用程序的生产级安全 SQL 查询分析器。
可检测慢查询、危险的 SQL 模式以及性能问题——支持运行时和静态分析。可以将其视为 SQL 查询的 `golangci-lint`。
## 安装
```
go get github.com/KARTIKrocks/sqlguard
```
CLI 工具:
```
go install github.com/KARTIKrocks/sqlguard/cmd/sqlguard@latest
```
## 检测规则
| 规则 | 严重性 | 描述 |
| ------------------------------ | -------- | --------------------------------------------------------------------------------------------- |
| `select-star` | WARNING | `SELECT *` — 不必要地选择了所有列 |
| `leading-wildcard` | WARNING | `LIKE '%...'`(以及 `ILIKE`)— 无法使用索引 |
| `non-sargable-predicate` | WARNING | `WHERE LOWER(col) = ...` — 列上的函数导致其索引失效 |
| `add-not-null-without-default` | WARNING | `ALTER TABLE ... ADD COLUMN ... NOT NULL` 不带 `DEFAULT` — 在已填充数据的表上会失败/重写 |
| `implicit-join` | WARNING | `FROM a, b` — 逗号连接;遗漏条件会变成笛卡尔积 |
| `cartesian-join` | WARNING | 没有连接条件或 `WHERE` 的多表查询 — 笛卡尔积(包含 `CROSS JOIN`) |
| `in-list-too-large` | WARNING | 包含超过 `max-length`(默认为 100)个元素的 `IN (...)` 值列表 |
| `large-offset` | WARNING | 高于 `threshold`(默认为 1000)的 `OFFSET` — 深度分页会扫描/丢弃跳过的行 |
| `select-distinct` | INFO | `SELECT DISTINCT` — 通常掩盖了由意外连接产生的重复行 |
| `delete-without-where` | CRITICAL | 不带 `WHERE` 的 `DELETE` — 删除所有行 |
| `update-without-where` | CRITICAL | 不带 `WHERE` 的 `UPDATE` — 更新所有行 |
| `insert-without-columns` | WARNING | 不带显式列列表的 `INSERT`(`VALUES` 或 `... SELECT`)— 在 schema 更改时会出错 |
| `select-without-limit` | WARNING | 不带 `LIMIT` 或 `WHERE` 的 `SELECT` — 可能会返回过多的行 |
| `orderby-without-limit` | INFO | 不带 `LIMIT` 的 `ORDER BY` — 对整个结果集进行排序 |
| `n-plus-one` | WARNING | 同一查询模式重复执行 N 次(仅限运行时) |
| `slow-query` | WARNING | 查询超过延迟阈值(仅限运行时) |
| `seq-scan` | WARNING | 通过 EXPLAIN 检测到的顺序扫描(postgres) |
| `full-table-scan` | WARNING | 通过 EXPLAIN 检测到的全表扫描(mysql) |
| `high-cost` | WARNING | 查询计划中的高成本操作 |
| `no-index-used` | WARNING | 通过 EXPLAIN 检测到表访问未使用索引(mysql) |
| `filesort` | INFO | 查询计划中出现 `Using filesort` — `ORDER BY` 未被索引覆盖(mysql) |
## 配置
在你的项目根目录下放置一个 `.sqlguard.yml`。sqlguard 会从扫描目录(或工作目录)
向上查找,直到找到该文件或 git 根目录。CLI 支持 `--config ` 和 `--no-config`;
该文件是可选的——如果没有它,每条规则都将按默认值运行。带有完整注释的模板位于
[`.sqlguard.example.yml`](.sqlguard.example.yml)。
```
version: 1
rules:
disable: [orderby-without-limit]
severity:
select-star: info # info | warning | critical | off
select-without-limit: "off" # "off" disables the rule
settings:
leading-wildcard:
min-length: 3 # ignore short LIKE '%x%' patterns
in-list-too-large:
max-length: 100 # flag IN (...) lists longer than this
large-offset:
threshold: 1000 # flag literal OFFSET above this
redact: true # redact literals out of Result.Query (default)
slow-query:
threshold: 200ms # runtime middleware threshold
dedup:
window: 1m # report each repeated finding at most once per window ("0" disables)
scan:
exclude-paths: ["(^|/)legacy/"] # static scanner only, regex
```
未知的键和规则名称是警告,而不是错误,因此为较新版本的 sqlguard 编写的配置
仍然可以在较旧的二进制文件上加载;设置 `strict: true` 可使其变为致命错误。
`only: [rule, ...]` 可切换至白名单模式。
**内联抑制** — 无需配置:
```
SELECT * FROM users -- sqlguard:ignore
DELETE FROM users /* sqlguard:ignore:delete-without-where */
```
```
// sqlguard:ignore
db.Exec("DELETE FROM users")
db.Query("SELECT * FROM users") // sqlguard:ignore:select-star
```
SQL 内指令在运行时和静态扫描器中均有效;当 Go 源码形式的指令位于调用的同一行或
正上方时,扫描器会识别它。
将相同的配置应用于运行时中间件:
```
opts, _ := config.Middleware("", ".") // discover from cwd
sqlguard.Register("sqlguard-pg", "pgx", opts...)
```
## 安全与脱敏
sqlguard 的分析结果会进入日志,因此**默认情况下它永远不会发出原始的
字面值**。在任何 `Result` 离开进程之前,其 `Query` 都会被脱敏——单引号
字符串和数字字面量会变成 `?`,同时保留关键字、标识符(包括 `"引号"` /
`` `反引号` `` 形式的名称)以及结构:
```
[SQLGUARD WARNING] select-star
Query: SELECT * FROM users WHERE email = ?
```
每个 `Result` 还包含一个 `Fingerprint`:即折叠了空白字符并将
`IN (?, ?, ?)` 折叠为 `(?)` 的脱敏查询。它是一个稳定的、无 PII(个人身份信息)、
低基数的标识——可作为 metrics 标签或日志键安全使用,这也是 N+1 检测器用于
分组的相同值。JSON 报告器会将其输出为 `fingerprint`。
仅在查询文本受信任的情况下(如本地调试)才可退出此机制:
```
a := analyzer.Default().WithRawQuery() // standalone analyzer
sqlguard.Register("pg", "pgx", middleware.WithAnalyzer(a))
```
或者在 `.sqlguard.yml` 中设置 `redact: false`。无论哪种方式都会填充 `Fingerprint`。
## 使用方法
### 运行时中间件
sqlguard 包装在 `database/sql` **驱动**层,因此你取回的是真实的
`*sql.DB`,并且每个查询都会被自动分析——包括由 ORM 和查询构建器
(sqlc, ent, sqlx, gorm, pgx-stdlib)发出的查询。无需在你的代码中
穿透包装器类型,也无需保持方法列表同步。
```
import (
"database/sql"
"github.com/KARTIKrocks/sqlguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"time"
)
func main() {
// Register an analyzed driver by wrapping an existing one...
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(5, 2*time.Second),
)
db, _ := sql.Open("sqlguard-pg", "...") // db is a plain *sql.DB
// ...or wrap a driver.Connector directly (e.g. pgx stdlib):
// db := sqlguard.OpenDB(connector, middleware.WithN1Detection(5, time.Second))
// Use as normal — warnings are logged automatically
db.Query("SELECT * FROM users")
// Output:
// [SQLGUARD WARNING] select-star
// Query: SELECT * FROM users
// Issue: SELECT * detected. Selecting all columns can hurt performance.
// Fix: Select only the columns you need.
}
```
### N+1 查询检测
中间件可检测同一查询模式何时重复执行——这是典型的 N+1 问题:
```
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithN1Detection(5, 2*time.Second), // flag after 5 similar queries in 2s
)
db, _ := sql.Open("sqlguard-pg", "...")
```
N+1 模式是在配置的时间窗口内检测到的。在原始的 `database/sql`
驱动路径上,你取回的是普通的 `*sql.DB`,因此检测是跨进程范围的(窗口化)——
没有处理器能将其范围限制在单个请求内。集成适配器(`gormguard`、`pgxguard`、`sqlxguard`、`bunguard`、
`xormguard`、`entguard`)持有保护器并公开 `ResetN1()` 以将检测范围
限制在单个工作单元;请在请求边界处调用它。
### 噪音控制(结果去重)
否则,重复执行的查询会在每次执行时重新发出相同的静态警告。默认情况下,
运行时中间件**每分钟最多报告一次**每个结果(规则 + 查询指纹),因此热点查询
不会淹没你的日志。可调整或禁用它:
```
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithFindingDedup(5*time.Minute), // quieter
)
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithFindingDedup(0), // disable: report every occurrence
)
```
或者在 `.sqlguard.yml` 中设置 `dedup.window`。慢查询和 N+1 结果有其
自身的发出策略,不受此影响。
中间件还会对每个不同的查询字符串进行 memoize 分析(以精确查询为键的 LRU ——
即使对于对字面量敏感的规则也是正确的),因此重复执行的查询只会被解析和进行
一次规则检查,而不是在每次执行时都进行。随后,重复查询的成本仅是一次
缓存查找,而不是完整的解析(便宜约 1000 倍,在重复的情况下零内存分配)。
默认 1024 个条目;使用 `middleware.WithAnalysisCacheSize(n)` 进行调整,或
设置 `n == 0` 以禁用。
### CLI 静态扫描器
无需运行应用程序即可扫描 Go 源代码中的 SQL 问题:
```
# 扫描当前目录
sqlguard scan .
# 扫描特定 package
sqlguard scan ./internal/repository
# JSON output (用于 CI pipelines)
sqlguard scan --format json ./...
```
发现问题时退出代码为 **1**,干净时为 **0**——适用于 CI/CD 流水线。
### EXPLAIN 计划分析器
连接到实时数据库并分析查询计划:
```
# PostgreSQL
sqlguard explain --db "postgres://user:pass@localhost/mydb?sslmode=disable" \
"SELECT * FROM orders WHERE user_id = 42"
# MySQL
sqlguard explain --dialect mysql --db "user:pass@tcp(localhost:3306)/mydb" \
"SELECT * FROM orders WHERE user_id = 42"
# JSON output
sqlguard explain --db "..." --format json "SELECT * FROM orders"
```
可检测顺序扫描、缺失索引、文件排序以及高成本操作。
为了安全起见,EXPLAIN 会在一个**始终会回滚的事务**中运行,并且
从不使用 `ANALYZE`——语句仅被规划,从不执行。输入会经过具备注释和
字符串字面量感知能力的多语句检查进行验证(隐藏在注释或字符串中的 `;`
无法走私第二条语句)。默认情况下仅允许 `SELECT`/`WITH`;传递
`--allow-dml` 可对 `INSERT/UPDATE/DELETE` 执行 EXPLAIN
(仍会回滚)。DDL/`SET`/事务控制始终会被拒绝。
该事务在除 MySQL/MariaDB(在 `--allow-dml` 下)以外的所有地方都是
额外**只读**的:这些服务器会拒绝只读事务中的*每一条*语句(错误 1792),
包括仅对其进行规划的 EXPLAIN。在这种情况下,保证依赖于验证、纯 `EXPLAIN`
从不执行该语句以及无条件回滚。
MariaDB 通过 `--dialect mysql` 运行。MySQL 的计划要求以
`EXPLAIN FORMAT=TRADITIONAL` 形式请求,因为 MySQL 9 将
`@@explain_format` 默认设置为 `TREE`。
### GORM 集成
```
go get github.com/KARTIKrocks/sqlguard/integrations/gormguard
```
```
import (
"github.com/KARTIKrocks/sqlguard/integrations/gormguard"
"github.com/KARTIKrocks/sqlguard/middleware"
)
gormDB, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Register as GORM plugin — hooks into all queries automatically
gormguard.Register(gormDB)
// Or customize via the standard middleware options
gormguard.Register(gormDB,
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)
```
### sqlx 集成
```
go get github.com/KARTIKrocks/sqlguard/integrations/sqlxguard
```
```
import (
"github.com/KARTIKrocks/sqlguard/integrations/sqlxguard"
"github.com/KARTIKrocks/sqlguard/middleware"
)
sqlxDB := sqlx.MustConnect("postgres", dsn)
db := sqlxguard.WrapSqlx(sqlxDB,
middleware.WithSlowQueryThreshold(500*time.Millisecond),
)
var users []User
db.Select(&users, "SELECT * FROM users") // warns about SELECT *
```
### pgx 集成(原生 pgx / pgxpool)
`database/sql` 驱动包装器涵盖了 pgx-stdlib(`pgx/v5/stdlib`)。对于
**原生 pgx API**(`pgxpool.Pool`、`pgx.Conn` —— 它们完全绕过 `database/sql`),
请使用 `pgxguard`。它挂载到 pgx 自身的 tracer 接口上,因此每个
`Query`/`QueryRow`/`Exec` 以及每个 `SendBatch` 都会被分析,而无需
包装器类型或方法列表。
```
go get github.com/KARTIKrocks/sqlguard/integrations/pgxguard
```
```
import (
"github.com/KARTIKrocks/sqlguard/integrations/pgxguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"github.com/jackc/pgx/v5/pgxpool"
)
cfg, _ := pgxpool.ParseConfig(dsn)
pgxguard.ApplyPool(cfg,
middleware.WithSlowQueryThreshold(50*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)
pool, _ := pgxpool.NewWithConfig(ctx, cfg)
```
`Apply`(用于 `*pgx.ConnConfig`)和 `ApplyPool`(用于 `*pgxpool.Config`)
可与通过 pgx 自身的 `multitracer` 安装的任何 tracer **组合**使用,
因此 sqlguard 可以与 `otelpgx`、`ddtrace` 等共存,而不是
默默地覆盖它们。配置采用标准的 `middleware.Option`
集合——与驱动包装器相同,无需学习并行的接口。
覆盖范围:`Query` / `QueryRow` / `Exec`(通过 `pgx.QueryTracer`)和
`SendBatch`(通过 `pgx.BatchTracer`)。预编译语句的执行已经由
`QueryTracer` 涵盖,因此特意省略了 `PrepareTracer` 以避免重复报告。
`CopyFrom` 不携带 SQL,不在覆盖范围内。
### bun / xorm 集成
bun 和 xorm 通过其自身的查询层构建 SQL,并公开原生的
前后钩子接口。`bunguard` 和 `xormguard` 插入这些接口,并使每条
语句都通过相同的共享核心——采用相同的 `middleware.Option`
集合,没有并行的接口。
```
go get github.com/KARTIKrocks/sqlguard/integrations/bunguard
go get github.com/KARTIKrocks/sqlguard/integrations/xormguard
```
```
// bun — register a QueryHook
db.AddQueryHook(bunguard.New(
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
))
// xorm — register a Hook
engine.AddHook(xormguard.New(
middleware.WithSlowQueryThreshold(500*time.Millisecond),
))
```
### ent 集成
ent 在 `database/sql` 上运行,因此最简单的覆盖方式是将 `entsql` 指向来自
`sqlguard.Register`/`OpenDB` 的 `*sql.DB`。`entguard` 是专门的
替代方案:它装饰了 ent 自身的 `dialect.Driver`,因此无论 `*sql.DB`
是如何创建的,它都能覆盖每一个 `Exec`/`Query`(及其打开的事务)。
```
go get github.com/KARTIKrocks/sqlguard/integrations/entguard
```
```
drv, _ := entsql.Open(dialect.Postgres, dsn)
guarded := entguard.Wrap(drv,
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)
client := ent.NewClient(ent.Driver(guarded))
```
每个适配器(`gormguard`、`bunguard`、`xormguard`、`entguard`、`pgxguard`、
`sqlxguard`)都会公开一个 `ResetN1()`,你可以在每个请求的边界处调用它,以将
N+1 检测范围限制在一个工作单元内。
### SQL 解析器(准确性与零依赖)
默认情况下分析器使用**零依赖的回退解析器**:它会在模式匹配之前剥离 SQL 注释和
字符串字面量的内容,因此注释/字符串中的关键字以及像 `update_at` 这样的标识符
不再会导致误报。它从不报错——它无法完全理解的 SQL 仍然会尽最大努力得出结果,
因此分析永远不会破坏你的查询路径。
要进行**精确、结构化的分析**,请选择启用真正的语法。它们位于
独立的模块中,以保持核心无依赖:
```
go get github.com/KARTIKrocks/sqlguard/parsers/pgparser # PostgreSQL (pure Go, no cgo)
go get github.com/KARTIKrocks/sqlguard/parsers/mysqlparser # MySQL (pure Go, no cgo)
```
```
import (
"github.com/KARTIKrocks/sqlguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"github.com/KARTIKrocks/sqlguard/parsers/pgparser"
)
sqlguard.Register("sqlguard-pg", "pgx", middleware.WithParser(pgparser.New()))
db, _ := sql.Open("sqlguard-pg", dsn)
// Or with the standalone analyzer:
a := analyzer.Default().WithParser(pgparser.New())
```
真正的解析器从 AST 中提取容易误报的事实(语句类型、
WHERE/LIMIT/ORDER BY/FROM 的存在、`SELECT *`、`SELECT DISTINCT`、`OFFSET`、
显式的 INSERT 列),而不是使用正则表达式。CTE、子查询和
方言语法都能被正确处理;语法拒绝的任何内容(动态 SQL、
驱动占位符)都会透明地降级到回退解析器。
即使使用真正的解析器,有些事实仍然是基于词汇启发式的,因为它们读取的是 AST 丢弃的
字面值,或者是故意的文本级别检查:IN 列表大小
(`in-list-too-large`)、逗号/笛卡尔积连接(`implicit-join` /
`cartesian-join`)以及字面量/文本检查(`leading-wildcard`、
`non-sargable-predicate`、`add-not-null-without-default`)。无论使用哪种解析器,
这些都会保持其零依赖、尽力而为的行为。
### 自定义规则
```
import "github.com/KARTIKrocks/sqlguard/analyzer"
// Create analyzer with only the rules you want
a := analyzer.New(
analyzer.CheckDeleteWithoutWhere,
analyzer.CheckUpdateWithoutWhere,
)
// Or use all defaults
a := analyzer.Default()
// Analyze a query
results := a.Analyze("DELETE FROM users")
for _, r := range results {
fmt.Printf("[%s] %s: %s\n", r.Severity, r.RuleName, r.Message)
}
```
## 开发
```
make help # List all targets
make all # tidy, fmt, vet, lint, build, test (all modules)
make build # Compile all modules; `make cli` builds bin/sqlguard
make test # Run tests across all modules (test-race adds -race)
make lint # Run golangci-lint across all modules
make fmt # gofmt -s + goimports
make tidy # go mod tidy across all modules
make install # Install the CLI to $GOPATH/bin
```
## 覆盖范围
中间件包装了 `database/sql` **驱动**链,因此_每一个_查询
都会被分析,无论它是如何发出的(`Query`/`Exec`/`Prepare`/`Tx`、
context 变体,以及基于其上的任何 ORM/查询构建器 —— sqlc, ent, sqlx, gorm、
pgx-stdlib)。没有需要同步的方法白名单;你取回的是
真实的 `*sql.DB`。
可选的适配器模块,每一个都建立在相同的 `middleware.Guard` 核心之上,
将覆盖范围扩展到绕过或位于 `database/sql` 驱动路径
之上或之外的 API:
- **`pgxguard`** — 原生 pgx / pgxpool(从不通过 `database/sql`),通过 pgx 自身的 tracer 接口。
通过 `multitracer` 与现有的 tracer(otelpgx, ddtrace)组合。涵盖
`Query`/`QueryRow`/`Exec` 和 `SendBatch`。
- **`gormguard`** / **`bunguard`** / **`xormguard`** — 挂载每个 ORM 原生的
前后回调接口(`gorm.Plugin`、`bun.QueryHook`、xorm
`contexts.Hook`)。
- **`entguard`** — 装饰 ent 的 `dialect.Driver`(Exec/Query 及其打开的
事务)。
- **`sqlxguard`** — 仅限 sqlx 的辅助工具,用于在驱动程序路径之外构建 SQL:
`Select` / `SelectContext`、`Get` / `GetContext`、`Queryx`、`NamedExec` /
`NamedExecContext`。
所有这六个模块都从共享核心继承了默认脱敏、稳定的指纹、解析器接口
以及慢查询/N+1 检测功能,并公开了 `ResetN1()` 以用于按请求划定的作用域。
## 限制
- 静态扫描器可解析内联字面量、同包/跨包常量、
常量连接,以及带有常量格式字符串的 `fmt.Sprintf`
(通过 `go/types`);它无法解析仅在运行时已知的值。
- 默认的回退解析器只能尽力而为;要进行精确的结构分析,请使用真正的解析器模块(参见上文的 _SQL 解析器_)
- EXPLAIN 分析器需要实时数据库连接;仅支持 `postgres` 和 `mysql` 方言(`mysql` 也涵盖 MariaDB)
## 许可证
[MIT](LICENSE)
标签:Apache Flink, EVTX分析, Go, Ruby工具, SQL分析, 代码规范检查, 性能优化, 数据库, 日志审计, 检测绕过, 测试用例, 错误基检测, 静态代码分析