コンテンツにスキップ

コードセキュリティ監査

duduclaw secauditをリポジトリに向けると、注意深いレビュアーがするのと同じやり方でセキュリティ上の問題を見つけ出します。高速スキャナーを実行し、エージェントにリスクの高いモジュールを読ませ、最後にもう1体のエージェントに、結果があなたに届く前に各検出結果を反証させます。

静的スキャナーは高速で確実ですが、複数ファイルにまたがる推論が必要なものは見逃します。3つのモジュールを経由してから使われる汚染された値、悪い状態へ追い込まれうるステートマシン、特定の順序でのみ壊れるビジネスルールなどです。言語モデルはそうした経路を追えますが、もっともらしく聞こえて実在しないバグをでっち上げもします。duduclaw secauditは両方を実行し、互いにチェックさせます。

プラットフォームが既に持っている仕組みを再利用します。マルチランタイムエージェント層が深い読み込みを担当し、コンテナサンドボックスがネットワークなしで概念実証コードを実行し、他の場所で使われているのと同じmaker-checkerの規律が、ここでは対抗的レビューの1パスになります。唯一自前で作っていないのが静的エンジンで、すでに信頼しているスキャナー(gitleaks、semgrep、cargo-audit、osv-scanner)をオーケストレーションするだけです。

レポートの第2版では、実行結果を信頼するために必要なものが加わりました。AIの検出結果ごとの構造化された主張、モデル呼び出しを使わずに捏造を弾く検査、実際にどのモジュールを読んだかの記録、そして最後まで終わらなかった実行への正直な表示です。

  1. インテーク/脅威モデリング(決定論的、LLM不使用)。リポジトリのプロファイルを作成し(言語構成、エントリーポイント)、セキュリティ関連のコミットが頻発するモジュールをgit履歴からマイニングします。これにより、コストのかかる後続のステップが最初からリスクの高い領域に集中できます。

  2. 静的スキャン。 インストール済みのスキャナーを実行し、その出力を1つの検出結果フォーマットに正規化します。インストールされていないスキャナーは、理由付きで「不足」として報告され、黙ってスキップされることはありません。

  3. AI深度監査(--profile deep)。固定のプロンプト予算内で、ランク付けされた各モジュールを読み込みます。モデルに送るファイル抜粋にはすべて行番号が付き(<n> | code)、モデルは自分で行を数えず、その番号をそのまま回答に写す必要があります。モデルは厳密なJSON配列を1つだけ返す必要があります。前後に説明文が付いた返答や、JSON値が2つある返答は、修復されずにそのモジュールの回答ごと破棄されます。各候補には次の記入が必須です。

    • 6つのスロットを持つ脅威モデル:principal(誰が攻撃するか)、input(何を制御できるか)、control(本来止めるはずだったチェック)、boundary(越えた信頼境界)、affected(何が被害を受けるか)、result(何が起きるか)。空欄や不可視文字だけのスロットがあると、その候補は成立しません。
    • entrypointから複数のpropagationを経てsinkに至るtrace。各ステップにファイル、行、スコープが付きます。
    • 攻撃の成立に必要なconditions(認証レベル、ユーザー操作、システム設定など)。

    プロンプトには深刻度のアンカーとアンチパターン一覧も含まれ、「この関数は危険そうに見える」だけでHighと報告されることを防ぎます。深刻度は影響の大きさだけを表し、確からしさは後続のレビューステップが別に扱います。

  4. 決定論的な事前チェック(LLM不使用)。レビューに回す前に、各候補をリポジトリ自体と照合します。ファイルパスは安全な相対パスで、かつ読み込んだモジュールに属していること。行がそのファイルに実在すること。traceの全ステップが実在するファイルと行を指すこと。脅威モデルの6スロットに可視テキストがあること。通らなかった候補はその場で反証され、違反内容がレポートに記録され、レビュアーには届きません。でっち上げのパスや行番号はここで弾かれます。

  5. 対抗的レビュー。 事前チェックを通った候補は、最初のパスの文脈を一切持たない新しいエージェントに渡され、反証するよう指示されます。レビュアーには判断に十分な材料が渡されます。主ファイルの報告行まわりの広い範囲(最大24 KiB)、他のtraceステップ周辺のコード、脅威モデル、conditionsです。回答はrefutedかplausibleです。refutedは「主張された欠陥がそこにない」場合にだけ使われます。欠陥は実在するが報告された位置が違う場合、レビュアーはcorrected_lineやcorrected_traceを付けてplausibleを返します。この修正は適用前にもう一度リポジトリと照合され、検出結果のidとroot fingerprintは変わらず、verifier_corrected: line 10→14という証拠が変更を記録します。plausibleとする場合は、自動確認できない理由(blockers)と、人間が検証する方法(validation_plan、ローカルおよび/またはデプロイ環境)を示す必要があり、どちらかが欠けるとその判定は差し戻され、候補は候補のまま残ります。静的スキャナーの検出結果はこのステップをスキップします。それらはすでに決定論的な証拠だからです。

  6. 概念実証(PoC)(--poc、High以上の深刻度のみ)。PoCを生成し、コンテナサンドボックス内で実行します(ネットワークなし、tmpfs、ハードタイムアウト)。コンテナランタイムが利用できない場合はPoCを「スキップ」として記録し、ホスト上で実行することは決してありません。

