Semgrep 代码审计把过去靠人工翻代码找漏洞的过程,变成了一条可以重复执行、结果可对比的自动化检查链路。本文从零搭一套 SAST 流水线:先用官方规则集覆盖常见漏洞模式,再用自定义 taint 规则追踪「用户输入 → 危险函数」的完整传播路径,最后把它接进提交钩子与持续集成,让注入类问题在代码合并前就被拦住。全文命令与规则文件均可直接复制执行,适用于有代码仓库权限、能在开发机或构建容器里安装 Python 环境的中小团队。
适用场景
以下四类情况适合直接套用本文方案:
- 网站或后台业务系统上线前需要一轮代码层安全检查,但预算不足以请外部审计团队;
- 已经接入 WAF 却仍被 SQL 注入、命令注入绕过,需要从源码根因上补一层;
- 团队多人协作、提交频繁,希望把安全检查前置到代码合并环节;
- 需要长期留存扫描结果,用于合规检查或漏洞修复进度跟踪。
前置条件
- 一台能访问代码仓库的 Linux 主机(或直接用官方容器镜像),内存建议不低于 2GB;
- Python 3.8+,可联网拉取 Semgrep Registry 规则;
- 代码仓库已用 Git 管理,能取到主分支引用(用于增量扫描对比);
- 若要在 CI 中上传结果,需要一个支持 SARIF 展示的平台(GitHub Code Scanning、GitLab SAST 报表或自建 DefectDojo 均可)。
原理说明
语法模式匹配与轻量数据流
Semgrep 不做完整编译,而是把源码解析成抽象语法树后,用「模式」去匹配结构。模式的写法接近目标语言本身,例如 $CURSOR.execute(...) 中的 $X 表示任意表达式,因此不需要为每种框架写正则。对于跨函数的数据流,Semgrep 使用 taint 模式:声明「污染源」(pattern-sources)、「汇聚点」(pattern-sinks)与「净化函数」(pattern-sanitizers),由引擎自动判断从源到汇聚点的路径上是否存在未被净化的变量,命中即告警。
规则分级与去噪思路
SAST 落地最大的阻力不是查不到,而是告警太多。Semgrep 的规则可以按 severity 分为 ERROR、WARNING、INFO 三档,配合增量扫描与 # nosemgrep 注释,可以把「存量历史问题」和「本次新增问题」分开处理:CI 只对新增代码阻断,历史问题进台账慢慢消化。
操作步骤
步骤一:安装并验证环境
# 方式一:pip 安装(推荐,便于与 Python 项目共用环境)
python3 -m pip install --upgrade semgrep
semgrep --version
# 方式二:容器运行,无需污染主机环境
docker run --rm -v "$(pwd):/src" semgrep/semgrep semgrep --version
首次运行需要联网同步 Registry 规则,离线环境可把规则文件提前 git clone 到本地,用 --config ./rules/ 指定本地路径。
步骤二:跑一轮全量基线扫描
# 官方规则集,覆盖 OWASP Top 10 常见模式
semgrep --config=p/owasp-top-ten --exclude=tests --exclude=vendor .
# 输出 JSON 便于归档与统计,退出码非 0 表示有命中
semgrep --config=p/ci --json --output=semgrep-baseline.json --error .
# 统计命中数量
python3 -c "import json;d=json.load(open('semgrep-baseline.json'));print(len(d['results']))"
把这一轮的 JSON 作为基线归档。后续每次只关注新增命中,避免历史问题反复刷屏。
步骤三:编写自定义 taint 规则
官方规则不一定覆盖自研框架,需要自己写。新建 rules/sql-injection.yaml:
rules:
- id: python-flask-sql-taint
mode: taint
languages: [python]
severity: ERROR
message: >-
用户输入未参数化直接进入 SQL 执行,存在 SQL 注入风险,
请改用参数化查询或 ORM 绑定变量。
metadata:
category: security
cwe: "CWE-89: SQL Injection"
owasp: "A03:2021 - Injection"
pattern-sources:
- pattern: flask.request.args.get(...)
- pattern: flask.request.form.get(...)
- pattern: flask.request.values.get(...)
pattern-sinks:
- pattern: $CURSOR.execute(...)
- pattern: $CURSOR.executemany(...)
pattern-sanitizers:
- pattern: db.escape(...)
- pattern: $INT(...)
关键点:pattern-sources 要覆盖框架真实的入参入口(args / form / values / json / cookies),pattern-sanitizers 要把类型转换和转义函数全部列进去,否则会出现「转换后仍告警」的误报。若同一规则要覆盖多个框架,可把 sources 写成 pattern-either 分支,或直接拆成多条规则、用 id 前缀统一归类。
步骤四:接入提交钩子与持续集成
# 本地提交前只扫暂存区变更,速度最快
git diff --cached --name-only --diff-filter=ACM \
| grep -E '\.(py|js|ts|java|go|php)$' \
| xargs -r semgrep --config=rules/ --config=p/ci --error
把它保存为 .git/hooks/pre-commit 并 chmod +x 即可生效。CI 侧建议用官方镜像,只对变更做增量阻断:
name: sast
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
container: semgrep/semgrep
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
semgrep ci --config=p/ci --baseline-commit=origin/main \
--sarif --output=semgrep.sarif
--baseline-commit 让引擎只报告相对基线分支新增的问题,老代码里的历史命中不会阻断流水线。
配置验证
# 1. 校验规则文件语法是否合法
semgrep --validate --config=rules/
# 2. 用单元测试文件验证规则能否准确命中与放行
semgrep --test --config=rules/
# 3. 造一个含漏洞的样张,确认规则真的会触发
cat > /tmp/vuln_demo.py <<'EOF'
from flask import Flask, request
app = Flask(__name__)
@app.route("/user")
def user():
uid = request.args.get("id")
return app.db.execute("select * from t where id = " + uid)
EOF
semgrep --config=rules/sql-injection.yaml /tmp/vuln_demo.py
正常情况下第 3 步会输出一条 python-flask-sql-taint 命中,并给出文件行号与数据流路径;若没有任何输出,优先检查 sources 的写法是否与代码里的真实调用一致(例如 request.args.get 与 request.args["id"] 是两种不同写法,需要各写一条 pattern)。
常见问题
FAQ 1:误报太多,逐条排查不现实怎么办?
先区分三类告警:确认无害的用 # nosemgrep: <rule-id> 就地标注并附原因;规则本身写得太宽的(例如 sinks 写成 $F(...)),回到规则文件收窄;确实是框架特性导致的,用 paths.exclude 把测试目录和 SDK 目录整体排除。Semgrep 还支持 --triage-mode 交互式逐条标记,判定结果落盘到本地基线文件,团队可复用同一份结论,不必每人重复判断。
FAQ 2:全量扫描太慢、CI 经常超时怎么办?
三个方向的优化:一是弃用 --config=auto 这类规则庞大的配置,按语言精准指定;二是用 --max-target-bytes 跳过超大文件(如前端打包产物),并用 .semgrepignore 排除 node_modules、dist、vendor;三是把全量扫描放到每日定时任务,PR 流水线只跑 --baseline-commit 增量模式。
FAQ 3:Semgrep 能替代人工代码审计吗?
不能。SAST 擅长发现模式化的注入、硬编码密钥、不安全随机数等问题,但对业务逻辑缺陷、越权设计、加密方案误用基本无能为力。合理定位是把容易出错的机械检查自动化,把人力留给逻辑层评审。
总结
Semgrep 的落地路径可以概括为四步:官方规则集跑基线、自定义 taint 规则补盲区、提交钩子做本地拦截、CI 做增量阻断。真正决定效果的不是工具本身,而是两件事——污染源与净化函数是否覆盖完整,以及误报是否被及时收敛到可接受水平。建议先用两周时间把误报压到个位数,再开启阻断模式,否则流水线很快会被绕过。