KARTIKrocks/sqlguard

GitHub: KARTIKrocks/sqlguard

面向 Go 应用的生产级 SQL 查询分析器,支持运行时和 CI 双模式,自动检测慢查询、N+1 问题和危险 SQL 模式。

Stars: 0 | Forks: 0

# sqlguard [![Go Reference](https://pkg.go.dev/badge/github.com/KARTIKrocks/sqlguard.svg)](https://pkg.go.dev/github.com/KARTIKrocks/sqlguard) [![Go Report Card](https://goreportcard.com/badge/github.com/KARTIKrocks/sqlguard)](https://goreportcard.com/report/github.com/KARTIKrocks/sqlguard) [![Go Version](https://img.shields.io/github/go-mod/go-version/KARTIKrocks/sqlguard)](go.mod) [![CI](https://github.com/KARTIKrocks/sqlguard/actions/workflows/ci.yml/badge.svg)](https://github.com/KARTIKrocks/sqlguard/actions/workflows/ci.yml) [![CodeQL](https://static.pigsec.cn/wp-content/uploads/repos/cas/53/539e9a6bf48ad24469a4363bff3aa68124154549e26592783d3d8577f2acbbfc.svg)](https://github.com/KARTIKrocks/sqlguard/actions/workflows/codeql.yml) [![GitHub tag](https://img.shields.io/github/v/tag/KARTIKrocks/sqlguard)](https://github.com/KARTIKrocks/sqlguard/releases) [![License](https://img.shields.io/github/license/KARTIKrocks/sqlguard)](LICENSE) [![codecov](https://codecov.io/gh/KARTIKrocks/sqlguard/branch/main/graph/badge.svg)](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分析, 代码规范检查, 性能优化, 数据库, 日志审计, 检测绕过, 测试用例, 错误基检测, 静态代码分析