首页 › tutorials › 造依赖漏洞扫描Skill实战

用 OSV.dev 给 Claude Code 造一个依赖漏洞扫描 Skill

概述

本教程带你从零构建一个名为 dep-vuln-scan 的 Claude Code Skill。它会读取项目里的依赖清单(package.json / requirements.txt / go.mod),调用开源的 OSV.dev 漏洞数据库批量查询已知漏洞,并按可利用性和修复成本输出一份可执行的修复报告。适合已经会基本使用 Claude Code、想掌握 Skill 三层结构(frontmatter + 渐进披露 + 可执行脚本)与动态上下文注入的开发者,学完能独立交付一个团队级复用的安全扫描技能。

前置条件


第一部分:先理解 Skill 的运作机理

在动手之前,先把三个最容易踩坑的概念钉死,否则后面写的指令会"不触发"或"乱触发"。

1. 一个 Skill 就是"一个目录 + 一个 SKILL.md"

Skill 的最小单元是一个目录,目录里必须有 SKILL.md 作为入口。目录名就是斜杠命令名:.claude/skills/dep-vuln-scan/SKILL.md 会生成 /dep-vuln-scan。

目录可以放在三个位置,作用域逐级收窄:

位置 路径 作用域
个人级 ~/.claude/skills/dep-vuln-scan/SKILL.md 你所有项目
项目级 .claude/skills/dep-vuln-scan/SKILL.md 仅当前项目
插件级 <plugin>/skills/dep-vuln-scan/SKILL.md 启用插件处

同名时优先级是 企业级 > 个人级 > 项目级。本教程把 Skill 放在项目级,方便随仓库走、随团队共享。

2. frontmatter 决定"谁来触发、何时触发"

SKILL.md 顶部是 YAML frontmatter,夹在两个 --- 之间。关键字段(全部可选,但强烈建议写 description):

---
name: dep-vuln-scan
description: 扫描依赖清单中的已知漏洞并生成修复报告。当用户问"有没有漏洞/依赖是否安全/供应链风险"时使用。
when_to_use: 触发短语如"扫一下依赖漏洞"、"这个库安全吗"、"依赖审计"。
allowed-tools: Read Bash(python3 *) Grep
disable-model-invocation: false
argument-hint: [manifest-path]
arguments: manifest
paths: "**/package.json, **/requirements.txt, **/go.mod"
---

几个关键点(这是新手最容易翻车的地方):

3. 渐进披露:SKILL.md 越瘦越好

一旦某个 Skill 被加载,它的正文会一直留在上下文里,每一行都是持续的 token 成本。所以正确姿势是:

目标结构如下:

dep-vuln-scan/
├── SKILL.md                       # 精炼指令 + 导航
├── references/
│   └── remediation-priority.md    # 修复优先级评分参考(按需加载)
└── scripts/
    └── osv_scan.py                # 调 OSV API 的核心脚本(执行,不读)

第二部分:分步构建 dep-vuln-scan

步骤 1:创建 Skill 骨架

在项目根目录执行(用 Claude Code 的终端或直接 shell):

mkdir -p .claude/skills/dep-vuln-scan/references
mkdir -p .claude/skills/dep-vuln-scan/scripts

预期结果:生成上面那张目录树。Claude Code 有实时变更检测——你在会话期间新增/编辑 SKILL.md 立即生效,无需重启。

常见问题:如果新建的是会话启动时还不存在的顶层 skills/ 目录,需要重启一次 Claude Code 让它能 watch 到新目录。

步骤 2:写核心脚本 scripts/osv_scan.py

这是 Skill 的"计算心脏"。它做三件事:解析依赖清单、批量查询 OSV、按严重度排序输出 JSON。脚本用纯标准库,零依赖:

#!/usr/bin/env python3
"""osv_scan.py — 调用 OSV.dev 批量查询依赖漏洞,输出 JSON 报告。
用法: python3 osv_scan.py <manifest-path> [--ecosystem npm|pypi|go]
"""
import json, sys, urllib.request, re
from pathlib import Path

OSV_BATCH = "https://api.osv.dev/v1/querybatch"
OSV_VULN  = "https://api.osv.dev/v1/vulns/"

def parse_npm(path):
    pkg = json.loads(Path(path).read_text())
    deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
    return [{"package": {"name": n, "ecosystem": "npm"}, "version": v.lstrip("^~>=< ")} for n, v in deps.items()]

def parse_pypi(path):
    out = []
    for line in Path(path).read_text().splitlines():
        m = re.match(r"\s*([A-Za-z0-9_.\-]+)\s*[=<>~!]=*\s*([0-9A-Za-z.\-]+)", line)
        if m:
            out.append({"package": {"name": m.group(1), "ecosystem": "PyPI"}, "version": m.group(2)})
    return out

def query_batch(queries):
    req = urllib.request.Request(OSV_BATCH, data=json.dumps({"queries": queries}).encode(),
                                 headers={"Content-Type": "application/json"})
    return json.loads(urllib.request.urlopen(req, timeout=30).read()).get("results", [])

