← 返回蜂巢洞察

如何在代码中的安全漏洞被提交到你的拉取请求之前发现它们?

当开发人员在代码还记忆犹新的时候就收到反馈,安全审查的效果会最好。等到提交拉取请求、进行持续集成构建或渗透测试时才发现存在暴露的凭据或不安全的编码模式,就会导致不必要的返工。 一种将安全检查提前到开发阶段的简单方法,就是通过Git的预提交钩子在本地执行静态应用安全测试。 在本指南中,你将安装DevSkim命令行工具,将其配置为在文件被添加到暂存区时进行代码检查;同时还会使用专门的工具来检测敏感信息;通过故意设置错误来验证配置是否正确;最后会在持续集成流程中强制执行这些安全检查。 我们将涵盖以下内容: 为什么要在预提交阶段进行安全扫描? 什么是DevSkim? 先决条件 创建预提交配置 安装Gi

当开发人员在代码还记忆犹新的时候就收到反馈,安全审查的效果会最好。等到提交拉取请求、进行持续集成构建或渗透测试时才发现存在暴露的凭据或不安全的编码模式,就会导致不必要的返工。

一种将安全检查提前到开发阶段的简单方法,就是通过Git的预提交钩子在本地执行静态应用安全测试。

在本指南中,你将安装DevSkim命令行工具,将其配置为在文件被添加到暂存区时进行代码检查;同时还会使用专门的工具来检测敏感信息;通过故意设置错误来验证配置是否正确;最后会在持续集成流程中强制执行这些安全检查。

我们将涵盖以下内容:

为什么要在预提交阶段进行安全扫描?

Git的预提交钩子会在创建提交之前自动执行。这些钩子能够有效检测出硬编码的密码、令牌、连接字符串、不安全的加密方式、弱输入验证机制、危险的文件处理或进程操作,以及其他与特定编程语言相关的安全问题。

我们的目标并不是取代持续集成中的安全扫描或人工审核,而是要在开发过程的最早阶段为开发者提供快速且可行的反馈。

开发人员工作站
  -> 预提交安全检查
  -> 提交拉取请求审核
  -> 持续集成/持续交付过程中的静态应用安全测试及依赖项扫描
  -> 运行时监控与漏洞管理

什么是DevSkim?

DevSkim是由微软开发的一款安全检查工具。它既以IDE扩展的形式提供,也支持跨平台使用的命令行界面,通过可配置的规则集来识别不安全的编码模式。

作为面向开发人员的防护机制,DevSkim能够快速扫描代码,并详细解释为什么某些编码方式存在风险。它的规则涵盖了危险的API使用、弱加密算法、不安全的反序列化操作、TLS证书验证问题,以及潜在的命令注入风险等等。

在开始使用DevSkim之前,有一点需要特别注意:它实际上是一种安全检查工具,而非专门用于扫描敏感信息的工具。虽然它包含了一些通用的身份验证信息检查规则,但这些规则都是针对那些长度较长、类似令牌的字符串设计的。

通常情况下,团队会将DevSkim与专门的敏感信息扫描工具结合使用,例如Gitleaks、detect-secrets或TruffleHog。Gitleaks和detect-secrets都提供了官方的pre-commit钩子,因此可以很容易地集成到开发流程中。本指南以Gitleaks为例进行讲解。

与任何安全扫描工具一样,扫描结果需要结合具体上下文来理解。出现警告并不一定意味着存在漏洞,但在代码被提交之前仔细检查这些警告仍然是很有必要的。

先决条件

您需要安装Git、Python 3以及DevSkim所依赖的.NET SDK。

首先安装pre-commit工具:

pip install pre-commit

然后将DevSkim CLI作为全局工具进行安装:

dotnet tool install --global Microsoft.CST.DevSkim.CLI

如果您不想安装.NET SDK,也可以在DevSkim发布页面上找到适用于不同平台的二进制文件。

在新终端中确认两次安装是否成功,以确保PATH环境变量已经更新:

pre-commit --version
devskim --version

如果您希望在输入代码时就能实时获得反馈,还可以安装DevSkim VS Code扩展程序。Git钩子正是使用这个扩展程序来执行检查操作的。

创建pre-commit配置文件

DevSkim的CLI在命令后加上-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安全检查工具
        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安全检查工具
        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 "测试安全钩子功能"
这次提交应该会被拒绝。pre-commit会为每个钩子打印一条状态信息,其中会包含失败钩子的ID、退出代码以及扫描工具找到的问题:
DevSkim安全检查工具....................................................失败
- 钩子ID:devskim
- 退出代码:1

