☰
Claude Security 报告规范深度解析:CLAUDE-SECURITY-RESULTS.md 的撰写规则与验证流水线
2026/10/1 2:41:37 网站建设 项目流程
  • AI 插件
  • 开发工具
  • 插件系统

【免费下载链接】claude-plugins-official

Official, Anthropic-managed directory of high quality Claude Code Plugins.

项目地址:https://gitcode.com/GitHub_Trending/cl/claude-plugins-official
点击查看免费下载

Claude Security 插件(claude-plugins-official仓库中的 plugins/claude-security)每次扫描交付的最终产物是CLAUDE-SECURITY-RESULTS.md——一份写给人类工程师阅读的 Markdown 安全报告。本文基于仓库内的 report-spec.md 完整解读这份报告从结构、字段到写作纪律的全部规范,并结合 render_report.py、write_scan_meta.py 与 finding.py 等源码,还原报告背后"模型叙述、脚本落盘、投票背书"的验证流水线。读完你将掌握:如何按规范组装一份可被读者快速信任的扫描报告,每个 Coverage 字段对应的底层数据来源,以及严重性/置信度如何被验证面板钳制。

一、报告在交付流水线中的位置:唯一由人阅读的散文

CLAUDE-SECURITY-RESULTS.md是扫描工作流中唯一"以散文形式书写"的产物。报告规范开篇就明确了它的读者画像:拥有这份代码的工程师,很忙,会在约九十秒内决定是否对每条 finding 采取行动。因此报告的全部行文纪律都服务于"快速、可信、可行动"。

报告规范同时划清了书写边界:render_report.py会从findings.json与votes.json生成机器可读的配套产物——CLAUDE-SECURITY-RESULTS.jsonl(每条 finding 一行,字段顺序固定)、CLAUDE-SECURITY-RESULTS.sarif(SARIF 2.1.0 日志)以及CLAUDE-SECURITY-REVISION-<sha12>.json修订戳。规范明确要求:

  • 不要手写JSONL、SARIF 或 stamp;
  • 不要在报告里复述JSONL 的内容——本文件是给人读的部分。

从源码看,这份分工是硬性的。render_report.py 的render()函数会读取运行目录中的scan-meta.json、findings.json、coverage.json、votes.json,校验每一条记录,然后一次性写出全部机器可读产物(stamp 最后写)。它甚至要求报告 Markdown 文件必须已存在,否则直接拒绝渲染("CLAUDE-SECURITY-RESULTS.md is missing. Write the human-readable report before running this script.")。verification.status是渲染器从投票记录推导出来的,不是由写报告的人声称的——"Never claim a verification status the renderer did not print"。

二、Shape:报告的整体骨架

报告的 Markdown 结构固定为五部分:开篇段落(header)、## Coverage、## Findings、## What was verified、以及贯穿全程的## Rules纪律(规则以 prose 形式存在,不单列章节标题)。整体模板如下:

# Claude Security results <one paragraph: what was scanned (path, revision, mode, scope), when, at what effort, and the headline: how many findings at what severities, or that there were none. Read `revision.dirty` in the run dir's scan-meta.json: on `true`, say the repository's working tree held uncommitted changes or untracked files -- which ones is not recorded, so never name or explain them; on `null`, say the tree state could not be determined; otherwise say nothing of it.> ## Coverage <...> ## Findings ### F1 — <title> (HIGH, confidence medium) ... ## What was verified <...>

开篇段落必须一次说清四件事:

  1. 扫了什么:路径(path)、修订(revision)、模式(mode)、范围(scope);
  2. 何时、以什么 effort(低/中/高)执行;
  3. 结论头条:多少个 finding、分别是什么严重级别,或"一个都没有"。

关于revision.dirty的处理有一条非常具体的规则:从运行目录的scan-meta.json中读取revision.dirty——

  • 为true:说明工作树含有未提交更改或未跟踪文件,报告必须如实说明;但"具体是哪些"并未被记录,绝不能点名或解释;
  • 为null:说明树状态无法确定,照实说;
  • 其他情况:保持沉默。

这一字段的采集在 write_scan_meta.py 的worktree_dirty()中实现:通过git status --porcelain --untracked-files=all判断,且会跳过报告目录前缀的路径。扫描时的工作树状态直接影响修订戳文件名(-dirty后缀,见revision_tag()),其目的正是让报告永远与它所描述的代码绑定。

三、Coverage:让读者可以校准一切的开诚布公

