Semgrep:把源码规则做成可回归的快速门禁
Semgrep 直接读取源码,按语言语法、模式组合和数据流条件执行规则。它不需要先生成代码数据库,因而适合编辑后检查、提交前反馈和 PR 门禁。速度快不代表结果天然可信:规则没有反例、扫描命令没加失败参数、目标路径被 exclude 掉,都会让一条“成功执行”的命令留下假绿。
本文使用 Semgrep 1.173.0 作为实验坐标。CLI、规则仓库和自定义规则必须分别锁定;版本升级时用同一组 fixture 比较命中、位置、指纹和退出码,不能只确认 semgrep --version 有输出。
安装与来源锁定
CLI 可以装进隔离工具环境。受控 CI 更适合使用带哈希的 Python 依赖锁或固定 digest 的官方镜像:
python3 -m venv .tools/semgrep
. .tools/semgrep/bin/activate
python -m pip install 'semgrep==1.173.0'
test "$(semgrep --version)" = "1.173.0"受限网络要提前同步包或镜像并保留来源摘要。关闭 TLS 校验不是代理故障的解决方案。扫描器读取完整源码,云端规则、遥测和托管功能是否上传代码或元数据,也要按当前部署模式单独核对。
正反 fixture 先于生产规则
下面的规则只识别实验函数名,不包含真实漏洞利用:
set -euo pipefail
LAB_DIR="$(mktemp -d)"
mkdir -p "$LAB_DIR/src" "$LAB_DIR/rules" "$LAB_DIR/artifacts"
cat > "$LAB_DIR/src/demo.py" <<'PY'
def unsafe(user_value):
return run_query(user_value)
def safe(user_value):
return run_query(validate(user_value))
PY
cat > "$LAB_DIR/rules/demo.yml" <<'YAML'
rules:
- id: training.direct-input-to-query
languages: [python]
message: Unvalidated fixture value reaches the demo helper
severity: ERROR
patterns:
- pattern: run_query($X)
- pattern-not: run_query(validate($X))
YAML正例应命中 unsafe,反例必须保护 safe。只有正例时,规则可能只是见到函数调用就报警;只有反例时,又无法证明扫描器确实进入目标文件。
扫描成功与门禁成功不是一回事
先验证配置并保存机器报告:
semgrep scan --validate --config "$LAB_DIR/rules/demo.yml"
semgrep scan \
--config "$LAB_DIR/rules/demo.yml" \
--metrics=off \
--json-output "$LAB_DIR/artifacts/semgrep.json" \
--sarif-output "$LAB_DIR/artifacts/semgrep.sarif" \
"$LAB_DIR/src"普通扫描即使发现结果,也可能按所选模式返回成功。作为门禁时要显式要求命中产生非零状态:
set +e
semgrep scan \
--config "$LAB_DIR/rules/demo.yml" \
--metrics=off \
--error \
"$LAB_DIR/src"
scanner_rc=$?
set -e
test "$scanner_rc" -eq 1
test -s "$LAB_DIR/artifacts/semgrep.json"包装脚本不能用 || true 抹平结果。规则命中、配置错误、解析失败、网络失败和进程崩溃要保留各自状态;报告上传可以在失败分支执行,但不能反过来覆盖扫描退出码。
规则是需要测试的代码
Semgrep 支持把规则与带注释的测试文件放在一起,并通过 semgrep --test 运行。测试应覆盖应命中、应通过、路径过滤、语言版本和常见语法变体:
semgrep --test "$LAB_DIR/rules"规则 ID 进入 SARIF、抑制记录和工单后就成为稳定接口。重命名、合并规则或改变严重度,需要迁移既有状态。直接引用浮动远端 ruleset 会让同一提交在不同时间得到不同结果;生产门禁应固定规则提交或包版本,并在升级 PR 中展示新旧差异。
模式规则适合明确语法结构,metavariable 条件用于约束变量,taint 规则表达 source、propagator、sanitizer 与 sink。数据流模型越复杂,越需要用安全反例证明 sanitizer 真的生效,也要用跨函数、别名和框架封装样例判断当前引擎的覆盖边界。产品名称里有“数据流”不等于所有语言和框架都已精确建模。
文件覆盖必须可观察
Monorepo 中最常见的假绿不是规则错,而是扫描路径错。CI 应记录目标路径、配置摘要、扫描文件数、跳过原因和当前 revision。生成代码、vendor、压缩产物和超大文件可以按风险排除,但排除必须有 owner、原因和复核入口。
本机增量扫描用于反馈,PR 扫描覆盖变更相关文件,默认分支或周期任务承担全仓规则回放。三者可以有不同范围,规则版本与严重度口径必须一致。没有匹配文件是 skipped,不是 passed;统计时不能合并。
SARIF 是输出,不是分析本身
SARIF 记录规则、位置、级别、指纹和部分源码上下文。上传成功只证明平台接收了文件,不证明扫描覆盖正确,也不证明门禁已经裁决。CI 要先确认扫描命令、报告非空条件和退出码,再由单独权限上传。
Fork PR 不应获得高权限代码扫描 Token。开源或外部贡献路径可以执行离线规则并保存受控报告,由受信分支完成平台上传。SARIF 可能暴露源码片段、内部路径和规则策略,保留期与读取权限不能按普通测试日志处理。
误报、抑制与升级
误报不是“开发者不喜欢的告警”,而是规则模型与真实语义不符。处置要保留规则 ID、位置、判断理由、owner 和到期时间。局部抑制优于宽泛 exclude;基线只能隔离存量,不能把新增问题也静默吞掉。
升级前用固定 fixture、代表性仓库快照和相同配置分别运行新旧 CLI,比较结果数之外的规则 ID、位置、数据流路径、解析错误、跳过文件和耗时。结果减少可能是精度提升,也可能是语言解析回归。回滚应恢复 CLI、规则提交和门禁参数,不能靠删除 --error 让流水线变绿。
故障证据与运行成本
本机命中而 CI 不命中,先比较版本、配置来源、工作目录、目标路径和 ignore。升级后告警暴增,先看规则差异与语言解析,再看业务代码。扫描很慢时记录文件数、语言、规则数和最慢规则,不要直接扩大超时。
Semgrep 进程只需读取源码和写报告,不需要生产云权限。自定义规则与插件配置同样属于可执行供应链。团队应同时观察规则测试通过率、扫描覆盖、policy-failed、tool-failed、skipped、P50/P95 和抑制年龄;finding 数量本身无法说明门禁质量。
性能优化要从最慢规则、解析语言和实际输入入手。把规则集粗暴拆成多个并行任务,可能重复解析同一批源码,也可能让多份 SARIF 在路径归一化和去重时产生新问题。先用固定仓库快照测量规则级耗时,再决定是否按语言、风险或反馈时限分层;任何分层都要保留一条周期性全量回放,证明快速通道没有成为永久盲区。
清理与退出
实验目录只在确认位于系统临时目录且包含本流程创建的 rules/demo.yml 后删除。卸载 CLI 时同步清理受控环境和缓存,但保留规则仓库、升级差异与必要审计记录。若迁移到其他 SAST,引入方先用原正反 fixture 证明新工具覆盖,再移除 Semgrep 门禁,避免出现无扫描空窗。
