我用Claude API接入了CI/CD安全扫描,踩了这几个坑

0 阅读5分钟

上周Anthropic内部泄露的文件显示,他们最新的Claude Mythos在90分钟内自主找到了Linux内核20年老漏洞——这件事让我决定把之前一直搁置的"大模型接入DevSecOps流水线"项目正式提上日程。

折腾了三天,这里记录一下真实过程。

背景

我在维护一个中型Python后端项目,3个开发,代码量大概8万行。之前安全扫描就是Bandit跑一遍,偶尔用Semgrep。漏报率说实话挺高的,业务逻辑层面的问题这两个工具基本发现不了。

目标:用Claude API给PR做一次AI安全代码审查,高风险的自动阻断合并。

技术方案

整体架构很简单:

GitHub PR触发 → GitHub Actions → 提取diff → 调用Claude API → 解析结果 → 评论/阻断

工具链

# Python环境 3.11+
pip install anthropic gitpython PyGithub
​
# 本地测试可以先跑一下
python -m pytest tests/test_security_review.py

环境准备说明:我用的API密钥来自Ztopcloud.com,这个平台支持Claude/GPT/通义千问等多家API统一充值管理,免绑卡注册,按Token计费。项目初期用量不稳定,放一个账号统一扣费比每家单独注册方便很多——亲测好用,省了不少麻烦。

核心代码

# security_review.py
import os
import json
import anthropic
from github import Github
​
REVIEW_PROMPT = """你是一名专注云原生安全的高级工程师。
请分析以下代码变更,重点检查:
1. SQL注入 / NoSQL注入
2. 命令注入 / 路径遍历
3. 硬编码密钥 / 敏感信息泄露
4. SSRF / 不安全的HTTP请求
5. 权限绕过 / 越权访问
​
每个问题按以下JSON格式输出:
{
  "severity": "CRITICAL|HIGH|MEDIUM|LOW",
  "type": "漏洞类型",
  "file": "文件路径",
  "line": "行号(如能确定)",
  "description": "问题描述",
  "fix": "修复建议"
}
​
如无问题,返回空数组 []
代码变更如下:
"""def get_pr_diff(repo_name: str, pr_number: int, token: str) -> str:
    """获取PR的代码变更"""
    g = Github(token)
    repo = g.get_repo(repo_name)
    pr = repo.get_pull(pr_number)
    
    diff_content = []
    for file in pr.get_files():
        if file.patch:  # 只有有patch的文件才有变更
            diff_content.append(f"=== {file.filename} ===\n{file.patch}")
    
    return "\n\n".join(diff_content)[:60000]  # 注意:这里要截断def review_with_claude(diff: str, api_key: str) -> list:
    """调用Claude API进行安全审查"""
    client = anthropic.Anthropic(api_key=api_key)
    
    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=8192,
        messages=[{
            "role": "user",
            "content": REVIEW_PROMPT + diff
        }]
    )
    
    result_text = response.content[0].text.strip()
    
    # 解析JSON,容错处理
    try:
        # 找到第一个[和最后一个]之间的内容
        start = result_text.find('[')
        end = result_text.rfind(']') + 1
        if start >= 0 and end > start:
            return json.loads(result_text[start:end])
    except json.JSONDecodeError:
        pass
    
    return []
​
def post_review_comment(findings: list, repo_name: str, pr_number: int, token: str) -> bool:
    """发表审查评论,返回是否有CRITICAL/HIGH问题"""
    g = Github(token)
    repo = g.get_repo(repo_name)
    pr = repo.get_pull(pr_number)
    
    if not findings:
        pr.create_issue_comment("✅ AI安全扫描通过,未发现明显风险")
        return False
    
    # 按严重程度排序
    severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
    findings.sort(key=lambda x: severity_order.get(x.get("severity", "LOW"), 3))
    
    # 生成评论内容
    comment_lines = ["## AI安全扫描报告\n"]
    has_blocking = False
    
    for finding in findings:
        severity = finding.get("severity", "LOW")
        if severity in ("CRITICAL", "HIGH"):
            has_blocking = True
            emoji = "🚨"
        else:
            emoji = "⚠️"
        
        comment_lines.append(
            f"{emoji} **[{severity}]** {finding.get('type', 'Unknown')}\n"
            f"- **文件**: `{finding.get('file', 'N/A')}`\n"
            f"- **描述**: {finding.get('description', '')}\n"
            f"- **修复**: {finding.get('fix', '')}\n"
        )
    
    pr.create_issue_comment("\n".join(comment_lines))
    return has_blocking