def severity(vuln_id):
    try:
        d = json.loads(urllib.request.urlopen(OSV_VULN + vuln_id, timeout=30).read())
        sev = d.get("severity", [])
        for s in sev:
            if s.get("type", "").startswith("CVSS"):
                # 取 CVSS 向量里的 baseScore
                vec = s.get("score", "")
                m = re.search(r"CVSS:3.[01]/AV:([LNAP])", vec)
                attack = m.group(1) if m else "?"
                return {"score": _cvss_base(vec), "vector": vec[:60], "attack_vector": attack,
                        "fixed": [r.get("fixed") for f in d.get("affected", []) for r in f.get("ranges", [{}])[0].get("events", []) if "fixed" in r]}
    except Exception:
        pass
    return {"score": None}

def _cvss_base(vec):
    # 简化估算:网络可达 + 高影响 → 偏高,仅用于排序,非精确分
    return 7.0 if "AV:N" in vec else 5.0

def main():
    path, eco = sys.argv[1], sys.argv[3] if len(sys.argv) > 3 and sys.argv[2] == "--ecosystem" else None
    suffix = Path(path).name
    if suffix == "package.json" or eco == "npm":
        queries = parse_npm(path)
    elif suffix == "requirements.txt" or eco == "pypi":
        queries = parse_pypi(path)
    else:
        print(json.dumps({"error": f"不支持的清单: {suffix}"})); return
    results = query_batch(queries)
    findings = []
    for q, r in zip(queries, results):
        for vid in r.get("vulns", []):
            info = severity(vid)
            findings.append({"package": q["package"]["name"], "version": q["version"],
                             "vuln": vid, **info})
    findings.sort(key=lambda x: x.get("score") or 0, reverse=True)
    print(json.dumps({"manifest": path, "total": len(findings), "findings": findings}, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

预期结果:对一个含旧版 lodash 的 package.json 跑 python3 scripts/osv_scan.py package.json,输出形如:

{ "manifest": "package.json", "total": 2,
  "findings": [
    {"package": "lodash", "version": "4.17.4", "vuln": "GHSA-35jh-r3h4-6jhm", "score": 7.0, "attack_vector": "N", "fixed": ["4.17.21"]},
    ...
  ]}

attack_vector: N 表示网络可达(远程可利用),这是排序的关键信号。

常见问题与解决:

步骤 3:写渐进披露的参考资料 references/remediation-priority.md

这份文件平时不进上下文,只在 Claude 需要给修复建议时才读,所以可以详细:

# 漏洞修复优先级评分(0–100)

| 维度 | 权重 | 取值 |
|------|------|------|
| 攻击向量 AV | 40 | 网络(N)=40 本地(L)=20 物理(P)=5 |
| 是否有官方修复 fixed 字段 | 30 | 有明确修复版本=30 无=0 |
| 依赖是否在生产链路 | 20 | dependencies=20 devDependencies=8 |
| CVSS 基础分 | 10 | score/10 |

优先级:≥70 必须立刻修(P0);40–69 本周修(P1);<40 可排期(P2)。
无 fixed 字段的漏洞:考虑替换该依赖或加运行时防护,不要简单升级。

步骤 4:写精炼的 SKILL.md

这是把上面所有零件串起来的"瘦"指令。注意三处技巧:动态上下文注入、$ARGUMENTS、${CLAUDE_SKILL_DIR}:

---
name: dep-vuln-scan
description: 扫描项目依赖清单(package.json/requirements.txt/go.mod)的已知漏洞并生成可执行的修复报告。当用户问"依赖有没有漏洞/供应链是否安全/这个库安全吗"时使用。
when_to_use: 触发短语——"扫依赖漏洞"、"依赖审计"、"supply chain risk"、"升级前检查"。
allowed-tools: Read Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/osv_scan.py *) Bash(python3 ${CLAUDE_PROJECT_DIR}/.claude/skills/dep-vuln-scan/scripts/osv_scan.py *) Grep
argument-hint: [manifest-path]
arguments: manifest
paths: "**/package.json, **/requirements.txt, **/go.mod, **/pom.xml"
---

## 扫描目标
当前要扫描的依赖清单:$ARGUMENTS
若用户未指定路径,先用 Grep 找到仓库根最近的 `package.json` 或 `requirements.txt`,确认后再扫描。

## 执行扫描
运行核心脚本(输出结构化 JSON):
!`python3 ${CLAUDE_SKILL_DIR}/scripts/osv_scan.py $ARGUMENTS`

## 报告规则
1. 上面的脚本输出已是按 CVSS 排序的 JSON,直接基于它生成报告,不要凭记忆猜测漏洞。
2. 给每个漏洞打"修复优先级分(0–100)",评分细则见 `${CLAUDE_SKILL_DIR}/references/remediation-priority.md`——仅在需要解释评分或写修复建议时再读取该文件。
3. 输出三张表:
   - **P0 立即修**:包名 / 当前版本 / 漏洞编号 / 攻击向量 / 建议修复版本
   - **P1 本周修**:同上
   - **可排期(P2)**:仅列包名与漏洞数
4. 每个 P0 漏洞附一条"可直接复制的修复命令"(如 `npm i [email protected]`)。
5. 若某漏洞 `fixed` 字段为空,明确标注"无官方修复,建议替换依赖或加防护",不要建议升级。

## 安全边界
- 只读依赖清单,禁止修改 `package.json` / `lock` 文件,除非用户明确说"帮我修"。
- 脚本只联网查询 OSV,不上传任何业务代码。

几个值得展开的设计决策:

步骤 5:验收测试

在项目根启动 claude,做三种验证:

A. 手动触发:

/dep-vuln-scan package.json

预期:脚本执行 → 看到 JSON 注入 → 输出 P0/P1/P2 三张表 + 每条修复命令。

B. 自然语言自动触发(验证 description 调得对不对):

我们这个项目上线前,依赖有没有已知漏洞?

预期:Claude 自动选中 dep-vuln-scan 并执行。

C. 负向测试(不该触发):

帮我重构一下 utils.js 里的日期处理

预期:不触发本技能(因为没有命中 when_to_use 短语,也不在编辑依赖文件)。

常见问题排查:

现象 原因 解决
自然语言不触发 description 没把核心用例放第一句,或超 1,536 字符被截 把"扫描依赖漏洞生成修复报告"放最前;删冗余
触发太频繁 paths 写太宽或 when_to_use 短语太泛 收窄 paths,删掉"安全吗"这类高频词
脚本路径找不到 没用 ${CLAUDE_SKILL_DIR},写了相对路径 全部改用 ${CLAUDE_SKILL_DIR}/scripts/...
每次都要确认执行 python allowed-tools 没覆盖到实际路径 把精确命令写进 allowed-tools 的 Bash(...) 规则

第三部分:实战案例——把扫描接入提交门禁

一个完整的、可落地的用法:在每次 git commit 前自动扫一遍变更过的依赖。结合 Claude Code 的 hooks 与本技能:

  1. 在 .claude/settings.json 注册一个 PreToolUse hook,匹配 Bash(git commit:*);
  2. hook 脚本里调用 python3 .claude/skills/dep-vuln-scan/scripts/osv_scan.py 扫描当前锁文件;
  3. 若发现 P0(score≥7 且有网络攻击向量),hook 返回非零退出码,阻止提交并在终端打印漏洞清单与修复命令。

这样"写技能"和"装护栏"形成闭环:技能负责判断,hook 负责拦截。注意 hook 是确定性执行的 shell,不会消耗模型 token;只有需要解读或建议修复时才进入 Skill 的模型推理环节,成本可控。

一个更轻量的进阶玩法:把 dep-vuln-scan 的 model 字段设为更快的模型(如 claude-haiku-4-5-20251001)做初筛,命中 P0 再用主力模型(claude-sonnet-4-6)出修复方案——利用 frontmatter 的 model 字段按任务难度分档,省钱又快。


工作流程总览

flowchart TD
    A[用户: 依赖安全吗?] --> B{匹配 description<br/>when_to_use?}
    B -- 是 --> C[加载 dep-vuln-scan]
    B -- 否 --> Z[不触发, 走普通对话]
    C --> D[SKILL.md 入上下文<br/>references/scripts 暂不加载]
    D --> E["动态注入: !`python3 osv_scan.py`"]
    E --> F[执行脚本 调 OSV.dev API]
    F --> G[JSON 漏洞结果原地注入正文]
    G --> H{需要解释评分/写建议?}
    H -- 是 --> I[按需读取 remediation-priority.md]
    H -- 否 --> J[直接出报告]
    I --> J
    J --> K[输出 P0/P1/P2 三表 + 修复命令]
    K --> L{用户说'帮我修'?}
    L -- 是 --> M[显式授权后改 lock 文件]
    L -- 否 --> N[结束, 仅给建议]

关键设计复盘

做完这个 Skill,记住四条可迁移的原则:

  1. 能执行就别让模型读:漏洞数据、复杂计算交给脚本,Claude 只负责解读和决策——既准又省 token。
  2. 瘦入口 + 按需加载:SKILL.md 是常驻成本,参考资料全部外置,用 references/ 拖到需要时才进上下文。
  3. 窄权限:安全类技能自身的 allowed-tools 要最小化,默认只读,写操作必须用户显式开口。
  4. 描述即触发器:description 第一句话决定生死,把"做什么 + 何时用"压在最前面,超 1,536 字符的部分会被默默砍掉。

延伸阅读


训练营适用性评估


参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skill 全字段 frontmatter 参考、渐进披露、动态上下文注入与 paths/allowed-tools 权威说明。
  2. OSV.dev API — 本技能调用的开源漏洞数据库,批量查询 /v1/querybatch 与单条 /v1/vulns/ 接口规格。
  3. Claude Code Skill Frontmatter: Every YAML Option Explained — 逐字段讲解 15 个 frontmatter 选项(含 disable-model-invocation、hooks、model、effort)的实战取舍。
  4. Claude Code Skills: The Complete Guide (2026) — Skill 目录结构、按需加载与团队共享的工程实践。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入