Coverage 章节是全报告可信度的根基。规范的原话是:"This section is what makes the rest of the report trustworthy: a reader who knows what you did not look at can calibrate everything else."(一个知道自己没看什么的读者,才能校准其余所有内容。)这一节必须回答"检查了什么、没检查什么、为什么"。

3.1 稀疏检出(sparse checkout)

如果write_scan_meta.py报告了稀疏检出(revision.not_checked_out_dirs保存列表),报告必须说明只扫描了检出部分,并逐一列出未检出的受跟踪顶层目录。源码中由 write_scan_meta.py 的sparse_checkout()检测core.sparseCheckout配置并记录缺失目录。

3.2 验证运行与丢失的候选人

Coverage 必须说明面板(panel)进行了多少次验证运行(coverage.verificationRun),并点名任何从未被验证的候选人及其原因:要么是在通往后续运行的途中丢失(coverage.lostCandidates),要么被交给了未完成的运行。

3.3 未返回的研究员(researchers)

当coverage.researchersReturned低于coverage.researchersDispatched时:

  • 说明有多少研究员未返回,以及他们的阅读成果缺失于本次扫描;
  • 按coverage.lostResearchers的每条记录,以其 reading 命名并引用记录的原因;
  • 条目超过 10 条时改为按原因分组,每组附上研究员被派去读什么;
  • 描述未返回原因时只允许引用记录的原因;
  • 报告任何"研究覆盖了什么/读了什么"的表述(包括 "What was verified" 一节),只能计入coverage.returnedReadings中的阅读。

3.4 主动跳过的组件

coverage.skippedComponents的每条记录都带有被略过的路径和 componentizer 的一行原因(vendored、generated、documentation 等)。规范强调:有意跳过的目录是披露(disclosure),不是失败——必须写明原因,而不是让该区域静默消失。

3.5 被修剪的调研镜头(pruned buckets)

如果coverage.prunedBuckets非空,每条记录是某组(或组件)的名称后跟":memory-and-unsafe"——即它没有获得的调研镜头(research lens)。报告必须完整写出每个名字,并直白说明:该组没有任何研究员被问及内存安全类缺陷,无论 tier 如何。

关于 "change boundary" 条目有一条特殊规则:它没有对应的coverage.components行,因为它不是第二组,而是从变更向外工作的研究员,对每个变更文件适用同一规则。给出的只能是工作流自身的理由:在研究员被简报之前,工作流仅凭语言判断(changes/commit 扫描看变更文件的扩展名,codebase 扫描看 inventory 的语言)就认定这些代码全在受托管语言中。不得补充"该镜头不适用于此类代码",也绝不能把剪枝描述成研究员或面板的决定,或"因为已有 finding 而放弃镜头"。没有条目的组则保留了镜头。

3.6 全库扫描的完整性检查

整库扫描要求 inventory 覆盖每个顶层目录——要么被扫描,要么被显式跳过。coverage.completenessCheckOutcome说明检查是否执行:

取值含义报告措辞要求
checked检查执行且通过说明整个树都有交代
partialinventory 把一些顶层目录留在两个账本之外逐一列出coverage.unaccountedTopLevelDirs,并直说它们既未被扫描也未被跳过——这正是"无 finding"会夸大覆盖度的地方
not-checkable目录列表未提供、不可读或为空而 inventory 却命名了子目录(coverage.topLevelRejected说明)直说完整性无法检查——这是把"无 finding"从"未检查"变成"干净"的关键
not-applicablediff/commit/范围扫描(目标是变更或范围),或低 effort 且无 inventory 的运行无需多言

3.7 inventory 回退(fallback)

若coverage.inventoryFallback被设置,说明 inventory 的分区未被使用,整棵树被当作一个组件阅读(完整但更粗粒度)。原因三选一:

  • "incomplete-partition":其答案会认可它从未命名的覆盖(跳过了整个目标,或只有爬出树的路径);拒绝项列在coverage.inventoryRejected;
  • "inventory-failed":它没有作答;
  • "empty-partition":它答了个空。

3.8 changes / commit 扫描的特殊规则

