第 8 章 · 04 自定义 skill 本节摘要:上一节我们理解了 skill 的机制、分类与标准结构。本节动手造——通过两个真实存在于 Strix 仓库的 custom skill,演示「从用到造」的完整闭环。第一个是 :用 trivy 扫 lockfile,把「已知 CVE 的依赖」作为一类一等公民发现上报,它是 Strix「无 PoC 不报告」铁律的唯一显式例外。第二个是 :把 semgrep、ast-grep、gitleaks、trivy 串成一条源码感知的静态分诊流水线,让静态信号去指导动态测试。两个 skill 合起来,完整展示了「为什么写 skill、skill 里放什么、怎么避免反模式」,也是你贡献自己 skill 前最好的范本。
本节摘要:上一节我们理解了 skill 的机制、分类与标准结构。本节动手造——通过两个真实存在于 Strix 仓库的 custom skill,演示「从用到造」的完整闭环。第一个是
dependency_cve_scanning:用 trivy 扫 lockfile,把「已知 CVE 的依赖」作为一类一等公民发现上报,它是 Strix「无 PoC 不报告」铁律的唯一显式例外。第二个是source_aware_sast:把 semgrep、ast-grep、gitleaks、trivy 串成一条源码感知的静态分诊流水线,让静态信号去指导动态测试。两个 skill 合起来,完整展示了「为什么写 skill、skill 里放什么、怎么避免反模式」,也是你贡献自己 skill 前最好的范本。
内容来源:原项目
strix/skills/custom/dependency_cve_scanning.md与strix/skills/custom/source_aware_sast.md,汉化并套用体系化模板。
⚠️ 本节演示的 skill 用于扫描你自己的仓库。依赖 CVE 扫描虽不动态执行漏洞,但仍属对代码与供应链的安全评估,仅限授权资产。
阅读完本节,你应当能够:
dependency_cve_scanning 解决的核心矛盾:已知 CVE 依赖无法动态 PoC,却是真漏洞。create_dependency_report。source_aware_sast 的四件套流水线:semgrep / ast-grep / gitleaks(+trufflehog)/ trivy。这两个 custom skill 都服务于「白盒/源码扫描」场景,但分工不同:
source_aware_sast:找动态可验证的漏洞,产出候选 → 动态验证 → 走 create_vulnerability_report(默认铁律)。dependency_cve_scanning:找已知 CVE 的依赖,无需动态 PoC → 走 create_dependency_report(唯一例外)。💡 两者会协作:
source_aware_sast在静态分诊时,如果撞到已知 CVE 依赖,会主动load_skill(["dependency_cve_scanning"])切到 SCA 工作流。这种「skill 之间互相调用」正是/custom类的威力——把专门流程封装成可复用单元。
dependency_cve_scanning:给「已知 CVE 依赖」一个名分Strix 的灵魂是「无 PoC 不报告」——避免误报。但有一类发现天然无法动态 PoC:锁定在 lockfile 里的已知漏洞依赖。
举个典型例子:lodash@4.17.4 带一个已知的原型污染 CVE。你想从外部动态打出 PoC?大概率打不动——漏洞代码路径可能根本没被任何端点调用。按默认铁律,它会被「发现了又默默丢掉」,因为它「无法动态利用」。
但这确确实实是个漏洞。它的证据不是 exploit 脚本,而是 「lockfile 条目 + 扫描器输出 + 公开 advisory」。这个 skill 的使命就是:确保这类发现被上报,而不是被丢弃。
⚠️ 这是 Strix「无动态 PoC 不报告」规则的唯一显式例外,且仅适用于
create_dependency_report这个专用工具。不要把这种「静态即证据」的逻辑外推到别的漏洞类——那会重新把 Strix 变成一个误报满天的扫描器。
从仓库根目录运行,输出存到 source-aware 流程共用的产物目录:
ART=/workspace/.strix-source-aware mkdir -p "$ART" # 先记录漏洞库年龄——库过时是一个可见信号,而不是一次"干净的误判" trivy version --format json 2>/dev/null | tee "$ART/trivy-version.json" # 检查 .VulnerabilityDB.UpdatedAt / NextUpdate # lockfile/manifest → 已知 CVE 匹配。 # 先尽力刷新库(有外联的沙箱能拿到最新 CVE);刷新失败则回退缓存,而不是让扫描失败。 # --offline-scan 让逐包 advisory 查询走离线。 trivy fs --scanners vuln --timeout 30m --offline-scan \ --format json --output "$ART/trivy-sca.json" . \ || trivy fs --scanners vuln --timeout 30m --offline-scan --skip-db-update \ --format json --output "$ART/trivy-sca.json" . \ || true
trivy 能读懂主流 lockfile/manifest:package-lock.json、yarn.lock、pnpm-lock.yaml、poetry.lock、requirements.txt、Pipfile.lock、go.mod/go.sum、Gemfile.lock、pom.xml/gradle.lockfile、Cargo.lock、composer.lock 等。
💡 「库过时」是信号,不是干净的假象。如果
.VulnerabilityDB.UpdatedAt已经好几周前(沙箱没外联刷新不了),要在发现的assumptions里注明「库可能过时」——这样读者知道「干净」未必真干净。反过来,如果仓库明明有依赖,trivy 却返回零漏洞,视其为可疑:确认库存在(trivy-version.json)、确认 lockfile 存在,再相信结果。
对 trivy-sca.json 里 .Results[].Vulnerabilities[] 的每一条,收集:VulnerabilityID(CVE/GHSA,优先 CVE)、PkgName、InstalledVersion、FixedVersion、Target(lockfile 路径)、.Results[].Type(包生态,归一化为小写:npm/pypi/go/maven/rubygems/cargo 等)、CVSS、PrimaryURL/references。
按 (CVE, PkgName, InstalledVersion) 三元组去重,每个 CVE 一份报告,不要批量合并。
然后填这些结构化字段(它们驱动专门的依赖报告卡片,别只写进自由文本):
| 字段 | 要求 |
|---|---|
cve |
已验证的 CVE-YYYY-NNNNN(必填);只有 GHSA 时去查映射的 CVE,真没有就别用这个工具报 |
package_name |
PkgName(必填) |
installed_version |
InstalledVersion(必填) |
package_ecosystem |
归一化生态名(必填) |
fixed_version |
FixedVersion,无修复版本时留空 |
advisory_cvss |
advisory 的公开基础分(0.0–10.0),必填——严重度完全由它决定 |
cwe |
advisory 命名了最具体的 CWE-NNN 时填写 |
assumptions |
放可达性/可利用性的说明 |
⚠️ 三个高频反模式:
- 用
create_vulnerability_report报依赖 CVE——错,它要求 PoC 字段非空,会拒绝;依赖 CVE 必须用create_dependency_report。- 漏填
advisory_cvss——工具会直接拒绝调用,因为它是决定严重度的唯一输入;瞎猜分会同时抬高低危、压低高危。- 因为「没动态复现」就把严重度压到 LOW——错,直接用 advisory 分。
另外,可达性是置信度修饰符,不是闸门:不要因为「没能证明漏洞代码路径可达」就抑制或降级一个已知 CVE。报它、按 advisory 设
advisory_cvss,再用assumptions注明可达性(如「漏洞的template()API 未在应用代码中导入,实际可利用性不确定」)。如果你能证明可达甚至串成动态 exploit,那就走create_vulnerability_report当普通动态发现报。
source_aware_sast:静态信号指导动态测试这个 skill 用于「源码为主」的分析,核心主张是:让静态与结构化信号去指导动态测试——先用一堆静态扫描器把候选捞出来、排好序,再让 agent 对高价值候选做 source-to-sink 追踪和动态 PoC。
它的工作流是「先广后深」:
💡 这条流水线的精髓是「静态信号是线索,不是判决」。skill 的反模式一节明确写了:别把扫描器输出当最终真相、别在低信号匹配上耗满周期、别在没有验证证据的情况下上报纯源码发现。这跟第 1 章「PoC 而非误报」完全一致——静态扫只是帮 agent 把劲使在对的地方。
每个仓库先跑一遍基线,再按需收窄。所有产物写进专用目录 /workspace/.strix-source-aware:
ART=/workspace/.strix-source-aware mkdir -p "$ART" # 1) semgrep:确定性规则集(配合 --metrics=off) semgrep scan --config p/default --config p/golang --config p/secrets \ --metrics=off --json --output "$ART/semgrep.json" . # 2) 从 semgrep 扫描范围构建确定性 AST 目标(不靠硬编码路径猜) python3 - <<'PY' import json from pathlib import Path art = Path("/workspace/.strix-source-aware") semgrep_json = art / "semgrep.json"; targets_file = art / "sg-targets.txt" try: data = json.loads(semgrep_json.read_text(encoding="utf-8")) except Exception: targets_file.write_text("", encoding="utf-8"); raise scanned = data.get("paths", {}).get("scanned") or [] if not scanned: scanned = sorted({r.get("path") for r in data.get("results", []) if isinstance(r, dict) and isinstance(r.get("path"), str) and r.get("path")}) bounded = scanned[:4000] targets_file.write_text("".join(f"{p}\n" for p in bounded), encoding="utf-8") print(f"sg-targets: {len(bounded)}") PY # 3) ast-grep:无规则的结构化扫描(无需 sgconfig.yml) xargs -r -n 200 sg run --pattern '$F($$$ARGS)' --json=stream < "$ART/sg-targets.txt" \ > "$ART/ast-grep.json" 2> "$ART/ast-grep.log" || true # 4) 密钥:gitleaks + trufflehog gitleaks detect --source . --report-format json --report-path "$ART/gitleaks.json" || true trufflehog filesystem --no-update --json --no-verification . > "$ART/trufflehog.json" || true # 5) trivy:专注 vuln/misconfig(密钥已由上面覆盖),大仓库调长超时 trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \ --format json --output "$ART/trivy-fs.json" . || true
⚠️ 几个工程细节,踩过才知道:
semgrep --config auto不要和--metrics=off混用——auto 配置依赖在线指标。- ast-grep 目标要从 semgrep 范围派生,而不是拍脑袋写路径——上面那段 Python 就是干这个的,保证确定性、避免漏扫。
- trivy 这里只扫
vuln,misconfig,密钥交给 gitleaks/trufflehog,避免重复。
ast-grep 用 $F($$$ARGS) 这类模式做结构匹配时,重点盯这几类高价值模式:
对前端与 Node 服务,在语言无关的基线之上再叠加:
retire --path . --outputformat json --outputpath "$ART/retire.json" || true eslint --no-config-lookup --rule '{"no-eval":2,"no-implied-eval":2}' \ -f json -o "$ART/eslint.json" . || true
遇到压缩 bundle,先 js-beautify <file> 再 grep;jshint --reporter=unix <file> 可作为 ESLint 过激时的轻量替代。深入挖 bundle 里的端点候选,可切到 katana skill 里的 JS-Snooper / jsniper.sh。
skill 给出的转换流程是:
把这两个 skill 摊开看,自定义 skill 其实遵循一个清晰的套路:
对照两个范本:
| 套路环节 | dependency_cve_scanning |
source_aware_sast |
|---|---|---|
| 痛点 | 已知 CVE 依赖无法动态 PoC 却是真漏洞 | LLM 静态分析浅,需要结构化信号指导 |
| 流程 | trivy 扫 lockfile 三段式(含回退) | 四件套基线扫描包 |
| 上报 | create_dependency_report 字段表 |
验证成功才走 create_vulnerability_report |
| 反模式 | 5 条(用错工具/漏填 cvss/降级等) | 3 条(扫描输出非真相/耗低信号/无验证上报) |
💡 「反模式」是被低估的一节。很多人写 skill 只写「该做什么」,但真正能防止 agent 跑偏的,是明确写清「千万别做什么」。这两个 skill 的反模式清单,直接对应了 Strix 报告质量的最常见雷区——你写自己的 skill 时,务必把踩过的坑写成反模式。
dependency_cve_scanning:用 trivy 扫 lockfile,把「已知 CVE 依赖」作为一等公民发现上报,是「无 PoC 不报告」铁律的唯一显式例外,且仅限 create_dependency_report。source_aware_sast:semgrep/ast-grep/gitleaks(+trufflehog)/trivy 四件套,静态信号指导动态测试,验证成功才上报。--metrics=off;ast-grep 目标从 semgrep 范围派生;trivy 分工只扫 vuln/misconfig。source_aware_sast 撞到依赖 CVE 会 load_skill 切到 dependency_cve_scanning——/custom 类的可复用单元威力。会造 skill 之后,最后一步是把这些成果回馈给社区——让更多的人用上你打磨的知识。下一节(也是全书最后一节)讲贡献流程:如何搭建开发环境、提交 PR、把你的 skill 合进上游,完成「从用户到贡献者」的闭环。