把安全检查前移到每次提交:Google 的多智能体代码防护方法

2026-09-19 22 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:13 分钟

当 AI 开始批量生成代码,安全团队面对的不只是更多代码,还有更快的变更速度和新的攻击方式。Google 的应对思路不是增加一次规模更大的全库扫描,而是把漏洞发现、验证和修复拆成多个智能体与确定性工具,并嵌入每一次代码提交。

这套机制持续检查部署到基础设施上的数亿行代码。据披露,它每月能阻止数百个漏洞进入代码库或生产环境;在部分场景中,误报率降至 3%,专用分诊智能体的精度超过 92%,并能在一分钟内完成处理。

从“定期体检”改成“每次提交都检查”

传统的大规模安全扫描通常发生在开发后期。扫描器需要理解庞大的代码库,结果返回慢,而且容易因为缺少业务上下文而产生大量误报。等问题被发现时,相关代码可能已经合并,甚至进入生产环境。

Google 将主要检查点移到了 pre-submit 阶段,也就是代码合并之前。扫描对象不再是整个仓库,而是当前变更及其直接依赖。这带来两个直接收益:

  • 上下文更小、更聚焦:智能体只需理解当前 diff、相关调用路径和局部威胁模型。
  • 反馈更及时:安全检查与代码规范、可读性审查一样,成为开发者日常工作流的一部分。

整条链路可以概括为:

  1. 扫描智能体分析代码变更并提出候选漏洞。
  2. 分诊智能体结合 AST、调用图和预索引规则验证漏洞路径。
  3. 通过验证的问题阻止合并或要求人工确认。
  4. 夜间集成测试再次检查跨多个提交形成的风险。
  5. 修复智能体根据漏洞证据生成补丁,并提交给人类审查。

这里的关键并不是让一个大模型包办所有工作,而是让不同组件承担不同责任。

局部威胁模型为什么能显著减少误报

通用安全规则只能告诉扫描器“什么代码看起来危险”,却不能回答“攻击者在这个服务里是否真的能触达它”。例如,同一个反序列化调用,在只处理内部可信数据的离线任务中,和在接收公网请求的服务中,风险完全不同。

Google 为安全智能体提供的是与具体代码区域绑定的局部威胁模型。它不只是静态文档,还会引用实时的代码库元数据、包依赖和跨库调用图。扫描智能体因此可以获得更精确的信息:

  • 哪些入口面向不可信用户;
  • 哪些参数已经经过验证或净化;
  • 哪些内部服务跨越了信任边界;
  • 当前变更会影响哪些下游调用路径;
  • 团队为特定领域定义了哪些安全约束。

Google 还扩展了开源多智能体审查框架 Mantis,用它协调扫描、分诊和修复环节。模型固然重要,但稳定的上下文组织、角色隔离和验证机制同样决定最终效果。

AI 负责找候选,结构化工具负责证明

直接让大模型决定是否阻止代码合并,风险很高。模型可能遗漏细节,也可能给出听起来合理但无法复现的判断。因此,这套方案采用两步验证:先让轻量级扫描快速发现可疑点,再由专用分诊智能体做结构化检查。

分诊阶段会使用 AST 解析、调用图遍历和领域规则,确认危险代码是否存在、调用路径是否可达,以及攻击者控制的数据能否流入危险操作。夜间 post-submit 扫描则负责补充检查单次 diff 看不到的跨变更风险。

这种职责分离还应延伸到提示词、规则和上下文:

  • 开发智能体只负责实现需求,不应看到安全扫描器的判定捷径。
  • 扫描智能体负责提出候选问题,不直接宣布漏洞成立。
  • 分诊智能体使用独立规则和结构化证据验证候选问题。
  • 修复智能体只接收已验证的问题、触发证据和编码规范。

独立的上下文可以减少多个智能体相互强化同一个错误结论的概率。

可以这样实践:为 CI 增加一个最小分诊层

下面是一个可运行的简化示例。假设扫描智能体已经把 Python 中的命令执行调用标记为候选问题,CI 再使用 AST 验证代码是否真的设置了 shell=True

这个示例只验证危险调用结构,并不能完成真正的攻击可达性分析。生产环境还需要加入输入来源追踪、跨函数调用图、净化规则和项目自己的威胁模型。

将以下内容保存为 triage.py

#!/usr/bin/env python3
import ast
import json
import sys
from pathlib import Path

DANGEROUS_CALLS = {"run", "call", "Popen", "check_output"}