对 changes 或 commit 扫描,报告要说"审查的是变更而非仓库":

  • 用平实语言命名变更(遵循jobs/scan-changes.md中关于提交数与分支的规则),而不是用coverage.range里的提交 id;
  • 按组列出变更文件(coverage.components;helper 统计的coverage.changedFileCount个文件中的coverage.diffFiles个、coverage.diffLines行);
  • 报告派出多少研究员、返回多少(coverage.researchersDispatched/coverage.researchersReturned),以及被要求投入的 effort(coverage.researchEffort——是"被要求"而非"实测",模型运行时可能自行调整);
  • 明确声明:变更未参与其中的缺陷不属于本次审查范围,留给 codebase 扫描;
  • 列出coverage.preExisting的每条记录:面板判定为真实但早于变更的候选人;
  • 当基线是分支自身的已推送副本(diff 只含未推送提交)时,说明已推送提交不在此次审查内;
  • coverage.outsideScope非空时,说明这些 finding(它命名了它们)在请求范围之外,因变更触及它们而报告;
  • coverage.changedFilesRejected被设置时:引用记录的值,说明变更被作为一个组审查、其研究员自行列出了文件;若coverage.changedFilesMiscounted也被设置,给出两个数字、说明审查了哪个列表(其reviewed字段)以及那是多少文件(coverage.diffFiles)。

3.9 折叠形状与范围大小被拒

  • coverage.collapsed为"small-scope":说明中等 effort 的小范围(给出coverage.scopeFiles)折叠成了成比例的单研究员形态——一次快速定向扫描,仍经面板验证,但不是穷尽阅读;
  • coverage.scopeSizeRejected:引用记录值,说明其对实际运行 tier 的后果——medium 下范围未被当作小范围处理,因此跑的是完整流水线而非快速路径,且空范围无法被短路。

3.10 运行规模的报告

当coverage.targetComponents被设置时,说明运行如何被定大小:目标约有coverage.targetFiles个受跟踪文件、约coverage.targetComponents个组件(每个约coverage.filesPerComponent个文件,或当目标持有的组件数超过coverage.componentCap时更大),最多保留coverage.componentCap个。

3.11 研究员自己的"未读声明"与对照账

最后,报告要转述研究员自己报告没读过的东西——作为他们的陈述而非事实:

  • coverage.research.components:按组件列出其研究员声明未触及的路径及原因——按目录归纳成一行(背景树——vendored 树、焦点下的测试与 fixture 树被留作背景——是预期中的背景而非缺口),单个文件只在每组件少量时点名;
  • coverage.research.tree(存在时):把该陈述与组件内受跟踪文件对照——多少文件读到了结论、多少位于声明未触及的路径下、多少没有任何研究员交代(coverage.research.capped为 true 时账目被截断,即至少读了这么多、至多有这么多未交代)——并列出coverage.research.tree.unaccountedPaths(列前几个,总数是全部),因为一个没人读完的组件绝不能冒充干净的组件;所有组件之外的文件(outsideComponents)是本小节已点名的跳过/丢弃区域加上无组件认领的根文件,不是研究员的缺口;
  • 当coverage.research为 null 或缺少 tree 时,该检查未运行,对此只字不提(但已声明的路径仍然成立)。

这些数据在渲染器里被整理成run_shape(render_report.py),并同步反映到 SARIF 的通知(notifications)中。

四、Findings:每条发现的八段式模板

## Findings章节中,每条 finding 的标题里的F<n>是它在findings.json中的id,逐字照抄——finding 到达时已处于报告顺序,因此绝不重新编号、不重排、不发明 id。编号中的缺口是面板未在该 id 下保留的候选人;只有当完整面板驳斥它时才可称其为"被拒",绝不能因为coverage.adversarialCasualties、coverage.preExisting或coverage.lostCandidates(按候选人 id)点名它就那样说。

每条 finding 的模板:

### F1 — <title> (HIGH, confidence medium) **Impact.** <what an attacker gets. Lead with this: it is what decides priority.> **Where.** `path/to/file.py:123` in `function_name` — <cwe_id, then "(also <other_cwe_ids>)" when the finding carries further CWEs> **Link to the change.** <a changes or commit scan only: the finding's via_change, the changed line that takes part in the attack and how, named by file:line and never quoted; omit the line for a scan of the codebase> **What.** <the vulnerability, in two or three sentences. Name the untrusted source, the dangerous operation, and why nothing in between stops it.> **Exploit scenario.** <a concrete walk-through. Not "an attacker could inject SQL" -- what they send, what happens, what they get.> **Preconditions.** <bullets: what must be true. Authentication? A non-default config? Victim interaction? An empty list means none, which is worth saying.> **Fix.** <what to change, in outcome terms. The root cause at the sink, not a patch at one caller.> **Verification.** <n>/3 lens verifiers confirmed.

