FR-134 · 需求討論稿 · 2026-10-10

讓不懂程式的顧問,自己看得出專案有多危險

上傳源碼包或現成 SBOM,既有沙箱裡跑 Syft/Trivy/Grype,產出白話報表

顧問輔導客戶時,專案弱點本該由客戶工程師盤點回報,實務上常被一句「那個不嚴重」帶過。本案讓顧問不靠工程師就能拿到一份有工具原始輸出背書的弱點報表:產出 SBOM(軟體物料清單,列出程式用了哪些第三方套件與版本)、多工具比對弱點、事實統計(不替任何人下結論)、歷次比對、可交付的 PDF/DOCX。工具一律包現成的,功夫花在沙箱隔離、離線弱點資料、把三家工具的輸出攤在同一列。第一版只收上傳(源碼壓縮檔與 SBOM 檔),掃描工具併進既有沙箱、容器數不變;Git 來源列第二版。本稿盤點現況、畫出架構,拆成八個階段,記錄 14 項決策定案。

14 項全數定案(D2/D10/D11 移未來項) 已定:工具併進既有沙箱,容器數不變 已定:不做燈號,只給事實統計 已定:弱點資料更新包(連網自動、封閉手動) 已定:第一版只收上傳,Git 列第二版 已定:SBOM 同時支援 CycloneDX 與 SPDX 已定:只做純靜態解析,缺 lock 檔列為發現 兩種輸入:源碼包/SBOM 檔 八階段 A~H 全在第一版 一個核心、兩個入口,同版交付 已接受取捨:拆檔與掃描共用一顆沙箱

狀態:討論稿定稿(14 項定案,見 design.md) | 日期:2026-10-10 | 白話需求草稿:draft-requirement.md

§1

分工概述(30 秒版)

整案一句話:顧問把客戶的原始碼壓縮檔或現成的 SBOM 上傳進系統,十幾分鐘後拿到一份看得懂、工程師賴不掉的弱點報表;三個月後再掃一次,修了沒一眼看到。

給非工程讀者的背景:現代程式九成以上是拼裝現成的第三方套件。套件被發現有安全漏洞(弱點)時,用到它的程式就跟著有風險。SBOM 是一份「這支程式用了哪些套件、哪個版本」的清單;拿這份清單去比對公開的弱點資料庫,就知道哪裡有洞、多嚴重、有沒有修正版。市面上的工具都會做這件事,但產出全是給工程師看的代號與分數。本案補的是「給顧問看的那一層」。

八個階段:A 先做,B 與 C 可平行,C 之後 D/E/H 三線平行

A 把掃描工具裝進既有沙箱(不新增容器);B(弱點資料更新包)與 C(後端核心)可同時開工;D(畫面)、E(報表)、H(檢測工具清單多一個工具)都接在 C 之後、彼此平行;F(歷次比對)要 C 與 D;G 收尾文件等全部完成。獨立模組與檢測工具兩個入口同一版交付。

階段 做什麼 產出 完成怎麼判定(決策者檢查法)
A 沙箱擴充 掃描工具 Syft、Trivy、Grype 裝進既有的檔案沙箱(不新增容器);加一個放弱點資料的儲存區;沙箱的加固設定抽成共用樣板 sandbox image 加工具、vulndb volume、compose 樣板、守衛測試擴充 進沙箱跑 syft version、trivy --version、grype version 三支都有;在沙箱裡 curl https://github.com 會失敗;守衛測試全綠
B 弱點資料更新 build 機產出一包有簽章的「弱點資料更新包」;安裝包隨帶一份;封閉網路手動匯入,連網客戶在設定頁按「檢查更新」或排程自動下載同一份包 更新包、簽章驗證、匯入指令 guidantai vulndb import、設定頁檢查更新、升級流程 在拔網路線的機器上匯入更新包 → 系統設定頁顯示「弱點資料日期:YYYY-MM-DD」→ 掃一個已知有洞的範例專案,弱點數不是零;把更新包改一個位元組再匯入 → 被拒
C 掃描後端 資料表、背景工作、兩種輸入路徑(源碼包、SBOM 檔)、把三家工具的結果整理成同一套格式、SBOM 多格式多版本輸出、權限與授權、API migration、job handler、正規化層、API 用 API 送一個範例源碼包、一份 SBOM 檔,兩筆都跑完,查得到元件數、弱點數、事實儀表板六區塊的數字
D 結果與操作畫面 掃描對象清單、新增精靈三頁、結果頁三層分頁、四段白話進度 前端頁面 不看任何文件,照畫面新增一筆掃描,等進度走完四段,點進結果頁看得到事實儀表板六區塊統計,弱點清單表頭顯示目前的排序規則;畫面與競品無可辨識的版面雷同
E 報表與 AI 分析 三種樣式(顧問摘要/完整技術/客戶交付)× 三種格式(PDF/HTML/DOCX);報告頁「AI 分析」按鈕接 FR-122 AI 閘道 報表產生器、一個提示樣板 匯出九種組合各一份,PDF 中文不缺字;任挑一個弱點,看得到哪個工具報的、NVD 連結、修到哪版;按「AI 分析」才出現一段標示「AI 生成」的文字,AI 呼叫紀錄多一筆
F 歷次比對 同一對象兩次掃描比:修掉/新增/仍在 比對頁、回訪報告 對同一範例專案升一個套件版本再掃,比對頁「修掉」欄出現那個弱點
G 收尾 SPEC 頁、使用手冊、安裝文件、資安規則檔補路徑 文件 手冊找得到「怎麼新增掃描、怎麼更新弱點資料、每個數字的資料來源」三件事
H 檢測工具入口 檢測工具清單裡多一個工具「SBOM 弱點掃描」,與 OpenSCAP、SonarQube 並列;整條鏈都在:建任務時可選 → 任務頁執行 → 結果進檢測紀錄 → 轉證據。這個工具不派遠端 Agent,在我們自己的伺服器上跑,使用者不必選執行位置 工具目錄一列、伺服器端執行路徑、任務頁共用 專案規劃建檢測工具任務時,下拉能選「SBOM 弱點掃描」,而且不出現 Agent 指派欄;任務啟動或按執行後,不需任何 Agent 在線,執行列表出現一筆並跑完;結果檔出現在任務證據清單;點「看完整結果」進獨立模組結果頁,數字一致

依賴:A(沙箱擴充)→ B、C 平行;C 之後 D、E、H 平行;D 之後 F;B、D、E、F、H 全部完成後 G 收尾。


§2

需求分析

要解決的事

顧問沒有技術背景,問工程師「有沒有已知弱點」只能得到「都有在更新」。本案要讓顧問自己回答四個問題,而且答案有工具原始輸出撐腰:

  1. 這個專案用了哪些第三方套件、哪個版本?(SBOM)
  2. 其中哪些有已知弱點、多嚴重、有沒有人真的在攻擊它?(弱點比對+KEV/EPSS)
  3. 哪些已有修正版、修正版出來多久了?(可修未修統計)
  4. 上次說要修的,修了沒?(歷次比對)

兩種輸入(Git 來源第二版)

輸入 誰會用 走哪條路 版次
上傳源碼壓縮檔(zip/tar.gz,100MB 內) 客戶給了一包程式碼 沙箱內安全解壓 → Syft 產 SBOM → 比對弱點 第一版
上傳現成 SBOM 檔(CycloneDX/SPDX,JSON 或 XML,50MB 內) 工程師或其他工具(Snyk、Dependency-Track、我們自己的出貨管線)交出來的清單 跳過 Syft,沙箱內驗格式 → Trivy、Grype 直接吃 SBOM 比對弱點 第一版
填 Git 位址(可含私有倉庫帳密) 顧問拿得到 repo 讀取權限 抓檔容器 clone → 交給沙箱 → 同第一種 第二版(見「未來項」節)

SBOM 檔輸入的價值:顧問可以驗工程師交來的 SBOM——品質檢查(是否齊備 NTIA 最低要素,即美國商務部訂的 SBOM 七個必填項)對外來 SBOM 尤其有意義,「你交的清單連版本都沒寫齊」本身就是一個發現。兩條路在「正規化層」匯合,後面的結果頁與報表完全共用;第二版加 Git 時是在最前面多一段「抓檔」,正規化層之後不動。

與市面工具差在哪

工具本身(Syft、Trivy、Grype)是業界標準,不自己造。差異化全在呈現層:

  • 同列對照:同一個弱點,Trivy 怎麼說、Grype 怎麼說、分數差多少,攤在同一列,不是兩張表
  • 事實統計、不下結論:嚴重度、KEV、EPSS、可修未修、涵蓋完整度,全部來自工具與公開權威來源,沒有我們自訂的燈號(D6)
  • KEV/EPSS:標出「已知被利用」與「被利用機率」,不是只看 CVSS 分數
  • 歷次比對:修掉、新增、仍在
  • 可追溯:每個數字都能點回工具原始輸出與檔案指紋

第一版的邊界

  • 只收上傳:源碼壓縮檔與 SBOM 檔。Git 來源(含私有倉庫憑證)列第二版——它需要一個可出網的抓檔容器與一整套憑證管理,第一版先把「上傳就能出報表」做紮實。
  • 不新增容器:掃描工具併進既有沙箱(D1)。
  • 只做純靜態解析:讀 lock 檔(套件鎖定檔,記錄每個套件的確切版本)與釘死版本的宣告檔,不執行客戶的建置工具(已定,理由見 D3)。沒有 lock 檔時照樣出報表,但明確標示涵蓋範圍不完整、SBOM 品質分扣分,並把「缺 lock 檔」列為一項供應鏈管理缺失的發現。

§3

功能使用流程(顧問視角)

UI 設計約束:自行設計,不仿照競品

畫面版面、分頁結構、欄位命名、用語一律自行設計,依 Guidant 既有設計系統(guidant-design-system skill、PrimeVue 元件)與本文的顧問情境出發。不得仿照 ITRI SecureScan 或任何競品的版面與結構,競品截圖不進前端派工卡。未實作的功能不在畫面上預告或留灰色入口(例如 Git 來源在做出來之前,精靈、清單、設定頁都不出現任何 Git 字樣)。

