十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

统一前后端代码扫描:Semgrep选型与落地复盘

统一前后端代码扫描:Semgrep选型与落地复盘 先说结论这次选型落地之后我们组的前端和后端同学终于不用再各扫各的了。之前的前端代码检查跑在 ESLint Stylelint 上后端代码检查跑在 SpotBugs Checkstyle 上另外还残留一个快没人维护的 SonarQube 看板。每次发版前后两个团队要分别去翻各自流水线里的报告规则口径也对不上。经过三周的选型和切换我们把代码扫描收敛成了一套 Semgrep 工作流同一份规则仓库、同一个 CI 扫描入口、同一套质量门禁。这篇文章把需求拆解、评分模型、工具对比和落地过程从头到尾复盘一遍适合正打算统一前后端扫描链路、或者在做代码扫描工具选型的团队参考。1. 这次选型到底在解决什么问题1.1 从“各扫门前雪”到两套体系并存的真实处境先交代一下背景。我们的产品是一个典型的前后端分离项目前端团队负责 Vue3 TypeScript 的 SPA后端团队负责 Java Spring Boot 微服务。两个团队代码仓库虽然分开但上游 CI 用的都是同一套 GitHub Actions。按理说基础设施同源扫描这件事不该有太大割裂但实际跑起来完全是另一回事。前端这边规则由 ESLint 和 Stylelint 承担我们维护了一套自认为很完善的前端规范包括变量命名、组件写法、禁止 dangerouslySetInnerHTML 这类安全要求。提交 PR 时流水线会跑eslint --max-warnings 0如果告警超过阈值就直接红掉。后端那边Checkstyle 管代码风格SpotBugs 管静态缺陷另外还有一个早期部署的 SonarQube 用来做覆盖率展示但那个 Sonar 服务器已经很久没人升级规则停留在 Java 8 时代基本形同虚设。痛点不止“工具多”这么简单。前端说前端安全后端说后端安全可真正出问题的往往是接口对接那一层。举个例子前端有一个页面会把用户输入直接拼到 URL 参数里请求后端规则层面只看得到 props 使用不规范根本不会提示这个拼接可能构成 SSRF 的入口。后端的 Controller 接收外部参数时也没有统一的校验规则Checkstyle 只查命名和注释这种接口级问题它一概不管。两边各扫门前雪的结果就是跨端调用链条上的漏洞反而成了灰色地带。还有个很现实的问题是规则维护成本。我们前端规则和后端规则分散在两套配置里既有 YAML 又有 XML前端团队不敢动后端配置后端团队也不清楚前端那边到底定了哪些规范。新人入职要先学两套扫猫体系和两套门禁策略团队成员平均要一个月才能搞清楚“这个报错应该找谁”。这不是某个人的问题而是两套工具天然带来的信息孤岛。1.2 统一扫描的真正收益不是“少装一个工具”很多人听到统一工具第一反应是“少维护一套系统”其实这个想法格局小了。工具统一之后最值钱的变化是规则变成了唯一数据源门禁策略变成了唯一事实标准跨语言的安全问题开始有办法在规则层面对齐。拿我们内部复盘的一个 SSRF 场景举例。前端页面允许用户填一个 URL然后页面直接把这段输入塞给后端接口后端拿到地址后又去请求了这个外部地址。前端规则应该拦“用户输入未做白名单校验就进入请求参数”后端规则应该拦“对外请求的地址未校验来源”。两套工具时你必须在 ESLint 和 SpotBugs 里分别写两条完全不同的规则而且没有任何机制保证它们同时生效。换成一套支持多语言的扫描工具后两条规则可以放在同一个规则仓库里甚至可以共享同一个元数据标记category: security团队 review 一次就能同时覆盖两端。当然减少工具数量本身确实有好处。CI 环节少一个失败源告警渠道统一之后开发不需要再掌握两套“这个警告到底会不会挂掉流水线”的经验。我可以负责任地说真正让团队愿意切换的动机不是省了那点服务器资源而是消除了“前后端标准不一致”带来的扯皮。1.3 什么条件的团队适合照搬这套方案如果你看完上面这些描述觉得似曾相识那你可以评估一下自己团队的情况。我观察下来适合做这种统一迁移的团队一般有这几个特征前后端都在同一个组织下面CI 基础设施能共用团队规模在十人左右不必为大团队考虑复杂的角色权限体系愿意接受“规则即代码”也就是说希望规则能被 review、被版本管理而不是靠某个老大哥在平台页面上点来点去。反过来如果你的前端仓库是给外部客户单独交付的CI 和后端完全隔离或者后端使用的是非常偏门的私有框架前端又极度依赖 ECMAScript 最新提案语法那我不建议硬融合。强行统一可能带来大量规则兼容性的适配成本这个账你自己得算清楚。我们当时的判断是前端 TS 语法栈和后端 Java 生态都很主流统一工具后规则编写难度可控投入产出比是正向的。2. 选型前先做足功课需求拆解和评分模型2.1 把“想统一”翻译成工具需求清单选型最忌讳上来就打开官网看功能列表然后被 Demo 里的炫酷界面带跑偏。我当时的做法是先坐下来写一份“最低必要功能清单”所有候选工具都拿这个清单过一遍满足不了就直接淘汰。这份清单包括六项。第一语言覆盖度这是硬指标必须原生支持 JavaScript、TypeScript 和 Java最好还能顺带覆盖 YAML 和 Python因为我们的 CI 脚本也有不少逻辑。第二规则扩展性光靠内置规则远远不够必须支持用类似代码片段的方式定义自定义规则而不是只能开关平台预置项。第三扫描性能单次全量扫描在三分钟内完成差分扫描最好控制在三十秒以内能支持十个以上并发任务。第四CI 集成必须能把结果直接回写到 PR 评论或者 MR 注解上开发不用跳转到外部站点看报告。第五门禁能力扫出来的问题要分 Block、高、中、低等级别门禁能按严重级别决定是否阻断合并。第六运维成本我们团队没有专职运维人员工具太重容易烂尾。清单写出来之后我心里已经大致有数了。SonarQube 在门禁和语言覆盖上非常强但运维成本不低CodeQL 的深度安全分析是长板但它对团队技能有要求。这些后面会展开说。2.2 评分维度和权重为什么误报率占比这么高需求清单只是“过不过”的问题真正决定选谁要靠量化打分。我定了一个六维评分模型总权重加起来百分之百每个候选工具按一到十分打分最后加权求总分。评分维度权重核心考量语言覆盖度20%是否原生支持 JS/TS/Java 及主流框架规则扩展性20%能否用类代码模式自定义规则并入库版本管理扫描性能15%全量、差分扫描耗时并发能力CI 集成20%PR 回写、SARIF 报告、门禁命令支持误报率控制15%规则是否容易裁剪是否会导致告警疲劳运维成本10%部署、升级、数据存储和备份的复杂度这个权重分配当时在团队里也争论过一轮。有人觉得误报率不该占这么高认为工具只要能力强误报完全可以接受。我的观点是静态扫描工具最大的敌人恰恰是告警疲劳。一旦误报太多开发者就会养成“看一眼不关痛痒直接忽略”的习惯真正的严重问题也会被淹没。我们团队规模不大工程效率上容错空间有限所以宁可选一条“规则少而准”的路线也不要一个“规则多而杂”的路子。2.3 性能测试别在本地 Mac 上自嗨评分之前一定要做性能预跑而且测试环境要尽量贴近真实 CI。我第一次测 Semgrep 时直接在本地 Mac 上跑了一个 Java 仓库感觉三秒就结束了当场觉得这个工具快得离谱。后来才发现本地机器硬盘是 SSD处理器占用也没人抢跟 CI 容器里默认双核的环境完全是两种状态。真到了 GitLab Runner 或者 GitHub Actions 上跑时间翻了五六倍都不奇怪。后来我统一用测试脚本在三个样例仓库上跑一个 Vue3 TS 前端 repo一个 Java 后端 repo一个前后端混合 monorepo。每次扫描都固定提交点保证对比的是同一个代码快照Diff 测试就模拟 PR 分支拿目标分支和基线分支做差量扫描。这样的数据才具备可比性。性能测试至少重复三次取中位数而不是平均数避免某次容器调度抖动拉高数据。3. 候选工具横向评测与最终选择3.1 SonarQube大而全但运维要提前算账先聊 SonarQube因为很多团队第一反应就是“上 Sonar”。我不否认它的强大多语言支持广泛Java 和 JS/TS 的规则都很丰富质量门禁、增量分析报告、权限系统一应俱全社区版完全免费就能把基础功能跑起来。如果团队里有个愿意深耕平台运维的专职角色SonarQube 确实能撑起企业级代码质量管理的门面。但它对我们这种规模的团队来说有个绕不开的问题运维成本。社区版虽然免费但 JVM 调优、PostgreSQL 数据迁移、插件升级、规则库备份这些东西每一项都要踩坑。我们团队没有严格意义上的 DevOps后端同学平时还要兼职处理 CI真要上 Sonar前期部署少说一周后期每个季度升级一次每次都要提心吊胆这个账我算过并不便宜。另一个痛点是对新语法支持有滞后我们的前端在用 Vue3 的 script setup 语法糖Sonar 解析 TS 5.x 时出现过识别不到组件内声明的情况导致误报漏报都有。我这番话不是劝退 Sonar。如果你的团队体量更大、有人专盯平台运维或者你需要一个传统意义上的“质量管理平台”给管理层看可视化报表Sonar 依然是第一梯队的选择。但放到我们的语境里它的重量级反而成了负担。3.2 Semgrep把规则当代码维护前后端天然统一Semgrep 是我这次选型最惊喜的一个候选。它的核心思路不是像 Sonar 那样做“编译器级别的抽象语法树分析”而是用代码模式匹配的方式来找问题。你可以把规则文件理解成一段“想匹配的代码片段”比如我想禁止在整个仓库里调用eval直接在 YAML 文件里写一个 pattern 就行非常简单直观。它的优势恰好压在我们的痛点上。第一规则即代码所有规则可以放进 Git 仓库跟着项目一起做代码评审前端团队可以 review 后端的规则 Pull Request反之亦然。第二性能好C 核心引擎对多语言都有不错的扫描速度配合 baseline 提交模式做差量扫描PR 级别的基本半分钟内出结果。第三原生支持 JS/TS/Java/Python/Go 等主流语言规则文件可以用同一套 YAML schema 表达前后端规则维护起来几乎没有学习成本。它的短板也很明显。基于模式匹配的分析方式天然就很难做跨过多个数据流的深度路径分析。举个例子一个复杂的三层调用链从一个 Controller 方法到 Service 再到 Repository中间涉及 OTEL 追踪和动态代理这种场景下 Semgrep 很容易漏报。后来我在分享会上打了个比方Semgrep 像一个眼神很好的保安盯着有没有人带危险品入园区CodeQL 更像一个侦探能把一个包裹从进门到被拆开的全过程轨迹都关联起来。如果你要抓的是深度漏洞Semgrep 是不够的。3.3 CodeQL安全深度强但落地门槛偏高CodeQL 在我们组内部一直有“重武器”的称呼。它的数据流分析能力在开源工具里几乎没有对手能够建模非常复杂的污点传播路径用来做安全专项审计特别合适GitHub 仓库接 CodeQL 也只要几个文件就能跑起来。但落到我们“前端后端统一”这个需求上它有一个很致命的问题查询语言是 QL要让所有开发都学会写 QL 规则培训成本和时间成本都太高。我们是个十四人规模的团队前端同学平时主要写 TypeScript后端同学主要集中在 Java大家没有精力再学一门查询语言。而且 CodeQL 在接入自托管 CI 时没有想象的那么轻需要准备编译数据库Java 项目要跑完整构建才能生成有效数据流这对我们这种中大型微服务仓库来说扫描时间很难受。我最后给它的定位是“安全团队的专项审计工具”不作为日常开发的门禁工具。日常 MR 由 Semgrep 负责安全团队在发版前或者对核心服务做定期的 CodeQL 深度扫描两边各取所长。3.4 加权评分结果与最终选型结论三个候选工具在我的六维模型下跑出来的分数如下。评分维度SonarQubeSemgrepCodeQL语言覆盖度 (20%)987规则扩展性 (20%)696扫描性能 (15%)696CI 集成 (20%)898误报率控制 (15%)778运维成本 (10%)586加权总分7.058.406.90最终胜出的是 Semgrep。它并不是每一项都是满分但在我们最看重的“规则扩展性”“扫描性能”“运维成本”这三个维度上取得了均衡和团队的工程能力很匹配。Sonar 适合愿意养一个运维角色的团队CodeQL 适合安全深度要求极高的场景而我们当下最需要的是一个“前后端能共用、规则能维护、PR 能跑得快”的轻量工具。选型结论定了之后我们还做了一个保守的补充方案本地留 ESLint 做 IDE 实时纠错后端保留 SpotBugs 作为编译期辅助但 CI 门禁完全由 Semgrep 承担。也就是说工具可以并存但质量门禁只有一个入口。后面证明这个策略非常有效团队没有因为切换工具而出现“检查断档”。4. 落地过程怎么从两套体系平稳切到一套4.1 双跑两周不靠感觉用数据对比新旧工具我见过太多团队换了工具之后直接下线旧系统结果新工具有几个规则没覆盖到线上出问题才知道漏了。为了不重蹈覆辙我们设置了整整两周的并行期新老工具同时跑在 CI 里但只有旧工具的结果阻塞 MRSemgrep 结果先以注释形式通知开发者。并行期里我让每组轮流抽一个同学做“裁判员”专门负责对比新旧工具的报告差异。发现旧工具能识别、Semgrep 漏报的问题就记录到 backlog 里判断是规则缺失还是引擎能力上限发现 Semgrep 能识别、旧工具漏报的问题就当作“新增收益”写进复盘文档。两周下来我们一共收集到二十多个差异项其中九成是规则配置问题补两条规则就能解决。真正靠引擎能力差异才能发现的只有两条一条涉及深度数据流我们标注了 CodeQL 补充扫描。这个数据给了团队极大的切换信心。这里给个建议并行期一定要指定明确的责任人否则对比工作会变成“俩工具都看看但一个都不细看”。我们当时在群里建了一个共享表格每条差异必须写明“旧工具有没有报、新工具有没有报、确认人、是否接受”。没有这个闭环对比很可能变成走过场。4.2 统一规则集设计和维护一个仓库搞定前后端规则仓库是我们切换后最核心的资产。目录结构从一开始就设计成按技术栈分目录安全和风格分离。这样前端同学只关注frontend/下的规则后端同学维护backend/但两者都放在同一个 repo 里。semgrep-rules/ ├── backend/ │ ├── java/ │ │ ├── security/ │ │ └── correctness/ │ └── common/ ├── frontend/ │ ├── javascript/ │ ├── typescript/ │ └── framework/ ├── shared/ │ ├── security/ │ └── performance/ ├── tests/ │ ├── positive/ # 必须能命中的代码样例 │ └── negative/ # 不应命中的代码样例 └── .semgrepignore重点说一下tests/目录。每一条规则我们都配了至少一组正例和反例正例是为了确认规则真能抓到问题反例是为了防止规则误伤正常代码。这条经验是从一次事故里学到的刚开始我们把一条“禁止使用 eval”的规则写得太宽结果有个后端同学在日志脱敏的场景里合法使用了 eval扫描直接把他的 PR 标红了。后来我们给规则补上 negative 测试明确排除“开发者显式标记的安全用途”才把误报压下去。规则变更也走代码评审流程。前端团队想加一条关于 Vue 的事件绑定限制规则提交后必须由后端团队的一个同学 review 一下描述是否清晰、是否可能过度报告。因为规则语义是跨端共享的后端同学也常常能提出现场没想透的点。这个评审流程大概花了团队一周去适应但后面收益非常大规则质量提升非常明显。4.3 接入 CI 流水线MR 差量扫描加每日全量CI 接入是落地过程中的重头戏。我们要实现的效果是PR 启动时只扫描新增和修改的代码减少噪音和耗时主干分支合并后跑一次全量扫描做整体体检。Semgrep 的--baseline-commit参数正好对上了这个需求。name: code-scan on: pull_request: push: branches: [main] jobs: semgrep: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install Semgrep run: pip install semgrep - name: Run Semgrep Differential Scan run: | semgrep scan --config rules/ \ --baseline-commit origin/main \ --sarif-output semgrep.sarif \ --jobs 4 - name: Upload SARIF to PR uses: github/codeql-action/upload-sarifv3 with: sarif_file: semgrep.sarif这里面有几个细节值得单独拆解。actions/checkout必须设置fetch-depth: 0确保 git 历史完整否则 Semgrep 无法计算 baseline。--jobs 4是在使用 GitHub Actions 标准跑者的前提下做的保守配置因为我实测过这个 runner 默认双核硬拉到八个任务反而会内存溢出。最后upload-sarif是 GitHub Advanced Security 的能力没有这个组件的话也可以直接用官方semgrep-action它会把结果自动标注到 PR 的 Files changed 页面。每天凌晨的全量扫描我们单独做了一条 schedule 任务规则命令类似只是去掉 baseline 参数跑全部代码。全量结果通过 webhook 推到一个内部的告警群只有新增的高危问题才会触发人为处理中低危问题自动进 backlog。4.4 存量问题治理先冻结基线再逐步清零把门禁从旧工具切换到 Semgrep 的那一刻最危险的不是新增问题而是仓库里积累了大半年、可能已经几百上千条的存量问题。如果一上来就把所有历史问题当阻塞项团队的正常迭代会直接被拖垮。我们的策略是利用--baseline-commit做“基线冻结”。第一次切换时把origin/main当作基线基线里已经存在的问题会进入报告但不会导致 CI 失败只有新增的、基线里没有的问题才会触发门禁。这样团队看到 MR 失败时第一反应是“我的改动引入了新问题”而不是“历史遗留问题凭什么让我背锅”。存量问题我们也做了治理不是放任不管。每周五下午各个小组轮流抽出半小时从存量问题列表里挑十个左右按照严重级别和影响范围排优先级逐步清理。这个周期跑了一个月后存量问题数量下降了百分之四十左右最关键的是新问题增量控制在了个位数。这个节奏没必要定得太激进否则团队会疲于应付历史账反而把新代码的质量忽视了。5. 实战常见问题与排查技巧实录5.1 扫描从 3 分钟拖到 40 分钟原因有三Parallel 期结束后我们遇到过一段非常诡异的时期semgrep 扫描在某些仓库上速度从三分钟暴涨到四十分钟。第一反应是规则多了导致变慢于是我把规则拆成一半测试结果没改善。后来逐一排查发现三个真实原因。第一是没有正确做差量扫描。CI 的 YAML 里漏配置了fetch-depth: 0checkout 到的代码没有完整 git 历史--baseline-commit origin/main找不到对应提交Semgrep 就退化成全量扫描时间自然变长。第二是缓存没持久化Semgrep 的中间缓存每次都被清掉同一批文件反复解析。理论上只要在 CI 的缓存目录里持久化~/.cache/semgrep第二次之后的扫描速度会有非常大的提升。第三是并发参数太激进前面提到标准 runner 只有双核我们一开始--jobs 8直接把容器内存打爆进程不停重试。调试方法也分享一个semgrep scan --time会在扫描结束时打印各阶段耗时统计。下次遇到扫描慢先看时间都消耗在 parse、match 还是 report 阶段再对症下药不要瞎试。5.2 误报太多开发者开始无视报告怎么办告警疲劳是所有静态扫描工具的死敌。Semgrep 默认会带上社区规则包本来是好意但社区规则良莠不齐里面有不少规则在特定技术栈下会产生大量误报。我们有个仓库接上社区 Java 规则后一个 PR 一下冒出来二十多个告警开发者习惯性关闭页面真正的一条注入漏洞反而不被在意。应对方案是把规则分类处理。安全相关规则保持WARNING以上并参与门禁而风格类、最佳实践类的规则通通降低到INFO级别只进报告不阻塞流水线。另外不建议把“规则数量多”当成团队 KPI。一两条精准规则压得住一个具体风险比五十条各种边缘 case 规则有价值得多。5.3 前端框架类漏洞扫不出来怎么办纯原生模式匹配对dangerouslySetInnerHTML这类 React 特有 API 能识别但对 Vue 的事件绑定、组件传参这类框架语义默认规则很难覆盖周全。我们一开始就吃了这个亏团队已经统一了扫描入口结果某天测试发现一个 Vue 模板里的 v-html 插值存在潜在 XSS 风险Semgrep 竟然没有任何告警。后面补了两步。第一步引入社区维护的前端框架规则包Semgrep 官方 registry 里就有 React、Vue、Angular 的现成规则直接用--config p/typescript这类方式引入就行。第二步针对我们的业务组件特征写自定义规则比如业务里字段htmlContent进入v-html的场景用 one line 的 pattern 就能覆盖。下面是一条简化版的自定义规则样例rules: - id: vue-v-html-no-user-input languages: [vue] severity: WARNING message: - 检测到将动态内容绑定到 v-html 若内容包含用户输入可能产生 XSS 风险。 patterns: - pattern: v-html$VAL - pattern-not-inside: $VAL ... $SAFE_LIST ... metadata: category: security technology: [vue]这一类框架特有规则只能靠团队自己在业务上下文里总结经验没有现成的后端规则可以直接平移。好在 Semgrep 规则上手快前端同学花一个下午就能写出第一条可用的。5.4 常见问题速查表症状可能原因解决动作PR 扫描耗时特别长没有fetch-depth: 0baseline 找不到检查 checkout 配置补全 git 历史并发一提高就内存溢出容器核数有限jobs 配太多压到--jobs 2或--jobs 4同一条代码反复扫描很慢缓存未持久化缓存~/.cache/semgrep大量低风险误报刷屏社区规则包过宽按严重度裁剪风格类全部降级框架 API 漏洞漏报缺少框架规则引入 p/typescript 等官方规则包存量问题阻塞新 PR未做基线冻结使用--baseline-commit origin/main我个人在实际操作中最深的一点体会是统一工具只是第一步真正难的是团队习惯的迁移。旧体系下大家各扫门前雪谁也不会多花精力去看另一端的规则换到统一工具后规则 review 变成了一种共创前端同学在写规则时自然会去想后端同学会不会用到这条语义后端同学也会反过来审视前端的安全盲区。这种交互带来的收益比所谓“少一个工具”大得多。最后再分享一个执行层面的小技巧别一次性把上百条规则迁过去哪怕你很想一口吃成胖子。我们从第一版只配置了十几条高确定性规则开始跑了两周稳定后再逐步扩容。规则数量增加的频率控制在每周四到六条每次只加“经过 negative 样例验证、确认不误报”的规则。慢是慢一点但这个节奏把团队的信任感一步步建立起来了。现在你再去问组里的同学“前端和后端用的是不是同一套扫描”他们反而会奇怪这个问题怎么会存在。
返回列表