如何在代码进入 Pull Request 之前发现安全漏洞
安全审查最有效的时机,是开发者对代码印象还很新鲜的时候。等到 pull request、CI 构建或渗透测试才发现暴露的凭证和不安全的写法,只会带来不必要的返工。
一种轻量级的“安全左移”方式,是通过 Git pre-commit hook 在本地运行静态应用安全测试(SAST)。
在本指南中,你将安装 DevSkim CLI,把它接入 pre-commit 来对暂存文件做 lint,再加一个专门的密钥扫描器,通过故意触发失败来验证配置,最后在 CI 中执行相同的检查。
本文内容包括:
为什么要在 Pre-Commit 阶段做安全扫描?
Pre-commit hook 会在 Git 创建提交之前自动运行,可以帮你发现硬编码的密码、token 和连接字符串、不安全的加密方式、薄弱的输入校验、有风险的文件或进程操作,以及其他各类语言特有的安全反模式。
它的目标不是取代 CI 安全扫描或人工审查,而是让开发者在尽可能早的阶段就获得快速、可操作的反馈。
一个健康的模式是这样的:
开发工作站
-> 提交前安全检查
-> 拉取请求审查
-> CI/CD 中的 SAST 与依赖扫描
-> 运行时监控与漏洞管理
什么是 DevSkim?
DevSkim 是微软开发的安全代码检查工具。它以 IDE 扩展和跨平台 CLI 的形式提供,利用可配置的规则集标记不安全的编码模式。
作为面向开发者的防护栏,它的表现非常出色,因为扫描速度快,且能解释某类模式为何存在风险。其规则覆盖范围包括危险 API 用法、弱加密、不安全反序列化、弱 TLS 或证书验证,以及潜在的命令注入风险。
在依赖 DevSkim 之前,有一处区别需要注意:DevSkim 是安全代码检查工具,而非密钥扫描器。它内置了一些通用的凭据规则,但这些规则主要针对长令牌状值的正则表达式模式。
团队通常会将 DevSkim 与专用的密钥扫描工具(如 Gitleaks、detect-secrets 或 TruffleHog)配合使用。Gitleaks 和 detect-secrets 均提供官方的 pre-commit 钩子,因此很容易集成到该工作流中。本指南使用 Gitleaks。
与任何安全扫描器一样,扫描结果需要结合上下文理解。警告并不等同于漏洞,但在代码向下传递之前值得审查。
前提条件
你需要安装 Git、Python 3 以及 .NET SDK(DevSkim 的 CLI 通过此方式分发)。
安装 pre-commit:
pip install pre-commit
作为 .NET 全局工具安装 DevSkim CLI:
dotnet tool install --global Microsoft.CST.DevSkim.CLI
如果不想安装 .NET SDK,可以在 DevSkim 发行版页面获取特定平台的二进制文件。
在新终端中确认两项安装均已生效,以便获取更新后的 PATH:
pre-commit --version
devskim --version
为了在打字时获得即时反馈,建议安装 DevSkim VS Code 扩展。Git 钩子实际调用的是 CLI。
创建 Pre-Commit 配置
DevSkim 的命令行工具在 -I 参数后只接受单个源码路径,而 pre-commit 则会将所有暂存文件追加到执行的命令中。因此,直接传入 devskim analyze 会在提交涉及多个文件时出错。
最简单的修复方法是创建一个小型包装脚本,为每个文件名单独调用一次 DevSkim。在仓库中创建 scripts/run-devskim.py:
import subprocess
import sys
exit_code = 0
for filename in sys.argv[1:]:
result = subprocess.run(["devskim", "analyze", "-I", filename])
exit_code = exit_code or result.returncode
sys.exit(exit_code)
接着在仓库根目录创建 .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: devskim
name: DevSkim security lint
entry: python scripts/run-devskim.py
language: system
types_or: [python, javascript, typescript, json, yaml]
pass_filenames: true
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
第一个钩子会对匹配指定类型的暂存文件运行 DevSkim。第二个钩子引入了 Gitleaks,用于检测 DevSkim 并不针对的机密泄露。
如果你希望保持更简单的流程,并在每次提交时扫描整个仓库,可以移除包装脚本,改用固定的单一路径:
- id: devskim
name: DevSkim security lint
entry: devskim analyze -I .
language: system
pass_filenames: false
这种方式逻辑更直观,但随着仓库规模增长,速度会变慢。当项目具备一定历史积累后,基于暂存文件的扫描是更优的默认选择。
安装 Git 钩子
在仓库根目录执行一次以下命令:
pre-commit install
此后,每次执行 git commit 都会触发配置好的检查。
也可以手动对整个代码库运行这些钩子:
pre-commit run --all-files
在将工具引入现有项目时,这一步特别有用。请做好初次发现问题的准备,旧仓库中往往存在早于当前安全标准的代码模式。
通过故意失败验证配置
在信任钩子之前,需要确认它确实能拦截问题。创建一个名为 insecure-example.js 的文件,其中包含一个弱哈希调用:
const crypto = require("crypto");
const hash = crypto.createHash("md5").update("password").digest("hex");
暂存并尝试提交:
git add insecure-example.js
git commit -m "Test security hooks"
这次提交应该会被拒绝。pre-commit 会为每个 hook 打印一行状态信息,后面跟着失败 hook 的 ID、退出码以及扫描器自身的检测结果:
DevSkim security lint....................................................Failed
- hook id: devskim
- exit code: 1
[DevSkim output listing the weak hash finding appears here]
DevSkim 的检测结果包含规则 ID、严重级别以及文件和行号,具体格式因版本和规则集而异。关键在于非零的退出码和被拦截的提交。
如果 hook 意外地通过了,可以直接运行扫描来定位问题:
devskim analyze -I insecure-example.js
如果直接扫描能发现问题但 hook 没有,那问题出在你的 .pre-commit-config.yaml,而不是 DevSkim。
示例:捕获硬编码的密钥
密钥检测正好处于 linter 和密钥扫描器的分工边界上。DevSkim 的通用凭证规则基于正则表达式,针对的是较长的、类似 token 的值,所以可读的占位字符串通常能绕过它们。用形似真实密钥的值来测试会更贴近实际:
const apiKey = "4f2a9c1e7b6d3a8f0c5e9b2d7a41c6";
暂存更改并运行 hook:
git add config.js
pre-commit run
这正是密钥扫描器的用武之地。Gitleaks 专门针对凭证调整了熵值和模式检查,所以你应该预期由它来拦截这类值,它的状态行会报告失败和非零退出码。至于 DevSkim 的结果,可以当作意外之喜,而不是你依赖的防线——这正是两个工具要搭配使用的原因。
修复方法是把密钥移出源代码:
const apiKey = process.env.PAYMENT_API_KEY;
真实密钥应存放在受认可的密钥管理服务中,比如 Azure Key Vault、GitHub Actions Secrets 或其他托管平台。
这个小改动很重要。密钥一旦提交,事后清除要困难得多,因为 Git 历史、克隆副本、CI 日志和部署系统里可能早已留下了它。
确认两个钩子都符合预期后,请删除测试文件。切勿使用真实凭证来测试扫描器。
保持钩子快速且专注
开发人员会绕过缓慢的钩子。优秀的本地安全扫描应快速完成,并聚焦于高置信度的发现。
首先,仅在本地扫描暂存或已修改的文件,将更广泛的扫描留给 CI。优先阻断高严重级别的发现,如泄露的密钥和关键的不安全 API 用法。低置信度的发现可以报告供审查,以便团队调整噪音规则并记录合理的抑制措施。
处理误报
静态分析中出现误报是正常的。错误的应对方式是完全禁用扫描器。应核实发现是否可利用,若代表真实风险则需修复。当例外情况合理时,使用范围受限的抑制,记录原因,并定期审查。
除非有明确的技术原因,否则避免广泛的排除,如跳过整个目录。广泛的排除往往变成盲区。
在 CI 中也添加安全检查
预提交钩子改善了开发者反馈,但并非强制手段。你可以通过以下方式跳过钩子:
git commit --no-verify
因此,相同的或等效的安全检查应在 CI 中运行。下面是一个在每个 pull request 上运行这两种工具的 GitHub Actions 工作流。将其保存为 .github/workflows/security.yml:
name: Security Checks
on:
pull_request:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Install DevSkim
run: dotnet tool install --global Microsoft.CST.DevSkim.CLI
- name: Run DevSkim
run: devskim analyze -I . -f sarif -O devskim.sarif
- name: Upload DevSkim results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: devskim.sarif
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
这里有两个细节值得注意。fetch-depth: 0 让 Gitleaks 获取完整的历史记录,从而扫描过往的提交,而不仅仅是最新的提交。SARIF 上传功能会将 DevSkim 的发现同步到仓库的 Security 选项卡,这样结果就能直接在 GitHub 中审阅,而不会淹没在构建日志里。如果你不想自行管理 CLI 安装,Microsoft 还提供了 DevSkim Action。
在此基础上,利用 pre-commit 对变更代码进行快速反馈,在 Pull Request 流水线中执行 SAST、密钥扫描和依赖项检查,在主分支构建时进行全量扫描和报告生成,并在发布流水线中设置针对关键发现的门禁。这种分层策略平衡了开发体验与治理要求。
实用落地建议
建议先在一两个仓库中试点,在强制实施新规则前,先建立现有发现的基线。初期仅拦截高置信度、高影响的问题,随后向开发者分享发现示例及修复方法。追踪反复出现的模式以指导安全编码培训,并重点衡量采纳率和修复时长,而非仅关注发现数量。
目标是通过默认机制协助开发者做出安全选择,而不是再引入一个被忽视的工具。
结语
安全工具的价值在于在开发者能够采取行动的时刻及时出现。Pre-commit hooks 让安全编码反馈即时化,而 CI 则提供生产系统所需的强制力和更广的覆盖范围。
对于正在构建左移安全实践的团队来说,DevSkim 是一个实用的轻量级切入点。将其与专用密钥扫描器配合使用,可以弥补 Linter 无法覆盖的领域。从小处着手,调优规则,保持扫描速度,并通过 CI 验证和安全编码指南支持工作流程。
最好的安全控制手段,往往是能防止问题在提交时就已发生的措施。