跳到內容

程式碼安全稽核

把 duduclaw secaudit 指向一個 repository,它會像一個仔細的審查者一樣揪出安全問題:先跑快速掃描器,再讓一個 agent 讀有風險的模組,最後讓第二個 agent 在結果送到你面前之前試著推翻每一個發現項目。

靜態掃描器又快又準,但抓不到需要跨檔案推理的問題:一個受污染的值流過三個模組才被用到、一個狀態機可能被逼進壞的狀態、一條商業邏輯只在特定順序下才會出錯。語言模型能跟著這些路徑走,卻也會編出聽起來合理、其實不存在的 bug。duduclaw secaudit 兩種都跑,還讓它們互相檢查。

它重用平台原本就有的機制:multi-runtime agent 層負責深度閱讀,容器沙盒在沒有網路的環境跑概念驗證程式碼,別處用的同一套 maker-checker 紀律,在這裡變成一次對抗式覆核。它唯一沒有自己蓋的是靜態引擎,直接指揮你已經在信任的掃描器(gitleaks、semgrep、cargo-audit、osv-scanner),沒有重新發明一遍。

第 2 版報告多了幾樣讓人敢信任一次稽核的東西:每個 AI 發現項目都有結構化的主張、一道不花模型呼叫就能擋掉捏造內容的檢查、一份實際讀過哪些模組的紀錄,以及稽核沒跑完時的誠實標示。

  1. 情蒐與威脅建模(確定性、不用 LLM)。替 repo 建立輪廓(語言組成、進入點),並從 git 歷史挖掘經常出現安全相關 commit 的模組,讓後面比較貴的步驟先聚焦在風險高的地方。

  2. 靜態掃描。 跑機器上已安裝的掃描器,把輸出統一成同一種發現項目格式。沒裝的掃描器會被回報成缺少並附上原因,不會被悄悄跳過。

  3. AI 深度稽核(--profile deep)。在固定的 prompt 預算內逐一讀取排序後的模組。送給模型的每段檔案內容都帶行號(<n> | code),模型必須照抄這些行號,不能自己數行。模型必須只回一個嚴格的 JSON 陣列;回覆前後夾了說明文字、或是出現兩個 JSON 值,該模組的答案整份作廢,不會替它修補。每個候選項目都要填:

    • 威脅模型六格:principal(誰在攻擊)、input(他能控制什麼)、control(本來該擋住他的檢查)、boundary(跨過的信任邊界)、affected(誰受害)、result(結果是什麼)。空白或只有看不見字元的格子,整個候選項目就不成立。
    • trace:從 entrypoint 經過若干 propagation 到 sink,每一步有檔案、行號與 scope。
    • conditions:攻擊成立所需的前提(驗證層級、使用者互動、系統設定等)。

    prompt 也帶有嚴重程度的錨點與一份反模式清單,避免「這個函式看起來很危險」就被報成 High。嚴重程度只談影響,可信度由後面的覆核步驟另外處理。

  4. 確定性前置檢查(不用 LLM)。送去覆核之前,先拿 repo 本身驗證每個候選項目:檔案路徑必須是安全的相對路徑,而且屬於剛才讀過的那個模組;行號必須真的存在於該檔案;trace 的每一步都要指向真實的檔案與行號;威脅模型六格都要有看得見的文字。沒通過的候選項目當場被推翻、違規原因記進報告,不會送到覆核者手上。捏造的路徑與行號就是在這裡被擋下。

  5. 對抗式覆核。 通過前置檢查的候選項目交給一個全新的 agent,不帶第一輪的任何脈絡,任務是推翻它。覆核者現在拿到足夠的材料:主檔案報告行號周圍一大段(最多 24 KiB)、trace 其他每一步附近的程式碼、威脅模型與 conditions。它回答 refuted 或 plausible。refuted 只用在「所說的缺陷根本不存在」。如果缺陷是真的、只是回報的位置不對,覆核者會判 plausible 並附上 corrected_line 和/或 corrected_trace。這些修正在套用之前會再拿 repo 重新檢查一次;發現項目保留原本的 id 與 root fingerprint,並多一筆 verifier_corrected: line 10→14 的證據記錄這次更動。判 plausible 時必須寫出為什麼不能自動確認(blockers),以及人要怎麼驗證(validation_plan,本機和/或實際部署);兩者缺一,這個判定被退回,候選項目維持候選。靜態掃描器的發現項目跳過這一步,它們本來就是確定性的證據。

  6. 概念驗證(PoC)(--poc,僅限 High 以上嚴重程度)。產生一份 PoC,放進容器沙盒裡跑(沒有網路、tmpfs、有硬性逾時)。找不到容器 runtime 時就把這個 PoC 記成已跳過,絕不會改到 host 上跑。

排序出來的每個模組都有一筆覆蓋紀錄,連 --max-modules 排除在外的也有。狀態有六種:covered(讀過、零候選)、candidate(讀過、至少一個候選)、deferred(因 --max-modules、候選數上限或 AI 引擎連不上而沒審)、unreadable、llm_failed、parse_failed。只要有模組沒讀到,摘要就會印出「部分覆蓋:N 個模組未審」。對一個只讀了一部分的 repo,結果全綠的意思是「讀過的部分沒發現問題」,報告會照這個意思寫。