def is_subprocess_call(node: ast.Call) -> bool:
    func = node.func
    return (
        isinstance(func, ast.Attribute)
        and isinstance(func.value, ast.Name)
        and func.value.id == "subprocess"
        and func.attr in DANGEROUS_CALLS
    )


def has_shell_true(node: ast.Call) -> bool:
    for keyword in node.keywords:
        if keyword.arg == "shell":
            return isinstance(keyword.value, ast.Constant) and keyword.value.value is True
    return False


def scan_file(filename: str) -> list[dict]:
    source = Path(filename).read_text(encoding="utf-8")
    tree = ast.parse(source, filename=filename)
    findings = []

    for node in ast.walk(tree):
        if isinstance(node, ast.Call) and is_subprocess_call(node) and has_shell_true(node):
            findings.append({
                "file": filename,
                "line": node.lineno,
                "rule": "python-subprocess-shell-true",
                "severity": "high",
                "evidence": ast.get_source_segment(source, node),
            })

    return findings


def main() -> int:
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} FILE [FILE ...]", file=sys.stderr)
        return 2

    findings = []
    for filename in sys.argv[1:]:
        findings.extend(scan_file(filename))

    print(json.dumps({"findings": findings}, indent=2, ensure_ascii=False))
    return 1 if findings else 0


if __name__ == "__main__":
    raise SystemExit(main())

再创建一个用于测试的 app.py

import subprocess


def check_host(host: str) -> None:
    subprocess.run(
        f"ping -c 1 {host}",
        shell=True,
        check=True,
    )

执行检查:

python3 triage.py app.py

脚本会输出带文件名、行号和代码证据的 JSON,并以非零状态码退出,因此可以直接接入 CI。一个可改造的 GitHub Actions 步骤如下:

name: security-triage

on:
  pull_request:

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Validate changed Python files
        shell: bash
        run: |
          set -euo pipefail
          git diff --name-only --diff-filter=ACMR \
            "${{ github.event.pull_request.base.sha }}" \
            "${{ github.sha }}" \
            | grep '\.py$' > changed-python-files.txt || true

          if [[ ! -s changed-python-files.txt ]]; then
            echo "No changed Python files"
            exit 0
          fi

          mapfile -t files < changed-python-files.txt
          python3 triage.py "${files[@]}"

如果再接入一个扫描智能体,可以要求它只输出候选问题,不直接决定 CI 结果。例如使用这样的提示词约束输出:

你是候选漏洞扫描智能体。只分析本次代码变更及提供的局部威胁模型。

任务:
1. 找出可能的数据源、危险操作和两者之间的候选路径。
2. 为每个候选项输出文件、行号、漏洞类别和需要验证的结构条件。
3. 不要断言漏洞已经成立,也不要建议绕过分诊规则。
4. 只返回 JSON;无法获得证据时返回空数组。

随后由独立的 AST 或调用图工具验证这些结构条件。这样,大模型擅长的语义理解与编译器工具擅长的确定性分析可以互补,而不是互相替代。

修复自动化必须保留人工审批

发现漏洞只完成了一半工作。如果报告进入积压列表,修复时间仍可能以周计算。Google 的修复智能体会利用扫描结果和可复现证据生成符合内部编码规范的补丁,并把补丁放入原变更的审查流程。

这种方式适合自动处理边界明确的修改,例如替换危险 API、补充参数校验或调整权限配置。但自动修复不应直接获得无条件合并权限,尤其是身份认证、加密、内存安全和跨服务授权等高风险区域。补丁仍应经过测试、代码所有者审批和必要的安全复核。

落地时优先检查这五件事

团队不必一开始就复制超大规模架构,可以从一个仓库、一类高置信漏洞开始:

  • 把检查放到合并前:先缩短反馈时间,再扩大规则覆盖率。
  • 维护局部威胁模型:明确入口、信任边界、敏感数据和允许的调用方式。
  • 隔离智能体职责:开发、扫描、分诊和修复使用不同提示词、规则与权限。
  • 用确定性证据把关:让 AST、数据流、调用图和测试验证模型的结论。
  • 度量实际效果:持续记录误报率、漏报、处理延迟、修复时间和开发者采纳率。

真正改变安全效率的,不是单独引入一个更强的模型,而是把小范围上下文、结构化验证、持续扫描和人工审批连接成闭环。安全检查越靠近代码产生的时刻,修复成本通常越低,也越不容易让漏洞穿过整个交付链路。


相关推荐