[这里会显示DevSkim找到的关于弱哈希值的问题]
DevSkim报告的信息中会包含规则ID、严重性等级以及出问题的文件和行号。具体的显示格式可能会因版本或规则集的不同而有所差异,但关键在于退出代码非零这一事实,以及提交操作被成功阻止这一点。 如果钩子意外地通过了检查,那么可以直接运行DevSkim来进一步排查问题:
devskim analyze -I insecure-example.js
如果此时DevSkim能够检测到问题,而之前的钩子却未能发现,那么问题就出在你的.pre-commit-config.yaml文件中,而不是在DevSkim本身。

示例:捕获硬编码的保密信息

在识别保密信息这一点上,这个安全检查工具与专门的保密信息扫描工具之间存在差异。DevSkim所使用的通用凭证规则是基于正则表达式的,因此它们主要针对那些长度较长、类似令牌的字符串;而对于那些格式类似于真实密钥的保密信息,这些规则就无法有效检测到。
const apiKey = "4f2a9c1e7b6d3a8f0c5e9b2d7a41c6";

将更改添加到版本控制系统中,然后运行相应的钩子脚本:

git add config.js
pre-commit run

Secret Scanner正是为这种情况而设计的。Gitleaks会应用针对密码信息定制的熵检查与模式识别机制,因此它确实应该能够阻止这类敏感数据的传递;当其执行失败时,状态行会显示错误信息并返回非零退出码。对于DevSkim的检查结果,应将其视为一种辅助手段,而非主要的依赖机制——正因如此,这两种工具才会被同时使用。

解决这个问题的方法是将该敏感数据移出版本控制范围:

const apiKey = process.env.PAYMENT_API_KEY;

真正的敏感信息应该存储在经过官方认证的秘密管理工具中,比如Azure Key Vault、GitHub Actions Secrets或其他受管理的平台。

这个小小的调整其实非常重要。一旦数据已经被提交到版本控制系统中,要将其移除就会变得非常困难,因为Git的历史记录、克隆副本、持续集成日志以及部署系统可能都已经包含了这些敏感信息。

const apiKey = process.env.PAYMENT_API_KEY;

一旦确认这两个钩子都能正常工作,就可以删除那些用于测试的文件了。在测试扫描工具时,绝对不能使用真实的敏感信息。

保持钩子的高效性与针对性

开发者很可能会绕过那些运行速度较慢的钩子脚本。一个优秀的本地安全扫描工具应该能够快速完成扫描,并且重点关注那些可靠性较高的安全问题。

首先,只需在本地扫描那些已经被添加到版本控制系统中或发生了变化的文件;而范围更广泛的扫描则可以留到持续集成环节再进行。要优先处理那些严重性的安全问题,比如暴露的敏感信息或存在安全隐患的API调用;对于那些可靠性较低的检测结果,可以在团队调整相关规则并确认哪些情况确实需要被忽略之后,再将其提交给相关人员进行审核。

处理误报现象

在静态分析过程中出现误报是正常的。但是,如果因此而完全禁用扫描工具,那就太糟糕了。需要先验证这些检测结果是否真的具有攻击性,只有当它们确实代表真正的安全风险时,才需要对其进行修复。而在某些特殊情况下,如果认为忽略某个误报是合理的,也可以使用范围有限的抑制机制来处理它,同时要记录下原因,并定期重新审查这一决定。

git commit --no-verify

正因为如此,所以在持续集成环节中也应该执行相同或类似的安全检查。以下是一个GitHub Actions工作流示例,它会在每个拉取请求中同时运行这两种扫描工具。将这个工作流保存为.github/workflows/security.yml文件:

name: 安全检查

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: 安装DevSkim
        run: dotnet tool install --global Microsoft.CST.DevSkim.CLI

      - name: 运行DevSkim
        run: devskim analyze -I . -f sarif -O devskim.sarif

      - name: 上传DevSkim的结果
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: devskim.sarif

      - name: 运行Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0这一设置使 Gitleaks 能够扫描所有提交记录,而不仅仅是最新的提交内容。通过 SARIF 功能,DevSkim 找到的安全问题会被显示在仓库的“安全”选项卡中,因此这些结果可以在 GitHub 上直接查看,而不会被隐藏在构建日志中。如果你不想自己管理 CLI 的安装过程,微软还提供了 DevSkim Action 这一工具。