稽核本身有 run_status:complete 或 incomplete。沒跑完的會附 incomplete_reason,例如 engine_unavailable(AI 引擎呼叫失敗,還沒試到的模組會標成 deferred,原因同為此項)或 validation_budget_exhausted(候選數上限用完)。quick profile 沒有 AI 步驟,永遠是 complete。

預設情況下,稽核會讀同一個 repository 最新的一份已儲存報告(在 <home>/secaudit/reports/),用一組穩定指紋比對發現項目。這組指紋不含行號、程式碼片段與嚴重程度,所以程式碼在周圍移動時,同一個發現項目仍認得出來。只有檔案內容雜湊值沒變時才承接:

  • 先前被抑制或被推翻,檔案也沒變:沿用原判定,不花任何模型呼叫;
  • 先前已確認,檔案沒變:重新覆核一次,報告把它計成「重新驗證的先前確認項」;
  • 檔案變了:不承接,報告記下有幾個。

最新的先前報告若讀不了、或是第 1 版格式,會略過並在 stderr 印一行,接著往更舊的檔案找。--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。一台完全沒裝掃描器的機器一樣回 0,所以能乾淨接進 CI。被推翻與被抑制的發現項目仍然留在報告裡看得到,但不計入嚴重程度統計或關卡判定,所以對抗式覆核其實是在降低會讓建置失敗的雜訊。

留給人看的項目。 覆核判 plausible 的發現項目狀態是 needs_human。它的嚴重程度是模型替自己報的,報告標成 model_self_reported。這類項目另外統計在 needs_human_by_severity,儀表板也分開顯示,預設不會讓建置失敗。想讓它們也擋 CI,加上 --fail-on-needs-human,這些項目會併入 by_severity、needs_human_by_severity 歸零,關卡只看一張表。道理很直接:模型對嚴重程度的猜測不算證據,預設不該拿它弄壞你的流程,但要嚴格由你決定。

誰來覆核。 預設情況下,覆核者和做深度稽核的是同一個 agent,也就是同一個模型可能在批改自己的作業。用 --verifier-agent <id> 可以讓覆核與 PoC 步驟改由另一個 agent(自己的 runtime 與模型)執行。報告的 verifier 區塊把 independence 記成 same_agent、different_agent 或 not_run,並寫出兩邊的 agent。工具不會強迫換模型,只是把這個選擇攤在報告上。

報告驗證。 報告寫出之前(--report 或 --save),驗證器會先檢查:id 不重複、檔案路徑安全、被推翻的項目有覆核證據、needs_human 的項目有 blockers、摘要數字與發現項目對得上、每筆覆蓋紀錄指向真實存在的發現項目、沒跑完的稽核寫明原因。帶有 verifier_corrected 記錄的發現項目必須通過過前置檢查。已被前置檢查推翻的項目不套用檔案與行號規則(位置有問題正是它被推翻的原因)。重複的發現項目 id 會先合併再檢查,並在 stderr 留一行說明。只要有違規,就印出來並以 2 結束,不會寫出一份壞報告。duduclaw secaudit-validate <report.json> 對已存檔案跑同一套檢查:exit 0 合法、1 有違規(逐條列出)、2 讀不到或不是 JSON。第 2 版以前的報告沒有 schema_version,視為第 1 版,驗證器不接受。

--save 會把報告寫進 <home>/secaudit/reports/,儀表板會從這裡撿起來。

安全修正。 舊版會把模型在候選項目裡回傳的檔案路徑直接接到 repository 根目錄後面,沒有驗證。在 deep 模式(對抗式覆核與 PoC 步驟)下,像絕對路徑這種值就能讓讀檔跑到 repository 外面。現在每個模型回傳的路徑在開檔之前都會先驗證:只允許相對路徑,不得含 ..、磁碟機或 UNC 前綴、控制字元、Windows 保留名稱。

安全稽核頁列出已儲存的報告。打開一份,最上面會看到:

  • 未完成橫幅,run_status 是 incomplete 時顯示,附原因;
  • 覆蓋卡:模組總數、已審、候選、延後、失敗,有模組沒讀到時標「部分覆蓋」;
  • 覆核者標籤,顯示「同一 agent 覆核」或「獨立 agent 覆核」,有承接先前結果時另有承接標籤。

發現項目依嚴重程度分組,計數列下方另有一列「待人工判斷(模型自評,不計入 fail-on)」。每個發現項目可展開證據鏈(靜態掃描命中、AI 推理過程、對抗式覆核的判定、任何 PoC 執行紀錄),AI 發現項目還會多出威脅模型六格、trace、conditions、blockers、驗證計畫與前置檢查的違規清單。嚴重程度是模型自己選的,旁邊會有「模型自評」小字。每個發現項目帶三個覆核動作:確認、抑制、反駁,會寫回報告:動作以一筆 operator_review 證據存下,該項目的 severity_basis 改為 operator,嚴重程度統計也會重算,所以覆核過的報告仍能通過 secaudit-validate。這一頁受 manager 把關。沒有任何新欄位的第 1 版報告一樣能正常打開。

稽核本身從不會把一個發現項目標成已確認;看起來可信的項目會停在安全稽核頁等你決定。如果你稽核的是一個開源依賴,把漏洞回報給上游是一個對外的動作,這一步留給人。