扫描器直接接进 CI,最常见的翻车不是“扫得不准”,而是第一次运行就报出一串历史遗留问题。
团队看到几十条 High,通常只剩两个选择:
全部修完再合并
或者把扫描步骤直接关掉
前者排期不现实,后者等于没有门禁。
我做了个小实验,用 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}
这里有两个关键点:
baseline_total=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 时的遗留债务,不是替代代码审计。
公开规则、示例报告和命令说明:
需要本地扫描器完整源码版:Gumroad