项目里已有 SQL 拼接,怎么接 CI?我用 baseline 让历史问题放行,新增问题失败

23 阅读4分钟

扫描器直接接进 CI,最常见的翻车不是“扫得不准”,而是第一次运行就报出一串历史遗留问题。

团队看到几十条 High,通常只剩两个选择:

全部修完再合并
或者把扫描步骤直接关掉

前者排期不现实,后者等于没有门禁。

掘金-封面-baseline-CI-20260912.png 我做了个小实验,用 baseline 把这两种极端拆开:

历史高危:登记为已有债务,不阻塞当前合并
新增高危:立即失败,必须处理

实验目录

假设项目里已经有一段历史代码:

def legacy_lookup(db, username):
    db.execute(f"SELECT id FROM users WHERE username = '{username}'")
    return db.fetchone()

这段代码确实存在 SQL 拼接,但在接入扫描器之前已经进入项目。

第一次运行:

code-audit . --format json --output initial-report.json

结果:

exit_code=1
summary={'high': 1, 'medium': 0, 'low': 0, 'total': 1}

如果 CI 直接采用这个命令,当前分支会因为历史问题失败。

先把历史问题写成 baseline

生成基线:

code-audit . \
  --format json \
  --output initial-report.json \
  --write-baseline baseline.json

baseline 保存的是当前已经接受的发现。之后再用它扫描:

code-audit . \
  --baseline baseline.json \
  --format json \
  --output existing-report.json

这时历史高危仍然存在,但不再被算作“本次新增”:

exit_code=0
baseline_total=1
new_total=0
summary={'high': 0, 'medium': 0, 'low': 0, 'total': 0}

这一步的含义不是“历史问题消失了”,而是:

这次合并没有增加新的高危写法。

新增一段 SQL 拼接,CI 立即失败

项目继续开发,新功能里出现:

def new_login(db, username):
    db.execute(f"SELECT id FROM users WHERE username = '{username}'")
    return db.fetchone()

再次使用同一个 baseline 扫描:

exit_code=1
baseline_total=2
new_total=1
summary={'high': 1, 'medium': 0, 'low': 0, 'total': 1}

这里有两个关键点:

  1. baseline_total=2 表示项目里现在总共有两条匹配发现;
  2. new_total=1 表示其中一条不在旧基线里,属于本次新增。

CI 只需要检查退出码。新增 High 后退出码为 1,合并被拦截。

修复新增问题后恢复通过

把新功能改成参数化查询:

def new_login(db, username):
    db.execute(
        "SELECT id FROM users WHERE username = ?",
        (username,),
    )
    return db.fetchone()

再次扫描:

exit_code=0
baseline_total=1
new_total=0
summary={'high': 0, 'medium': 0, 'low': 0, 'total': 0}

历史代码仍然留在债务清单里,但新增代码已经回到安全边界之内。

这套方案解决的不是“扫描”,而是接入顺序

完整流程可以固定成:

第一次接入
  -> 生成 baseline
  -> 人工标记每个历史发现的负责人和清理期限

日常提交
  -> 使用 baseline 扫描
  -> new_total > 0 或 high > 0 时失败

修复历史问题
  -> 修复代码
  -> 重新生成 baseline
  -> baseline_total 下降

baseline 最适合解决的是“遗留债务怎么过渡”,不是“把问题永久隐藏”。

一个必须说清的边界

当前 baseline 的发现键包含:

file + line + pattern

这意味着代码移动、文件重命名或行号变化后,旧 baseline 可能无法继续匹配,最终表现为新增发现。

这不是坏事,它避免一个文件被整体忽略,但也要求团队:

  • 每次接受新债务都要有代码评审;
  • 删除问题或移动代码后,同步更新 baseline;
  • 记录每个历史发现的责任人和清理期限;
  • 不把重新生成 baseline 当成日常无条件操作。

更稳妥的管理方式是让 baseline 成为“限时债务清单”:

发现 ID / 负责人 / 计划清理版本 / 到期时间

如果把 baseline 当成永久白名单,它很快会从治理工具变成遮蔽工具。

最适合的 CI 门禁

第一次接入时,可以先只拦 High:

code-audit . \
  --baseline baseline.json \
  --format json \
  --output report.json \
  --fail-on high

等历史 High 清理到可控范围,再把 Medium 纳入门禁。

比“一次把所有规则全部拉满”更现实的路线是:

先阻止新增 High
  -> 清理历史 High
  -> 再收紧 Medium
  -> 最后让 baseline 逐步归零

自动扫描仍然只是第一轮定位,发现结果必须经过人工确认。baseline 解决的是接入 CI 时的遗留债务,不是替代代码审计。

公开规则、示例报告和命令说明:

github.com/yuan1521913…

需要本地扫描器完整源码版:Gumroad