各字段的写作要求:

  • Impact:攻击者能得到什么。必须放在最前——它决定优先级;
  • Where:文件:行加函数名,后跟主 CWE id;携带更多 CWE 时追加(also <other_cwe_ids>);
  • Link to the change:仅 changes/commit 扫描使用,取 finding 的via_change,点名参与攻击的变更行及其方式(以file:line命名,从不引用原文);codebase 扫描省略此行。从 finding.py 可见via_change以file:line开头、由LINK_SITE正则识别并规范化;
  • What:两到三句话说明漏洞——点出不受信任的来源、危险操作,以及中间为什么没有任何东西阻止它;
  • Exploit scenario:具体走一遍——不是"攻击者可注入 SQL",而是"他们发送什么、发生什么、得到什么";
  • Preconditions:必须为真的条件列表(认证?非默认配置?受害者交互?)——空列表意味着无前置条件,这一点值得明说;
  • Fix:以结果导向说明改什么——针对 sink 处的根因,而非某个调用点的补丁;
  • Verification:<n>/3个镜头验证者确认。

F2 — ...依此类推。验证者计数对应源码中的固定面板规模:PANEL_VOTER_COUNT = 3、PANEL_KEEP_QUORUM = 2(见 finding.py),即每条 finding 由三名验证者组成的对抗性面板审查、2/3 通过才保留。

五、What was verified:验证状态必须如实交代

## What was verified用一段话说明:产出这些 finding 的流水线、每条 finding 通过的投票、以及 stamp 的verification.status。

  • 状态不是"verified"时:用平实语言解释其含义和应对措施,不得掩盖;
  • 状态是"verified"但存在未返回的研究员时:说明"verified"不覆盖缺失的阅读。

verification.status的推导逻辑在 render_report.py 的verification_summary()中,它只会在每一项都满足时给出"verified",否则给出"unverified"并附带机器可读的reason_kind。源码中可见的失败原因种类包括:

reason_kind触发条件
no-vote-record运行目录没有 votes.json 或它不是扫描工作流的记录
no-candidate-countvotes.json 缺candidates字段
nothing-examined派出了研究员但一个都没返回
finding-panel-incomplete有 finding 缺少完整 3 票面板轮次
finding-below-quorum报告的 finding 中有未达保留法定人数的
candidates-not-paneled记录了候选人但无一入面板
no-panel-completed派发了面板轮次但无一完成完整 3 票审查
candidate-panel-incomplete有候选人未经完整面板轮次即被丢弃
continuation-incomplete有候选人被交给未完成的验证运行
findings-refused渲染时被拒绝的 finding 缺席报告

这套枚举证明了一个关键设计:报告对自己严谨程度的说明是由代码计算出来的,而非产生 finding 的模型所声称的(README 中亦明示:"the record of how thoroughly a run was verified is computed in code rather than asserted by the model that produced the findings")。

六、Rules:贯穿全篇的写作纪律

6.1 严重性是可利用性与影响,不是置信度

  • CRITICAL:严重影响且攻击者面前毫无阻碍;
  • HIGH:严重影响但存在一个真实障碍;
  • MEDIUM:影响有界,或严重影响但需多个条件;
  • LOW:影响有限且利用苛刻。

findings.json中的严重性是最终裁决:当面板的确认投票者将 finding 评为低于研究员所评的级别时,工作流已将其调低,coverage.severityLowered会点名每条此类 finding 并给出两个评级——报告必须在它的Verification.行说明这一点,不得恢复被调低的严重性。

不确定性与confidence无关,而是置信度词:low、medium、high,由面板投票钳制——只有全票通过的面板才配得上high,工作流已把其他任何high调低;若报告者擅自提高,render_report.py会再把它降回来。这一钳制在源码vote_confidence_ceiling()(finding.py)中有精确对应:面板完整时,只有true >= PANEL_VOTER_COUNT(全票)才是high,否则是medium。

6.2 排序与分批

  • 先按严重性、再按置信度排序——读者会中途停下,把最重要的放最上面;
  • 超过二十条 finding 分批写:一次调用容纳全部 finding 可能超出模型输出上限,被截断的 Write 会被整体拒绝。因此第一个 Write 包含开篇段落、Coverage、前二十条 finding 和 What was verified,之后每二十条用 Edit 插入到## What was verified标题之前。

6.3 每个 finding 必须引用真实的 file:line

指向错误行的 finding 比漏报更伤人——读者在追查时会失去对报告其余部分的信任。渲染器为此做了路径校验:file_field()/relative_path()会拒绝任何逃出仓库或指向缺失文件的路径(finding.py),无法承载路径的 finding 会被点名拒绝(refused <id>)并从产物中剔除,stamp 相应标记为未验证。

6.4 硬编码凭据 finding 的特殊规则