UI 不要直接抄襲,會沒辦法對外交代。 — 決策者原話(2026-10-10)

主要入口在側選單「原始碼掃描」(以下五步都在這裡)。另一個入口是檢測工具清單裡多一個工具「SBOM 弱點掃描」,與 OpenSCAP、SonarQube 並列:專案規劃建檢測工具任務時從下拉選它(不出現 Agent 指派欄)→ 任務頁按執行 → 結果進該任務的檢測紀錄 → 轉成證據;不建掃描對象,「看完整結果」連到本模組的結果頁(D9)。整個流程沒有工程師的位置——顧問自給自足是賣點;三個月後回訪時,在同一個掃描對象下上傳新的壓縮檔即可重掃。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TD
  M["側選單:原始碼掃描"] --> L["① 掃描對象清單"]
  L -->|"新增"| W1["② 精靈頁 1:這是什麼"]
  W1 --> W2["頁 2:程式在哪<br/>源碼包/SBOM 檔"]
  W2 --> W3["頁 3:怎麼掃<br/>標準掃描,進階可勾工具與格式"]
  W3 --> P["清單上顯示四段進度<br/>接收檔案→產出物料清單→比對弱點→整理報告"]
  L -->|"重新掃描"| P
  P --> R["③ 結果頁<br/>顧問層/對照層/原始層"]
  L -->|"看結果"| R
  R -->|"匯出"| X["報表:樣式 × 格式"]
  L -->|"歷次比對"| H["④ 選兩次比:修掉/新增/仍在"]
  H -->|"匯出回訪報告"| X
  S["⑤ 設定頁(管理員)<br/>弱點資料・資源上限"] -.->|"弱點資料日期與上限"| W3
圖 1 — 顧問操作流程:清單、精靈、進度、結果、比對;設定頁由管理員維護

① 掃描對象清單

每一列是一個「掃描對象」(一套被掃的系統),跨稽核專案重用。

欄位 說明
對象名稱 顧問自取,如「客戶 X 內部請假系統」
客戶/備註 自由文字
來源 源碼包/SBOM 檔
最近掃描日 最後一次完成的日期
重點數字 KEV 目錄內幾個、High 以上幾個(Trivy/Grype 取較多者並註明來源)——只放事實,不放燈號(D6)
趨勢 與上一次比:修掉幾個、新增幾個

按鈕:新增、重新掃描、看結果、歷次比對。

② 新增掃描對象精靈(三頁)

頁 1:這是什麼——名稱、客戶備註、聯絡工程師(選填,只是備忘,系統不會聯絡他)。

頁 2:程式在哪(兩選一)

  • 上傳源碼包:zip/tar.gz,100MB 內。畫面提示「請確認壓縮檔包含 package-lock.json、poetry.lock 這類鎖定檔,沒有的話版本會不準——請工程師提供 lock 檔,或改用下方『上傳 SBOM 檔』」。
  • 上傳 SBOM 檔:CycloneDX 或 SPDX,JSON 或 XML,50MB 內,系統自動辨識格式與版本。

頁 3:怎麼掃——預設「標準掃描」(Syft 產清單,Trivy+Grype 比對弱點,CycloneDX 與 SPDX 都產)。「進階」摺疊區可勾選要跑的弱點工具,以及要另外產出的 SBOM 格式與版本(CycloneDX 1.4~1.7、SPDX 2.2/2.3,兩種格式同時列出)。

送出後回到清單,該列顯示四段白話進度,對應背景工作的進度回報(ctx.report_progress):

接收檔案 → 產出物料清單 → 比對弱點 → 整理報告

第一段是「檢查並解開上傳的檔案」。上傳 SBOM 檔時第二段顯示「已提供,略過」。

③ 結果頁(三層分頁)

事實儀表板(預設)

六個區塊,只放事實、不下結論(D6):

  1. 嚴重度統計(Trivy、Grype 各一組並列)
  2. 在 CISA KEV 目錄上的弱點(逐筆列出)
  3. EPSS 達門檻的弱點(門檻顯示在旁,預設 10%)
  4. 可修未修:已有修正版幾個、最早的修正版發布於何時
  5. 涵蓋完整度:缺 lock 檔的生態系、NTIA 要素齊否
  6. 跟上次比:修掉/新增/仍在

另有授權風險摘要(用到幾個傳染性授權的套件,如 GPL)。右上角「弱點資料日期」,超過 30 天黃色、90 天紅色。

對照層

每個弱點一列,排序可選(預設 KEV → EPSS → CVSS,規則顯示在表頭):

套件/版本 Trivy Grype KEV/EPSS 修正版 白話說明

兩家說法不同時,該列標「工具意見不一致」,點開看各自理由(D7)。

原始層:下載 SBOM(選 CycloneDX 或 SPDX、選版本)、下載各工具原始 JSON、工具版本與每份檔的 sha256 指紋。

匯出報表:選樣式(顧問摘要/完整技術/客戶交付)× 格式(PDF/HTML/DOCX)。

④ 歷次比對

選同一對象的任意兩次掃描,三欄並排:修掉/新增/仍在。可匯出「回訪報告」。

⑤ 設定頁(只給管理員)

  • 弱點資料庫狀態(資料日期、各來源筆數)+匯入更新包
  • 資源上限(單次時限、上傳大小上限)

兩條設計要點

工程師不在流程裡;進度用白話四段

顧問自給自足是本功能的賣點:上傳就能出報表,不必等工程師配合。進度不顯示「job running」這類字樣,一律四段白話,讓顧問知道現在卡在哪一步。


§4

現況盤點

