关于 mantis-dedupe
mantis-dedupe 是用于整理安全发现的分诊技能,可合并重复或高度重叠的报告。它会检查此前轮次的归档,并依据快照判断重复项,避免将可能的回归问题静默丢弃。
主要功能
- 01聚类重叠的安全发现
- 02检查各轮次的归档发现
- 03依据快照判定重复项
- 04对不确定匹配作标记而不删除
- 05记录变更并合并发现记录
适用场景
- 01清理发现队列中的重复报告
- 02保留可能表示回归的问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ google/ mantis --skill mantis-dedupe安装后在 Claude Code 中输入/mantis-dedupe确认已加载。
详细介绍
去重器(/mantis-dedupe)
系统目标
重复发现合并器。评估原始发现列表,将相同或高度重叠的问题聚类并合并为单条描述性记录。
命令定义
- 命令:
/mantis-dedupe - 描述: 合并原始安全发现,以消除冗余报告。
- 参数(可选;由编排器提供,由 Block A 使用):
--snapshot_root/--snapshot_id/--state_root。全部缺省 -> MODE-OFF/旧模式(行为与当前相同;禁用快照门控)。
输入/输出约定
- 读取:
workspace/findings/(原始发现 JSON 文件,忽略.trash/)。workspace/archive/findings_pass_*/*.json和workspace/archive/loop*_findings/*.json(跳过先前轮次中已评估并分流的发现)。workspace/.mantis_state.json(跟踪当前循环轮次)。
- 写入:
- 将重复发现移至
workspace/findings/.trash/,并设置"status": "DUPLICATE"和"duplicate_of"。 - 对 NOT_MATCHED 匹配项设置
"possible_duplicate_of"(软性、非终止状态),并为缺少该字段的当前发现添加"discovery_commit"时间戳。读取.mantis_state.json中的active_snapshot/snapshot_pinned。 - 将事务日志追加到
workspace/.tx_log.jsonl。 - 生成/执行合并脚本
workspace/helpers/merge_findings.py。 - 更新主要发现
workspace/findings/<primary_id>.json(合并字段和历史记录)。
- 将重复发现移至
- 前置条件:
workspace/findings/必须存在且包含发现文件。
- 幂等性保证:
- 对照
workspace/archive/中的已归档发现,筛除本次运行先前轮次中已处理的发现。 受快照门控:如果已解决的归档发现对应不同快照,则标记为 POSSIBLE REGRESSION(绝不静默筛除)。将事务记录到workspace/.tx_log.jsonl,以便跟踪和潜在回滚。merge_findings.py实现了确定性合并规则。
- 对照
说明
步骤 0:定位器解析(首先运行)
LOCATOR RESOLUTION (before reading ANY target code or artifact):
0. ROLE: If this skill NEVER reads target source (report, calibrate, reflect),
you are a FINDINGS-ONLY stage: skip steps 2-6; still read active_snapshot from
state for provenance/annotation; NEVER stop merely because a code root is unset.
1. Determine CODE_ROOT, in this priority order:
a. If --target_root is passed on THIS invocation, CODE_ROOT = --target_root.
It is AUTHORITATIVE and OVERRIDES SNAPSHOT_ROOT and the state fallback
(used when a caller hands you a prepared tree, e.g. a patched shadow).
b. Else if --snapshot_root (or SNAPSHOT_ROOT) is passed, use it.
c. Else read state_root/workspace/.mantis_state.json (state_root from
--state_root if passed, else ./workspace/... relative to the current dir)
-> active_snapshot.root / .snapshot_id / .snapshot_pinned.
d. Else (no arg AND no readable active_snapshot): CODE_ROOT = current directory,
treat snapshot_pinned = false (MODE-OFF). Do NOT stop.
2. SENTINEL CHECK (only if snapshot_pinned is true AND you did NOT take path 1a):
verify CODE_ROOT/.mantis_snapshot_id exists and equals SNAPSHOT_ID. If missing
or different -> STOP "snapshot sentinel mismatch". (A --target_root tree (1a) is
deliberately mutated and is sentinel-EXEMPT.)
3. PATH FIELDS:
- SNAPSHOT-RELATIVE (read under CODE_ROOT): code_paths entries; plan target_files
that are file paths. Strip ONLY a trailing ":<digits>". A code_paths entry
containing "://" is a URL/endpoint, NOT a file read. A code_paths entry that is
NOT of the form <existing-path>:<integer> is a non-source LOCATOR
(symbol/offset/endpoint): only check that the artifact/symbol exists; skip ALL
line-range and line-existence logic.
- STATE-RELATIVE (read/write under state_root/workspace, NEVER prefix CODE_ROOT):
kb_references, repro_file_path, reattack_file_path, helper scripts, report
files, and all state/findings JSON.
4. Never WRITE under CODE_ROOT when snapshot_pinned is true. Any command that
compiles, generates, or writes artifacts MUST run in a PRIVATE SHADOW copy
(mktemp -d from CODE_ROOT), never with cwd=CODE_ROOT. Read-only inspection may
cd into CODE_ROOT.
5. VCS-METADATA CARVE-OUT: history-log extraction and any VCS diff/blame command
run in the LIVE repository root (which still has .git/.hg/.repo), NOT CODE_ROOT
(the snapshot copy strips VCS metadata). Do NOT stop merely because CODE_ROOT
lacks .git/.hg/.repo.
6. Every shell command uses ABSOLUTE paths and sets its own working directory on
that call. Do NOT assume the working directory persists between calls.
说明
**当前轮次检查(防御性措施;绑定保证由
mantis-pipeline-adapter场景 2 中的处理框架负责):**如果active_snapshot存在且active_snapshot.pass != state.pass_number,则将此快照视为本轮次的过期快照——停止并报告 "stale active_snapshot: pass mismatch",或降级为 HALT(snapshot_pinned实际上为 false:没有权威判定,Block B 返回 NOT_MATCHED,复现状态为not_attempted)。这可以捕获跨 Stage 15 轮次递增时保留了active_snapshot、却未重新固定快照的自定义处理框架。参考 meta-agent 每轮都会重新固定快照,因此不会触发此检查。Block B 本身无法检测此问题(它只识别snapshot_id,不识别pass)。
注意:workspace/findings/、workspace/archive/、workspace/.tx_log.jsonl、
workspace/helpers/ 和 .mantis_state.json 均相对于状态目录(位于
--state_root 下)。为发现而检查的任何代码片段均相对于快照(位于 CODE_ROOT 下)。绝不要在 CODE_ROOT 下写入。
检查安全发现列表,并合并指向完全相同安全漏洞或相邻代码路径的重复发现。
按以下步骤执行任务:
-
加载原始发现和归档发现队列:
- 列出
workspace/findings/目录的内容并读取其中的文件。如果该目录为空或不存在,请通知用户并退出。 - 重要: 列出或处理发现项时,忽略隐藏文件和目录(例如
.trash/子目录)。 - 如果存在,请从
workspace/archive/findings_pass_*/*.json和workspace/archive/loop*_findings/*.json下查找并加载所有先前循环轮次归档的发现项 JSON 文件。这些文件代表在先前轮次中已完成全面评估、分类并可能已修复的漏洞。 - 重要: 不要读取
workspace/historical_learnings.jsonl,也不要与其进行去重(VCS 历史记录),因为我们希望捕获旧漏洞重新引入后的回归。
- 列出
-
过滤循环重复项(受快照门控)。 首先,为任何缺少
discovery_commit的当前发现项设置该字段,值取自.mantis_state.json中的active_snapshot.snapshot_id(未固定快照时跳过)。然后,对于每个与归档发现项匹配的当前发现项(依据code_paths+ 标题相似度),执行:基于签名的候选匹配(Phase 3)— 只会收紧条件,绝不取代原有匹配:
signature只能将一对发现项提升为“成对快照检查候选项”;绝不能仅凭signature将其判定为硬性重复项/移入垃圾箱。只有同时满足以下两项的配对,才是下文“成对快照匹配检查”的候选项:- 按照今天的
code_paths+ 标题相似度进行匹配,并以逐行包含方式比较code_paths条目(包含末尾的:line);并且 - (当两个发现项都有
signature时)它们的signature字段相同。 仅signature匹配,但code_paths+ 标题不匹配,不能视为重复项;最多只能标记为软性possible_duplicate_of(保留该发现项为 ACTIVE),绝不能移入垃圾箱。原因:signature会去掉行号和除第一个以外的所有code_paths条目,因此同一文件中的两个不同漏洞(例如parser.c:100与parser.c:900)若标题和 CWE 相同,就会拥有相同的signature;仅凭签名移入垃圾箱会悄无声息地删除真实发现项。如果任一发现项缺少signature,则照常使用今天的code_paths+ 标题相似度匹配,不作更改。
成对快照匹配检查(决定 MATCHED 与 NOT_MATCHED)— 对于这对发现项,将当前发现项的
discovery_commit与归档发现项的discovery_commit进行比较(而不是与SNAPSHOT_ID比较):- 如果
snapshot_pinned为 false 且状态中没有active_snapshot(MODE-OFF)→ NOT_MATCHED。停止。(在 HALT 中——存在active_snapshot, 但snapshot_pinned=false——此处不要提前结束;继续执行下面的成对比较, 结果将是 NOT_MATCHED,因为当前发现的discovery_commit是一个live:ID, 不会与归档的 ID 相等。) - 读取当前发现的
discovery_commit和归档发现的discovery_commit:- 如果任一项缺失、为空或为字面值
"MIXED"→ NOT_MATCHED。 - 如果二者并非逐字节完全相同 → NOT_MATCHED。
- 如果二者逐字节完全相同(均存在且非 MIXED)
→ MATCHED。没有其他途径可以得到 MATCHED;绝不进行模糊比较。全局的
“为字段设置默认值并继续”向后兼容规则不适用于
discovery_commit: 缺失 = NOT_MATCHED。(没有单独的“dirty”门槛:dirty 树的 SNAPSHOT_ID 已包含工作树内容哈希,因此同一轮次内的发现会 MATCH,而跨轮次的裸提交 发现不会。)注意:这是成对检查(当前项与归档项),而不是与全局SNAPSHOT_ID比较——去重会在上面的步骤 2 中将当前发现的discovery_commit设为SNAPSHOT_ID,因此与SNAPSHOT_ID比较总会 得到 MATCHED,并会把重新引入的问题或回归缺陷误判为 DUPLICATE。
- 如果任一项缺失、为空或为字面值
然后,依据幂等性规则(输入/输出约定 → 幂等性保证)避免重复写入,并 机械地作出判断:
- MATCHED(二者均存在且相等):像之前一样,将当前发现软删除为循环重复项——
设置
"status": "DUPLICATE"和"duplicate_of": "<archived_uuid>",清除possible_duplicate_of(如果存在),确保执行mkdir -p workspace/findings/.trash/, 将其移至该目录,并在workspace/.tx_log.jsonl中记录一条loop_filter事务。如果当前发现缺少lineage_id,但归档发现有该字段,则在 移动之前将归档发现的lineage_id继承到当前发现上(以保留合并后的谱系链)。 - NOT_MATCHED(二者不同或任一缺失):不要设置
DUPLICATE,也不要移至回收站。 保留当前发现为 ACTIVE,并设置"possible_duplicate_of": "<archived_uuid>"(软性、 非终态提示)。如果当前发现缺少lineage_id,但归档发现有该字段,则将归档发现的lineage_id继承到当前发现上(即使发现位于不同快照,也能保留用于报告折叠的谱系链)。
重要
状态与重复项不变量:
- 一个发现不得同时携带指向同一目标 UUID 的
duplicate_of和possible_duplicate_of。 status = "DUPLICATE"不得与指向同一目标 UUID 的possible_duplicate_of并存。- 在 NOT_MATCHED 情况下(MODE-OFF 回退例外除外),该发现的
status必须保持为有效状态(例如VALID、PROVISIONALLY_VALID、NEEDS_RESEARCH),不得设置duplicate_of,且不得将该发现移至.trash/。设置possible_duplicate_of仅是非终态提示。
- **可能的回归:**如果归档匹配项的状态为 RESOLVED
(
patch_status属于 {VERIFIED_SECURE,MITIGATION_PROPOSED},或status==FALSE_POSITIVE,或production_viability==NON_VIABLE),且 该配对为 NOT MATCHED,则保持当前发现为 ACTIVE,添加历史备注 "POSSIBLE REGRESSION vs <archived_uuid>",并且不要过滤掉它。(在新代码中重新发现已撤销的修复时,绝不能将其丢弃。) - **完全相同 UUID 的重试例外(保持不变):**如果当前发现的 UUID 与归档项的 UUID 完全相同,则说明它是有意复制回来重试的——不要过滤掉它(保持原样)。
- 永久未固定例外(仅 MODE-OFF——没有
active_snapshot): 如果状态中没有active_snapshot(MODE-OFF = 今天的默认模式;例如实时端点等没有快照边界的目标),则在 PASS 级别snapshot_pinned为 false(active_snapshot.snapshot_pinned——它不是逐条发现的字段),且 Block B 不提供有用信息时,回退到今天的去重逻辑:如果有signature,则按signature去重;否则按stable_key去重,stable_key= normalized_title + 第一个code_paths条目,包括其末尾的:line(包含行号,与 Step 2 相同)。理由:去掉:line会把同一文件中的两个不同错误(例如parser.c:100和parser.c:900)与相同标题+CWE 合并为一个,从而悄然删除真实发现。注意:按设计,signature本身会去掉:line(它是用于跨轮次溯源的粗略标识,而不是去重键);因此此回退仅在stable_key的行号包含匹配也一致时才优先采用signature,绝不单独依赖signature。这样可保留对永远无法 MATCH 的目标进行去重的能力。当存在active_snapshot但未固定时(HALT 模式),此例外不适用:保留上述由快照门控的行为(NOT_MATCHED → 保持 ACTIVE +possible_duplicate_of,绝不标记为DUPLICATE)。 **可能的回归优先于此回退逻辑:**与已解决的归档发现配对的 NOT_MATCHED 当前发现,始终保持为 ACTIVE(绝不丢弃),无论处于何种模式。
- 按照今天的
-
**3 层去重阶梯(确定性与语义匹配):**在解析发现项的溯源关系并评估去重候选项时,去重器采用分层的 3 层阶梯,旨在以亚毫秒级速度执行快速路径,并在必要时回退到语义 RAG:
-
Tier 1(快速路径精确启发式锚点——< 1ms,0 个 token):
- 精确内容身份签名匹配:
hashlib.sha256(canonical_fp | canonical_cwe | target_symbol)(不受行号偏移、反引号和标题释义改写的影响)。 - 精确匹配
canonical_filepath + normalized CWE + target_symbol。 - 当 target symbol 为空时,在精确 canonical filepath 上使用严格的行号邻近窗口($\le 3$ 行)。
- 结果:如果匹配,则立即继承祖先的
lineage_id,无需 LLM 或 embedding 开销。
- 精确内容身份签名匹配:
-
**Tier 2(RCA 规范化):**如果 Tier 1 启发式匹配未能产生精确锚点,则合成一份标准化的 5 行根因分析(RCA)摘要:
Component:规范化的 canonical filepath 和符号。Vulnerability Class:规范 CWE 分类标识符及名称。Root Cause Mechanism:底层编程或逻辑缺陷。Failure Condition:具体的输入、状态或边界条件。Taint Dataflow:从源到汇的数据流轨迹。
-
Tier 3(向量嵌入与余弦相似度扫描):
- 使用配置的多提供商嵌入引擎,将标准化的 RCA 摘要转换为密集向量嵌入(默认:
vertex_ai/gemini-embedding-001)。 - 使用有界余弦相似度,在数据库中的历史
lineage_vectors上执行最近邻扫描。 - CWE 家族与类别结构性保护: 将查询发现项向量与候选溯源记录进行比较时,如果两个发现项都有明确的 CWE 分类(例如 CWE-89 与 CWE-78),则进行标准化。如果它们属于不同且不兼容的 CWE ID,则完全跳过候选向量比较,从结构上防止不同漏洞类别被错误合并,无论余弦相似度得分如何。
- 正向阈值($\ge 0.90$): 语义等效的发现项即使措辞、扫描器标签或代码行有所不同,也会合并到同一个祖先
lineage_id中(默认值:0.90,可通过EMBEDDING_SIMILARITY_THRESHOLD配置)。 - 负向阈值($< 0.70$): 不同的漏洞类别(例如 SQLi 与命令注入、存储型 XSS 与反射型 XSS)会保持较低的相似度,并在安全失败的同时生成新的唯一
lineage_id。
- 使用配置的多提供商嵌入引擎,将标准化的 RCA 摘要转换为密集向量嵌入(默认:
-
-
筛除当前批次中的重复发现项: 相互比较当前发现项以找出重复项。只有当两个发现项具有相同的
code_paths条目(包含行号,即带有末尾的:line),且标题相同或高度相似时,才算重复。如果多个发现项指向同一位置的完全相同缺陷,则必须合并。 同一文件中不同行上的发现项是不同的——绝不能合并。 -
映射/归约分块策略(适用于大规模处理): 如果发现项文件很多(例如,超过 20 项),请采用映射/归约方法,先按目标文件或组件对它们分组,再检查重叠,以避免超出上下文窗口限制。
-
令牌优化的归并与合并: 为尽量减少 LLM 输出令牌并防止数据丢失,不要在回复中手动重写或输出合并后的 JSON 文件。 而应遵循以下模式:
- 识别重复项: 在内部映射哪些发现与某个主要发现重复。
- 可复用的确定性脚本(带版本): 编写一个可复用的辅助脚本(例如
workspace/helpers/merge_findings.py),其第一行必须恰好为# MANTIS_HELPER_VERSION = 2。复用现有辅助脚本之前,先 grep 其前几行,查找MANTIS_HELPER_VERSION = 2;如果该标记不存在或整数不同(说明辅助脚本由较旧版本的流水线留下),则重新生成该脚本。只有标记匹配时才可复用。脚本必须遵循以下确定性规则:- 标题: 选择最全面、描述最充分的标题。
- ID: 保留所保留的主要发现的唯一
"id"。 - 严重性: 选择合并项中指定的最高严重性级别。
- 所需权限: 继承最严格的权限要求(优先级:
NONE>LOW>HIGH)。 - 攻击者位置: 继承最关键的位置要求(优先级:
EXTERNAL>INTERNAL_NETWORK>IN_CLUSTER>LOCAL>HOST_SYSTEM>SUPPLY_CHAIN>PHYSICAL_TEMPORARY>PHYSICAL_LONG_TERM)。 - 用户交互: 继承最严格的用户交互要求(优先级:
NONE>REQUIRED)。 - 代码路径: 将所有文件路径和行号收集并去重,合并到一个唯一数组中。
- 描述、缓解措施和影响: 整洁地拼接。
- 历史记录: 拼接并保留合并后各发现中的所有
"history"条目。向"history"数组追加一条符合架构要求的本次合并记录(包含"stage": "dedupe"、"action": "merge"、"details": "Merged duplicate findings: [comma-separated-ids]"、"pass_number": <current_pass_number>和"timestamp": "<current_iso8601_timestamp>")。 - 未知键和来源信息(强制): 脚本必须将所有未明确处理的键(包括
discovery_commit、repro_snapshot_id、patch_base_snapshot、possible_duplicate_of、signature、lineage_id、cwe)从主要发现复制到合并后的对象中,绝不能丢弃未知字段。如果两个发现的discovery_commit值不同,脚本必须拒绝合并(它们描述的是不同的代码版本);将它们分开保留并记录拒绝原因。合并的所有发现具有相同的discovery_commit时,必须原样保留该值。
- 执行脚本: 运行脚本,在磁盘上更新主要发现文件(
workspace/findings/<primary_id>.json)。
-
事务式暂存清理: 不要永久删除冗余文件。确保垃圾目录存在(例如,
mkdir -p workspace/findings/.trash/)。移动文件之前,脚本必须更新重复发现文件,将"status"设为"DUPLICATE",并将"duplicate_of"设为"<primary_uuid>"。将已合并的重复.json文件移至垃圾暂存目录(workspace/findings/.trash/)。每移动一个文件,都要向workspace/.tx_log.jsonl追加一条事务记录。事务日志架构格式(
workspace/.tx_log.jsonl)每一行都必须是一个独立的 JSON 对象,用于记录该事务:
{"timestamp": "2026-07-14T15:13:00Z", "action": "loop_filter | dedupe_merge", "primary_uuid": "[UUID] (or null for loop_filter)", "moved_uuid": "[UUID]"}这样可以为后续阶段清理目录,同时保留回滚能力。
完成后,通知用户。