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(現在のループのパスを追跡するため)。
- 書き込み対象:
- 重複する検出結果に
"status": "DUPLICATE"と"duplicate_of"を設定した後、workspace/findings/.trash/に移動します。 - 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の Scenario 2 におけるハーネス側の責務です):active_snapshotが存在し、かつactive_snapshot.pass != state.pass_numberの場合、このパスではスナップショットを STALE として扱います。STOP "stale active_snapshot: pass mismatch" とするか、HALT に切り替えます(snapshot_pinnedは実質的に false となり、権威ある判定は行われず、Block B は NOT_MATCHED、再現はnot_attemptedとなります)。これは、Stage 15 のパス番号のインクリメント後に再ピン留めせずactive_snapshotを保持してしまったカスタムハーネスを検出します。リファレンスのメタエージェントは各パスで再ピン留めするため、このチェックが発動することはありません。Block B 自体ではこれを検出できません(snapshot_idのみを確認し、passは認識しません)。
注:workspace/findings/、workspace/archive/、workspace/.tx_log.jsonl、
workspace/helpers/ および .mantis_state.json は STATE-RELATIVE です(--state_root の下にあります)。検出結果のために調査するコードスニペットはすべて SNAPSHOT-RELATIVE です(CODE_ROOT の下にあります)。CODE_ROOT の下には決して書き込まないでください。
セキュリティ検出結果のリストを確認し、まったく同じセキュリティ上の欠陥、または隣接するコードパスに関する重複した検出結果をマージします。
次の手順で作業を実行します。
-
生の検出結果とアーカイブ済み検出結果のキューを読み込む:
workspace/findings/ディレクトリの内容を一覧表示し、ファイルを読み取ります。ディレクトリが空、または存在しない場合は、ユーザーに通知して終了します。- 重要: 一覧表示または処理の際、隠しファイルと隠しディレクトリ(
.trash/サブディレクトリなど)は無視してください。 - 存在する場合は、以前のループパスからアーカイブされたすべての検出結果 JSON ファイルを、
workspace/archive/findings_pass_*/*.jsonおよびworkspace/archive/loop*_findings/*.jsonの下から検索して読み込みます。これらのファイルは、以前のパスですでに十分に評価、トリアージされ、場合によっては修正された脆弱性を表します。 - 重要:
workspace/historical_learnings.jsonl(VCS 履歴)は読み取ったり、重複判定の基準にしたりしないでください。古いバグが再導入された場合に検出するためです。
-
ループ内の重複をフィルタリングする(スナップショットによるゲート付き)。 まず、
discovery_commitがないすべての現在の検出結果に、.mantis_state.jsonのactive_snapshot.snapshot_idを設定します(ピン留めされていない場合はスキップします)。次に、アーカイブ済みの検出結果と一致する各現在の検出結果について(code_pathsとtitleの類似度に基づく)、次の処理を実行します。シグネチャベースの候補照合(フェーズ 3)— 条件を厳しくするものであり、置き換えるものではありません:
signatureによってペアを「ペア単位のスナップショット照合の候補」に昇格させることはできますが、signatureだけを根拠に確定的な重複またはゴミ箱行きにしてはなりません。ペアが以下の両方を満たす場合にのみ、次の「ペア単位のスナップショット照合」の候補となります。- 今日の
code_pathsとtitleの類似度に基づく照合で一致すること。code_pathsのエントリーは行番号を含めて(末尾の:lineを含めて)比較します。 - 両方の検出結果に
signatureがある場合、そのsignatureフィールドが一致すること。code_pathsとtitleの一致がないsignatureの一致は、重複ではありません。ソフトなpossible_duplicate_ofにできる場合でも(検出結果は ACTIVE のまま維持し)、ゴミ箱行きにはしないでください。理由:signatureは行番号と、先頭以外のすべてのcode_pathsエントリーを取り除くため、同じファイル内にある異なるバグ(例:parser.c:100とparser.c:900)が同じタイトルと CWE を持つと、同一のsignatureになります。signatureだけを根拠にゴミ箱行きにすると、実在する検出結果が気付かれないまま削除されます。いずれか一方にsignatureがない場合は、今日のcode_pathsとtitleによる類似度照合を変更せずに使用します。
ペア単位のスナップショット照合(MATCHED と NOT_MATCHED を判定)— このペアについて、現在の検出結果の
discovery_commitとアーカイブ済みの検出結果のdiscovery_commitを比較します(SNAPSHOT_IDとは比較しません)。snapshot_pinnedが false であり、かつ state にactive_snapshotが存在しない場合 (MODE-OFF) -> NOT_MATCHED。停止します。(HALT の場合 —active_snapshotは存在するがsnapshot_pinned=false— ここでショートサーキットしないでください。以下の ペアワイズ比較へフォールスルーします。これは NOT_MATCHED になります。というのも、現在の finding のdiscovery_commitはlive:id であり、アーカイブされたものと 等しくならないからです。)- 現在の finding の
discovery_commitと、アーカイブされた finding のdiscovery_commitを読み取ります:- どちらかが欠落しているか、空であるか、リテラル
"MIXED"である場合 -> NOT_MATCHED。 - それらが互いにバイト単位で等しくない場合 -> NOT_MATCHED。
- それらが互いにバイト単位で等しい場合 (両方とも存在し、MIXED でない)
-> MATCHED。MATCHED への他の経路はありません。決してファジー比較しないでください。
グローバルな「フィールドをデフォルトにして続行する」後方互換ルールは
discovery_commitには適用されません: 欠落 = NOT_MATCHED。(別個の 「dirty」ゲートはありません: dirty なツリーの SNAPSHOT_ID はすでにワーキングツリーの コンテンツハッシュを埋め込んでいるため、パス内の finding は MATCH し、パスをまたぐベアコミットの finding はそうなりません。)注: これはペアワイズチェック (現在 vs アーカイブ) であり、 グローバルなSNAPSHOT_IDに対するチェックでは ありません — dedupe は現在の finding のdiscovery_commitを上記の Step 2 でSNAPSHOT_IDに スタンプするため、SNAPSHOT_IDに対するチェックは常に MATCHED となり、 再導入された/リグレッションのバグを DUPLICATE として破壊してしまいます。
- どちらかが欠落しているか、空であるか、リテラル
次に、冪等性ルール (Input/Output Contract → Idempotency Guarantee) を使用して二重書き込みを避け、機械的に判断します:
- MATCHED (両方とも存在し等しい): 現在の finding をこれまでと同様に
ループ重複としてソフト削除します —
"status": "DUPLICATE"と"duplicate_of": "<archived_uuid>"を設定し、存在する場合はpossible_duplicate_ofをクリアし、mkdir -p workspace/findings/.trash/を実行し、そこへ移動し、workspace/.tx_log.jsonlにloop_filterトランザクションをログ記録します。現在の finding にlineage_idがなく、アーカイブされた finding にそれが存在する場合、 移動する前にアーカイブされた finding のlineage_idを現在の finding に継承させます (マージをまたいでリネージチェーンが保持されるように)。 - NOT_MATCHED (異なる、またはどちらかが欠落):
DUPLICATEを設定せず、 ゴミ箱へ移動もしません。現在の finding を ACTIVE のままにし、"possible_duplicate_of": "<archived_uuid>"(ソフトで非終端のヒント) を設定します。 現在の finding にlineage_idがなく、アーカイブされた finding にそれが存在する場合、 アーカイブされた finding のlineage_idを現在の finding に継承させます (所見が 異なるスナップショット上にある場合でも、レポートの折りたたみのためにリネージチェーンが 保持されるように)。
重要
STATUS & DUPLICATE の不変条件:
- finding は、同じターゲット UUID を指す
duplicate_ofとpossible_duplicate_ofの 両方を保持してはなりません。 status = "DUPLICATE"は、同じターゲット UUID を指すpossible_duplicate_ofと 共存してはなりません。- NOT_MATCHED の場合 (MODE-OFF フォールバック例外を除く)、
finding の
statusはアクティブのまま (例:VALID、PROVISIONALLY_VALID、NEEDS_RESEARCH) でなければならず、duplicate_ofを設定してはならず、 また finding を.trash/へ移動してはなりません。possible_duplicate_ofの設定は非終端のヒントにすぎません。
- POSSIBLE REGRESSION: アーカイブされた一致項目のステータスが RESOLVED の場合
(
patch_statusが {VERIFIED_SECURE,MITIGATION_PROPOSED} に含まれる、またはstatus==FALSE_POSITIVE、またはproduction_viability==NON_VIABLE)、かつその ペアが MATCHED でない場合、現在の検出事項を ACTIVE のまま維持し、履歴ノート "POSSIBLE REGRESSION vs <archived_uuid>" を追加し、フィルタリングしないでください。(新しいコードで 再発見されたリバート済みの修正は、決して破棄してはなりません。) - EXACT-UUID 再試行の例外 (変更なし): 現在の検出事項がアーカイブされたものと まったく同じ UUID を持つ場合、それは再試行のために意図的にコピーして戻されたものです — フィルタリングしないでください (そのまま維持)。
- 恒久的に未ピン留めの例外 (MODE-OFF のみ —
active_snapshotなし): state にactive_snapshotが存在しない場合 (MODE-OFF = 現在のデフォルト。スナップショット境界を 持たないターゲット、たとえばライブエンドポイント)、snapshot_pinnedは PASS レベルで false になり (active_snapshot.snapshot_pinned— これは 検出事項ごとのフィールドではありません)、Block B は情報を与えません — 存在する場合はsignatureによる 現在の dedup にフォールバックし、そうでなければstable_key= normalized_title + 最初のcode_pathsエントリ (末尾の:lineを含む) (行を含む、Step 2 と同じ) を使用します。根拠::lineを除去すると、同じファイル内の 2 つの DISTINCT なバグ (たとえばparser.c:100とparser.c:900) が同じ title+CWE で 1 つに統合され、実際の検出事項が黙って削除されてしまいます。注意:signature自体は 設計上:lineを除去します (これはクロスパス系譜のための粗い識別子であり、 dedup キーではありません)。したがってこのフォールバックは、stable_keyの 行を含む一致も ALSO 同意する場合にのみsignatureを優先し、signature単独では決して優先しません。これは MATCH し得ないターゲットの dedup を保持します。active_snapshotが存在するが未ピン留めの場合 (HALT モード)、この例外は発動しません: 上記のスナップショットでゲートされる動作を維持してください (NOT_MATCHED → ACTIVE +possible_duplicate_ofを維持、DUPLICATEには決してしない)。 POSSIBLE REGRESSION はこのフォールバックより優先されます: resolved-archived の 検出事項と NOT_MATCHED の現在の検出事項がペアになった場合は、 モードに関係なく ALWAYS アクティブに維持されます (決して破棄されません)。
- 今日の
-
3 層の重複排除ラダー (決定論的 & セマンティックマッチング): 検出事項の系譜を 解決し、重複排除候補を評価する際、deduplicator はサブミリ秒の ファストパス実行とセマンティック RAG フォールバックのために設計された 階層的な 3 層ラダーを使用します:
-
Tier 1 (ファストパス 完全一致ヒューリスティックアンカー — < 1ms、0 トークン):
- 完全なコンテンツ同一性 signature 一致:
hashlib.sha256(canonical_fp | canonical_cwe | target_symbol)(行のずれ、バッククォート、タイトルの言い換えに対して不変)。 - 完全な
canonical_filepath + normalized CWE + target_symbol一致。 - target symbol が空の場合、完全な canonical filepath 上での厳密な行近接ウィンドウ ($\le 3$ 行)。
- 結果: 一致した場合、LLM や埋め込みのオーバーヘッドなしで
祖先の
lineage_idを即座に継承します。
- 完全なコンテンツ同一性 signature 一致:
-
Tier 2 (RCA 正規化): Tier 1 のヒューリスティックマッチングが 完全なアンカーを生成しない場合、標準化された 5 行の Root Cause Analysis (RCA) サマリーを合成します:
Component: 正規化された canonical filepath と symbol。Vulnerability Class: canonical CWE タクソノミー識別子と名前。Root Cause Mechanism: 根本的なプログラミングまたは論理の欠陥。Failure Condition: 特定の入力、状態、または境界条件。Taint Dataflow: source から sink へのデータフローの軌跡。
-
Tier 3 (ベクトル埋め込み & コサイン類似度スキャン):
- 設定されたマルチプロバイダー埋め込みエンジン(デフォルト:
vertex_ai/gemini-embedding-001)を使用して、標準化された RCA サマリーを 密なベクトル埋め込みに投影します。 - データベース内の過去の
lineage_vectorsに対して、上限付きコサイン類似度を 用いた最近傍スキャンを実行します。 - CWE ファミリーおよびクラスの構造的ガード: クエリの所見ベクトルと候補の リネージレコードを比較する際、両方の所見に明示的な CWE 分類(例: CWE-89 と CWE-78)がある場合は、それらを正規化します。それらが異なる互換性のない CWE ID に属する場合は、候補ベクトルの比較を完全にスキップし、コサインスコアに かかわらず、異なる脆弱性クラスの誤統合を構造的に防止します。
- 正のしきい値($\ge 0.90$): 表現、スキャナーラベル、行のずれが異なる
ものの意味的に等価な所見は、同じ祖先
lineage_idに統合されます (デフォルト: 0.90、EMBEDDING_SIMILARITY_THRESHOLDで設定可能)。 - 負のしきい値($< 0.70$): 異なる脆弱性クラス(例: SQLi とコマンド
インジェクション、Stored XSS と Reflected XSS)は低い類似度を維持し、
フェイルクローズして、新しい一意の
lineage_idを生成します。
- 設定されたマルチプロバイダー埋め込みエンジン(デフォルト:
-
-
現在のバッチ内の重複所見をフィルタリング: 現在の所見を相互に照合して 重複を検出します。2 つの所見が重複であるのは、それらが同じ
code_pathsエントリを行を含めて(末尾の:lineを含む)共有しており、かつタイトルが 同一または非常に類似している場合のみです。複数の所見が同じ場所のまったく同じ 欠陥を指している場合は、それらを統合する必要があります。同じファイル内の異なる 行にある所見は別個のものであり、決して統合しないでください。 -
Map/Reduce チャンキング戦略(規模対応): 所見ファイルが多い場合(例: 20 件超)、コンテキストウィンドウの制限を回避するため、重複をチェックする前に 対象ファイルまたはコンポーネントごとにグループ化する Map/Reduce アプローチを 使用します。
-
トークン最適化された統合とマージ: LLM の出力トークンを最小化し、データ 損失を防ぐため、レスポンス内でマージされた JSON ファイルを手動で書き換えたり 出力したりしないでください。 代わりに、次のパターンに従ってください:
- 重複の特定: どの検出結果が主となる検出結果の重複であるかを内部で対応付けます。
- 再利用可能な決定論的スクリプト(バージョン管理): 再利用可能なヘルパースクリプト(例:
workspace/helpers/merge_findings.py)を作成します。その FIRST LINE は正確に# 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]"}これにより、ロールバック機能を維持しながら、後続のステージで使用するディレクトリをクリーンアップできます。
完了したら、ユーザーに通知します。