接下来,你可以使用“预提交钩子”来快速获取对修改代码后的反馈;通过拉取请求流程来进行安全扫描、秘密信息检测以及依赖项检查;利用主分支进行全面扫描并生成报告;而发布流程则可用于对那些关键的安全问题进行审核。这种分层式的处理方式能够在保障开发效率的同时,确保各项安全措施得到有效执行。

实际应用建议

刚开始时,可以先从一两个仓库入手,在实施新规则之前先了解现有的安全问题情况。初期阶段,只需拦截那些置信度较高、影响较大的安全问题,然后将这些问题及其解决方法分享给开发人员。通过分析这些问题的重复出现模式,可以有针对性地开展安全编码培训,并且应该关注安全措施的采用率以及问题得到解决所需的时间,而不仅仅是关注发现的安全问题数量。

我们的目标是要帮助开发人员在默认情况下就能做出安全的选择,而不是再创建一个会被人们忽视的工具。

结语

只有当安全工具能够在开发人员采取行动的瞬间发挥作用时,它们才真正具有价值。“预提交钩子”能够立即为开发人员提供关于安全编码的建议,而持续集成系统则能为生产环境提供必要的监督机制和更全面的覆盖范围。

对于那些正在努力推行“左侧安全防护”策略的团队来说,DevSkim 是一个非常实用且轻量级的工具。将它与专门的秘密信息扫描工具结合起来使用,就可以弥补代码检查工具所无法满足的需求。可以先从小规模开始尝试,逐步调整规则设置,确保扫描速度足够快,并通过持续集成验证和安全编码指导来支持整个开发流程。

最有效的安全控制措施往往就是那些能够从源头上防止问题发生的机制。

相关文章

技术实践

生产领域中的人工智能应用:与会专家强调了那些“无声的故障”、工程技术的重要性以及智能代理的管理机制。

最近,InfoQ举办了一场网络研讨会,讨论了在部署智能系统过程中所面临的运营挑战。会议重点提到了诸如语义差异与嵌入信息偏差等问题,这些问题可能会导致严重的错误。专家们认为,有必要实施自动化的代码审查机制、持续评估模型性能,并加强隐私保护措施。本次研讨会强调了在系统部署与安全管理方面应采取的战略性方法。 作者:Olimpiu Pop

阅读全文
技术实践

“从设计层面确保安全性”:在编写任何代码之前,如何将安全机制纳入系统架构之中

在大多数工程团队中,都会进行这样的对话——不过往往为时已晚。 想象一下:某个功能被部署到生产环境中之后,才会进行安全审查;或者在进行渗透测试时发现了问题;更糟糕的是,还会收到事故报告。 这些漏洞确实是存在的,修复它们也需要投入大量成本。而在这些讨论中,总会有人说:“我们需要为这个功能添加安全措施。” 但这个问题就出在这里:安全并不是事后才添加到软件中的东西,而应该从项目一开始就被纳入设计之中。一旦开始考虑为已经存在的东西添加安全功能,那就意味着你已经进入了“事故应对模式”。 “安全驱动设计”是一种完全不同的思维方式——在制定架构决策之前,就应该明确安全要求;在编写任何代码之前,就必须了解潜在的

阅读全文
技术实践

如何利用MCP让AI代理安全地访问内部API

我们的值班团队使用了一个小型内部笔记API。与之配套的网页应用可以显示当前谁正在负责处理紧急事务、哪些日期被安排为部署禁用期,以及运行脚本的位置。该团队中的一位开发人员希望能够在不打开网页应用的情况下,通过类似“Cursor”这样的工具进行搜索。 他们按照大多数人在周五下午会采取的方式设置了这个系统:将API的基本URL复制到助手设置中,将从共享操作库中获取的长期有效令牌粘贴到笔记本电脑的环境变量中,然后在助手的使用说明中添加了一条提示:“仅用于读取笔记内容,不得进行任何修改。” 在他们的机器上,这个系统确实能够正常工作。当他们询问下一次部署禁用期是什么时候时,助手会调用相应的搜索接口,从而返

阅读全文
技术实践

Cloudflare新收购的Vite+版本在加入了GitLab持续集成支持、相关钩子功能以及tsup迁移工具后,其性能已经提升到了1.0级。

VoidZero推出了Vite+ 1.0,这是其JavaScript工具链的第一个稳定版本。该版本配备了一个命令行工具,用户可以通过一个配置文件来管理Node.js项目、进行包管理以及进行前端开发。此次更新还包括与GitLab的集成,以及增加了对Docker的支持。未来还会继续进行升级,重点提升系统的稳定性及诊断功能。 作者:Daniel Curtis

阅读全文