ランク付けで得られたすべてのモジュールに1件のカバレッジ記録が付きます。--max-modulesで対象外になったものも含みます。ステータスはcovered(読んだ、候補なし)、candidate(読んだ、候補1件以上)、deferred(--max-modules、候補数の上限、またはAIエンジンに到達できないためレビューせず)、unreadable、llm_failed、parse_failedのいずれかです。読まれなかったモジュールが1つでもあれば、サマリーに「部分カバレッジ:Nモジュール未レビュー」と表示されます。一部しか読んでいないリポジトリでの結果オールグリーンは「読んだ範囲では何も見つからなかった」という意味で、レポートもそのように書きます。

実行そのものにはrun_statusがあり、completeかincompleteです。未完了の実行にはincomplete_reasonが付きます。たとえばengine_unavailable(AIエンジンの呼び出しが失敗。まだ試していないモジュールは同じ理由でdeferredになります)やvalidation_budget_exhausted(候補数の上限に到達)です。quickプロファイルにはAIステップがないため、常にcompleteです。

デフォルトでは、同じリポジトリの最新の保存済みレポート(<home>/secaudit/reports/)を読み込み、行番号・スニペット・深刻度を含まない安定したフィンガープリントで検出結果を照合します。そのため周囲のコードが動いても同じ検出結果と認識できます。引き継ぎはファイルの内容ハッシュが変わっていない場合に限られます。

  • 以前に抑制または反証され、ファイルも同じ:判定をそのまま引き継ぎ、モデル呼び出しは使いません。
  • 以前に確認済みで、ファイルも同じ:もう一度レビューし、レポートは「再検証した以前の確認項目」として数えます。
  • ファイルが変わった:引き継がず、その件数をレポートに記録します。

最新の以前のレポートが読めない場合やv1形式の場合はスキップされ、stderrに1行出力されたうえで、より古いファイルの探索が続きます。--no-priorでこの仕組み全体を無効にできます。レポートのprior_runブロックには、どのファイルから何件ずつ引き継いだかが記載されます。

duduclaw secaudit . # クイック: スキャナーのみ
duduclaw secaudit . --profile deep --max-modules 5 # + AI監査とレビュー
duduclaw secaudit . --poc --fail-on high --save # + サンドボックス化されたPoC, レポート保存
duduclaw secaudit . --profile deep --verifier-agent reviewer-bot --fail-on-needs-human
duduclaw secaudit . --profile deep --no-prior # 以前のレポートを引き継がない
duduclaw secaudit-validate report.json # 保存済みレポートを検証

--fail-onのしきい値(デフォルトhigh)に達するものが何もなければexit codeは0、何か達すれば1、インフラエラーの場合は2です。スキャナーが1つもインストールされていないマシンでも0で終了するため、CIにそのまま組み込めます。反証済みおよび抑制済みの検出結果はレポート上に見えたまま残りますが、深刻度の統計やゲート判定にはカウントされません。対抗的レビューは、ビルドを失敗させるノイズを実際に減らします。

