<!-- title: 造依赖漏洞扫描Skill实战 -->

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

## 概述

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

## 前置条件

- Claude Code v2.1.196 及以上（需要 `${CLAUDE_PROJECT_DIR}` 变量与稳定的 `allowed-tools` 解析）
- 本机装好 `python3`（脚本用标准库，无需额外依赖）
- 能联网访问 `api.osv.dev`（OSV 由 Google 维护，免费、无需鉴权）
- 有一个带 `package.json` 或 `requirements.txt` 的真实项目用于验收测试

---

## 第一部分：先理解 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`）：

```yaml
---
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"
---
```

几个关键点（这是新手最容易翻车的地方）：

- **`description` 第一句话最重要**：技能列表里 `description + when_to_use` 合计会被截断到 **1,536 字符**，把核心用例放最前面。
- **`disable-model-invocation: true`** 表示只能手动 `/dep-vuln-scan` 触发，Claude 不会自动加载；想让它"用户一问就自动用"就设为 `false` 或不写。
- **`allowed-tools`** 是"该技能激活时无需二次确认就能用的工具"，写窄一点（如只给 `Read` 和受限的 `Bash(python3 *)`），既省确认弹窗又降低误操作面。
- **`paths`** 让技能只在编辑匹配文件时自动出现——非常适合"只有碰到依赖文件才弹出来"的安全技能。

### 3. 渐进披露：SKILL.md 越瘦越好

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

- `SKILL.md` 只放"做什么"的精炼指令 + 文件导航；
- 大段参考资料（评分表、修复指南）放进 `references/`，让 Claude **按需读取**；
- 真正的计算逻辑放进 `scripts/`，让 Claude **执行而不是阅读**。

目标结构如下：

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

---

## 第二部分：分步构建 dep-vuln-scan

### 步骤 1：创建 Skill 骨架

在项目根目录执行（用 Claude Code 的终端或直接 shell）：

```bash
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。脚本用**纯标准库**，零依赖：

```python
#!/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`，输出形如：

```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` 表示网络可达（远程可利用），这是排序的关键信号。

**常见问题与解决**：

- **超时 / `urlopen` 报错**：内网代理环境下给 `urllib` 加 `HTTPS_PROXY`，或在脚本里把超时调到 60 秒，并分批每 50 个依赖一组查询。
- **解析 `package.json` 报 JSONDecodeError**：清单里有注释或尾逗号。脚本假设标准 JSON，遇 BOM 先 `Path.read_text(encoding='utf-8-sig')`。
- **OSV 返回 `vulns` 为空但你知道有问题**：版本可能带了 `^`/`~`，脚本已 `lstrip` 掉；若是 `latest` 这类非语义版本，OSV 无法匹配，应提示用户锁定版本。

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

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

```markdown
# 漏洞修复优先级评分（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}`：

```markdown
---
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 lodash@4.17.21`）。
5. 若某漏洞 `fixed` 字段为空，明确标注"无官方修复，建议替换依赖或加防护"，不要建议升级。

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

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

- **`!`python3 ...`** 这一行是**动态上下文注入**（dynamic context injection）：Claude Code 会先执行该命令、把输出**原地替换**进正文，于是指令到达 Claude 时，漏洞数据已经在上下文里了。这避免了 Claude"凭想象编漏洞编号"——它直接看到真实查询结果。
- **`${CLAUDE_SKILL_DIR}`** 永远指向当前 Skill 目录，无论你从哪个工作目录调用，脚本路径都能解析对；`allowed-tools` 里也用同一变量，权限规则与正文路径一致。
- **`allowed-tools` 收得很窄**：只放行 `Read`、受限的 `python3` 和 `Grep`，不给 `Edit`/`Write`，这样即使用户说"全自动修复"，技能本身也只能读、只能查，改文件必须用户显式授权——安全技能自身的安全面要小。
- **`paths`** 让技能只在碰到依赖文件时自动浮现，不会在你改业务代码时跳出来。

### 步骤 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` 字段按任务难度分档，省钱又快。

---

## 工作流程总览

```mermaid
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 字符的部分会被默默砍掉。

---

## 延伸阅读

- **OSV.dev Schema 与批量查询接口**：`https://google.github.io/osv.dev/` ——理解返回结构、支持哪些生态（npm/PyPI/Go/Maven 等）。
- **Claude Code 官方 Skills 文档**：`https://code.claude.com/docs/en/skills` ——frontmatter 全字段、字符串替换、`paths` 激活的权威来源。
- **Skill 与 Hook 的联动**：官方 Hooks 文档，把本技能的判断能力接进 `PreToolUse` 门禁。
- **Agent Skills 开放标准**：`https://agentskills.org/` ——Skill 跨工具（Claude Code / Codex 等）通用的规范背景。

---

## 训练营适用性评估

- **DevSecOps 实战训练营**：高度匹配。可作为"Day 2：供应链安全"的实操项目，半天教学 + 半天学员改造自家项目，产出立等可用的扫描技能。
- **AI 工程化 / Prompt 工程训练营**：作为"Skill 三层结构与渐进披露"的标准教具，预计 3–4 课时（含 frontmatter 调优与负向测试环节）。
- **团队 Onboarding 训练营**：作为"把团队规范沉淀成可复用能力"的范例，2 课时即可让新成员学会交付自己的 Skill。
- **预计教学时长**：讲师演示 90 分钟，学员独立完成 + 验收 120 分钟，合计约半个工作日。

---

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方 Skill 全字段 frontmatter 参考、渐进披露、动态上下文注入与 `paths`/`allowed-tools` 权威说明。
2. [OSV.dev API](https://google.github.io/osv.dev/) — 本技能调用的开源漏洞数据库，批量查询 `/v1/querybatch` 与单条 `/v1/vulns/` 接口规格。
3. [Claude Code Skill Frontmatter: Every YAML Option Explained](https://allahabadi.dev/blogs/ai/claude-code-skills-frontmatter-complete-guide/) — 逐字段讲解 15 个 frontmatter 选项（含 `disable-model-invocation`、`hooks`、`model`、`effort`）的实战取舍。
4. [Claude Code Skills: The Complete Guide (2026)](https://duet.so/guides/claude-code-skills-complete-guide) — 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
