找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3924

积分

0

好友

538

主题
发表于 昨天 06:52 | 查看: 10| 回复: 0

AI 正在深度融入开发工作流,用它来生成代码已经司空见惯。但一个核心问题随之而来:AI 引入的代码和依赖,是否真的安全可靠?传统的软件成分分析(SCA)扫描往往在代码提交甚至构建之后才进行,当 AI 编程助手能直接操作依赖文件时,这种事后审计是否已经力不从心?

最近看到腾讯安全的一篇关于《幽灵依赖》的文章,其中提出了一个颇具启发性的思路——将安全检测的节点左移至 Coding Agent 做出决策的瞬间,在 AI 真正执行依赖操作之前就完成拦截。这相当于为 AI 编程配备了一位实时在线的安全审计员。

本文就基于这个思路,结合 Claude Code 强大的 Hook 机制与开源 SCA 工具 OpenSCA,动手实现一个能在 AI 写代码时实时拦截高危依赖的方案。

Claude Code Hook 机制解析

Claude Code 提供了一套完善的 Hook(钩子)机制,允许开发者在特定事件发生时执行自定义脚本。为了实现“决策前拦截”,PreToolUse 事件是我们的最佳选择。它会在 AI 输出操作方案但尚未实际执行(如写文件、执行命令)之前被触发。

Claude Code Hook机制事件表格

从上图的事件列表可以看到,PreToolUse 在工具调用执行前触发,并且可以被阻止。这正是我们需要的时机。通过让 Hook 脚本返回特定的退出码(例如 2),我们可以终止 Claude Code 的当前操作。思路很清晰:在 PreToolUse 钩子中扫描即将被写入或修改的依赖内容,一旦发现高危漏洞,立即终止任务。

Hook 配置与具体实现

第一步:配置全局 Hook

首先,需要在 Claude Code 的全局配置文件 ~/.claude/settings.json 中添加钩子配置。我们需要针对三种常见的依赖操作方式进行拦截,以确保覆盖全面性:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/opensca_guard/opensca_guard.py"
          }
        ]
      },
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/opensca_guard/opensca_guard.py"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/opensca_guard/opensca_guard.py"
          }
        ]
      }
    ]
  }
}

这段配置从三个方面进行了覆盖:

  • Write:拦截直接创建或覆盖依赖文件(如 requirements.txt, package.json)。
  • Edit:拦截修改已有依赖文件的内容。
  • Bash:拦截通过命令行安装依赖的操作(如 pip install, npm install)。

第二步:编写 Hook 扫描脚本

接下来是核心的 Python 扫描脚本。脚本的核心逻辑是:从标准输入接收 Claude Code 传递的上下文信息,解析出即将被操作的依赖内容,调用 OpenSCA 进行安全扫描,并根据漏洞等级决定是否阻断。

完整的脚本代码可以通过 https://github.com/Jumbo-WJB/ai-code-guard 获取。下面展示其关键结构和部分核心函数:

#!/usr/bin/env python3
import sys
import json
import os
import re
import subprocess
import tempfile
import shutil
from datetime import datetime
from typing import Optional, Tuple

# ── 配置区 ───────────────────────────────────────────────────────────────────
OPENSCA_CLI   = os.path.expanduser("~/Downloads/opensca-cli-v3.0.9-darwin-arm64/opensca-cli")
OPENSCA_TOKEN = "your-token-here"  # 替换为你的 OpenSCA Token
OPENSCA_PROJ  = "claude-hook-scan"
BLOCK_LEVEL   = 2  # 1=Critical, 2=High, 3=Medium, 4=Low
LOG_FILE      = os.path.expanduser("~/.claude/hooks/opensca_guard/opensca_guard.log")
DEPENDENCY_FILES = {
    # Python - Pip
    "requirements.txt",
    "requirements.in",
    "requirements-dev.txt",
    "requirements-prod.txt",
    "setup.py",
    "Pipfile",
    "Pipfile.lock",
    # JavaScript - Npm
    "package.json",
    "package-lock.json",
    "yarn.lock",
    # Java - Maven
    "pom.xml",
    # ... 其他语言依赖文件列表
}

# ── 核心:处理三种工具的不同字段结构 ─────────────────────────────────────────
def get_scan_target(tool_name: str, tool_input: dict) -> Optional[Tuple[str, str]]:
    """
    返回 (filename, content_to_scan) 或 None
    三种工具的字段结构:
    - Write : {"file_path": "...", "content": "..."}
    - Edit  : {"file_path": "...", “old_string”: "...", “new_string”: "..."}
    - Bash  : {"command": “pip install django==4.2.7”}
    """
    # ── Write ────────────────────────────────────────────────
    if tool_name == "Write":
        path    = tool_input.get("file_path", "")
        content = tool_input.get(“content”, "")
        if not is_dependency_file(path):
            return None
        return os.path.basename(path), content
    # ── Edit ─────────────────────────────────────────────────
    elif tool_name == “Edit”:
        path       = tool_input.get("file_path", "")
        old_string = tool_input.get(“old_string”, "")
        new_string = tool_input.get(“new_string”, "")
        if not is_dependency_file(path):
            return None
        # 模拟替换后的完整文件内容用于扫描
        full_content = get_post_edit_content(path, old_string, new_string)
        return os.path.basename(path), full_content
    # ── Bash ─────────────────────────────────────────────────
    elif tool_name == “Bash”:
        command = tool_input.get(“command”, "")
        # 解析 pip/npm/go 等安装命令,提取包名并模拟成临时文件内容
        for pattern, extractor, filename, formatter in BASH_EXTRACTORS:
            if re.search(pattern, command, re.IGNORECASE):
                pkgs = extractor(command)
                if not pkgs:
                    return None
                content = formatter(pkgs)
                return filename, content
        return None
    return None