元件 座標 現況 可複用 缺口
沙箱容器 docker/production/docker-compose.yml:580-622 guidant-sandbox:唯讀、去全部權限、禁提權、tmpfs /tmp 2g、mem 2g/cpus 2/pids 256;身分靠 image 的 USER classifier,compose 未寫 user: 掃描工具直接裝進這顆(D1);加固形狀抽成樣板 寫死在單一服務內,沒有樣板;記憶體 2g 不夠掃描(D12)
沙箱網段 docker-compose.yml:743-749 guidant-sandbox/guidant-sandbox-errors 皆 internal: true 原樣沿用,掃描不需要出網 —
沙箱呼叫設定 docker-compose.yml:146-150、config/config.py:198-204、core/plugins/ai_gateway.py:185-214 SANDBOX_MODE、CLASSIFIER_SERVICE_URL 在共用環境變數;行程內模式只准 DEV 模式判斷與 DEV 限制 —
沙箱客戶端 jedi_ai_gateway/infra/extract/sandbox_client.py:36-72 只有 .extract(job_uid, files, options),POST /v1/extract 共享密鑰、逾時處理的寫法 只為拆檔設計,無通用執行介面,要加掃描方法
沙箱服務 jedi-evidence-classification/docker/classifier_service.py stdlib http.server,/healthz、/v1/extract 服務骨架,加一支 /v1/scan 掃描端點要新寫
守衛測試(容器) test/security/code/test_security_shape_container_hardening.py:17,28-38 四個容器名寫死;只守 cap_drop/no-new-privileges/mem/pids 斷言寫法 不守 read_only、tmpfs、cpus、網段、internal;未來新沙箱不會自動被納入
守衛測試(外呼) test/security/code/test_security_shape_external_calls.py:159-160 登記 SandboxClient._remote::requests.post 登記格式 新增的掃描呼叫要登記
installer 服務名 scripts/installer/install.sh:1209-1211,2144,2149,2548-2561、scripts/installer/guidantai:300,843、docker/production/.env.example:121-126 寫死 guidant-sandbox 容器不增,服務名不必改 要加 vulndb volume 與更新包匯入
沙箱 image build scripts/build/build_classifier_image.sh;寫死處 sbom.sh:101、vuln_gate.py:19、build_all.sh:191、build_bundle.sh:93-108、push_harbor.sh:33,46 不走 Nuitka 編譯 在這支腳本加裝三支工具 image 變大(三支工具本身,弱點資料不進 image);出貨弱點掃描會一併掃到這三支工具
出貨 SBOM 管線 scripts/build/sbom.sh:44-104,工具版本見 scripts/build/README.md:354 syft 1.54.0/trivy 0.75.0/grype 0.120.0 裝在 build 機;--source 複製 lock 檔到暫存目錄跑 syft dir: 指令組合直接搬(lock 檔掃法、Trivy/Grype 參數) 只服務出貨,產品零處呼叫
弱點判定 scripts/build/vuln_gate.py:84,121-134 三工具任一白名單外 Critical 即擋;relatedVulnerabilities 收別名 別名收集(GHSA↔︎CVE)可借做跨工具配對 判定邏輯是「擋不擋」,不是「多危險」
離線弱點資料 — 每次掃描自動線上更新;Grype DB 約 3GB 在 ~/.cache/grype/db — 完全沒有離線機制
安裝包 scripts/build/build_bundle.sh:8-27,258-297,369-385 manifest 只對 images/*.tar 算 sha256;storage-seed 升級每次重灌 storage-seed 的「非 image 資料隨包」做法;ai_gateway_rules.sh:1-30 的出廠預設與客戶修改分目錄 無「資料可單獨更新」先例
背景工作 infra/background_job/model/background_job.py:7-26、app/background_job/service/background_job_app_service.py:55、app/background_job/handler_registry.py:25 四態;enqueue(job_type, payload, max_attempts=3);payload 不可放金鑰;租約 300 秒 整套直接用;範本 core/plugins/_evidence_classification_job_queue.py:19-33 沒有對外查工作狀態的 API
上傳限額與檢查 config/config.py:212-219、jedi_detection/common/detection_source_file.py:48-101、detection_profile_archive.py:99、archive_limits.py:51-99 100MB/解壓後 500MB/5 萬檔;副檔名白名單;zip-slip、符號連結、檔頭、壓縮炸彈檢查 限額與檢查器直接用 後端沒有「安全解壓到磁碟」函式(解壓都在 agent 端)
檔案儲存 jedi-file-upload ports.py:54、app/upload_file/service/managed_file_upload_service.py:35 save_file/get_file 原檔、原始輸出、報表都存這 —
祕密加密 infra/cloud_integration/crypto/fernet_crypto.py:6、config/config.py:187-195、jedi_detection/common/detection_secret_params.py:62-130 三把 Fernet 金鑰;偵測域的 envelope 寫法 {"__enc__":"fernet","value":...} 第二版 Git 私有倉庫憑證用 第一版不需要
PDF 報表 api/project_summary_report/routes/project_summary_report_route.py:163-182 WeasyPrint、bleach 消毒、只放行 data URI(防伺服器端請求偽造)、Noto Sans CJK TC 整條產生方式 沒有共用的品牌頁首頁尾
DOCX 報表 app/oscal/service/export/ssp_docx_generator.py:46、ssp_libreoffice_converter.py:57 docxtpl+python-docx;LibreOffice 轉 PDF 範本填值方式 —
差異比對 app/oscal/service/import_diff/ssp_diff_service.py:44 unchanged/changed/added/gone,以 key 配對 四態比對骨架 偵測結果沒有逐筆指紋、沒有跨次比對
新模組接線 config/app_modules.py、di_containers/containers.py:145-146 四層樣板 commit 6a35e2cf3;薄樣板 commit 98cc3c934 樣板 —
能力點 core/host_capabilities.py、scripts/deliverables/gen_permission_matrix.py、範例 scripts/sql/2026-10-05-fr129-2-tenant-scan-zones.sql <模組>.<資源>.<動詞>;加值包必須整包落在單一模組 整套 checklist —
授權照 common/authz/license.py:107-125,352 require_license;基礎設施豁免清單 守門 歸既有檢測工具包(D8)
RLS FR-129.2 migration :21-60;守衛 test/security/database/test_rls_all_tenant_tables.py 前綴比對+超管放行,FOR ALL 一條規則涵蓋增查改刪 照抄形狀 —
偵測結果表 scripts/sql/2026-07-26-fr056-4-detection-executions.sql detection_executions 綁任務(job_execution_uid NOT NULL)、summary JSONB、report_file_id、agent_task_uid 檢測工具入口的結果落點(D9、階段 H);伺服器端執行時 agent_task_uid 留空 —
檢測工具目錄 config.detection_tools;清單 GET /detection-tools(jedi_detection/api/routing.py:69)只列 status='available' 且啟用;參數 schema config.detection_tool_param_schemas;任務綁定寫 config.job_execution_detection_tools(detection_job_binding_handler.py:164),一任務只綁一工具 建任務只在 job_type='detection_tool' 顯示工具下拉(FE components/grc/planning/AoTaskForm.vue:472-475) 清單、參數 schema、綁定整套沿用 本案動作:加欄位 execution_target(agent/server,預設 agent,既有列不動);seed 一列 sbom-scan;AoTaskForm 依此欄隱藏 Agent 指派區塊
檢測派工 jedi_detection/app/service/detection_orchestration_service.py:184 start_execution → _dispatch_one(:464)建 Agent 工單;_pick_agent(:1417)找不到回 NO_AVAILABLE_AGENT;入口 POST /detection-tools/jobs/<job_uid>/execute(detection_tool_route.py:178,守門 require_license("plugin")+grc.job.update);任務啟動自動首掃(app/detection_tools/task_type_declaration.py:43) 一律派給遠端 Agent 入口、守門、首掃觸發、detection_executions 建列 本案動作:execution_target=='server' 時分岔到新檔(見風險節),入列 background_jobs(job_type sbom_scan),不找 Agent
檢測結果轉證據 Agent 回報 → detection_lifecycle_listener.py:24 → DetectionResultHandler.handle_result(detection_result_handler.py:71)存 JOB_EVIDENCES/<job_uid> → 寫 job_evidences(source=DETECTION_TOOL)→ 回填 detection_executions.report_file_id/evidence_id/summary — 整支直接呼叫,轉證據流程零改動 本案動作:worker 跑完以多檔結果(報表、SBOM、原始 JSON)呼叫它
檢測檔案輸入 參數欄位沒有通用的檔案型別(FE DetectionConfigField.vue:186-265);FR-058.7 SonarQube 寫死 scan_mode='upload',走隱藏的檔案選擇上傳 upload_type:'DETECTION_SOURCE',執行時送 source_files(FE JobExecutionDrawer.vue:343-410,470-500;BE _resolve_row_source_files :718) — 上傳機制沿用 不新造檔案欄位型別
源碼上傳掃描 FR-058.7(jedi-detection scan_mode=upload,sonar) 已上線 限額與檢查器(D13) 解壓在 agent,不在後端
目標授權 core/plugins/remote_agent.py:19,87,215(FR-129 allowed_targets) 掃描目標必須來自登記資料 第二版 Git 站台白名單的形狀(未來項) 第一版不需要
內部憑證 FR-130:/etc/guidant/internal-ca.crt 掛載慣例 不碰沙箱 第二版抓檔容器信任客戶自簽 Git 站台時可借 第一版不需要
前端樣板 FE commit 6ae57765(14 檔:router、api.js、Service、config、views、i18n) 選單由後端 ui_routes 驅動 整套 —
角色 shipped roles:系統管理員/稽核主管/稽核員/一般成員/唯讀 沒有「顧問」角色 顧問對應系統管理員或稽核主管 —

§5

業界對照:ITRI SecureScan(ARGYUS)

工研院的 SecureScan 是國內同類產品,這裡只作為參考,設計以草稿中的顧問情境為準。

它有什麼

  • 一次掃描一份報告,分頁包含風險檢視、SBOM 報告、AI 摘要、執行紀錄。
  • 用 Syft 產清單,Trivy 與 Grype 查弱點;兩家結果分在不同子頁各自呈現。
  • 弱點欄位以英文原始資料為主(弱點編號、套件、版本、修正版、嚴重度、英文說明),統計為各嚴重度計數。
  • SBOM 頁直接呈現 bom-ref、cpe、purl 這類機器識別碼;有「品質異常」狀態字。

我們不同在哪

  • 使用者不同:它的呈現對象是看得懂弱點資料的人;本案從「沒有技術背景的顧問要跟工程師對話」出發,所以每個數字都要附白話與出處(D6、D7)。
  • 兩家工具攤在同一列:同一個弱點兩家各怎麼說直接對照,意見不一致時標出來(對照層)。
  • 公開權威來源的事實:加入 CISA KEV、EPSS、可修未修統計、涵蓋完整度,並附資料來源說明,不自訂燈號(D6)。
  • 歷次比對:同一對象的回訪比對,修掉/新增/仍在(階段 F)。
  • 缺 lock 檔本身是發現:涵蓋不完整明確標示,並對應 ISO 27001 管理缺失(D3)。
  • AI 分析是使用者主動按的,預設畫面只放事實(D14)。

工具組合(Syft、Trivy、Grype)是業界通用做法,兩邊相同並非參照它而來,出貨管線早已在用(FR-127/FR-128)。

UI 設計約束:自行設計,不仿照競品

畫面版面、分頁結構、欄位命名、用語一律自行設計,依 Guidant 既有設計系統(guidant-design-system skill、PrimeVue 元件)與本文的顧問情境出發。不得仿照 ITRI SecureScan 或任何競品的版面與結構,競品截圖不進前端派工卡。未實作的功能不在畫面上預告或留灰色入口(例如 Git 來源在做出來之前,精靈、清單、設定頁都不出現任何 Git 字樣)。

UI 不要直接抄襲,會沒辦法對外交代。 — 決策者原話(2026-10-10)


§6

端到端流程

兩種輸入在「正規化層」匯合。後端行程不解壓、不解析任何客戶檔案,一律交給沙箱。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant C as 顧問瀏覽器
    participant API as guidant-api
    participant OBJ as SeaweedFS
    participant DB as guidant-db
    participant W as guidant-worker
    participant S as guidant-sandbox

    C->>API: 新增掃描(上傳源碼包或 SBOM 檔)
    API->>OBJ: 存原檔(先擋大小與副檔名)
    API->>DB: 建掃描列、background_jobs 入列(payload 只放 uid)
    API-->>C: 回掃描 uid
    W->>DB: 撿單(FOR UPDATE SKIP LOCKED)
    Note over W: 進度①接收檔案
    W->>OBJ: 取原檔寫入共用工作 volume
    Note over W: 進度②產出物料清單
    W->>S: 內網掃描請求(工作路徑、輸入類型)
    alt 源碼包
        S->>S: 安全解壓、syft 產 SBOM
    else SBOM 檔
        S->>S: 辨識格式版本、驗 schema
    end
    Note over W: 進度③比對弱點
    S->>S: trivy sbom 與 grype sbom(離線弱點資料)
    S->>S: syft convert 產 CycloneDX 與 SPDX 各版本
    S-->>W: 原始輸出與 SBOM 檔(共用工作 volume)
    Note over W: 進度④整理報告
    W->>W: 正規化、跨工具配對、KEV/EPSS、六區塊統計、品質分
    W->>OBJ: 存原始輸出與 SBOM(附 sha256)
    W->>DB: 寫元件、弱點、摘要,掃描列標完成
    C->>API: 輪詢掃描狀態
    API-->>C: 四段進度與結果
圖 2 — 端到端時序:兩條輸入路徑,在既有沙箱跑完工具後於 worker 正規化匯合

§7

架構

現況與新增對照

先看落地版現在有哪些服務,再看本案動了哪裡。下面兩張圖依 docker/production/docker-compose.yml 實查繪製:現況共 10 個服務(其中 guidant-db-init 只在裝機/升級時跑一次)、3 個網段、4 個 named volume。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  BR["瀏覽器<br/>顧問/管理員操作介面"]
  AG["遠端 Agent(FR-039)<br/>客戶主機上的檢測代理"]
  HB["Harbor 映像倉庫<br/>只在升級時拉 image"]
  subgraph NDEF["網段 default(一般內網,非 internal;只有 guidant-fe 對外開 80/443)"]
    FE["guidant-fe<br/>nginx:唯一對外入口,SPA+同源反代"]
    API["guidant-api<br/>REST API,收單、查狀態"]
    SIO["guidant-socketio<br/>即時通知"]
    WK["guidant-worker<br/>背景工作:撿單、證據分類、經 AI 閘道打 AI"]
    DB[("guidant-db<br/>PostgreSQL 16:主庫+bugsink 庫")]
    RD[("guidant-redis<br/>Redis 8:快取與佇列,只開 TLS")]
    SW[("guidant-seaweedfs<br/>物件儲存:上傳檔與報表")]
    BS["guidant-bugsink<br/>錯誤追蹤站"]
    INIT["guidant-db-init<br/>一次性:裝機建庫、升級套 migration"]
  end
  subgraph NSB["網段 guidant-sandbox(internal,不能出網)"]
    SB["guidant-sandbox<br/>檔案沙箱:拆不可信證據檔"]
  end
  subgraph NERR["網段 guidant-sandbox-errors(internal)"]
    ERRL["成員只有沙箱與 Bugsink<br/>沙箱送錯誤事件專用"]
  end
  subgraph VOL["named volume(資料落點)"]
    VPG[("guidant-pgdata<br/>DB 資料")]
    VSWD[("guidant-seaweedfs-data<br/>物件儲存資料")]
    VCJ[("guidant-classifier-jobs<br/>分類工作暫存<br/>api/socketio/worker/sandbox 共掛")]
    VBS[("guidant-bugsink-data<br/>錯誤站資料")]
  end
  BR -->|"https 443"| FE
  AG -->|"https 443:註冊、回傳結果"| FE
  HB -.->|"只在升級時拉新版 image(各服務同)"| API
  FE -->|"/api/1.0"| API
  FE -->|"/socket.io"| SIO
  FE -->|"/bugsink/"| BS
  API --> DB
  API --> RD
  API --> SW
  SIO --> DB
  SIO --> RD
  WK --> DB
  WK --> RD
  WK --> SW
  WK -.->|"經第二網段 guidant-sandbox:內網 8080 拆檔請求"| SB
  API -->|"錯誤事件(socketio/worker 同)"| BS
  SB -.->|"第二網段:送錯誤事件"| ERRL
  ERRL -.-> BS
  BS -->|"bugsink 庫"| DB
  INIT -->|"profile init,跑完退場"| DB
  DB --- VPG
  SW --- VSWD
  WK --- VCJ
  SB --- VCJ
  BS --- VBS
圖 3-1 — 現況:落地版所有服務(依網段分組;虛線為第二網段或只在升級時發生的連線)

怎麼看這張圖:

  • 對外只有 guidant-fe 一個入口(80/443),瀏覽器與遠端 Agent 都從這裡進,再由 nginx 轉給 guidant-api、guidant-socketio、guidant-bugsink。
  • guidant-sandbox 只在 internal 網段 guidant-sandbox(成員只有它和 worker),連不到 DB、Redis、物件儲存,也出不了網;另一個 internal 網段 guidant-sandbox-errors 只讓它把錯誤事件送給 Bugsink。
  • Harbor 不是常駐連線,只有升級時從映像倉庫拉新 image。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  BR["瀏覽器<br/>顧問/管理員操作介面"]
  AG["遠端 Agent(FR-039)<br/>客戶主機上的檢測代理"]
  HB["Harbor 映像倉庫<br/>只在升級時拉 image"]
  subgraph NDEF["網段 default(一般內網,非 internal;只有 guidant-fe 對外開 80/443)"]
    FE["guidant-fe<br/>nginx:唯一對外入口,SPA+同源反代"]
    API["guidant-api<br/>REST API,收單、查狀態"]
    SIO["guidant-socketio<br/>即時通知"]
    WK["guidant-worker<br/>背景工作:撿單、證據分類、派掃描、產報表"]
    DB[("guidant-db<br/>PostgreSQL 16:主庫+bugsink 庫")]
    RD[("guidant-redis<br/>Redis 8:快取與佇列,只開 TLS")]
    SW[("guidant-seaweedfs<br/>物件儲存:上傳檔與報表")]
    BS["guidant-bugsink<br/>錯誤追蹤站"]
    INIT["guidant-db-init<br/>一次性:裝機建庫、升級套 migration"]
  end
  subgraph HARD["套用共用加固樣板(唯讀、去權限、資源上限);未來新沙箱套同一份"]
    subgraph NSB["網段 guidant-sandbox(internal,不能出網)"]
      SB["guidant-sandbox:拆檔+SBOM 弱點掃描<br/>image 擴充:加 Syft・Trivy・Grype"]
    end
  end
  subgraph NERR["網段 guidant-sandbox-errors(internal)"]
    ERRL["成員只有沙箱與 Bugsink<br/>沙箱送錯誤事件專用"]
  end
  subgraph VOL["named volume(資料落點)"]
    VPG[("guidant-pgdata<br/>DB 資料")]
    VSWD[("guidant-seaweedfs-data<br/>物件儲存資料")]
    VCJ[("guidant-classifier-jobs<br/>分類工作暫存<br/>api/socketio/worker/sandbox 共掛")]
    VBS[("guidant-bugsink-data<br/>錯誤站資料")]
    VDB[("guidant-scan-vulndb<br/>弱點資料庫,D4 更新包匯入")]
  end
  BR -->|"https 443:上傳源碼包/SBOM 檔"| FE
  AG -->|"https 443:註冊、回傳結果"| FE
  HB -.->|"只在升級時拉新版 image(各服務同)"| API
  FE -->|"/api/1.0"| API
  FE -->|"/socket.io"| SIO
  FE -->|"/bugsink/"| BS
  API --> DB
  API --> RD
  API -->|"存上傳原檔"| SW
  SIO --> DB
  SIO --> RD
  WK --> DB
  WK --> RD
  SW -->|"取上傳原檔"| WK
  WK -.->|"經第二網段 guidant-sandbox:內網 8080 拆檔/掃描請求"| SB
  SB -->|"唯讀掛載"| VDB
  API -->|"錯誤事件(socketio/worker 同)"| BS
  SB -.->|"第二網段:送錯誤事件"| ERRL
  ERRL -.-> BS
  BS -->|"bugsink 庫"| DB
  INIT -->|"profile init,跑完退場"| DB
  DB --- VPG
  SW --- VSWD
  WK --- VCJ
  SB --- VCJ
  BS --- VBS
  style SB fill:#fff3cd,stroke:#d39e00
  style VDB fill:#fff3cd,stroke:#d39e00
  style HARD fill:#ffffff,stroke:#d39e00,stroke-dasharray:6 4
圖 3-2 — 本案新增(淺黃色):掃描工具併進既有沙箱,只多一個弱點資料 volume,容器數不變
新增元素 是什麼 為什麼獨立
guidant-sandbox image 擴充 既有檔案沙箱的 image 加裝 Syft、Trivy、Grype,同一顆容器同時負責拆證據檔與 SBOM 弱點掃描 不另開容器:上傳的源碼包與 SBOM 檔一樣是不可信的客戶檔,正好落在沙箱既有的隔離邊界內(internal 網段、唯讀、去權限、資源上限),直接沿用最省事也最一致
named volume guidant-scan-vulndb Trivy/Grype 弱點資料庫與 KEV、EPSS 資料,由 D4 的弱點資料更新包匯入,唯讀掛進 guidant-sandbox 弱點資料以天為單位更新、產品版本以月為單位,放 volume 才能不出新版產品就換資料;燒進 image 會讓每次升級重拉數 GB

容器數不變:第一版只收上傳(源碼包+SBOM 檔),不做 Git 來源,因此不需要可出網的抓檔容器。沙箱改吃共用加固樣板後,未來若真要新增沙箱(例如 Git 抓檔),套同一份樣板即可。

掃描架構細節

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  BR["瀏覽器<br/>獨立模組頁/稽核任務頁"] -->|"443:上傳源碼包/SBOM 檔"| NG["guidant-fe(nginx)"]
  NG --> API["guidant-api<br/>入口 1 模組 API・入口 2 檢測工具執行<br/>(execution_target=server 不派 Agent)"]
  API --> PG[("guidant-db<br/>掃描表・background_jobs<br/>detection_executions")]
  API -->|"存原檔"| OBJ[("SeaweedFS<br/>原檔・原始輸出・報表")]
  WK["guidant-worker<br/>撿單・正規化・產報表"] --> PG
  OBJ -->|"取原檔"| WK
  WK -->|"存原始輸出與報表"| OBJ
  subgraph INT["網段 guidant-sandbox:internal,不能出網"]
    SB["guidant-sandbox<br/>拆證據檔+Syft・Trivy・Grype"]
  end
  WK -->|"內網 8080:掃描請求"| SB
  WK -->|"寫輸入"| WV[("volume<br/>guidant-classifier-jobs<br/>既有共用工作區")]
  WV -->|"讀輸入、寫輸出"| SB
  VDB[("volume<br/>guidant-scan-vulndb")] -->|"唯讀"| SB
  CLI["guidantai vulndb import"] -->|"匯入更新包"| VDB
圖 3 — 掃描架構:工具與弱點資料都在既有沙箱裡;只有 worker 碰 DB 與物件儲存,沙箱不能出網
容器 網路 有什麼 沒有什麼
guidant-sandbox(既有,擴充) 既有 internal 網段(guidant-sandbox、guidant-sandbox-errors),不能出網 拆檔工具+Syft、Trivy、Grype、唯讀弱點資料 對外網路、DB 連線、金鑰
guidant-worker(既有) 既有 派掃描、正規化、產報表 不解壓、不解析客戶檔案

工作暫存沿用既有的 guidant-classifier-jobs volume,不另開。第二版加 Git 時才會多一個可出網的抓檔容器,形狀見「未來項」節。


§8

資料模型草案

共同欄位(每張租戶表都有):id、uid、tenant_id NOT NULL、org_unit_id、created_user/updated_user、created_at/updated_at。RLS 照 FR-129.2 的形狀:一條 FOR ALL 規則、USING 與 WITH CHECK 都寫,增查改刪四命令都有規則;守衛 test_rls_all_tenant_tables.py 會自動掃到新表。新表 GRANT ... TO cm_app 含 sequence。

表 用途 關鍵欄位 關聯
compliance.source_scan_targets 掃描對象(清單上一列) name、client_note、contact_note、source_type(upload/sbom,預留 git)、source_ref(JSONB,來源細節)、latest_scan_uid 1 對多 scans
compliance.source_scans 一次掃描 target_uid(可空,檢測工具入口不建對象直接跑)、detection_execution_uid(可空,從檢測工具入口跑時對應 detection_executions)、source_type、source_ref(JSONB,當次來源快照)、input_file_id、status、background_job_uid、tools(JSONB)、tool_versions(JSONB)、vulndb_date、summary(JSONB:六區塊統計)、sbom_quality(JSONB)、raw_outputs(JSONB:檔案 id+sha256)、report_file_id、error、started_at、finished_at 多對 1 target;1 對多 components/findings
compliance.source_scan_components SBOM 裡的套件 scan_uid、purl、name、version、ecosystem、licenses(JSONB)、license_risk 多對 1 scan
compliance.source_scan_findings 正規化後的弱點(一列一個「套件×弱點」) scan_uid、component_purl、vuln_id(以 CVE 為主)、aliases(JSONB)、severity、trivy(JSONB:有無、等級、分數)、grype(JSONB)、fixed_versions、in_kev、epss、match_key、fingerprint 多對 1 scan
public.scan_vulndb_imports 弱點資料匯入紀錄(平台層,無 tenant_id) bundle_date、sha256、sources(JSONB:各來源日期與筆數)、imported_by、imported_at —

來源欄位可擴充,日後加 git 不改資料模型:source_type 用字串值,第一版只有 upload(源碼壓縮檔)與 sbom(SBOM 檔)。第一版 UI 與 API 枚舉只開放 upload/sbom,不接受 git 值(送 git 一律回參數錯誤);第二版加 git 時,位址、分支、commit、憑證 uid 都放進 source_ref(JSONB),既有表不加欄位。Git 站台白名單與租戶憑證兩張設定表屬第二版,見「未來項」節。

兩個關鍵 key:

  • match_key(跨工具配對):ecosystem + 套件名 + 版本 + 正規化弱點編號。正規化弱點編號用別名表收斂(GHSA、PYSEC 對到 CVE),做法借 vuln_gate.py:84 收 relatedVulnerabilities 的寫法。
  • fingerprint(歷次比對):ecosystem + 套件名 + 正規化弱點編號,不含版本——升了版但仍在受影響範圍內,算「仍在」而不是「修掉一個、新增一個」。

summary 的形狀刻意對齊 detection_executions.summary,兩個入口(D9)共用同一份摘要:檢測工具入口跑完時,source_scans 存完整結果(target_uid 空、detection_execution_uid 填上),detection_executions 經 DetectionResultHandler 存摘要、報告檔與證據 id。


§9

階段拆分

階段 做什麼 repo 依賴 完成怎麼判定(決策者檢查法)
A 沙箱擴充 既有 guidant-sandbox image 加裝 Syft、Trivy、Grype(容器數不變),加一支掃描端點 /v1/scan;新增 named volume guidant-scan-vulndb(唯讀掛入沙箱);共用加固設定抽成 compose 樣板 x-guidant-sandbox-hardening,guidant-sandbox 改吃樣板;沙箱資源上限依 D12 調整;守衛測試改為自動納入名字以 -sandbox 結尾的服務,並補唯讀與 internal 網段斷言;installer、bundle 加 volume 與更新包位置 BE+jedi-evidence-classification 無 進沙箱 syft version、trivy --version、grype version 三支都有;沙箱內 curl 外網失敗;守衛測試全綠,且把沙箱的 read_only 拿掉會紅;證據分類照常可用
B 弱點資料更新 build 機產出更新包(Trivy DB、Grype DB、CISA KEV JSON、EPSS CSV)+sha256 manifest+簽章;安裝包隨帶一份;guidantai vulndb import <檔> 驗簽章與指紋後寫入 volume;連網客戶由 api/worker 從自家站台下載同一份包、走同一條匯入(沙箱不出網);日期較舊的包拒收;系統設定頁顯示資料日期與「檢查更新」 BE A 斷網機器匯入後,設定頁顯示日期;掃範例專案弱點數非零;拿舊日期的包、或改過一個位元組的包匯入都會被拒;連網機器按「檢查更新」拿到同一份包,沙箱仍連不到外網
C 掃描後端 資料表與 migration;job handler(max_attempts=1);掃描呼叫(登記外呼守衛);兩條輸入路徑(源碼包、SBOM 檔)與 SBOM 格式辨識;安全解壓函式(D13);正規化層(三工具 → 統一弱點模型、跨工具配對、KEV/EPSS、六區塊統計、NTIA 品質分、授權風險);syft convert 多格式多版本輸出;能力點、授權照;API(含掃描狀態查詢) BE+jedi-detection A(B 未完成前可在 DEV 用線上資料) 兩種輸入各送一筆都跑完;同一份 SBOM 檔與它的源碼包掃出的弱點數一致;無權限角色打 API 回 403;source_type 存 upload/sbom
D 前端 掃描對象清單、新增精靈三頁、四段進度、結果頁三層分頁、設定頁 FE C 照畫面新增一筆、進度走完、結果頁看得到事實儀表板六區塊、排序可切換且表頭顯示規則;唯讀角色看不到「新增」;畫面與競品無可辨識的版面雷同
E 報表與 AI 分析 三種樣式 × PDF/HTML/DOCX;每個弱點列標工具、CVE(連 NVD)、advisory 連結、修正版;附錄一頁(工具與版本、弱點資料日期、原始報告指紋與下載位置)+資料來源說明一頁;共用品牌頁首頁尾;報告頁「AI 分析」按鈕接 FR-122 AI 閘道(只加提示樣板) BE+FE C 九種組合各匯出一份,中文不缺字;報表裡沒有任何結論句;按「AI 分析」才呼叫,結果標示「AI 生成,以上方統計為準」
F 歷次比對 兩次掃描比對(借 SspDiffService 骨架);比對頁;回訪報告 BE+FE C、D 升一個套件版本再掃,「修掉」欄出現該弱點;換一個仍受影響的版本,該弱點留在「仍在」
G 收尾 SPEC 頁、使用手冊、安裝文件(更新包怎麼匯入)、.claude/rules/security/ 補新模組路徑 BE A–F 手冊三件事找得到
H 檢測工具入口 工具目錄 config.detection_tools 加 execution_target(agent/server,預設 agent)並 seed sbom-scan 一列;參數 schema 一份(scan_mode、sbom_formats、tools);AoTaskForm 依 execution_target 隱藏 Agent 指派;start_execution 遇 server 分岔到新檔、入列 background_jobs;worker 跑完呼叫 DetectionResultHandler.handle_result;任務頁抽屜 summary 中文對照加 SBOM 的 key、加「看完整結果」連結 BE+jedi-detection+FE C(與 D、E 平行) 專案規劃建檢測工具任務時下拉能選「SBOM 弱點掃描」且不出現 Agent 指派欄;任務啟動或按執行後,不需任何 Agent 在線,執行列表出現一筆並跑完;結果檔出現在任務證據清單;點「看完整結果」進獨立模組結果頁,數字一致
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  A["A 沙箱擴充"] --> B["B 弱點資料更新"]
  A --> C["C 掃描後端"]
  C --> D["D 前端"]
  C --> E["E 報表與 AI 分析"]
  C --> F["F 歷次比對"]
  D --> F
  B --> G["G 收尾"]
  D --> G
  E --> G
  F --> G
  C --> H["H 檢測工具入口"]
  H --> G
圖 4 — 階段依賴

§10

決策定案 D1~D14(D2/D10/D11 已移未來項)

D1 沙箱怎麼擴充(已定:掃描工具併進既有沙箱,不新增容器)

已定事項:掃描工具併進既有沙箱,不新增容器

  • Syft、Trivy、Grype 併進既有 guidant-sandbox 的 image,容器數不變。
  • 新增 named volume guidant-scan-vulndb 放弱點資料庫,唯讀掛入沙箱。
  • 共用加固設定(唯讀、去全部權限、禁提權、tmpfs、資源上限、執行身分)抽成 compose 的 x-anchor 樣板 x-guidant-sandbox-hardening。本案只有一顆套用,但未來新沙箱套同一份。樣板除 image、網段、volume 三個參數外,預留第四個「出網政策」(egress policy):第一版只有 none;第二版 Git 抓檔容器用 git-allowlist;若日後做建置型依賴解析再加 registry-allowlist。
  • 守衛測試改為「名字以 -sandbox 結尾的服務自動納入」,並補唯讀與 internal 網段斷言。
  • 沙箱記憶體上限從 2g 提高到 4g(D12)。

合併原則(之後新增容器都照這條判):容器只按三個信任邊界分——能不能出網、碰不碰客戶輸入、有沒有金鑰。三個答案都一樣就合併進同一顆,不按功能開新顆。拆證據檔與掃描源碼包三個答案完全相同(不出網、碰客戶輸入、無金鑰),所以合併;第二版的 Git 抓檔容器「要出網」,答案不同,才另開。

被排除的做法

  • 另開一顆掃描沙箱:排除——容器數膨脹(決策者點名的顧慮),而它的信任邊界與既有沙箱完全相同,多一顆換不到額外隔離。
  • 每個容器各寫一份完整加固設定:排除——下一個沙箱出現時就會有人漏一行,守衛測試又是寫死清單,不會發現。

已接受的取捨:拆證據檔與弱點掃描共用一顆沙箱,大型源碼包掃描期間,證據分類要排隊。緩解靠兩件事:沙箱的 --max-concurrent 同時處理數,以及背景工作佇列本身的排隊機制(兩種工作都經 background_jobs,不會互相搶死)。若實測排隊時間不可接受,再依合併原則評估——但那時拆開的理由是「資源」,不是「信任邊界」,優先考慮調高同時處理數。

D3 第一版支援哪些語言生態系(已定:純靜態解析,Python/npm/Java 標已驗)

已定事項:第一版只做純靜態解析,不執行客戶的建置工具

只讀 lock 檔與釘死版本的宣告檔(如 pom.xml 寫死版本號),不跑 npm install、mvn、cdxgen 這類建置工具。

理由:建置型解析(cdxgen、npm install --package-lock-only 等)本質上是向套件索引(npm registry、PyPI、Maven Central 這類公開套件倉庫)查版本,純離線的客戶原理上做不到。只有自建內部套件鏡像(Nexus、Artifactory)的客戶才可行,而且需要另一種容器——「可出網到套件索引白名單、又要執行客戶程式碼」,依 D1 合併原則必須另開——再加上網路層出網代理(egress proxy),等於把隔離環境再做六成。

客戶環境 建置型解析做不做得到
完全封閉網路 不行:查不到任何套件索引
有內部套件鏡像 可以:設定頁要填鏡像位址,客戶 IT 要開防火牆讓我們連鏡像
客戶提供已建好的環境 繞過:直接掃建好的產物,但要工程師配合,與「顧問自給自足」的賣點衝突

背景:業界三檔做法

  1. 純靜態:讀 lock 檔與宣告檔,沒有 lock 檔就只能猜版本(Syft、Trivy)。
  2. 執行建置工具取真實依賴樹:準,但要出網、要執行客戶程式碼(cdxgen;商用產品如 Snyk、Black Duck 多走這條)。
  3. 掃已建置的產物:掃 node_modules、.jar、容器 image,最準,但要先有建好的東西。

第一版走 ①,並把「缺 lock 檔」這件事本身變成有價值的發現(見下)。

缺 lock 檔怎麼處理(第一版必做,這是終局做法,不是過渡)

  • 掃完偵測到某生態系「有宣告檔、沒有 lock 檔」→ 結果頁顧問層與報表明確標示,例如:「此專案 npm 部分缺少 package-lock.json,弱點清單只涵蓋直接宣告的 N 個套件,版本為估算值,傳遞依賴(套件自己再用到的套件)看不到,實際風險可能更高。」
  • SBOM 品質分扣分。
  • 這件事本身列為一項**「供應鏈管理缺失」發現**,可對應 ISO 27001 A.8.28(安全程式設計)與 A.5.21(資通訊供應鏈的資訊安全管理)——專案連自己用了哪個版本都沒鎖定,本來就是管理缺失。
  • 精靈頁 2 提示:「請工程師提供 lock 檔,或直接提供 SBOM 檔」,並連到 SBOM 上傳入口。

問題:Syft 支援的生態系很多,但我們只能對自己驗過的負責。

選項

  • A(採用)全收、但分「已驗」與「未驗」:Syft 支援的都收;第一版實測並標「已驗」的是 Python(poetry.lock/requirements.txt/Pipfile.lock)、npm(package-lock.json/yarn.lock/pnpm-lock.yaml)、Java(pom.xml 固定版本、gradle.lockfile);Go/.NET/Ruby/Rust 標「工具支援、未驗」。
  • B 只收三種:其他語言直接拒絕,顧問會覺得「功能不能用」。
  • C 全收不標示:出錯時沒有說法。

定案:採 A。缺 lock 檔的處理照上方。

為什麼:顧問拿到的系統什麼語言都有,拒收不如照收並誠實標示可信度。

D4 弱點資料怎麼更新(已定:獨立弱點資料更新包)

定案內容:獨立的「弱點資料更新包」guidant-vulndb-<日期>.tar,附 sha256 manifest 與簽章,內含 Trivy DB、Grype DB、CISA KEV JSON(美國網路安全暨基礎設施安全局公布的「已知正被攻擊」弱點目錄)、EPSS CSV(FIRST 組織發布的「每個弱點未來 30 天被利用機率」估計)。存進 named volume guidant-scan-vulndb,沙箱唯讀掛入;掃描時 Trivy 帶 --skip-db-update --offline-scan、Grype 關閉自動更新。報表與結果頁標「弱點資料日期」,超過 30 天黃色警示、90 天紅色。

封閉網路客戶:安裝包隨帶一份;之後單獨下載更新包,用 guidantai vulndb import <檔> 匯入。

連網客戶:不分兩套,只多一個取得管道

  • 系統設定頁按「檢查更新」,或設排程,從我們自家站台下載同一份更新包,自動走同一條匯入流程。
  • 沙箱仍不出網:下載由 api/worker 做,驗完簽章才寫進 volume;沙箱只讀 volume,網段照舊 internal。
  • 更新包帶簽章,匯入前驗證,驗不過就拒收——與 FR-064 防竄改同一精神:客戶端只接受我們簽過的資料,被替換或改過的包進不來。封閉網路手動匯入時同樣驗。
  • 匯入日期較舊的包會被拒,避免誤把資料倒退。

被排除的做法

  • 讓 Trivy/Grype 在客戶機上直接線上更新:排除——資料版本不可控(同一天兩台客戶機可能拿到不同版本,報表無法重現),而且沙箱得開網,違反 D1 的信任邊界。
  • 跟 agent 升級走:排除——agent 沒有這套機制,FR-039 也不做升級流程。
  • 燒進 image:排除——Grype DB 約 3GB,每次升級都要重拉整顆 image,而且資料過期就得出新版產品。

為什麼:弱點資料的更新頻率(天)遠高於產品版本(月),兩者必須能分開更新;連網與封閉用同一份包,報表上的「弱點資料日期」在任何客戶機上意義都一樣。B 階段要實測:Trivy 掃 Java 是否另需 java-db、Trivy DB 實際大小(目前沒有文件)、簽章沿用哪一把金鑰。

D5 工具組合(已定:Syft+Trivy+Grype,SBOM 同時支援 CycloneDX 與 SPDX)

已定事項:SBOM 同時支援 CycloneDX 與 SPDX

Syft 一支工具產生,原生輸出 CycloneDX 與 SPDX 兩種格式;版本用 syft convert 轉(CycloneDX 1.4~1.7、SPDX 2.2/2.3)。精靈頁 3 的進階選項與結果頁原始層的下載選單,兩種格式一律同時列出。CycloneDX 是應用安全圈主流,SPDX 是法規與授權合規主流,美國政府採購兩者皆收;版本要可選是因為有些下游工具只認舊版(如國家資通安全研究院的 ex-sbom 只認 CycloneDX 1.6)。

問題:除了格式,產清單與查弱點各用哪些工具。

四個 SBOM 產生器差在哪

工具 看什麼列清單 強項 弱項 對我們的意義
Syft 純讀 lock 檔、套件中繼資料、image 分層 最普及;CycloneDX+SPDX 原生多版本;不執行程式、不出網 沒有 lock 檔只能猜版本 沙箱的最佳選擇,出貨管線已在用
Trivy 同類讀法 一個指令同時出 SBOM 與弱點 SBOM 不是主業、格式選項少、元件數偶爾較少 只用它查弱點、不產 SBOM,避免兩份清單打架
cdxgen 實際呼叫各語言的建置工具,取得真正的依賴樹 沒有 lock 檔也準、支援語言最廣 必須執行客戶的建置腳本,且要出網下載依賴 與沙箱「不執行、不出網」的邊界衝突,第二版再評估
osv-scalibr Google 的函式庫型核心 新、架構乾淨 不是成品;配套的 osv-scanner 對 Debian/Ubuntu 判不準 觀察

結論:只選 Syft 的用意,是在「純讀檔、不執行、不出網」這一類工具裡它做得最好——這正好就是沙箱的安全邊界——而且一支就能出兩種格式、多個版本。

選項

  • A(採用)Syft+Trivy+Grype:與出貨管線相同、團隊已熟、白名單與別名解析可借。格式轉換用 syft convert,不另包 CycloneDX CLI。NTIA 最低要素七項(供應商、元件名、版本、唯一識別碼、依賴關係、SBOM 作者、產生時間)自己算,規則簡單。
  • B 再加 sbomqs 算品質分:第二版。
  • C 再加 cdxgen、osv-scanner:第二版(理由見上表)。

定案:採 A。

為什麼:第一版的價值在呈現層,不在工具數量;工具組合與業界一致(見業界對照節),多加工具只會增加不一致要解釋。

D6 結果怎麼呈現(已定:不做風險總評燈號,只給事實統計,判斷留給顧問)

理由:任何我們自訂的燈號或分數,客戶只要問一句「評定標準是什麼」就站不住——「為什麼 3 個 High 是橙燈、不是 4 個」沒有權威答案。系統只呈現工具與公開權威來源的事實,不替任何人下結論;顧問拿事實去跟工程師對話,判斷是顧問的專業。

顧問層改名「事實儀表板」,六個區塊

  1. 嚴重度統計:Critical/High/Medium/Low 各幾個,Trivy 一組、Grype 一組並列。
  2. 在 CISA KEV 目錄上的弱點:逐筆列出(套件、版本、弱點編號、列入目錄日期)。
  3. EPSS 達門檻的弱點:門檻可調,目前值顯示在區塊旁,預設 10%。
  4. 可修未修:已有修正版的弱點數量;其中最老的修正版發布於何時(「最早一個修正版 2023-03 就已發布」)。
  5. 涵蓋完整度:哪些生態系缺 lock 檔、NTIA 最低要素是否齊備(D3)。
  6. 跟上次比:修掉/新增/仍在。

弱點清單的排序可選(取代「最急的五個」):預設依序 KEV → EPSS → CVSS,目前的排序規則顯示在表頭,顧問可以切換。排序是檢視方式,不是評等。

報表只出事實句、不出結論句

不寫 改寫
「本專案屬高風險」 「含 1 個 CISA KEV 目錄內弱點;9 個 High,其中 7 個已有修正版」
「建議立即處理」 「最早一個可用修正版發布於 2023-03」

報表附「資料來源說明」一頁:每個數字背後的來源各是誰維護——

來源 維護者 提供什麼
NVD/CVSS 美國國家標準暨技術研究院(NIST) 弱點編號與嚴重度分數
CISA KEV 美國網路安全暨基礎設施安全局(CISA) 已知正被攻擊的弱點目錄
EPSS FIRST(國際資安事件應變組織論壇) 未來 30 天被利用機率估計
套件官方修正紀錄 各套件維護者(經 GitHub 安全公告等) 哪一版修正、何時發布

被排除的做法

  • A 分級規則表(紅橙黃綠):排除——門檻(如「≥3 個 High」)答不出為什麼是 3。
  • B 取 CVSS 最高分:排除——一顆 10 分但實際打不到的弱點會壓過全部,而且一樣是我們替客戶下結論。
  • C 加權平均:排除——算得出數字,但沒人解釋得了為什麼是 7.3。

要誠實標示的一點:「有沒有公開攻擊程式」在離線環境沒有可靠資料源(exploit-db 這類資料未納入更新包),事實儀表板以 KEV 與 EPSS 呈現,不另做推估。

D7 「每個數字追到原始輸出」怎麼呈現(已定:報表帶出處,不帶原始輸出)

報表每個弱點列標四件事

  • 哪個工具報的(Trivy、Grype,或兩家都報)
  • CVE 編號,連到美國國家弱點資料庫(NVD)的公開頁面
  • 工具的公告連結(advisory,例如 GitHub 安全公告頁)
  • 修到哪一版(fixed version)

附錄一頁:工具名稱與版本、弱點資料庫日期、原始報告的 sha256 指紋與系統內下載位置;另附 D6 的「資料來源說明」一頁。

不把原始輸出拉進報表。原始報告在系統的原始層可以下載,指紋讓「這份就是當時那份」可以被驗證,工程師無法說報表是編的。

被排除的做法

  • 整份原始 JSON 附在報表後面:排除——動輒幾十到上百頁,顧問看不懂,客戶也不會翻。
  • 每個數字都做成連回系統的超連結:排除——報表常以 PDF 離線傳遞,客戶點不開系統連結。

D8 能力點、角色與授權照(已定:歸既有「檢測工具包」,不另開加購)

這就是檢測工具的一種,未來要不要拆開老闆會跟市場部討論,產品還沒上市。 — 決策者原話(2026-10-10)

授權:歸屬既有的「檢測工具包」(FR-062 四個加購包之一,含 remote-agent-manage、detection-profile、plugin),不另開加購項。沒買檢測工具包的租戶,側選單不出現「原始碼掃描」,檢測工具清單也不列「SBOM 弱點掃描」。日後若市場決定拆賣,只改簽發站 plan、不改程式——能力點本來就落在 detection 模組內,符合「加值包整包落在單一模組」的硬規則。

能力點:detection.source-scan.{read,create,update,delete,execute,export}(execute 是「觸發掃描」,export 是「匯出報表」,都在允許的額外動詞內)。弱點資料更新與資源上限的設定只給管理員——要開 detection.source-scan-setting.* 還是併入系統設定頁,design 階段定。

出廠角色

角色 權限
系統管理員 全部
稽核主管 read/create/execute/export
稽核員 read/create/execute/export
唯讀 read

產品沒有「顧問」角色,顧問對應系統管理員或稽核主管。

被排除的做法

  • 獨立加購:排除——產品還沒上市,不先拆。
  • 放進基礎包:排除——它屬於檢測工具這一類,跟其他檢測工具同一個販售單位。

D9 獨立模組還是檢測工具(已定:一個核心、兩個入口,同版交付)

問題:要做成獨立功能,還是做成檢測工具之一?

兩個入口同一版交付(階段 D 與 H)。下面說明的是「為什麼共用一個核心」與「入口 2 怎麼接進既有檢測工具鏈」。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  E1["入口 1:獨立模組<br/>側選單・掃描對象・歷次比對"] --> CORE["核心引擎<br/>輸入→沙箱跑工具→正規化→報表<br/>不知道被誰叫"]
  E2["入口 2:檢測工具「SBOM 弱點掃描」<br/>建任務可選→任務頁執行<br/>execution_target=server"] -->|"background_jobs"| CORE
  CORE --> T1[("source_scans 系列表")]
  CORE -->|"DetectionResultHandler"| T2[("detection_executions<br/>job_evidences(轉證據)")]
圖 5 — 一個核心、兩個入口:入口 2 是檢測工具清單裡的一個工具,在伺服器端執行、不派 Agent

採用:一個核心、兩個入口

  • 核心引擎:輸入 → 沙箱跑工具 → 正規化 → 報表,不知道是被誰叫的。
  • 入口 1 獨立模組:側選單、租戶層掃描對象清單、歷次比對、顧問報表。
  • 入口 2 檢測工具:檢測工具清單裡多一個工具「SBOM 弱點掃描」,與 OpenSCAP、SonarQube 並列,整條鏈都在:建任務可選 → 任務頁執行 → 結果進檢測紀錄 → 轉證據。
  • 核心契約約束(兩入口共用):① 核心的輸入輸出形狀對齊 detection_executions(summary JSONB+report_file_id+可重跑);② 能力點放 detection 模組(D8);③ 掃描表的對象 uid 可空,檢測工具入口不建對象直接跑。核心不得出現「判斷是哪個入口叫的」分支——入口差異只在呼叫端與結果落點。

入口 2 怎麼接(已定:不派 Agent,使用者不必選執行位置)

現況:FR-056 的檢測工具是「資料庫一列+Agent 上的執行器」,一律派給遠端 Agent(座標見現況盤點)。本案新增一條伺服器端執行路徑,以後任何「不需要連客戶主機」的工具都走這條。

環節 做法
工具目錄 config.detection_tools 加欄位 execution_target(agent|server,預設 agent,既有列不動)。seed 一列 code='sbom-scan'、execution_target='server'、requires_target_host=false、requires_credentials=false、不設 connection_type
建任務 使用者只是從工具下拉選「SBOM 弱點掃描」。AoTaskForm 依 execution_target 隱藏 Agent 指派區塊,不會出現 Agent 指派與目標主機欄位
參數 schema 一份:scan_mode(upload|sbom,沿用 SonarQube 的上傳機制,不新造檔案欄位型別)、sbom_formats(多選)、tools(預設三支)
派工 start_execution 看到 execution_target=='server' 就不走 _dispatch_one/_pick_agent,改入列 background_jobs(job_type sbom_scan),由 worker 送沙箱;detection_executions 照建 running 列,agent_task_uid 留空。新開的分岔檔依工具 code 查 job handler 登記表({'sbom-scan': handler}),不把 SBOM 寫死在分岔內;日後新增伺服器端工具只需登記一筆
結果 worker 跑完呼叫同一支 DetectionResultHandler.handle_result(報表、SBOM、原始 JSON 當多檔結果),既有轉證據流程零改動;同時 source_scans 存完整結果(target_uid 空、detection_execution_uid 對應)
任務頁抽屜 既有執行鈕、執行列表、summary 中文對照共用;summary 對照表加 SBOM 六區塊的 key;「看完整結果」連到獨立模組的結果頁(同一筆 source_scans)

被排除的做法

  • 只做成檢測工具:排除——顧問沒有稽核任務時也要能用;顧問 C 的三個月回訪情境跨專案,歷次比對要掛在「對象」上,不是掛在任務上。
  • 兩個入口各自實作:排除——兩套掃描與正規化邏輯,違反「先查再寫、不重複造輪子」。
  • 做成 Agent 上的執行器:排除——Agent 要帶三支工具加約 3GB 弱點資料,而且每台都要各自更新,違反 D4;Agent 的定位是「連客戶主機」,掃上傳檔不需要連任何主機。

為什麼:兩種用法都要有:顧問在稽核任務外也要能掃、要跨專案比對(入口 1);稽核員在任務裡要能把結果直接變成證據(入口 2)。共用一個核心,兩個入口算出來的數字才會一致,也只有一套掃描與正規化邏輯要維護。

D12 資源上限與時限(已定:照下表,全部可設定)

項目 定案值
沙箱(拆檔與掃描共用) 記憶體 4g(現行 2g)、cpus 2
單次掃描時限 20 分鐘
源碼包上傳 100MB;解壓後 500MB、5 萬檔(沿用 FR-058.7 限額)
SBOM 檔上傳 50MB
失敗重跑 不自動重跑(max_attempts=1),顧問手動按重掃

以上全部可在設定頁調整,表中是出廠預設。

為什麼:Grype 比對大型 SBOM 時吃記憶體,2g 不夠;20 分鐘涵蓋草稿情境中「568 個 npm 套件」量級還有餘裕;調高的上限證據分類一併受益。不自動重跑是因為重跑成本高、而且同一份輸入結果可重現,失敗多半要顧問換檔或調限額。C 階段要用實際大型源碼包驗證,並量測掃描期間證據分類的排隊時間(D1 的取捨)。

D13 FR-058.7 上傳分支能複用到哪裡(已定:只借檢查器,不借流程)

  • 複用:detection_source_file 的限額與副檔名白名單、ProfileArchiveValidator(zip-slip、符號連結、檔頭)、ArchiveBombGuard(壓縮炸彈)。
  • 新寫:一支「安全解壓到磁碟」放 jedi-detection 共用層,照 .claude/rules/security/uploads-parsing.md 四道(禁 extractall、每個項目做 realpath 檢查、邊解邊計量、外部工具報告視為外部輸入),在沙箱內執行。
  • 不接 SonarQube 的整條流程(orchestration)。

被排除:整條沿用 FR-058.7——那條的解壓在 agent 端,而且綁 SonarQube 的流程。

為什麼:檢查器是通用的;解壓位置與後續流程不同。安全解壓放在共用層,兩個入口與未來其他功能都能用。

D14 AI 分析(已定:有,但使用者主動按才跑)

  • 報告頁一顆「AI 分析」按鈕,按了才呼叫、才扣額度,不自動生成。
  • 結果標示「AI 生成,以上方統計為準」。
  • 直接借 FR-122 AI 閘道:服務商設定、額度、呼叫紀錄、提示注入防護都現成,本案只加一個提示樣板(prompt template),不建新的基礎設施。
  • 與 D6 不衝突:系統預設只給事實;分析是使用者自己要的,而且明確標示是 AI 產出。

被排除的做法

  • 每次掃描自動生成:排除——花額度,而且等於系統預設替使用者下結論,違反 D6。
  • 第一版不做:排除——使用者確實需要一段白話整理,閘道又是現成的,成本只有一個提示樣板。

§11

風險與陷阱

🔴 派工服務已嚴重超標,伺服器端分岔必須新開檔

detection_orchestration_service.py 已 2,500 多行,遠超檔案 800 行上限。本地執行分岔必須新開檔(例如 server_side_dispatch.py),再從 start_execution 一行呼叫,不准往原檔裡加邏輯(CLAUDE.md 體積規則)。reviewer 看到往原檔塞會列為阻斷級。分岔檔依工具 code 查 job handler 登記表({'sbom-scan': handler}),不把 SBOM 寫死在分岔內——這是給日後伺服器端自動化任務留的地基(見「未來項」節)。

🔴 守衛測試寫死四個容器名,新容器不會被檢查

test_security_shape_container_hardening.py:17 的清單是寫死的,而且不守唯讀與 internal 網段。本案雖然不加容器,但沙箱改吃樣板時若樣板漏了唯讀,不會有任何測試紅;第二版加抓檔容器時更是如此。A 階段必須先改守衛(按 -sandbox 結尾自動納入、補唯讀與 internal 斷言),再動 compose。

沒有 lock 檔的掃描結果會「看起來很乾淨」

Syft 讀不到確切版本、也看不到傳遞依賴時,比對弱點會少很多,報表上的弱點數可能很少,看起來很乾淨。顧問最容易被這個誤導。所以 D3 規定:缺 lock 檔的標示、品質扣分與「供應鏈管理缺失」發現都必須放在顧問層顯眼處,不能只在原始層;涵蓋不完整要在每一組統計旁同時顯示,數字不可單獨出現。

拆檔與掃描共用沙箱,掃描會讓證據分類排隊

D1 已接受的取捨。大型源碼包掃描最長 20 分鐘,這段時間證據分類要等。上線前要量測排隊時間;手冊要寫明「掃描進行中,證據分類可能變慢」。

沙箱 image 變大,出貨弱點掃描範圍跟著變

三支工具裝進沙箱 image 後,出貨管線(sbom.sh --images)掃這顆 image 時也會掃到工具本身的依賴。工具若有 Critical 弱點,會擋住出包——A 階段要先跑一次出貨弱點判定,確認白名單是否要調。

兩家工具意見不一致是常態

Trivy 與 Grype 的弱點來源、比對規則、分數來源都不同。結果頁要用白話說明「為什麼兩家說法不同」,不能丟兩個數字讓顧問自己猜。

弱點資料過期,結果就失真

封閉網路客戶可能半年不更新。資料日期必須出現在結果頁與每一份報表上,過期警示不能關。

外來 SBOM 檔是不受信任的輸入

工程師交來的 SBOM 可能格式錯誤、超大、或刻意構造來打解析器。一律在沙箱內解析,後端行程不碰內容,大小 50MB 封頂。


§12

未來項(第二版 Git 來源)

第一版只收上傳。下面三題是 Git 來源的設計,討論時已有建議,原文保留供第二版接續;第一版不實作、不需裁決。第二版要做時,依 D1 的合併原則,Git 抓檔容器「要出網」與既有沙箱信任邊界不同,會另開一顆 guidant-scan-fetcher,套同一份加固樣板,出網政策填 git-allowlist;資料模型只在 source_type 加 git、細節放 source_ref,不改表。另需兩張設定表:config.source_scan_git_hosts(Git 站台白名單:host、label)與 config.source_scan_credentials(租戶憑證:name、git_host、username、secret_params envelope、last_used_at),共同欄位與 RLS 同資料模型節。

未來:抓檔容器出網的範圍怎麼管(原 D2)

問題:抓 Git 一定要出網,但不能讓它變成「打客戶內網的跳板」。

選項

  • A(建議)兩層:① 應用層——後端接單時驗 repo 位址:只准 https、站台必須在租戶管理員維護的白名單、DNS 解析結果不得是內網位址(防伺服器端請求偽造,即借我們的伺服器去打客戶內網);② 容器層——git 設 protocol.allow=https、core.hooksPath=/dev/null(不跑 repo 內的掛勾程式)、不抓子模組、--depth 1。
  • B 再加網路層白名單(egress proxy,所有出網都經代理、只放白名單站台):最嚴,但多一顆容器與一套設定,第一版列未來項。
  • C 直接在 worker 裡 clone:排除——worker 有金鑰與 DB 連線,被惡意 repo 攻破代價太大。
  • D 沙箱自己出網抓檔:排除——違反 FR-122「拆檔與出網不同容器」,也違反 D1 的合併規則(能否出網不同就不能合併)。

建議:採 A,B 列未來項。

為什麼:A 擋掉絕大部分情境且成本低。已接受風險要寫明:抓檔容器若被攻破(例如 git 本身的漏洞),它可以出網;但它手上沒有工具、沒有弱點資料、沒有 DB 連線、沒有長存金鑰,能拿到的只有當次那一份 repo。

未來:Git 來源要不要先登記才能掃(原 D10,類比 FR-129 目標授權)

問題:FR-129 規定偵測目標必須來自登記資料。Git 位址要不要比照「逐個 repo 登記」?

選項

  • A(建議)站台白名單:沿用上一題的 Git 站台白名單,由租戶管理員維護,形狀類似 FR-129 的掃描網段;站台內的任何 repo 都可掃。
  • B 逐 repo 登記:最嚴,但顧問每換一個 repo 都要找管理員,違背「自給自足」。列第二版選項。

建議:採 A,B 列第二版。

為什麼:風險的核心是「伺服器會連去哪裡」,站台層白名單已經管住這件事;repo 層的差異只是同一站台內的哪個倉庫。

未來:私有倉庫憑證怎麼存、怎麼用(原 D11)

問題:顧問填的 Git token 是客戶的祕密。

建議做法

  • 存 detection_secret_params 的 envelope 寫法(Fernet 加密)。
  • 金鑰:復用 DETECTION_TOOL_ENCRYPTION_KEY(同屬偵測域,不新增環境變數)。被排除:另開新金鑰——多一把要保管、要輪替,換不到額外防護。
  • job payload 只放 credential_uid。
  • worker 解密後經內網請求本文交給抓檔容器(不進網址、不進命令列參數);抓檔容器以子行程環境變數餵 GIT_ASKPASS 小腳本,不落磁碟,clone 完即丟。
  • 精靈提供兩個選項:「只用這一次(不儲存)」與「存為租戶憑證」。讀取 API 只回「已設定」。

建議:照上述做法。

為什麼:完全照 .claude/rules/security/secrets.md 的規則(payload 只放 uid、明文不進磁碟、不進子行程參數、讀取只回有無);「只用這一次」讓不想把客戶帳密留在系統的顧問有選擇。

未來:伺服器端自動化任務的通用機制(大整理時處理)

決策者方向:顧問很多自己做的流程——產掃描報告、帳號清查後匯出清冊直接列入證據等——未來都要包進任務內,不只檢測工具。本案不做通用化,先把 SBOM 做出來,之後一次大整理。

大整理時要處理:

  • 工具目錄加分類(檢測/自動化作業)
  • 授權按工具列判,而不是按任務型態
  • summary 的中文對照由工具列自帶,而不是寫死在 Vue 元件裡
  • 任務型態的顯示名稱

本案只留一個地基:伺服器端分岔檔依工具 code 查 job handler 登記表(D9「派工」列),日後新增伺服器端工具只需登記一筆,不必再改分岔。

未來:抓檔容器可出網(已接受風險)

Git 抓檔容器被攻破時(例如 git 本身的漏洞),它可以出網。緩解靠它手上什麼都沒有:沒有掃描工具、沒有弱點資料、沒有 DB 連線、沒有長存金鑰,能拿到的只有當次那一份 repo。網路層出網白名單(egress proxy)隨第二版一起評估。

未來:Git 位址可能被拿來打客戶內網

若只檢查站台名稱、不檢查 DNS 解析結果,顧問(或拿到顧問帳號的人)可以把一個看似正常的站台名稱指到內網位址,讓伺服器替他連進去。接單時必須驗 DNS 結果不得是內網位址,clone 前再驗一次(防止兩次解析結果不同)。

未來:私有倉庫憑證是客戶的祕密

明文只能出現在 worker 與抓檔容器的單次請求記憶體裡,不進網址、不進命令列參數、不落磁碟、不進 log。「只用這一次」選項必須真的不寫進任何表。


§13

不在範圍

  • 動態掃描(DAST,對執行中的系統發動測試)、滲透測試
  • 修弱點:只報告,不改客戶程式碼
  • 取代我們自己的出貨管線:sbom.sh 維持不動;產品功能成熟後,日後可反過來讓出貨管線吃產品的服務,但不是這次
  • 把 ex-sbom 或 Dependency-Track 整包嵌進產品
  • 建置型依賴解析(執行客戶建置工具,cdxgen 那條路)——需客戶有套件鏡像,列未來項(見 D3、D5)
  • Git 來源(含私有倉庫憑證、Git 站台白名單、抓檔容器)——第二版,見「未來項」節
  • 網路層出網白名單(egress proxy)——隨 Git 來源一起評估
  • 逐 repo 登記——第二版,見「未來項」節
  • AI 自動生成分析:只做使用者主動按的版本(見 D14)
  • 掃描客戶的容器 image(Syft 支援,但第一版只做源碼壓縮檔與 SBOM 檔)
  • 定期自動重掃與弱點告警通知
  • 授權合規的法律意見:只標出傳染性授權等風險,不判定是否違約

§14

與其他需求的關係

FR 關係
FR-122 沙箱的加固形狀與「拆檔與出網不同容器」原則的來源;本案把掃描工具併進該沙箱,加固設定抽成樣板
FR-127、FR-128 出貨 SBOM 與雙工具弱點判定管線;指令組合與別名解析從這裡搬
FR-058 源碼上傳掃描的限額與壓縮檔檢查器(D13)
FR-056 檢測工具目錄、派工、detection_executions 與轉證據流程;入口 2(階段 H)在這條鏈上多一個工具,並新增伺服器端執行路徑(D9)
FR-129 目標必須來自登記資料的原則;第二版 Git 站台白名單對應(未來項)
FR-130 內部 CA 掛載慣例,第二版抓檔容器信任客戶自簽 Git 站台時可借
FR-065 落地版離線前提;弱點資料更新包走安裝包與 guidantai 指令
FR-132 能力點命名與「加值包整包落在單一模組」硬規則(D8)
FR-122 的 AI 閘道 D14 的 AI 分析按鈕直接借用