任何携带 CWE-798、CWE-259、CWE-321 或 CWE-671(在cwe_id或other_cwe_ids中)的 finding,不写 "Link to the change" 行——它本要点名的变更行就是凭据本身,而 JSONL 和 SARIF 已经收回了该链接。README 中亦说明:凭据 finding 的文件、行号与符号仍会定位,但绝不引用凭据所在源码行。

6.5 无控制字符

只允许\n和\t。报告在终端中被阅读,转义序列可能改写人所见的内容。若扫描源码中确实出现此类字节,描述它而非复现它。

6.6 不模糊、不填充

不要为了对代码客气而软化真实 finding,也不要为了显得周全而夸大细枝末节。"No findings" 本身就是一份完整的报告——把它写好(覆盖了什么、没覆盖什么)比一整页"可能"更有价值。

6.7 研究者的镜头不是面板的

研究者的镜头(lens)是一个漏洞类别(其组保留的类别之一或全部);面板投票者的镜头是可达性(reachability)、影响(impact)或防御(defenses)。绝不可把面板的三个当作研究者的三个。

6.8 从不声称运行过什么

扫描不执行仓库代码:没有运行过测试、没有发射过 exploit、没有验证过 PoC。每条 finding 都源自阅读。报告必须这样说,而不是暗示一次演示。这与插件整体的信任模型一致——仓库内容是"被审查的数据",永不是指令。

七、写作质量的标杆:Example of the bar

规范用一个对照展示了合格与不合格的 finding 写法。不要这样写:

The code may be vulnerable to SQL injection. Consider using parameterized queries as a best practice.

要这样写:

Impact.Any unauthenticated caller ofGET /users?name=can read every row of theuserstable, including password hashes and email addresses.

Where.api/app.py:3inget_user— CWE-89

What.namearrives from the query string inhandlers.py:41and is interpolated into the SQL string with%. No escaping or validation runs on the path between them; thevalidate_namecall inhandlers.py:38checks length only.

Exploit scenario.GET /users?name=' OR '1'='1makes the WHERE clause tautological and returns the full table in the JSON response.

对比可见:合格示例把"可能、请考虑"的模糊建议替换为可验证的具体事实链——来源(handlers.py:41的 query string)、危险操作(%插值)、缺失的防线(validate_name只查长度)、以及一条可复现的 exploit 路径。

八、从规范到实现:报告背后的验证闭环

将 report-spec 与仓库源码对照,可以还原出整条验证闭环:

  1. 扫描元数据:write_scan_meta.py从 git 自身捕获修订(commit、branch、dirty 状态)、顶层目录、文件计数,写入scan-meta.json——stamp 绝不依赖任何人转写的值(write_scan_meta.py);
  2. 研究工作流:claude-security:scanworkflow 派出研究员并按 tier 组织(low 单研究员、medium 组件×类别、high 双研究员),最终一律进入固定的三票验证面板;
  3. 投票记录:面板的投票进入votes.json,verification_summary()据此计算状态与各计数(render_report.py);
  4. 去重与落盘:one_per_site()把同一 site(规则×文件×行)的多个 finding 合并为最强一条,其余在产物中披露合并句(render_report.py);SARIF/JSONL/stamp 全部由脚本写出;
  5. 清洗与交付:交付后scan-redactor把产物中的凭据值替换为[REDACTED](这是模型的最佳努力而非保证),随后运行目录被整体移除,报告目录只留下用户阅读的产物。

这套设计在 patch-spec.md 中形成了对称结构:修复工作同样由人写工作记录(patches.json)、脚本渲染产物(F<n>.patch等),"no diff byte and no confidence claim is ever re-typed by a model on its way to the user"(没有任何 diff 字节或置信度声明在通往用户的路上被模型重新转写)。报告规范与补丁规范共同构成了 Claude Security 插件的契约层:模型叙述与决策,脚本落盘与验证,人类阅读与行动。

对于阅读或维护此类报告的开发者而言,最值得带走的三条原则是:报告的可信度取决于它如何交代"没看什么"(Coverage);每条 finding 的优先级由 Impact 与可复现的 Exploit scenario 决定,而不是措辞的轻重;而"这份报告有多可靠"这件事,永远以render_report.py盖下的verification.status为准。

  • AI 插件
  • 开发工具
  • 插件系统

【免费下载链接】claude-plugins-official

Official, Anthropic-managed directory of high quality Claude Code Plugins.

项目地址:https://gitcode.com/GitHub_Trending/cl/claude-plugins-official
点击查看免费下载

相关推荐

上一篇:CAP 消息序列化机制详解:从默认 JSON 到自定义 ISerializer 扩展
下一篇:显卡驱动残留清不净?DDU 显卡驱动清理,彻底卸载一次搞定

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询