# ── OpenSCA 扫描 ──────────────────────────────────────────────────────────────
def run_opensca_scan(filename: str, content: str) -> Optional[dict]:
    tmpdir = tempfile.mkdtemp(prefix=“opensca_hook_”)
    try:
        target_path = os.path.join(tmpdir, filename)
        with open(target_path, “w”, encoding=“utf-8”) as f:
            f.write(content)
        result_path = os.path.join(tmpdir, “result.json”)
        cmd = [
            OPENSCA_CLI,
            “-token”, OPENSCA_TOKEN,
            “-proj”,  OPENSCA_PROJ,
            “-path”,  tmpdir,
            “-out”,   result_path,
        ]
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=60,
            cwd=tmpdir,
        )
        if not os.path.exists(result_path):
            return None
        with open(result_path, “r”, encoding=“utf-8”) as f:
            result = json.load(f)
        return result
    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)

# ── 主流程 ───────────────────────────────────────────────────────────────────
def main():
    raw = sys.stdin.read()
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as e:
        sys.exit(0)
    tool_name  = data.get(“tool_name”, "")
    tool_input = data.get(“tool_input”, {})
    scan_target = get_scan_target(tool_name, tool_input)
    if scan_target is None:
        sys.exit(0)
    filename, content = scan_target
    scan_result = run_opensca_scan(filename, content)
    if scan_result is None:
        sys.exit(0)
    all_vulns      = collect_vulnerabilities(scan_result)
    vulns_to_block = [v for v in all_vulns if v[“level_id”] <= BLOCK_LEVEL]
    if not vulns_to_block:
        sys.exit(0)
    # 发现高危漏洞,构建阻断信息并退出码为2
    msg = build_block_message(vulns_to_block, all_vulns)
    print(msg, file=sys.stderr)
    sys.exit(2)

if __name__ == “__main__”:
    main()

你需要将脚本中的 OPENSCA_TOKEN 替换为你自己的 OpenSCA 令牌,并根据系统架构调整 OPENSCA_CLI 的路径。

方案效果展示

配置完成后,当 Claude Code 试图引入存在已知高危漏洞的依赖时,PreToolUse 钩子会立即触发安全扫描并进行阻断。

场景一:尝试写入包含高危漏洞的 Python 包
当要求 AI 创建一个 requirements.txt 并写入包含漏洞的老版本 requestsurllib3 时,操作会被实时拦截。

requirements.txt文件安全扫描拦截结果

场景二:尝试修改 Maven 配置引入低版本漏洞组件
在修改 pom.xml 文件,试图引入存在著名反序列化漏洞的低版本 Apache Commons Collections 组件时,同样会被安全 Hook 拦截。

Apache Commons Collections组件安全扫描拦截结果

拦截信息会清晰指出漏洞组件、CVE编号、危险等级,并给出升级建议,引导开发者(或 AI)选择安全的版本。

总结与思考

AI 编程工具的普及带来了效率的飞跃,但也开辟了新的安全盲区。当 AI 开始自主处理第三方依赖引入时,传统的“事后扫描”模式显得被动且滞后。

本文实现的方案,核心是将供应链安全防护的关口极致左移,其优势体现在三个“不等”:

  1. 不等代码提交:在 AI 修改依赖文件的瞬间触发扫描,问题在源头就被发现。
  2. 不等人工审查:由自动化 Hook 完成漏洞识别与阻断决策,响应零延迟。
  3. 不放过任何入口:无论是直接写文件、编辑文件还是执行安装命令,均在统一的安全扫描拦截范围之内。

这本质上是将安全审计的角色深度嵌入到 Coding Agent 的决策链路中,为 AI 编程增加了一道至关重要的“安全刹车”。在享受 AIGC 带来的开发便利的同时,我们必须用更智能、更前置的手段来守护软件供应链的安全基线。

欢迎在云栈社区分享你在 AI 编程安全方面的实践与见解。




上一篇:台积电2nm规划加速:月产能目标14万片,2026年新厂启动以应对AI芯片海量需求
下一篇:MongoDB备份恢复指南:详解mongodump与mongoexport工具使用与场景对比
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 09:49 , Processed in 0.523250 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表