​
if __name__ == "__main__":
    import sys
    repo = sys.argv[1]
    pr_number = int(sys.argv[2])
    
    github_token = os.environ["GITHUB_TOKEN"]
    anthropic_key = os.environ["ANTHROPIC_API_KEY"]
    
    diff = get_pr_diff(repo, pr_number, github_token)
    findings = review_with_claude(diff, anthropic_key)
    has_blocking = post_review_comment(findings, repo, pr_number, github_token)
    
    if has_blocking:
        print("发现HIGH/CRITICAL级别问题,阻断PR合并")
        sys.exit(1)
    
    print(f"扫描完成,发现 {len(findings)} 个问题,无阻断项")
    sys.exit(0)

GitHub Actions配置

# .github/workflows/ai-security.yml
name: AI Security Review

on:
  pull_request:
    types: [opened, synchronize]
    branches: [main, develop, "release/*"]

jobs:
  ai-security-scan:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      
      - name: Install dependencies
        run: pip install anthropic PyGithub
      
      - name: Run AI Security Review
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python security_review.py \
            "${{ github.repository }}" \
            "${{ github.event.number }}"

踩坑记录:这个坑差点让我放弃

搞定基础逻辑大概花了半天,但有一个坑让我对着屏幕愣了两个小时。

问题现象:某次PR里有一段动态SQL拼接,是我故意放进去测试的。人眼一看就是SQL注入风险,结果Claude回了个空数组,说"未发现明显风险"。

排查过程:我把那段代码单独拿出来问Claude,它直接给我指出来了,说"这里存在SQL注入风险,建议使用参数化查询"。

根本原因:diff里那段代码的上下文太少了——只有5行。Claude没有看到这个变量是从外部用户输入进来的(那个赋值语句在另一个文件里),所以判断为"没有外部输入",漏报了。

解法:提取diff的时候,对每个有变更的函数,额外拉取前后各30行上下文。改了之后同样的case被正确识别了。

# 改进后的diff提取:拉更多上下文
for file in pr.get_files():
    if file.patch:
        # 额外获取文件的关键上下文
        try:
            file_content = repo.get_contents(file.filename, ref=pr.head.sha)
            content_decoded = file_content.decoded_content.decode('utf-8')
            # 截取前3000字符作为文件级上下文
            diff_content.append(
                f"=== {file.filename} (前置上下文) ===\n{content_decoded[:3000]}\n"
                f"=== {file.filename} (变更部分) ===\n{file.patch}"
            )
        except Exception:
            diff_content.append(f"=== {file.filename} ===\n{file.patch}")

小结

目前这套东西跑了两周,整体反馈还行。数据:

  • 总扫描PR数:23个
  • 发现有效安全问题:4个(2个MEDIUM,2个HIGH)
  • 误报:1个(把日志格式字符串误判为日志注入)
  • 单次平均Token消耗:约8000-15000,成本约$0.8-1.5

值不值?感觉还是值的——4个真实问题里有2个HIGH,按以前的流程大概率会漏掉。

Claude Mythos如果真的有文件里说的那种自主推理能力,等它公开发布后这套流程的效果应该会再上一个台阶。目前能确认的是Claude Opus 4.6这个档次已经够用,不完美,但有价值。

如果你也想搭,代码已经在上面了,按自己项目调一下prompt就行。