人間の判断待ちの項目。 レビューがplausibleと判定した検出結果はneeds_humanになります。その深刻度はモデルが自己申告したもので、レポートではmodel_self_reportedと表示されます。これらはneeds_human_by_severityとして別集計され、ダッシュボードでも分けて表示され、デフォルトではビルドを失敗させません。CIも止めたい場合は--fail-on-needs-humanを付けると、これらはby_severityに合算され、needs_human_by_severityは0になり、ゲートが見る表は1つになります。モデルによる深刻度の推測は証拠ではないので、デフォルトでパイプラインを壊すべきではない、という考え方です。厳格にするかどうかはあなたが選べます。

誰がレビューするか。 デフォルトでは、レビュアーは深度監査を行ったのと同じエージェントで動くため、同じモデルが自分の仕事を採点している可能性があります。--verifier-agent <id>を渡すと、レビューとPoCのステップを別のエージェント(独自のランタイムとモデル)で実行できます。レポートのverifierブロックにはindependenceがsame_agent、different_agent、not_runのいずれかで記録され、両方のエージェント名も載ります。ツールは別モデルを強制せず、その選択をレポート上で見えるようにします。

レポートの検証。 レポートの書き出し前(--reportまたは--save)に、バリデーターが検査します。idが一意であること、ファイルパスが安全であること、反証済みの項目にレビューの証拠があること、needs_humanの項目にblockersがあること、サマリーの数値が検出結果と一致すること、各カバレッジ記録が実在する検出結果を指していること、未完了の実行に理由が書かれていること、などです。verifier_correctedの記録を持つ検出結果は、事前チェックを通過している必要があります。事前チェックですでに反証された項目には、ファイルと行のルールは適用されません(位置の不備が反証の理由だからです)。重複した検出結果のidは検査前に統合され、stderrにメモが出ます。違反があれば一覧を表示して2で終了し、壊れたレポートは書き出しません。duduclaw secaudit-validate <report.json>は保存済みファイルに同じ検査を行い、exit 0が正常、1が違反あり(1行ずつ表示)、2が読み込み不可またはJSONでない場合です。v2より前のレポートにはschema_versionがなく、バージョン1として扱われ、バリデーターは受け付けません。

--saveはレポートを<home>/secaudit/reports/に書き込み、ダッシュボードがそこから拾い上げます。

セキュリティ修正。 以前のバージョンは、モデルが候補で返したファイルパスを、検証せずにリポジトリのルートへ連結していました。deepモード(対抗的レビューとPoCのステップ)では、絶対パスのような値で読み込み先をリポジトリの外に向けられました。現在は、モデルが返したすべてのパスが、ファイルを開く前に検証されます。許可されるのは相対パスのみで、..、ドライブやUNCのプレフィックス、制御文字、Windowsの予約名は拒否されます。

セキュリティ監査ページは保存済みレポートを一覧表示します。1つ開くと、最上部に次が表示されます。

  • 未完了バナー:run_statusがincompleteの場合に、理由付きで表示されます。
  • カバレッジカード:モジュールの総数、レビュー済み、候補、延期、失敗。読まれなかったモジュールがあれば「部分カバレッジ」と表示されます。
  • レビュアーチップ:「同一エージェントによるレビュー」または「独立エージェントによるレビュー」。結果を引き継いだ場合は引き継ぎチップも出ます。

検出結果は深刻度別にグループ化され、件数の下に「人間の判断待ち(モデルの自己評価、fail-onには含まれません)」の行があります。各検出結果は展開でき、証拠チェーン(静的スキャンのヒット、AIの推論、対抗的レビューの判定、PoCの実行記録)に加え、AIの検出結果では脅威モデルの6スロット、trace、conditions、blockers、検証計画、事前チェックの違反一覧が表示されます。モデルが自分で選んだ深刻度の横には「モデル自己評価」の小さな注記が付きます。各検出結果には3つのレビューアクション(確認・抑制・反証)があり、レポートに書き戻されます。操作はoperator_reviewの証拠として保存され、その項目のseverity_basisはoperatorになり、深刻度の集計も再計算されるため、レビュー済みのレポートもsecaudit-validateを通ります。このページはmanagerでゲートされています。新しいフィールドを持たないv1レポートも、そのまま開けます。

監査自身が検出結果を「確認済み」にすることは決してありません。もっともらしいものはセキュリティ監査ページで、あなたの判断を待ち続けます。オープンソースの依存関係を監査している場合、脆弱性を上流に報告するのは対外的な行為であり、これは人間のステップのままです。