本教程带你从零构建一个名为 dep-vuln-scan 的 Claude Code Skill。它会读取项目里的依赖清单(package.json / requirements.txt / go.mod),调用开源的 OSV.dev 漏洞数据库批量查询已知漏洞,并按可利用性和修复成本输出一份可执行的修复报告。适合已经会基本使用 Claude Code、想掌握 Skill 三层结构(frontmatter + 渐进披露 + 可执行脚本)与动态上下文注入的开发者,学完能独立交付一个团队级复用的安全扫描技能。
${CLAUDE_PROJECT_DIR} 变量与稳定的 allowed-tools 解析)python3(脚本用标准库,无需额外依赖)api.osv.dev(OSV 由 Google 维护,免费、无需鉴权)package.json 或 requirements.txt 的真实项目用于验收测试在动手之前,先把三个最容易踩坑的概念钉死,否则后面写的指令会"不触发"或"乱触发"。
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 放在项目级,方便随仓库走、随团队共享。
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"
---
几个关键点(这是新手最容易翻车的地方):
description 第一句话最重要:技能列表里 description + when_to_use 合计会被截断到 1,536 字符,把核心用例放最前面。disable-model-invocation: true 表示只能手动 /dep-vuln-scan 触发,Claude 不会自动加载;想让它"用户一问就自动用"就设为 false 或不写。allowed-tools 是"该技能激活时无需二次确认就能用的工具",写窄一点(如只给 Read 和受限的 Bash(python3 *)),既省确认弹窗又降低误操作面。paths 让技能只在编辑匹配文件时自动出现——非常适合"只有碰到依赖文件才弹出来"的安全技能。一旦某个 Skill 被加载,它的正文会一直留在上下文里,每一行都是持续的 token 成本。所以正确姿势是:
SKILL.md 只放"做什么"的精炼指令 + 文件导航;references/,让 Claude 按需读取;scripts/,让 Claude 执行而不是阅读。目标结构如下:
dep-vuln-scan/
├── SKILL.md # 精炼指令 + 导航
├── references/
│ └── remediation-priority.md # 修复优先级评分参考(按需加载)
└── scripts/
└── osv_scan.py # 调 OSV API 的核心脚本(执行,不读)
在项目根目录执行(用 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 到新目录。
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 表示网络可达(远程可利用),这是排序的关键信号。
常见问题与解决:
urlopen 报错:内网代理环境下给 urllib 加 HTTPS_PROXY,或在脚本里把超时调到 60 秒,并分批每 50 个依赖一组查询。package.json 报 JSONDecodeError:清单里有注释或尾逗号。脚本假设标准 JSON,遇 BOM 先 Path.read_text(encoding='utf-8-sig')。vulns 为空但你知道有问题:版本可能带了 ^/~,脚本已 lstrip 掉;若是 latest 这类非语义版本,OSV 无法匹配,应提示用户锁定版本。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 字段的漏洞:考虑替换该依赖或加运行时防护,不要简单升级。
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,不上传任何业务代码。
几个值得展开的设计决策:
!python3 ...` 这一行是动态上下文注入(dynamic context injection):Claude Code 会先执行该命令、把输出原地替换进正文,于是指令到达 Claude 时,漏洞数据已经在上下文里了。这避免了 Claude"凭想象编漏洞编号"——它直接看到真实查询结果。${CLAUDE_SKILL_DIR} 永远指向当前 Skill 目录,无论你从哪个工作目录调用,脚本路径都能解析对;allowed-tools 里也用同一变量,权限规则与正文路径一致。allowed-tools 收得很窄:只放行 Read、受限的 python3 和 Grep,不给 Edit/Write,这样即使用户说"全自动修复",技能本身也只能读、只能查,改文件必须用户显式授权——安全技能自身的安全面要小。paths 让技能只在碰到依赖文件时自动浮现,不会在你改业务代码时跳出来。在项目根启动 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 与本技能:
.claude/settings.json 注册一个 PreToolUse hook,匹配 Bash(git commit:*);python3 .claude/skills/dep-vuln-scan/scripts/osv_scan.py 扫描当前锁文件;这样"写技能"和"装护栏"形成闭环:技能负责判断,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,记住四条可迁移的原则:
references/ 拖到需要时才进上下文。allowed-tools 要最小化,默认只读,写操作必须用户显式开口。description 第一句话决定生死,把"做什么 + 何时用"压在最前面,超 1,536 字符的部分会被默默砍掉。https://google.github.io/osv.dev/ ——理解返回结构、支持哪些生态(npm/PyPI/Go/Maven 等)。https://code.claude.com/docs/en/skills ——frontmatter 全字段、字符串替换、paths 激活的权威来源。PreToolUse 门禁。https://agentskills.org/ ——Skill 跨工具(Claude Code / Codex 等)通用的规范背景。paths/allowed-tools 权威说明。/v1/querybatch 与单条 /v1/vulns/ 接口规格。disable-model-invocation、hooks、model、effort)的实战取舍。[!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 等场景全覆盖。
