FR-058 檢測工具擴充(四工具接入+2026-07-31 追加 SonarQube 拉取與主動掃描)— 設計文件

狀態:全案已實作完成,決策者 2026-08-01 驗收通過(T-7.4 端到端驗收含 T-7.5/T-7.6 皆過;D1–D34 已拍板;D10 為 tombstone 空編號、D32 併入 D29,見 §2)|建立日期:2026-07-30|FR-056 檢測工具整合平台 → FR-057 OpenSCAP SSH connector 續作|2026-07-31 追加 FR-058.5 SonarQube 拉取快照(§4.6)與 FR-058.6 SonarQube 主動掃描(§4.7)|2026-08-01 追加 FR-058.7 SonarQube 上傳檔案掃描(§4.8,已實作完成並驗收) 討論稿(含流程圖與官方資源盤點):discussion.html(四工具母案)、discussion-sonarqube-scan-mode.html(FR-058.6 SonarQube 主動掃描) 吸收 Notion CM-953「FR-057 後續:異質 OS 環境(Windows 等非 Linux 主機)的檢測支援方案」(狀態=討論),於 FR-058.2 落地時一併結案,不另開平行卡。 2026-08-01 追加 FR-058.7 SonarQube 上傳檔案掃描(§4.8,拆分表 §5.12)——scan 模式的取源擴充;決策 D25–D31 定案、D32 併入 D29;討論稿:discussion-fr058.7-upload-scan.html(v4,決策者已全數過稿)。

變更紀錄

日期 變更 對應
2026-07-30 初版設計定案;D1–D9 + D11 全數拍板(D10 隨 SonarQube 移出本案,編號保留不重排);拆分為 5 子需求 + 1 橫向前置項 FR-058 母案
2026-07-30 D4 改版:ZAP 證據由 PDF 改 HTML(reports API 僅寫遠端磁碟、無回傳內容端點,issue #7821);雙格式精神不變 FR-058.1
2026-07-30 D2 補充:新增 D12——登入後掃描僅支援傳統表單登入,SPA(json 認證 + token session 管理)列後續另案;§4.2 補實測佐證 FR-058.1
2026-07-30 D12 後果更正:實測揭露對 SPA 登入掃描並非「安靜成功產出空報告」,而是認證失敗累積 100 次觸發 ZAP Insights 門檻、daemon 主動關閉自己,連帶中斷共用該 ZAP 的其他任務(營運層級風險);§4.2 補完整因果鏈與 log 摘錄、標明僅限登入模式;同步更正 setup_guide(改為「請勿對 SPA 使用登入後掃描」);新增「connector 自我保護只記錄不實作」待辦 FR-058.1
2026-07-31 補 T-2.2 揭露的架構事實:CINC 裝在 Agent 本機、由它自己主動連出去,與 OpenSCAP(引擎裝在目標主機)相反。這才是 D6「雙 transport 邊際成本很低」的真正原因,現有實證(CM-973,evidence-agent dcedb4f)。更正原「對照 openscap.py 的 SSH 模式」描述——openscap.py 僅多目標處理與 host_failures 可借鏡,連線層完全不同。同步記錄 T-2.2 建立的三個實作約定(憑證走 --config 不進 argv/授權紅線以 exit 172 硬失敗釘住/probe 憑證檢查排在引擎檢查之前)。非補不可的理由:T-3.2 content 外部載入接口直接建立在此架構假設上,若誤以為 content 要送到目標主機,設計會做錯且要到端到端驗收才會發現 FR-058.2 / FR-058.3
2026-07-31 T-3.2 改為「驗證 + 寫文件」,原規劃的 content 外部載入接口裁示不做——CINC 原生支援三種 content 來源,而 params.profile 直接進 cinc-auditor exec 的 argv,外部載入能力本來就在,另做抽象層只是把上游能力重新命名一次並限縮它。§4.4 補「D7 約束的達成方式」全段:三條約束逐條驗收(不硬編/不打包/換 content 零改動)、實跑佐證(兩份公開 content 對 SSH 與 WinRM 各跑通、error 0)、裸 GitHub 網址走 git fetcher 而非 tarball 下載(agent image 原無 git,首次實跑炸在 profile 解析階段,已於 agent 6d1a4fc 補裝)、tarball 形式對 git 零相依的對照實驗(移走 /usr/bin/git 後裸網址失敗、tarball 照常)。一併記錄兩個已知限制(封閉網路無法把 profile 送進 container、私有 repo 無憑證路徑),兩者併入後台 content 更新機制的評估範圍 FR-058.3
2026-07-31 SonarQube 重新納入,成為第五個子需求 FR-058.5。原規劃期決策者裁示「本期不做」而移出(D10 因此成為空編號),2026-07-31 決策者裁示重新納入;決策以 D13–D16 定案(D13 pull-snapshot 整合模式 / D14 任務參數 / D15 證據格式 / D16 認證與跨版本相容),D10 編號維持 tombstone 不重用、不重排。新增 §4.6 詳細設計、拆分表加 T-5.1〜T-5.3 三子任務、§6 加對應驗收項。SonarQube 是平台第一個「拉取快照(pull-snapshot)」型工具——server 無任何觸發掃描的 Web API,掃描由客戶端 CI / sonar-scanner 執行,connector 只拉取既有分析結果組報告 FR-058.5
2026-07-31 追加第六個子需求 FR-058.6:SonarQube 主動掃描模式(雛形)。決策者看到 FR-058.5 實際畫面後指出「流程好像不太對」——他要的是平台真的發動一次掃描(使用者提供源碼 → agent 跑 sonar-scanner → 推客戶 server → 輪詢 → 拉結果組報告),而非拉取客戶 CI 已推上去的既有結果。這是新需求不是 bug:D13 當時排除「agent 自跑 scanner」的理由是「agent 沒有原始碼、不可能備齊各語言 build 環境」,本案正是補上那個前提(源碼由使用者提供)並靠收斂語言範圍(D18 只做免 build 環境的 A 類)解掉後半段。決策以 D17–D24 定案(D17 模式共存形式 / D18 語言範圍 / D19 源碼來源 / D20 scanner 交付 / D21 projectKey 命名 / D22 資源保護 / D23 coverage / D24 預設值),D10 維持 tombstone 不重用、不重排。新增 §4.7 詳細設計、拆分表加 T-6.1〜T-6.3、§6 加對應驗收項。定位為雛形——取捨一律傾向最小可行,被排除項全數列於 §4.7「後續強化」並標明是裁示排除非遺漏 FR-058.6
2026-08-01 追加第七個子需求 FR-058.7:SonarQube 上傳檔案掃描(設計定案)。FR-058.6 scan 模式驗收通過後,決策者提出客戶要能直接上傳源碼壓縮包做檢測(宏觀流向拍板:上傳 → tenant 儲存空間 → agent 拉下來掃 → agent 端掃完即刪 → 磁碟不夠跳錯誤)。定位為 scan 模式的取源擴充,不是新模式——scan_mode 多一個 upload 選項,取源之後整條掃描管線全複用。決策以 D25–D31 定案(D25 取源通道 / D26 參數模型 / D27 格式與限額 / D28 磁碟防護 / D29 檔案生命週期 / D30 解壓實作 / D31 顯示與紀錄),D32 併入 D29 ④ 不獨立成案。上傳時機定在任務抽屜發起執行時(非任務設定表單);server 端壓縮包保留供重掃沿用+歷史不刪(D29,推翻討論過程中「終態即刪」的中間版本)。新增 §4.8 詳細設計、拆分表加 T-7.1〜T-7.4(§5.12)。討論稿:discussion-fr058.7-upload-scan.html(v4,決策者已全數過稿) FR-058.7
2026-08-01 追加 D33:scan_mode=upload 的檢測任務不進「開始執行任務」自動派發清單(FR-058.7 驗收發現)。自動派發(FR-056.5 #5,_auto_dispatch_detection_scans)不帶檔案,對 upload 任務必然觸發 DETECTION_TOOLS_400005 且被批次 try/except 吞掉只留 warning log——PM 看到成功 toast、執行者無感,且無執行紀錄使冪等條件永遠成立、每按一次再失敗一次。定案在挑選查詢(get_detection_jobs_without_execution)排除 tool_params->>'scan_mode'='upload' 的綁定;upload 任務一律由執行者在任務抽屜上傳檔案後手動執行;pull / scan 自動派發不變。FE 補批次發佈 confirm 文案一句提示。§4.8 補「自動派發與 upload 模式」段、拆分表加 T-7.5(§5.12) FR-058.7
2026-08-01 追加 D34:檢測掃描通知加厚——九項內容+失敗通知+三管道(FR-058.7 驗收期間決策者提出)。現況兩缺陷:成功通知只有「專案 X 的檢測工具掃描已完成」一句且只發 Email;失敗完全不通知(只寫 detection_executions,使用者要自己去抽屜看)。定案三子項:① 內容擴為九項(專案/控制項群組/控制項/AO/任務名稱/工具名稱/掃描設定/時間/成功或失敗,另把現成未用的 evidence 參數用起來附報告檔名);② 失敗也通知(on_scan_failed 補查 job/binding 後發,含 error_message,收件人與成功相同);③ 管道補齊 Discord/Telegram(HTML+純文字兩版文案、i18n 6 msgid);hosts 決策者裁示可出信。新查詢走 8 層 join 鏈落 detection_job_notify_query.py;⚠️ 兩坑:catalog_controls.control_id 跨 catalog 不唯一必帶 catalog_id 過濾、AO title 對 CMMC 全 NULL 用 title→prose→part_id fallback。無 migration、無 DI 變更。§2 加 D34、§4.8 補「掃描完成通知」段、拆分表加 T-7.6(§5.12) FR-058.7
2026-08-01 收尾:全案驗收通過。決策者 2026-08-01 完成 T-7.4 端到端驗收(含 T-7.5 upload 不自動派發、T-7.6 通知加厚),T-7.1〜T-7.6 全數完成,FR-058 全案(.0〜.7 + 橫向前置項)結案。T-7.4 四個未實測項裁示:磁碟不足與解壓炸彈接受單元測試涵蓋、環境變數調限額不另實測、nginx client_max_body_size 列上版必驗項。§5.12 拆分表補各 T-7.x 完成標記 FR-058 全案
2026-07-30 D9 改案:Nmap 由 CLI 型改 SSH 型 + 補 NPSL 授權法律依據。實作前發現「客戶自備」在容器化部署下無法成立(agent 在容器內看不到主機的 nmap),查證 NPSL v0.95 §3 後確認不能 bundle 且理由比原記載更強(衍生作品定義明文涵蓋「專門執行本軟體並解析其結果」的程式),同條款末段的自留出口正好讓 SSH 型成立。連帶:nmap 改為需 SSH 憑證(requires_credentials=TRUE)、T-4.1 的零憑證平台能力保留但 nmap 不再是其使用者、connection_type='CLI' 至今仍無實際使用者 FR-058.4

1. 需求背景與目標

FR-056 建立的檢測工具整合平台是 tool-agnostic 的:後端對任何具體工具零知識(不存在 if tool.code == "openvas" 這類分支),前端純 schema 驅動渲染,加一個工具原則上只要一條 SQL seed。FR-057 又補齊了 SSH 連線型態、setup_guide 說明欄位、多檔證據回收(result_ref.upload_uids)與兩條「靜默假成功」防護通道(host_failures / content_mismatch_hosts)。

本案站在這兩層地基上,一次接入四個工具,並補上平台唯一的結構性缺口——任務層敏感參數

範圍註記(2026-07-31):本文件各處的「四工具」指原始規劃的四棒(ZAP / InSpec / GCB / Nmap)。SonarQube 原於規劃期裁示「本期不做」移出,2026-07-31 決策者裁示重新納入,成為追加的第五棒 FR-058.5(拉取快照,詳見 §4.6);同日再追加第六棒 FR-058.6(主動掃描,詳見 §4.7)——兩者同一個工具的兩種模式(D17),不是兩個工具。既有段落的「四工具」措辭不逐一改寫。

順位 工具 類型 排這個位置的理由
1 ZAP DAST 網頁動態掃描 有可參考的既有實作(前同事的 auto-pentest 專案),API 呼叫序列與踩過的坑都已成文,摸索成本最低
2 InSpec / CINC Auditor 組態檢測(Windows / Linux) 先做這個,GCB 的引擎就是現成的——讓 GCB 只剩驗證鏈路一件事
3 GCB 政府組態基準(TWGCB) 引擎複用第 2 棒;本案只做引擎與一個 Windows 最小 Demo profile,content 產製與後台更新機制不納入(D7)
4 Nmap 網路埠掃描 技術上最單純(呼叫 CLI + XML 解析)。原以「排除平台零憑證阻礙」為主要工作,2026-07-30 改案為 SSH 型後(D9),主要工作變成複用 OpenSCAP 既有的 SSH 連線模式
5 SonarQube(拉取快照) SAST 靜態程式碼分析 2026-07-31 追加(原裁示本期不做,後重新納入,見 D13–D16)。平台第一個「拉取快照(pull-snapshot)」型工具——不觸發掃描、只拉取客戶端 CI 已推上 server 的分析結果組報告;平台面零改動,工作集中在 connector 與 seed
6 SonarQube(主動掃描) SAST 靜態程式碼分析 2026-07-31 追加(見 D17–D24,§4.7)。不是新工具,是第 5 棒的第二種模式——同一張工具卡加 scan_mode 參數分流(D17)。平台真的發動掃描:公開 Git repo 取源碼 → agent 跑 sonar-scanner → 推客戶 server → 輪詢處理佇列 → 拉結果(複用第 5 棒程式碼)組報告。定位為雛形,只做 A 類純源碼 7 語言

1.1 四工具的整合形態

以「連線型態」分,這四個工具分屬三種完全不同的模式——差別在於 connector 面對的是遠端服務 API、目標主機的遠端 shell,還是 agent 本機的命令列:

形態 已上線 本案新增
① 連遠端服務 API OpenVAS ZAP(FR-058.1);SonarQube(FR-058.5,拉取快照型,見 §4.6——歸此形態但語意不同:連的是 API 卻不下掃描指令,只讀取既有結果)
② 遠端登入目標主機執行(引擎裝在目標主機 OpenSCAP(SSH / Linux) Nmap(SSH,FR-058.4;2026-07-30 改案,原定 ③)
③ agent 本機 CLI 尚無(connection_type='CLI' 已定義但未被使用) 仍無——原定 Nmap 走此型態,2026-07-30 改案後改走 ②(D9),此型態至今仍無實際使用者
②′ agent 本機引擎、由引擎自己連出去(引擎裝在 agent 本機,目標主機 agentless) 尚無 InSpec / CINC(SSH + WinRM,FR-058.2)、GCB(複用 .2 引擎,FR-058.3)
①′ agent 本機執行 CLI **+ 連遠端服務 API(先在本機跑二進位,再把結果推 API、輪詢、讀回) 尚無 SonarQube 主動掃描**(FR-058.6,見 §4.7)——connection_type 仍是 API(憑證與連線標的都是 SonarQube server),但執行期多了「agent 本機跑 sonar-scanner」這一段

②′ 是 T-2.2 實作後才浮現的第四種形態(原歸在 ②,實際上方向相反)。connection_type 仍宣告為 SSH / WinRM——那是目標主機開放的協定,不代表 connector 自己建連線。詳見 §4.3「架構事實」。

①′ 是 FR-058.6 帶來的第五種形態。要注意的是 connection_type='CLI'(FR-056 定義至今無使用者)仍然沒有使用者——SonarQube 主動掃描雖然在 agent 本機跑二進位,但它的連線標的與憑證都是遠端 SonarQube server,歸 API 型而非 CLI 型;CLI 指的是「整個任務只在 agent 本機完成、不連任何遠端服務」。

1.2 平台唯一的結構性缺口:任務層敏感參數

現有設計把憑證與參數分成兩處放,而只有憑證那一條有加密

層級 存放位置 加密 適合放什麼
租戶層 tenant_detection_tool_configs Fernet 加密 ZAP 服務本身的位址與 API key——全租戶共用、設定一次就不動
任務層 job_execution_detection_tools.tool_params 明文 掃描目標、profile 這類非敏感參數
任務層敏感 目前不存在 「這次掃 A 網站用 A 的帳密,下次掃 B 網站用 B 的帳密」正好落在這一格

若直接把被掃網站的登入帳密塞進 tool_params,會發生兩件事:① 帳密明文躺在資料庫;② FE 執行紀錄視窗會顯示任務參數——BE 現有的 secret 剝除機制是針對租戶層設定的 secret 定義寫的,任務層參數沒有這套保護,等於帳密直接顯示在畫面上。

這不是 ZAP 專屬問題(Nmap 若要做認證掃描、GCB 若每台主機帳密不同都會撞上同一格),因此當成平台級能力補上,成為 FR-058.0,並且是 ZAP 的硬前置。

1.3 端到端流程(以 ZAP 登入後掃描為例)

稽核員發佈任務:填 target_url、選「登入後主動」、填測試帳密
  → BE 依 param_schema 對 secret:true 欄位 Fernet 加密後寫入 tool_params(敏感欄位為密文)
  → 按「開始執行任務」:挑 agent、解密租戶工具憑證、建 agent_tasks(pending)、建執行紀錄
  → agent 心跳(mTLS,預設 300 秒)領回 pending_tasks(含當場解密的明文參數,僅傳輸中存在)
  → agent 起 daemon thread:ack → get_connector → ZAP 設定 context/登入認證/logged_in_indicator
     → ajaxSpider 爬取 → ascan 主動掃描(輪詢,支援 cancel_event / timeout)
     → core.htmlreport / core.jsonreport 雙格式(HTML 上傳當證據 / JSON 僅記憶體解析 summary)
  → 報告 bytes 走既有 blob 通道上傳 → 回報 result
  → 雲端取回存 job_evidences(source=DETECTION_TOOL)→ 通知負責人 → 依完成模式收尾
  → FE 執行紀錄顯示參數時,敏感欄位已剝除

敏感參數的生命週期:資料庫為密文、僅在派工傳輸與 agent 執行期間為明文、agent 用完即丟不落地、FE 一律剝除


2. 決策定案(D1–D34)

D10 原隨 SonarQube 移出本案而成為空編號;2026-07-31 SonarQube 重新納入為 FR-058.5 後,決策以 D13–D16 重新定案,D10 編號維持 tombstone 不重用、不重排(避免與先前溝通過的編號錯位),其餘編號不變。同日追加 FR-058.6 SonarQube 主動掃描,決策以 D17–D24 定案(全數已拍板,無待決策項)。2026-08-01 追加 FR-058.7 SonarQube 上傳檔案掃描,決策以 D25–D31 定案(全數已拍板)、D32 併入 D29 ④ 不獨立成案(編號保留、內容指向 D29)。同日 FR-058.7 驗收期間追加 D33(upload 任務不進自動派發清單)與 D34(檢測掃描通知加厚:九項內容+失敗通知+三管道)並定案。

# 決策 定案 被排除的方案與原因
D1 任務層敏感參數的實作範圍 加密存取 + FE 執行紀錄顯示剝除 + 稽核 log 剝除,一起做。 只做加密等於「加密了卻在 UI 洩漏」——資料庫是密文,畫面上照樣看得到帳密,防護實質等於零。 「只做加解密、UI 顯示留待後續」——排除:稽核 log 寫入敏感值等於在另一張表留下明文副本,防護有洞就等於沒有。
D2 ZAP 掃描深度的預設值 schema 三種模式都支援(被動 / 主動 / 登入後主動),預設給被動;主動類選項在 FE 加明確警語(會產生攻擊流量,請確認已獲目標方授權)。 預設主動——排除:預設值選錯的代價不對等。預設被動最多是掃得淺;預設主動則可能在使用者未察覺的情況下對正式環境送出攻擊流量。
D3 ZAP 服務的部署形態 客戶自備已安裝的 ZAP daemon(同 OpenVAS / OpenSCAP 模型),agent 只負責連上去;setup_guide 寫清楚 daemon 啟動指令與 API key 設定方式。使用者公司已安裝 2.17.0,這條路本來就是現況。 agent 端自帶 ZAP container(參考碼的 ZAPContainerDomainService 那一套)——排除:agent image 膨脹且要背負拉容器與版本維護責任,與既有兩個工具的模型不一致。
D4 ZAP 證據格式 (2026-07-30 改版)雙格式取報告,精神對齊 OpenVAS connector 既有模式——core.htmlreport() 回傳的 HTML bytes 上傳當證據(檔名 ZAP掃描報告_<target>_<date>.html)、core.jsonreport() 回傳的 JSON 只在記憶體解析出 summary 不上傳。證據池維持人可讀,統計數字又有結構化來源,且 ZAP 端零設定、維持 D3 部署模型。 只存單一格式——排除:只存人可讀格式則 summary 要靠文字解析(脆弱);只存 JSON 則證據池不是人可讀。兩份都上傳——排除:在證據池塞入兩份等價內容。
2026-07-30 改版:原定案「PDF 上傳當證據(reports.generate)」實作時發現不可行——ZAP 的 reports API(reports.generate)只把檔案寫進 ZAP daemon 主機的磁碟、回傳路徑字串,官方沒有回傳檔案內容的端點(issue #7821 至今未實作);而 D3 定的部署模型是遠端 daemon,agent 取不到那個路徑的檔案。改版時被排除的走法:①「PDF + 要求客戶開檔案傳輸通道」——違反 D3 部署模型,等於從後門把部署負擔帶回來;②「檔案傳輸為主、HTML 自動退回」——兩條路徑的長期維護與測試成本換一個罕用 fallback,違反本案「不做無意義測試」紀律。已知風險與處置:證據池 PDF 預覽鏈不吃 HTML,接受「可下載、不可預覽」為第一批出貨標準;若日後要求可預覽,可在 agent 端用既有 LibreOffice --convert-to pdf 補(ZAP 端仍零設定),但轉檔品質未驗證故不預做。
D5 InSpec 版本選擇 CINC Auditor(Apache 2.0 自由重建版,Progress 官方認可的 drop-in 替代品,功能等價)。授權乾淨,採購與部署文件都要寫明。 官方 InSpec 6+ 商業 binary——排除:需接受 Chef EULA 並持有 license key,會把 license key 管理問題帶進每一個客戶部署。
D6 InSpec connector 的 transport 範圍 SSH + WinRM 雙 transport 一次到位。 同一套 CLI 與 profile 格式,多做一個 transport 的邊際成本很低,而 GCB 的 Linux 線立刻就能複用。這條同時是 D7 引擎層的實作依據。 只做 WinRM(Windows)——排除:GCB Linux 那條線之後還要回頭補一次,等於同一份工作做兩趟。
D7 GCB 的引擎優先與 content 後置 引擎層:Windows 與 Linux 雙邊一次到位,由 FR-058.2 的 CINC Auditor connector 承擔,GCB 不另立引擎、不另寫 connectorcontent 層:本案只做一份 Windows 最小 Demo profile,證明端到端鏈路(WinRM 連線 → 認證 → profile 執行 → 結果解析 → 證據上傳)可運作即可。FR-058.3 的驗收條件是「鏈路可運作」,不是「涵蓋多少規則」。 另有本案必須做到的設計約束:content 不可硬編進 connector 或 agent image,必須可從外部載入 ① content 產製(逐條 TWGCB-ID 轉檢查規則、Windows GPO 產生器、Linux 人工撰寫)——排除出本案:那是持續性人力投入,且是最沒有技術風險的部分,一條規則也能證明鏈路通。② 後台 content 更新機制(上傳/版更/套用)——排除出本案,日後另案。③ macOS 15(TWGCB-01-015)——排除:基準 2026/6 才發布、版本尚新,且現有 connector 未曾處理 macOS。④「接口留到日後再補」——排除:事後補要改動 connector 結構,現在留成本極低。
D8 GCB 的 content 格式 **InSpec profile。 Ruby DSL 比 XCCDF + OVAL 好寫也好維護,且 Windows GCB 只有這條路走得通。與 D7 引擎層一致:既然兩邊都由同一套 CINC Auditor 執行,content 自然只需維護一種格式。 XCCDF + OVAL(沿用 OpenSCAP)——排除:同一份 TWGCB 基準要用兩種語法各寫一次(Linux 用 XCCDF、Windows 用 InSpec),TWGCB 改版時要改兩份,長期成本明顯較高。引擎統一的真正目的是 content 格式統一。**
D9 Nmap 的整合型態與零憑證放行
(2026-07-30 改案)
(改案後)Nmap 走 connection_type='SSH'——nmap 由客戶安裝在自己指定的主機上,agent 透過 SSH 登入該主機執行,與 OpenSCAP 完全同構。法律依據:NPSL v0.95 §3 末段明文不主張控制「執行使用者早已安裝在自己系統上的 nmap」的軟體(原文見右欄),SSH 型正落在此描述內。連帶:nmap 需要 SSH 憑證requires_credentials=TRUE)。

(原定案,平台能力部分保留)放寬 start_execution 的憑證檢查——在 detection_tools 增加「是否需要憑證」的宣告欄位,讓平台知道零憑證是一種合法狀態。此為平台級能力,改案後仍然正確且保留,只是 nmap 不再是它的第一個使用者,日後其他真正零憑證的工具會用到。
為 Nmap 建立一筆空的租戶設定當佔位——排除:會留下一筆語意不明的資料,日後看到的人無法判斷那是刻意還是遺漏。

2026-07-30 改案(原定案為 connection_type='CLI'、agent 本機執行、不 bundle 理由僅記「NPSL 授權」)
① 觸發點:實作前發現「客戶自備」在容器化部署下有實務障礙——agent 跑在 Docker container 內,看不到主機上安裝的 nmap(容器有獨立檔案系統)。原 CLI 設計隱含假設 nmap 在 agent 可執行範圍內,但「不 bundle」又「不能裝進容器」,兩個條件互相矛盾。
② 查證 NPSL 後確認不能 bundle,且理由比原記載更強。 NPSL v0.95 §3(出處 https://svn.nmap.org/nmap/LICENSE)明文列舉的「衍生作品」定義包含這一條:
Is designed specifically to execute Covered Software and parse the results (as opposed to typical shell or execution-menu apps, which will execute anything you tell them to).
我們的 connector 正是「專門呼叫 nmap 並解析其 XML 輸出」,
正中此條
。因此「只用子行程呼叫、不連結函式庫」這個 GPL 慣用的免責理由在 NPSL 底下不成立——該條款是特意加來堵掉這個論點的(NPSL 基於 GPLv2 但加了額外限制)。GPLv2 的 mere aggregation 條款也救不了,因為它只適用於「not based on the Program」的作品,而 §3 已把我們定義為 based on。官方 Legal Notices(https://nmap.org/book/man-legal.html)另明說免費授權「doesn't allow Nmap to be used and redistributed within commercial software or hardware products」(明列 appliances / virtual machines / traditional applications),並為此販售 Nmap OEM Edition(https://nmap.org/oem/)。
③ NPSL §3 最後一段自己留了出口:軟體若解析的是使用者提供的結果、或執行的是「使用者早已安裝在自己系統上的 nmap」,授權方不主張控制(Licensor does not purport to control through this license any software which does not require the rights granted herein)。SSH 型態正好落在這個描述裡——nmap 是客戶裝在自己主機上的,我們只是遠端下指令。
④ 評估過並排除的方案
 • 掛載主機 binary 進容器-v /usr/bin/nmap:/usr/bin/nmap:ro 加上共享函式庫目錄)——排除:nmap 非靜態連結,依賴 libssl / libpcap / liblua 等一串共享函式庫,掛主機的 lib 目錄進容器會與容器內 glibc 版本賭運氣,且可能蓋掉容器自身函式庫把 agent 弄壞;架構也綁死 x86_64。把「客戶自備」變成一個需要客戶理解共享函式庫的任務,故障時難以遠端診斷。
 • 客戶自建一層 image(以我方 image 為 FROM,自行 apt install nmap 後 build)——技術上可行且乾淨(函式庫由套件管理員解決),法律上也成立(nmap 由客戶裝入,非我方散布)。排除理由:客戶每次 agent 升版都要改 Dockerfile 版號並重新 build,是持續性負擔;且「誰把它裝進去」雖在法律上是關鍵,技術結果與我方 bundle 完全相同,此種形式上的區隔在交付溝通上彆扭。
 • 購買 Nmap OEM 授權——本案不採,未評估成本;若日後產品策略需要零客戶設定的 nmap 整合,這是唯一能 bundle 的合法途徑,列為備查。
⑤ 拍板走 SSH 型的理由:「最沒爭議」——法律上落在 NPSL 自留出口內、技術上與已上線的 OpenSCAP 同構(connector 可複用 SSH 連線處理、私鑰暫存、host_failures 逐台回報)、客戶只需在指定主機 apt install nmap,無任何 docker 操作。
⑥ 連帶影響:nmap 不再是零憑證工具(requires_credentials 應為 TRUE);T-4.1 的零憑證放行平台能力保留(是平台級能力,只是換掉第一個使用者);connection_type='CLI' 因此仍然沒有實際使用者(FR-056 定義至今未被用過),原本「Nmap 是 CLI 型態首個實際使用者」的價值消失;agent 端 connector 範本從「本機子行程」改為對照 openscap.py 的 SSH 多目標模式。
D10 tombstone 空編號。原隨 SonarQube 移出本案而懸空;2026-07-31 SonarQube 重新納入為 FR-058.5,決策以 D13–D16 重新定案,本編號維持 tombstone 不重用(避免與先前溝通過的編號錯位)。
D11 _TOOL_ID_TO_CODE 硬編映射的處理 改為派工 payload 夾帶 tool code,agent factory 改依 code 取 connector,移除硬編 id 映射。趁本案一次加四個工具時一併解決。 維持硬編 id 映射——排除:一次加四個工具會讓脆弱性顯著放大。DEV / STG / POC 三環境的 seed id 必須完全一致,錯一位就整條派工錯配到別的 connector(例如 ZAP 任務被送給 OpenSCAP 執行),而且這種錯誤不會拋例外、只會安靜地跑錯工具
界線說明:這項改動不會讓新增工具完全免於動 agent——connector 實作本身仍必須裝進 agent,因此每加一個全新工具,agent 仍需更新一次。D11 消除的是另一個更脆弱的問題:額外維護一份 id 對照表、且三個環境的 seed id 必須完全一致。
D12 ZAP 登入後掃描的 SPA 支援範圍
(2026-07-30 追加,實測後補列)
本案(第一批)只支援傳統表單登入(form-based),param_schemalogin_url / login_username / login_password / logged_in_indicator 四欄即為此模型。SPA(單頁式應用)的登入後掃描列為後續另案,本案文件與 setup_guide 明白標示此限制,不做半套。技術原因有三,且三者必須同時解決才會通:① 表單登入無效——SPA 的登入是 JS 呼叫 API 取 token,頁面上沒有傳統表單提交;ZAP 的 form-based 認證是對登入頁 POST 帳密欄位,對 SPA 不會發生作用,需要的是 json-based 認證(對登入 API 送 JSON payload)。② token 不會自動帶——SPA 多用 JWT 存 localStorage、靠 Authorization header 帶;ZAP 預設 session 管理是 cookie-based,不會把 token 塞進後續請求,需要 script-based session management(要撰寫並上傳 ZAP script)。③ logged_in_indicator 對 SPA 不可靠——ZAP 比對的是 HTTP 回應的原始內容,而 SPA 初始 HTML 是空殼、登入後文字要 JS 渲染才進 DOM,原始回應裡找不到,ZAP 會誤判未登入並反覆重跑登入。
實測後果更正(2026-07-30):原記載「掃描仍會完成、只是涵蓋範圍侷限於登入前頁面」是錯的。第 ③ 項的重跑登入是無限迴圈,認證失敗會持續累積,達到 ZAP 2.17.0 Insights 機制的門檻後 ZAP daemon 主動關閉自己,掃描根本不會完成。log 原文關鍵行:Shutting down ZAP due to High Level Insight: HIGH : EXCEEDED_HIGH : http://192.168.50.151 : insight.auth.failure : 100,隨後 Discard SessionZAP 2.17.0 terminated.zap exited with code 2 (restarting)。由於 ZAP 是全租戶共用服務,單一 SPA 登入任務會連帶打斷同一台 ZAP 上其他正在執行的掃描(實測同時發生:另一任務在 ZAP 重啟空檔收到 Connection refused)。這已非報告品質問題,而是營運層級風險。範圍界定:僅限登入後掃描模式——被動與主動模式不建 context、不設認證、不比對特徵字串,不累積認證失敗,碰不到 Insights 門檻。
本案一併擴充 json-based 認證 + script-based session management——排除:那是「認證方式擴充 + script 管理 + 測試站台」三件獨立工作,塞進第一批會拖住已可出貨的部分。第一批的核心價值(ZAP 接入、HTML 證據進證據池、任務層敏感參數平台能力)不依賴登入後掃描,被動與主動掃描已實測可交付。
只做 json-based 認證、session 管理留待日後——排除:沒有中間地帶。認證送出去了但 token 帶不進後續請求,ZAP 一樣掃不進登入後頁面,結果與完全不做相同,卻已付出一次工程成本並在 UI 上留下「看起來支援了」的誤導。
改用 logged_in_indicator 以外的判定繞過——排除:那只解第三個原因,前兩個原因未解則掃描本來就進不去,判定準不準沒有意義。
D13 SonarQube 整合模式(pull-snapshot)
(2026-07-31 追加,FR-058.5)
任務執行拉取最近一次分析結果組報告,不觸發掃描。 本質差異:SonarQube Server 沒有任何觸發掃描的 Web API——掃描由 sonar-scanner 在有原始碼的環境(客戶 CI)執行後把結果推上 server,Web API 只能讀取既有結果(唯一例外是 SonarQube Cloud SaaS 的 Automatic Analysis,與 self-hosted 無關)。因此 connector 是平台第一個「拉取快照(pull-snapshot)」型工具。報告必含分析時間(api/project_analyses/search 最新一筆 date)供稽核員判斷資料新鮮度;setup_guide 與工具描述明寫「本平台不觸發掃描,掃描由客戶端 CI / sonar-scanner 執行」。 agent 端自跑 sonar-scanner——排除:agent 沒有原始碼,也不可能備齊各語言 build 環境。
D14 SonarQube 任務參數
(2026-07-31 追加,FR-058.5)
project_key 必填;branch 選填——branch 分析是付費版(Developer Edition 起)功能,Community Build 只有單一 branch。不填=不送 branch 參數(Community 相容);填了但 server 不支援時 connector 回明確錯誤訊息。 ① branch 必填——排除:Community 使用者直接不能用。② 自動偵測 edition——排除:多一次 API 往返,換不掉一個明確錯誤訊息的價值。
D15 SonarQube 證據格式
(2026-07-31 追加,FR-058.5)
connector 拉 JSON 自行 render 自包含 HTML 上傳當證據——Community Build 沒有任何報告匯出功能(付費版才有 PDF),自組報告是必做項不是 fallback。報告內容=Quality Gate 狀態+核心度量(bugs / vulnerabilities / code_smells / security_hotspots / coverage / duplicated_lines_density / ncloc)+severity 分佈+issue 清單(severity 由高至低取前 200 條,報告註明「僅列前 200 條/總數 N」——防 api/issues/search 的 10,000 筆硬上限;統計數字一律用 facets 與 measures 取全量,不逐頁撈)+Security Hotspots+分析時間。summary 數字從 measures API 取(bugs / vulnerabilities / code_smells / security_hotspots / quality_gate),完全繞開 MQR 模式的 severity 語意差異。出貨標準同 ZAP:證據可下載、不可預覽。 ① 依賴 server 端報告匯出——排除:Community Build 根本沒有此能力,PDF 匯出是付費版功能。② issue 清單逐頁撈全量——排除:api/issues/search 有 10,000 筆硬上限(第 21 頁起報錯),大專案必炸;且統計本可由 facets / measures 全量取得,不需逐條。
D16 SonarQube 認證與跨版本相容
(2026-07-31 追加,FR-058.5)
憑證=base_url + user tokensqu_ 前綴,secret,HTTP 用 Authorization: Bearer header),token 所屬帳號只需目標專案 Browse 權限。issue 解析 impacts[] 優先(MQR 語意,10.2+ 回應就有),缺席時 fallback 舊 type+severity(相容 9.9 LTS 至 2026.x LTA)。probe=api/system/status(免認證,驗連通與服務狀態)+帶 token 呼叫 api/authentication/validate陷阱:匿名呼叫也回 valid:true,必須帶認證 header 呼叫結果才有意義)。 sqp_ / sqa_ analysis token——排除:那兩種只能執行分析、不能呼叫讀取 API,對 pull-snapshot 型 connector 無用。只認 impacts[] 或只認舊 type+severity——排除:單押一邊會在另一個版本區間解析失敗,雙軌 fallback 才能覆蓋 9.9 LTS 至 2026.x LTA 全區間。
D17 SonarQube 兩模式的共存形式
(2026-07-31 追加,FR-058.6)
ZAP 式——同一個 sonarqube 工具 + scan_mode 參數分流。 工具清單只有一張卡,租戶層憑證設定一次、兩模式共用,模式差異只出現在「建任務時填什麼參數」。四個理由:① 概念一致——使用者心裡想的是「我要用 SonarQube」,不是「SonarQube-讀取版 / SonarQube-掃描版」,出現兩張同名卡等於把內部實作差異推給使用者;② 設定只做一次——server 位址與 token 填一次兩模式都能用,走兩張卡要填兩次,換 token 時漏改一邊會變成「一個模式能用一個不能用」的怪問題;③ FE 零改動——condition:{field,value} 是現成機制(DetectionConfigField.vue:23-27visible computed、:31-34 對不可見欄位跳過必填驗證),ZAP 已實證;④ 憑證共用(見右欄更正)。 GCB 式(另立 tool code、工具清單兩張卡)——排除:它唯一的優勢是「憑證 schema 各自乾淨」,而該優勢在本案不存在,只剩「工具清單多一張同名卡」的代價。
重要更正 · 規劃初期的「憑證分流痛點」在本案不成立:曾以為兩模式需要兩組憑證(pull 要讀取 token、scan 要推送 token + Git 認證),因而擔心 config_field_schema 是工具層、無法依任務參數分流。此顧慮被兩件事推翻——① squ_ User Token 推得上去也讀得回來(只有 sqp_ / sqa_ analysis token 是「只能推不能讀」),兩模式共用同一把 token;② D19 定案只支援公開 repo → 根本沒有 Git 憑證這回事。實查 DEV config.detection_tools,sonarqube 的 config_field_schema 目前就 base_url + token 兩欄,兩模式完全共用、一個字都不用改
InSpec 雙 transport 那個「probe 第一段檢查憑證完整性」的先例仍是有效的一般性技術參考,但不是本案的痛點——記錄於此以免日後誤讀。
D18 主動掃描的語言範圍
(2026-07-31 追加,FR-058.6)
只做 A 類純源碼語言:Python / JS / TS / Go / PHP / Ruby / Kotlin。 這一群的共同特徵是不需要編譯產出,把源碼交給 scanner 就能分析,agent 端零額外環境需求——正好解掉 D13 當初排除 agent 自跑 scanner 的第二個理由(「不可能備齊各語言 build 環境」)。 Java(B 類,需 .class)/C·C++(AutoConfig)——排除:技術上都評估過可行或部分可行,但雛形階段一律收斂(各自的已知技術結論見 §4.7「後續強化」)。Scala——排除:官方文件完全沒表態是否需要 bytecode,傾向 A 類但必須實測,不可憑推論寫進使用者說明。C# / VB.NET / Objective-C——不是取捨而是技術上真的做不到:SonarScanner CLI 明文 unsupported,begin/build/end 是硬性架構(分析發生在編譯器內部的 Roslyn analyzer),永遠只能走 pull 模式。
縮範圍後浮現的必要配套(不是決策項,是配套):使用者不知道範圍而送錯語言時,兩種失敗方式嚴重性差很多——Java 是硬失敗(沒有 .class,scanner 直接報錯中止,難看但安全);C# 是靜默淺掃(CLI 不支援但不會中止,很可能產出一份看起來正常、實際幾乎沒分析到東西的報告,稽核員可能把「沒掃到」誤讀成「很乾淨」)。三件事必須一起做:① 事前——UI 與 setup_guide 明列支援語言並寫清楚「清單外請改用拉取模式」;② 事中——可考慮讓使用者宣告主要語言,非清單內直接擋下不送進 scanner;③ 事後——報告產出時若分析檔案數 / 程式碼行數異常偏低,明確標示而非靜靜輸出(「零發現」與「沒掃到」必須看得出來是兩件事)。具體形式(擋在哪一層、訊息怎麼寫)留到實作計畫階段定。
D19 主動掃描的源碼來源
(2026-07-31 追加,FR-058.6;2026-08-01 註記:右欄排除的「檔案上傳」已由 FR-058.7 立案實作,見 D25–D31 / §4.8——當時排除是雛形階段取捨,非永久裁示;本列原文保留為歷史紀錄
公開 Git repo——一個 text 欄位填 URL,不需要任何憑證。決策依據是兩條路線的成本量級差:Git URL 路線要動 0 個新層param_schema 加一個 text 欄位;加密鏈一行不用改;agent 的 git 已裝且 CINC/InSpec 已實證「參數填 Git URL、引擎自己去拉」在本平台跑得通;FE 零改動,text 是既有型別)。 檔案上傳——排除:要動三層——① FE 元件(param_schema 目前 7 種型別無 file,權威來源是 DetectionConfigField.vue 的渲染分支,且沒有 else 分支=未知型別靜默不渲染,欄位會直接消失在畫面上且不報錯);② 上傳中繼(源碼包要先進雲端再送 agent,而 RemoteAgentAdapter 只在租戶 storage_type=REMOTE_AGENT 時才被實例化,MINIO 模式的客戶根本建不出這個物件,會出現「A 客戶能用、B 客戶按下去就爆」);③ 派工鏈路(payload 目前只有 JSON params,agent 端收到 task 後沒有任何下載檔案的邏輯,兩端都要補)。額外還要自訂大小上限、暫存空間與解壓炸彈防護。
私有 repo 認證——排除:雛形階段先求打通,且平台現況本來就只支援公開 repo(CINC/InSpec 的 Git URL 用法同樣只支援公開 repo,私有認證在本平台完全沒做過)。
決策者原話兩種都提了(「可能上傳 或是提供 server 位置」),但成本量級差一個數量級——雛形階段選成本低的那條先把鏈路打通。
D20 sonar-scanner 怎麼進 agent
(2026-07-31 追加,FR-058.6)
sonar-scanner CLI 打包進 agent image(同 CINC / InSpec 模式)。平台專屬 zip 自帶 JRE,且 v6.0 起有 JRE auto-provisioning(搭配 SonarQube 10.6+,驗收用的 server 是 26.7.0.124771 遠超門檻)→ 不必額外裝 JDK。Node 同理——JS/TS analyzer 自帶 Node runtime。 agent 內跑官方 sonar-scanner-cli 容器——排除:實查確認 agent 容器無 docker CLI、未掛 socket、無 privileged / DinD,且平台先前已刻意避開此路線兩次(zap.py:7-8nmap.py:1-6)。⚠️ 易誤判poetry.lock 內的 docker 7.2.0 是 testcontainers 經 jedi-common 拉進來的測試相依洩漏,不是既有能力,不可據此以為 agent 能操作 docker。
執行時下載 scanner——排除:客戶 agent 常在不能連外的內網。
D21 推上客戶 server 的 projectKey 命名
(2026-07-31 追加,FR-058.6)
**一律加前綴 Guidant-AI-<使用者輸入>——使用者只填後半段,前綴由平台組上。 直接用使用者輸入——排除**:① 可能撞到客戶既有專案 key,把一次性稽核掃描結果覆蓋到他們正式的專案歷史上;② 客戶無法辨識哪些專案是本平台建的。前綴同時是 R4 寫入副作用的緩解手段——「撞號」與「無法辨識來源」兩個問題由它解決,但「每掃一次多一筆專案」本身沒有解(SonarQube 無專門開關,想擋只能移除 token 擁有者的 "Create Projects" 全域權限,這點寫進 setup_guide)。
D22 資源保護與暫存位置
(2026-07-31 追加,FR-058.6)
只做掃描逾時上限;暫存用磁碟型 tmp 目錄、掃完即刪。agent 容器內 /data/content:ro,可寫的是容器自身 /tmp(已有 PDF_CACHE_DIR=/tmp/pdf-cache 前例,且刻意不做 bind-mount 以避開跨裝置 rename 的 EXDEV)。 磁碟配額 / 記憶體上限控管——排除:雛形階段不做,風險記錄在 §4.7「風險與已知限制」R2。
⚠️ /dev/shm——排除且必須明確避開:既有暫存慣例是 inspec.py:197-200_SHM_DIR = "/dev/shm"(0600 + contextmanager 必刪),但那是 tmpfs 記憶體型,是為「小的憑證檔」設計的。解壓一個幾百 MB 的源碼包到記憶體會直接吃掉容器的 RAM 配額,不可無腦沿用
D23 coverage 紅燈的處理
(2026-07-31 追加,FR-058.6)
**照實顯示,不濾除、不加特別說明。 決策者原話:「主要掃品質就好」。 濾除 coverage 條件後再判定 Quality Gate——排除**:等於我們自己改寫客戶的判定標準。加醒目說明——排除:雛形階段不做。
事實記錄(§4.7 R3):SonarQube 自己不產生 coverage、只匯入第三方報告,而我們不跑測試 → 報告必然無 coverage,客戶 Quality Gate 若含覆蓋率條件會亮紅燈,而那個紅燈不代表程式碼有問題。Go 更糟——無 coverage 資訊時視為「全部未覆蓋」而非「無資料」,會顯示 0%。實作時仍建議在 setup_guide 提一句讓管理員事前知道。
D24 scan_mode 的預設值
(2026-07-31 追加,FR-058.6)
預設 scan(原始碼掃描)。 決策者原話:「預設不要,預設一定是 scan mode」。理由:主動掃描是本需求的主線意圖,pull 只是先落地的那一個;預設值應指向主要使用情境,讓使用者少一步操作。
連帶要求——事前告知從「最好有」升級為「必須有」(三項配套實作必做):① 建立任務時的提示必須顯眼,hint 文字不夠(使用者會略過),建議在 scan 模式的參數區塊做明顯標示或送出前給明確提示(具體形式列實作建議,不在此定死);② setup_guide 必須明講「本工具預設為原始碼掃描模式,執行時會在貴公司的 SonarQube 建立 / 更新專案(key 為 Guidant-AI-<識別>)」,讓管理員在設定階段就知道,而非看到專案清單多東西才發現;③ 收緊模式寫進 setup_guide(預先建好專案 + 移除 token 擁有者的 "Create Projects" 全域權限)。
預設 pull——排除:雖然 pull 純唯讀可回復、scan 會寫入客戶系統不可回復,但那是配套要解決的問題,不該讓預設值偏離主線意圖。
⚠️ 與 ZAP D2 取捨方向相反是刻意的,不是疏漏。D2 預設被動的理由正是「預設值選錯的代價不對等」,本案結論方向相反,差別在代價的量級
 • ZAP D2 誤觸=對目標送出真實攻擊流量,影響第三方外部系統——可能觸發對方告警甚至造成服務中斷,不是我們能收拾的
 • 本案 D24 誤觸=在客戶自有的 SonarQube 建立一個專案——一筆專案紀錄、客戶可自行刪除,且有 Guidant-AI- 前綴可辨識來源(D21)。
兩者都是「預設值有副作用」,但一個是對外部系統送攻擊流量、一個是在客戶自有系統留一筆可刪除的紀錄,量級不同故結論不同。
D25 上傳檔案的取源通道:agent 如何拿到源碼包
(2026-08-01 追加,FR-058.7)
BE 新開 agent 專用純 mTLS 檔案下載端點 + 任務綁定驗證——GET /api/1.0/agents/files/<uid>,認證比照 tasks ack / result 的「純 mTLS 不掛 jwt」形狀(api/remote_agent/__init__.py:34-38 先例);授權由 client 憑證識別 agent 身分後,驗證該 uid 必須屬於該 agent 名下 pending / running 任務的 source_file(防任意檔案讀取,任務終態後參照即失效)。心跳 payload 夾檔案參照,agent 收到任務後主動串流下載進 scan workspace 並驗 sha256。BE 對 minio / remote_agent 後端串流轉發不落地(遵守 FR-039「雲端任何路徑不得把 binary 寫進雲端磁碟」不變式)。 簽章 URL(複用使用者面 GET /file/download/<uid>,心跳夾簽好的 URL)——排除:簽章 token TTL 只有 120 秒(common/authz/signed_token.py:41),是為瀏覽器「點了就下載」設計;agent 從心跳收到任務到實際開始下載的間隔不可控(前面可能排了別的任務),時效模型對不上。且把使用者面資料通道與 agent 資料面混用,授權語意混濁。
推檔給 agent(派工時 BE 主動 POST /blob)——排除:推檔時機與任務生命週期耦合(派工瞬間 agent 可能離線);POST /blob 走 FileUploadService 永久落地(寫 agent 自己的 upload_files 表),與 /tmp 即用即棄語意不同,agent 端要另管清理;且 tenant 儲存為 remote_agent 時檔案本來就在 agent 上,會變成「推自己」。
附註:tenant 儲存為 remote_agent 且掃描 agent 恰為同一台時,BE relay 會繞一圈(agent→BE→agent)。v1 接受此低效(罕見組態),列 §4.8 known limitation。
D26 上傳掃描的參數模型:scan_mode 三選項 + 執行時輸入
(2026-08-01 追加,FR-058.7)
scan_modeupload 選項——param_schema 出 v3(INSERT 新版 + v2 is_current=FALSE,版本策略照舊),v3 變更僅此一欄(options 由 pull / scan 增為 pull / scan / upload,附 hint「上傳模式於發起執行時提供源碼壓縮包」;預設值沿用 D24 裁示);project_key_suffix 沿用(scan 與 upload 皆出現)。source_file 不是 schema 欄位,而是執行時輸入:任務抽屜發起執行時上傳、隨 execute request body 送出,由 start_execution 合併進 agent_tasks.params_source_file,形狀見 §4.8)。 file 型 schema 欄位(把上傳放任務設定表單;初版方案,決策者裁示排除):會導致——① 每掃新版都要改設定再執行(兩段式繞路,且任務設定僅專案管理者可改,執行者不見得改得動);② 語意錯位(壓縮包是「這一次要掃的東西」——特定時點的源碼快照,該在執行當下提供,不該混進「怎麼掃」的設定表單)。改到執行時後還附帶兩個好處:FE 的 DetectionConfigField 完全不用動(不需新型別分支)、孤兒檔視窗大幅縮小(上傳與執行幾乎是同一個動作)。
scan 選項內做 repo_url / source_file 互斥——排除:condition 機制是單層 {"field","value"} 等值比對,表達不了「二欄擇一」;required 驗證也會亂(兩欄都掛 required 就都必填,都不掛就都可空)。硬做要改 condition 語法引擎,成本遠超過多一個選項。
D27 上傳檔案格式與限額
(2026-08-01 追加並定案,FR-058.7)
格式:收 .zip / .tar / .tar.gz / .tgz.gz(單檔 gzip)不收——解開只有一個檔,對源碼掃描無意義。
限額設定化,不寫死:四個限額——上傳上限 MB / 解壓後總量 MB / 解壓檔案數 / 磁碟預留公式(D28 的係數與底值)——做成 BE config class 屬性 + 環境變數可覆寫(現成前例 DRIVE_FILE_SIZE_LIMIT_MBconfig/config.py:166 同 pattern)。預設值:上傳 100MB / 解壓後 500MB / 50,000 檔(皆可由環境變數調整)——100MB 壓縮包對純源碼專案已相當大(源碼壓縮比高,約當數十萬行等級),不夠再調。
agent 不自己配置:BE 派工時把限額隨任務 payload 下發(與 credentials 下發同構),agent 只做執行時 enforcement——單一真相在 BE,不違反 FR-039「agent 不連雲端 DB」不變式。
待辦:驗證上版環境 BE 前 nginx 的 client_max_body_size 實際限制(BE 本身無 MAX_CONTENT_LENGTH,nginx 預設 1MB,沒調過會先在 nginx 被擋)——部署驗證項,列 T-7.4。
system_config——排除:那張表定位是租戶層業務設定(有 FE 設定頁),這幾個限額是平台營運護欄、無 per-tenant 差異化需求;走 system_config 要多做 reader / fallback / 快取 plumbing,不划算。缺點「改動要重啟 BE」可接受(調上限是罕見動作)。未來若需 per-tenant 放寬,payload 下發機制不變、只換讀取來源,升級容易。
D28 磁碟防護的具體規則
(2026-08-01 追加,FR-058.7)
agent 下載前執行 shutil.disk_usage("/tmp"),free < (壓縮包大小 × 12 + 1GB)即 fail——壓縮包 100MB 時需求約 2.2GB;×12 涵蓋壓縮包本體 + 解壓後源碼 + sonar-scanner 工作檔的粗估安全係數。係數與底值納入 D27 定案的同一組 BE 設定(環境變數可調,隨 payload 下發、agent 只 enforcement)。錯誤訊息明確標「agent 磁碟空間不足」並分類為 agent 環境問題、不是掃描失敗——呼應本 arc 教訓「錯誤要在最早能發現的地方被發現」,讓運維一眼知道該去清 agent 主機而不是排查源碼。實作放 _scan_workspace() 建目錄前,最小侵入(shutil 已 import)。另:解壓過程累計超過 D27 上限也即時中止並清理——與磁碟檢查是兩道獨立防線:一道防「空間真的不夠」,一道防「惡意壓縮包」。 現況背景:agent 全 repo 零磁碟檢查(grep disk_usage / statvfs / ENOSPC 全 0 命中);/tmp 在容器可寫層(無 volume 掛載),吃爆影響 docker host 分割區;容器無任何資源限制(R2 已記)。「解壓失敗才發現磁碟不足」的被動路線不採——錯誤要在下載前就攔下。
D29 檔案生命週期:server 端全保留 + 重掃沿用 + 歷史不刪
(2026-08-01 追加並定案,FR-058.7;推翻討論過程 v2 的「終態即刪」中間版本
「掃完即刪」只適用 agent 端 workspace(既有 finally: shutil.rmtree 機制+五路徑測試鎖約,零新工作,不變);tenant 儲存空間上的壓縮包保留。完整模型四點:
① 重掃沿用——scan_mode=upload 的任務,抽屜執行對話框顯示「目前檔案:xxx.zip(上傳於 M/D)」,直接按執行=沿用舊檔;要換版就上傳新檔替換。首次執行仍必須上傳(無檔不給執行)。
② 檔案參照落點在任務綁定層(sticky 狀態供沿用,形狀見 §4.8)。注意與被否決的 D26 初版方案不同:上傳動作仍在抽屜發起執行時,流程不變,只是參照回寫進綁定;初版被否決的主因「終態刪檔→參照懸掛」在保留檔案後自然消失。
③ 替換不刪舊檔,歷史版本全保留(決策者明示:留歷史,萬一未來 user 要找舊檔,還有容錯修正空間)。每筆執行紀錄 scan_params 快照的 {uid, file_name} 都能對回實體檔案,稽核可回溯「那次掃的是哪一包」。
④ 清理機制列 follow-up(v1 不做)——孤兒檔(上傳未執行)、歷史版本、殘檔統一由未來的清理機制管(原 D32 併入本項)。給未來清理機制的約束先立在此:必須以「綁定仍參照中的檔案」為保留名單,不能只看檔案年齡——否則會清掉三個月沒重掃、但仍要沿用的 zip。
終態即刪(討論過程 v2 曾定案,後推翻):其理由「掃完不留平台」不成立——tenant 儲存空間本來就是客戶自己設定的位置(local / minio / 客戶自家 agent),保留不構成「平台持有客戶源碼」問題;且終態即刪會造成「重掃必重傳」與「執行紀錄參照懸掛(快照指向已刪檔案)」兩個實際代價。特此記錄推翻軌跡,避免日後誤讀為反覆——是理由被檢驗後不成立,不是需求搖擺。
D30 解壓實作:標準庫 zipfile + tarfile + 共用額度檢查層
(2026-08-01 追加,FR-058.7)
兩個格式家族都用標準庫zip 走 zipfile——extractall() 內建路徑消毒(剝絕對路徑與 ..、不建 symlink),zip-slip 已由標準庫擋掉;**tar 系(.tar / .tar.gz / .tgz)走 tarfilefilter="data"——Python 3.11.4+ 官方 backport 的安全提取機制,擋絕對路徑、.. 逃逸、symlink 逃逸與裝置檔(agent image 基底 python:3.11-slim,Dockerfile:1,版本足夠)。額度檢查層兩家族共用同一層(約二十行):解壓前加總封存目錄宣告的總大小與檔數、超過 D27 上限即拒絕(解壓炸彈防護是標準庫沒有的部分)、逐檔解時看 cancel_event(沿用既有可取消框架的精神)。 shell 出去跑 unzip / tar CLI——排除**:無法逐檔做計量與取消——整包解完才知道大小,解壓炸彈防護等於沒有;路徑穿越保護依版本行為不一(image 內雖有 unzip CLI,那是裝 sonar-scanner 用的,Dockerfile:26)。
第三方解壓套件——排除:標準庫已涵蓋兩個家族的需求,agent 依賴保持精簡,不為二十行的事引套件。
D31 顯示與紀錄:完全複用既有上傳機制 + 兩點收緊
(2026-08-01 追加,FR-058.7)
完全複用既有上傳機制:FE 上傳統一打 POST /file/uploadAPI.UPLOAD_FILE),新 upload_type 分類(如 DETECTION_SOURCE);白拿清單(零新工作)——sha256(agent 下載後對帳防竄改)、RLS 租戶隔離、三種 storage backend 全支援。兩點收緊:
① FE 只送 uid,檔名不由 FE 送——BE 收到 execute 請求後從 upload_files權威取回 file_name / size / sha256 快照進執行紀錄;顯示歷史不依賴檔案仍存在,也不信任前端送來的檔名。BE 要補的驗證:uid 存在且屬本 tenant(RLS)、副檔名在允許清單(D27)、大小在 D27 限額內。
② 快照存 {uid, file_name} 物件——執行紀錄抽屜 paramValueLabel 對非 select 型參數原樣輸出,快照若只存 file uid 畫面會顯示裸 UUID;故 scan_params 快照存 source_file: {uid, file_name},FE 顯示 file_nameJobExecutionDrawer.vue_PRIMARY_PARAM_KEYS(:315)加 source_fileparamValueLabel 加物件型分支(取 file_name)。
信任 FE 送來的檔案 metadata——排除:檔名 / 大小由前端自報則快照可被竄改,稽核回溯失去意義;權威來源只有 upload_files 表。
另立新上傳通道——排除:既有 POST /file/uploadupload_files_for_tenant() 已涵蓋三後端與 RLS,另立通道違反「新增前先找既有能力」鐵則。
D32 孤兒檔處理
(2026-08-01,FR-058.7)
併入 D29 第 ④ 點,不獨立成案。 上傳後放棄執行的孤兒檔、替換後的歷史版本、其他殘檔,統一由未來的清理機制管理(v1 不做),完整說明與「以綁定參照為保留名單」的約束見 D29 ④。上傳時機移到發起執行時後,孤兒檔視窗已大幅縮小(上傳與執行幾乎是同一個動作)。
D33 scan_mode=upload 與「開始執行任務」自動派發的關係
(2026-08-01 追加,FR-058.7 驗收發現)
upload 任務不進自動派發清單——在挑選查詢排除。 背景:專案規劃頁「開始執行任務」會自動派發檢測掃描(FR-056.5 #5,TaskExecutionService.start_task_execution_auto_dispatch_detection_scansapp/grc/service/task_execution_service.py:90-133),自動路徑不帶檔案,對 scan_mode=upload 的任務必然觸發 DETECTION_TOOLS_400005(首次無檔 400),且該錯誤被批次派發的 try/except 吞掉只留 warning log——PM 看到成功 toast、執行者無感的靜默失敗;又因失敗不產生執行紀錄,「查無 detection_executions」的冪等條件永遠成立,每按一次「開始執行任務」就再失敗一次。定案:挑選查詢 get_detection_jobs_without_executioninfra/grc/repository/task_execution_query.py:134-159)排除 tool_params->>'scan_mode'='upload' 的綁定;upload 任務一律由執行者在任務抽屜上傳檔案後手動執行(抽屜已有完整前置擋門:首次無檔不給按執行,T-7.3)。pull / scan 模式自動派發行為不變。FE 配套:專案規劃頁批次發佈 confirm 文案補一句「上傳型掃描任務需由執行者上傳檔案後手動執行」。 ① 給 start_execution 加 auto 旗標、在 service 層判斷——排除:多一個參數汙染共用簽名(抽屜手動執行與自動派發共用同一 method),過濾在挑選階段做更乾淨——upload 任務根本不該出現在待派清單裡,而不是進了清單再被擋。
② 維持現狀讓它失敗——排除:靜默失敗會讓 PM 反覆觸發且無人察覺,是要修的洞不是可接受行為。
背景註記:自動路徑「悄悄沿用舊檔」的疑慮不會發生——自動派發只挑「從未執行過」的任務(冪等條件),已執行過的 upload 任務(有 sticky 檔案可沿用者)本來就不在清單內,故本修正只涉及首次執行段。
D34 檢測掃描通知加厚:九項內容+失敗通知+三管道
(2026-08-01 追加,FR-058.7 驗收期間提出;三子項決策者已逐項拍板)
① 內容擴為九項:專案(升級走 round 鏈取代現行 ORDER BY id LIMIT 1)、控制項群組、控制項、AO(title→prose→part_id fallback)、任務名稱、工具名稱、掃描設定(複用 _resolve_scan_params() 現成兩層剝除:_ 前綴+secret envelope;profile UUID 轉譯成名稱)、時間(started/finished)、成功或失敗;順手把現成卻沒用的 evidence 參數用起來(報告檔名/份數,零額外查詢)。② 失敗也通知on_scan_failed 補查 job/binding(抄 on_scan_succeeded :472-475 的 pattern)後發通知,含 error_message;收件人與成功相同(指派人+專案 manager+reviewer)——決策者裁示失敗更需要管理層知道。③ hosts(目標主機 IP)顯示——決策者裁示可出信(客戶自己的信箱,掃描目標是核心資訊)。④ 管道補齊 Discord/Telegram(對齊平台其他通知的三管標準)——文案準備 HTML(email 表格)+純文字(Discord/Telegram)兩版,i18n 6 個 msgid。技術要點(探查已實證):新查詢走 8 層 join 鏈(job→workflow_execution_control_mapping→round→project→extension→ssp→profile_imports→catalog_controls/groups/parts),加在 infra/detection_tools/repository/detection_job_notify_query.py 新增 get_notify_context(job_uid) 一次 SQL;⚠️ 兩坑:catalog_controls.control_id 跨 catalog 不唯一(DEV 實測同 control_id 41 筆)必須帶 catalog_id=profile_imports.source_catalog_id 過濾;AO 的 title 對 CMMC 全 NULL,用 title or prose or part_id fallback(job_export_query.py:145 前例)。無 schema migration、無 DI 變更(_profile_domain_tool_domain_notify_query 全已注入)。改動 4 實作檔+1 測試檔。明確不在範圍:站內鈴鐺通知(平台無 notifications 表,另案);收件人 locale 切換(通知在 agent request context 發、Accept-Language 是 agent 的,一律 zh_Hant_TW——既有限制另案)。 ① 失敗只發指派人(避噪音)——排除:決策者裁示失敗更需要管理層知道,收件人與成功通知相同。
② hosts 遮蔽——排除:客戶自己的信箱、掃描目標是核心資訊,遮蔽只會降低通知可用性。
③ 只修 Email 不補管道——排除:文案反正重寫,一次對齊平台三管標準(Email/Discord/Telegram),避免之後再開一案補管道。

2.1 補充:Linux GCB 為何不沿用既有的 OpenSCAP

技術上 Linux GCB 確實可以用 OpenSCAP 執行——它是 Linux 原生工具,FR-057 也已有可用的 SSH connector。不選它的原因不在 Linux 這一側,而在維護成本:Windows GCB 只有 CINC 這條路走得通(OpenSCAP 官方已聲明放棄 Windows native 支援,docs/windows.md 明文標示 no longer usable;引擎層也沒有 WinRM transport 與 Windows OVAL probe)。若 Linux 走 OpenSCAP,同一份 GCB 基準就要維護兩種 content 格式。

OpenSCAP 的定位不變,兩者按「基準來源」分工而非按 OS 分工

  • OpenSCAP 繼續服務 CIS / STIG 這類有官方 SCAP content(SSG)可直接取用的國際基準。
  • CINC Auditor 負責 GCB 這種沒有官方 content、必須自產,且 Windows 端別無選擇的基準。

同一台 Linux 主機要查 CIS 就派 OpenSCAP 任務、要查 GCB 就派 CINC 任務,兩個任務可掛在同一個控制項底下、證據進同一個證據池——這正是 CM-953 已定的**「一任務一工具,靠多任務處理異質環境」**模型,本案沿用,不需改架構。


3. 現況接入點盤點

3.1 既有派工機制(本案沿用,除 D11 那一點外不改)

核心設計:雲端不主動推,agent 定期來問。 雲端連不到客戶內網,故不能主動下指令。agent 每隔 heartbeat_interval_sec(預設 300 秒)向雲端報到並領回待辦。這同時滿足防火牆限制——只有 agent 對外連出,無對內連入。

階段 發生什麼 程式位置
① 使用者按「開始執行」 雲端做四件事:_pick_agent() 挑一台符合 detection_scan capability 的 agent(取 agents[0],尚無負載平衡)/_resolve_credentials() 解密租戶工具憑證/建一筆 agent_tasks(狀態 pending)/建執行紀錄。此階段完全沒有對外連線,雲端只是在 DB 放一張待辦單。 app/detection_tools/service/detection_orchestration_service.pystart_execution()
② agent 心跳 POST /api/1.0/agents/heartbeat(帶 mTLS client cert)。雲端回應夾帶 pending_tasks,每筆為 {uid, detection_tool_id, params, credentials}credentials 是雲端當場 Fernet 解密的明文,走 mTLS 下發,agent 用完即丟不落地(FR-056 D9)。 app/remote_agent/service/agent_enrollment_service.pyheartbeat() / _collect_pending_tasks() / _resolve_tool_credentials()
③ agent 分派 每筆各開一條 daemon thread(一筆失敗不影響其他筆、不阻塞心跳)。每條 thread:ack(pending→dispatched)→ get_connector(detection_tool_id, params, credentials)connector.run() → 報告走既有 blob 通道上傳 → 回報結果。 evidence-agent/core/task_executor.pydispatch_tasks()
④ 取消(唯一的反向推送) 取消不能等 5 分鐘,故雲端直接連 agent 的 base_url;agent 端 _RUNNING_TASKS(task_uid → threading.Event)找到執行中任務並 .set() 通知中止(CM-931)。 POST /detection/cancel

對加新工具的意義:派工鏈路本身是 tool-agnostic 的,四個新工具全部沿用,「發指令」機制完全不用改。真正要動的只有兩點——雲端一條 SQL seed(detection_tools 一列 + param_schema,零 Python);agent 加一筆 _TOOL_ID_TO_CODE 映射 + 寫 connector 實作。而 D11 要解的正是後者的映射脆弱性。

3.2 元件 × 現況 × 本案動作

元件 現況 本案動作
config.detection_tools 4 筆:openvas(available) / nessus / sonarqube(coming_soon) / openscap(available);已有 connection_type(API / CLI / SSH)、setup_guide(FR-057 加) seed 四筆新工具(ZAP API / InSpec SSH+WinRM / GCB / Nmap SSH——2026-07-30 改案,原定 CLI);加「是否需要憑證」宣告欄位(D9 平台能力,nmap 改案後其值為 TRUE);2026-07-31 追加:既有 id=3 sonarqube 佔位列 UPDATE 成可用工具(FR-058.5 / T-5.1,不是 INSERT 新列,見 §4.6 seed 特殊點)
config.detection_tool_param_schemas 版本化參數 schema(openvas / openscap 各一筆) seed 四份新 param_schema;schema 語意擴充 secret: true(D1);2026-07-31 追加 sonarqube param_schema v1(FR-058.5 / T-5.1)
job_execution_detection_tools.tool_params 明文 JSONB 依 param_schema 對 secret:true key 加密後才寫入(FR-058.0)
tenant_detection_tool_configs + Fernet 加密鏈 完備 直接沿用(ZAP API key 進 credentials 密文);任務層敏感參數複用同一條 Fernet 鏈,不另立加密機制
start_execution() 憑證檢查 憑證為空即擋,回 DETECTION_TOOL_CONFIG_NOT_FOUND 放行零憑證工具(D9 平台能力,仍要做)——原動機是 Nmap 會被誤擋,2026-07-30 改案後 Nmap 需 SSH 憑證不再是使用者,此能力保留供日後真正零憑證的工具使用
派工鏈(心跳夾帶 + 憑證解密下發) 完備 沿用;組裝派工時一併解密任務層敏感參數(FR-058.0);payload 加 detection_tool_code 欄位(D11)
agent factory _TOOL_ID_TO_CODE {1: openvas, 2: nessus, 3: sonarqube, 4: openscap} 硬編 id 映射 移除硬編映射,改依派工夾帶的 code 取 connector(D11,橫向前置項)
agent DetectionConnector base run() / probe();注入點 cancel_event(CM-931)/host_failures(CM-952)/content_mismatch_hosts(CM-954) 介面不動;新增 zap.py / inspec.py / nmap.py 三支實作(GCB 複用 inspec.py);2026-07-31 追加 sonarqube.py(FR-058.5 / T-5.2)
agent Dockerfile LibreOffice + curl + openssh-client + openscap-utils 加入 CINC Auditor引擎裝在 agent 本機、由它主動連出去掃目標,與 OpenSCAP 相反——見 §4.3 架構事實);Nmap 不 bundle(NPSL v0.95 §3 衍生作品條款,客戶裝在自己指定的主機上、agent 走 SSH 呼叫,見 D9 改案);ZAP 不 bundle(D3 客戶自備)
多檔證據回收鏈 FR-057 已擴為 result_ref.upload_uids 陣列(向下相容單數 key) 直接沿用,不再改
FE 設定頁 / 任務參數渲染 DetectionConfigField.vue 七種欄位型態(text / textarea / password / number / boolean / select / select_or_text)+ condition 條件顯示 不需新元件——ZAP 登入欄位用現成的 password + condition;需加 secret 欄位在執行紀錄的剝除規則(FR-058.0)

3.3 風險與既有技術債(實作時會撞到,拆任務時先知道)

項目 現況 本案的關聯
心跳延遲 5 分鐘 agent 心跳預設 300 秒,任務發佈到實際開跑最長有 5 分鐘延遲 使用者按下開始按鈕後畫面無反應、五分鐘後才動。這是設計上的正常行為,但 UI 需說明否則會被當成故障
FE 硬編死碼工具清單 WorkflowSetupEditor.vue 有一段 toolsMenu = ["Nessus","Nmap","OWASP ZAP","Wireshark"],與 detection_tools 完全脫鉤 本案加入 ZAP 與 Nmap 之後,畫面上兩處都出現 Nmap,其中一處是死的。建議一併清掉
Windows GPO tattoo settings HKLM\Software\Policies 底下屬正規政策區、套用前會先清空;不在此位置的設定會殘留(業界稱 tattoo settings) 檢查規則若讀取非 Policies 路徑,可能讀到上次殘留的舊值而誤判為通過,實際上政策根本沒生效。稽核情境屬嚴重問題。本案 Demo profile 必須避開此陷阱
tenant_config_id 從未被寫入 job_execution_detection_tools.tenant_config_id 永遠是 NULL,實際靠 (tenant_id, detection_tool_id) 的 UNIQUE 約束回退查詢 加工具不會惡化,但欄位存在卻不被使用,日後看到的人容易誤判其用途
證據關聯靠字串前綴 證據與執行的關聯靠 job_evidences.description 的字串前綴 [檢測工具] {agent_task.uid} 四個工具都會走這條路徑,任何改動 description 格式的需求都會全面波及
id=3 SonarQube 佔位列 FR-056 留下,status='coming_soon'config_field_schema 為空 (2026-07-31 更新)原裁示「本案不處理維持 coming_soon」已被推翻——SonarQube 重新納入為 FR-058.5,此列由 T-5.1 UPDATE 成可用工具(不是 INSERT 新列,見 §4.6 seed 特殊點)。原記載的「硬編 id 表中無對應 connector 的一格」問題在 D11 解耦 + T-5.2 補上 connector 後雙重消失
_pick_agent 無負載平衡 直接取 agents[0] 工具變多後同一站點併行任務量上升,單一 agent 被打滿的機率提高
文件債 docs/claude/database-schema.md 完全沒有 config schema 與 detection 三表的記載(grep 零命中) 本案要動這三張表,是補上記載的合適時機

4. 詳細設計

4.0 共通:參考檔案座標

實作前先讀這些檔案,四個子需求都會用到:

用途 檔案
平台三表 seed 範本 scripts/sql/2026-07-26-fr056-1-detection-tools-config-schema.sql
加工具的 SQL 範本(最重要) scripts/sql/2026-07-28-fr057-1-openscap-seed.sql——含 setup_guide 欄位、config_field_schemaparam_schemaschema_migrations 收尾。四個新工具的 seed 都照這支寫
connector 介面 evidence-agent/core/task_executor_connectors/base.pyDetectionConnector ABC、ScanResultScanCancelledErrorcancel_event / host_failures / content_mismatch_hosts 注入點)
connector 實作範本(API / daemon 型) evidence-agent/core/task_executor_connectors/openvas.py——連線設定解析、probe()、輪詢、雙格式取報告、cancel_event、timeout、summary 解析容錯,ZAP 照這支的形狀寫
connector 實作範本(SSH / 多目標多檔型) evidence-agent/core/task_executor_connectors/openscap.py——Nmap 照這支寫(SSH 型,D9 改案);InSpec 只借鏡多目標處理與 host_failures 用法,連線層完全不同(CINC 裝在 agent 本機自己連出去,見 §4.3 架構事實)
connector 實作範本(本機引擎 / shell out 型) evidence-agent/core/task_executor_connectors/inspec.py(T-2.2,dcedb4f)——GCB(FR-058.3)直接複用這支;--config 憑證機制、/dev/shm 暫存檔、probe 檢查順序都在裡面
factory(D11 要改的檔) evidence-agent/core/task_executor_connectors/__init__.py
派工編排 app/detection_tools/service/detection_orchestration_service.py
心跳派工 app/remote_agent/service/agent_enrollment_service.py_collect_pending_tasks / _resolve_tool_credentials
FE schema 驅動欄位 compliance-manager-fe/src/components/detection-tools/DetectionConfigField.vue(七種欄位型態 + condition
FE 工具管理頁 compliance-manager-fe/src/views/plugin/ToolPluginManage.vue
FE 執行抽屜 compliance-manager-fe/src/components/grc/JobExecutionDrawer.vue——注意 _PRIMARY_PARAM_KEYS 硬編 ['profile']summaryLabel()lang.my_tasks.summary_${key},找不到會印原始 key

SQL migration 撰寫鐵則(本案所有 seed 都要遵守):檔頭寫 -- Date:、每個語句加日期註解、新建 table 必加 GRANT ... TO cm_app + sequence 權限、收尾必 INSERT public.schema_migrations;套用一律 psql --single-transaction -v ON_ERROR_STOP=1 -f <檔>cmmgr 帳號(密碼請查 .env 或部署文件)。三環境(DEV / STG / POC)都要套。

4.X 橫向前置:_TOOL_ID_TO_CODE 解耦(D11)

現況evidence-agent/core/task_executor_connectors/__init__.py_TOOL_ID_TO_CODE = {1: "openvas", 2: "nessus", 3: "sonarqube", 4: "openscap"} 硬編 id 到 code 的映射。agent 不連雲端 DB 查工具目錄,只靠這張表認工具。

改動

  • BE _collect_pending_tasks() 組裝 pending_tasks 時,payload 加 detection_tool_code 欄位(值取自 detection_tools.code)。
  • agent get_connector() 簽章改為優先吃 code;detection_tool_id 保留為 fallback(舊 agent / 舊 payload 過渡期不壞),fallback 路徑加 warning log。
  • 移除 _TOOL_ID_TO_CODE 硬編表(或降級為僅 fallback 用的相容表,並在註解標明退役時機)。

為何排在最前面:三條開發線都會動到 agent 這個共用 repo。各自新增一支 connector 檔案彼此不衝突,但這個 factory 檔三條線都要改,同時開工會反覆衝突。而解耦本身工作量很小(改一個 factory + 派工 payload 加一個欄位),先做完三條線就不會撞同一個檔;等四個 connector 都寫完再回頭改,要重測的範圍會擴大四倍。

4.1 FR-058.0 任務層敏感參數(平台前置)

param_schema 語意擴充

detection_tool_param_schemas 的 schema JSON 每個欄位定義新增 "secret": true 旗標,語意與租戶層 config_field_schema 的 secret 定義一致。

BE 加解密接線(複用既有 Fernet 鏈,不另立機制)

時機 行為
寫入任務參數 依該工具當前版本的 param_schema 找出所有 secret: true 的 key,對其值 Fernet 加密後再寫入 tool_params;非 secret 欄位維持明文
組裝派工 _collect_pending_tasks()tool_params 中的 secret key 解密,與租戶層 credentials 同一時機、同一路徑下發(走 mTLS,agent 用完即丟)
讀取供 FE 顯示 secret key 一律剝除(不是遮罩成 **** 後仍回傳密文,是整個 key 不出現在 response)
稽核 log 寫事件內容時同樣剝除 secret key,避免在 system_logs 留下明文副本

已知坑param_schema 是版本化的。加解密要以「該任務當初綁定的 schema 版本」為準來判定哪些 key 是 secret,否則 schema 改版後舊任務會解錯(把明文當密文解、或把密文當明文送)。

FE

  • 欄位渲染:secret: truetype: password 走現成的 password 型態,不需新元件。
  • 執行紀錄(JobExecutionDrawer.vue):BE 已剝除,FE 不需再過濾;但要確認 _PRIMARY_PARAM_KEYS 之類的硬編邏輯不會意外把缺席的 key 顯示成空白或原始 key 名。

4.2 FR-058.1 ZAP(依賴 FR-058.0)

身分與整合形態

原 OWASP ZAP,現歸 Checkmarx 維護,仍為 Apache 2.0 開源授權。屬 DAST(動態應用程式安全測試)——對執行中的網站主動送出測試請求,找出網頁層弱點。

走 API / daemon 模式(與 OpenVAS 同型),不走 baseline CLI 模式。理由:API 模式能做「登入後掃描」,那是稽核場景真正有價值的能力;CLI baseline 只能被動掃表層,對合規證據的價值有限。

任務參數(param_schema)

key label type required secret condition
target_url 掃描目標網址 text
scan_mode 掃描模式 select(被動 / 主動 / 登入後主動)
login_url 登入頁網址 text 條件 scan_mode = authenticated
login_username 測試帳號 text 條件 scan_mode = authenticated
login_password 測試密碼 password 條件 scan_mode = authenticated
logged_in_indicator 登入成功特徵字串 text 條件 scan_mode = authenticated

沒有選登入模式時,後四欄整組不顯示(用 FE 現成的 condition 機制,不需新元件)。這對上「有帳密就深掃、沒有就一般掃描」的需求。scan_mode 預設值=被動(D2),主動類選項在 FE 加警語。

租戶層 config_field_schema:ZAP 服務位址 + API key(secret)。

登入後掃描的適用範圍限制:僅傳統表單登入,SPA 列後續另案(D12)

上表這組參數是傳統表單登入模型——ZAP 對登入頁 POST 帳密欄位、用 cookie 維持 session、用 logged_in_indicator 比對回應內容判定是否仍在登入態。這個模型對 SPA(單頁式應用)天生不成立,三個原因彼此獨立、必須同時解決才會通:

# 現象 根本原因 SPA 需要的做法
填了帳密但 ZAP 從未登入成功 SPA 的登入是 JS 呼叫 API(如 POST /api/.../login)取 token,頁面上沒有傳統表單提交可供 ZAP 對付 json-based 認證(對登入 API 送 JSON payload)
就算登入成功,後續請求仍是未登入身分 SPA 多用 JWT 存 localStorage、靠 Authorization header 帶;ZAP 預設 session 管理是 cookie-based,不會把 token 塞進後續請求 script-based session management(需撰寫並上傳 ZAP script)
ZAP 誤判未登入、反覆重跑登入 logged_in_indicator 比對的是 HTTP 回應的原始內容;SPA 初始 HTML 是空殼,登入後才出現的文字是 JS 渲染後才進 DOM 的,原始回應裡找不到 改用 API 回應特徵或其他非 DOM 判定(前兩項未解時此項無意義)

本案定位:第一批只支援傳統表單登入,SPA 登入掃描列後續另案(決策與被排除方案見 D12)。setup_guide 需明白寫出此限制——且措辭必須是禁止使用而非「結果會不完整」,理由見下方實測驗證。

⚠️ 後果更正(2026-07-30 實測):本節原先記載「對 SPA 掃描仍會完成、只是涵蓋範圍侷限於登入前頁面」(安靜成功),該描述是錯的。實際後果嚴重得多:ZAP daemon 會被自身的保護機制強制關閉,掃描不會完成,且連帶中斷同一台 ZAP 上其他任務。

實測驗證(2026-07-30,目標 http://192.168.50.151

完整因果鏈——

  1. SPA 表單登入無效(原因 ①),ZAP 未取得任何登入態;
  2. logged_in_indicator 在原始回應中找不到(原因 ③),ZAP 判定「未登入」;
  3. ZAP 依設計重跑登入 → 再次失敗 → 再判未登入 → 無限迴圈
  4. 認證失敗次數累積達 100 次,觸發 ZAP 2.17.0 的 Insights 門檻 insight.auth.failure
  5. ZAP 主動關閉自己(不是被 agent 中止,也不是逾時)。

daemon log 摘錄:

[ZAP-Insights] INFO org.parosproxy.paros.control.Control - Shutting down ZAP due to High Level Insight:
HIGH : EXCEEDED_HIGH : http://192.168.50.151 : insight.auth.failure : 100
[ZAP-Insights] INFO ... - Discard Session
... (後續大量 Selenium NoSuchSessionException,為 ZAP 死後的餘波,非原因)
ZAP 2.17.0 terminated.
zap exited with code 2 (restarting)

讀 log 時容易誤判的一點:NoSuchSessionException 洗版看起來像 Selenium 出問題,但它發生在 Shutting down ZAP 之後,是 ZAP 已死造成的下游噪音,不是根因。根因只有 insight.auth.failure : 100 這一行。

營運層級影響(本次更正的重點):ZAP daemon 是全租戶共用的單一服務,不是每個任務一個實例。因此一個 SPA 登入掃描任務可以把整台 ZAP 打掛,連帶中斷該台 ZAP 上其他正在執行的掃描——實測當下即發生:另一個任務在 ZAP 重啟的空檔收到 Connection refused 而失敗。這使問題性質從「報告涵蓋範圍不足」升級為「單一任務可造成全域服務中斷」。

範圍界定(避免過度解讀):此問題僅限登入後掃描模式。被動掃描與主動掃描不建立 ZAP context、不設定任何認證、不比對登入特徵字串,因此不會產生認證失敗、不會累積計數、碰不到 Insights 門檻。第一批出貨的被動與主動兩種模式完全不受影響

實測佐證(2026-07-30,對一個 Next.js SPA 站台)

  • 覆蓋範圍:掃到 10 個 URL,全部為登入前靜態資源——根路徑、_next/static 下的 JS / CSS chunk、favicon;無任何 API 端點、無任何登入後頁面
  • 產出:HTML 報告 57 KB,證據上傳鏈路本身正常(D4 的雙格式取報告已驗證可行)。
  • alert 分佈:2 High / 8 Medium / 7 Low / 32 Informational,性質集中在站台層級組態問題(安全標頭、CSP、cookie 屬性、資訊洩漏),非應用邏輯漏洞

這組數字同時也是匿名掃描的價值邊界的具體證據:對 SPA 而言,未解決登入即等同匿名掃描,能查出組態層問題但碰不到應用邏輯——這正是登入後掃描在稽核場景的價值所在,也是它值得列為獨立後續案、而非在第一批做半套的理由。

已知但本案不做:connector 端的自我保護(2026-07-30 裁示:只記錄不實作)

上述營運層級風險有一個成本很低的緩解手段,但本案不做:

  • 內容:connector 在登入模式下自行偵測認證失敗異常累積(例如監看 ZAP 的認證失敗計數或重複登入次數),在逼近 Insights 門檻前主動中止任務並回傳明確訊息(如「目標網站可能是單頁式應用,登入後掃描不適用」),避免撞上門檻把共用的 ZAP 服務打掛。
  • 決策:決策者 2026-07-30 裁示——本案只記錄不實作
  • 關聯與獨立性:與 Notion CM-984(SPA 支援後續需求)相關,但可獨立先做——它是防護措施,不需等 SPA 的 json-based 認證與 script-based session management 完成;即使 SPA 支援永遠不做,這道保護對「使用者誤把 SPA 當表單登入站台來掃」的情境仍然有效。

ZAP 2.17.0 相容性(使用者公司已安裝此版,實作前必須對齊)

項目 內容 影響
Python 套件版本 zaproxy 0.6.0(2026-06 發布)。舊參考碼用的 0.4.0(2025-01)太舊;0.5.0 與 ZAP 2.17.0 同日(2025-12-15)發布以對齊新版 API ZAPv2 類別仍是進入點,呼叫風格不必改寫
警報去重 2.17.0 加入警報去重與 Systemic alert 支援(Issue 9067 / 9097) alert 數量會比舊版少,統計解析要對齊新語意,不能沿用舊版計數假設
報告時間欄位 JSON / XML 報告新增 ISO 8601 格式的 created 欄位;舊的 generatedString 官方已預告未來移除 解析器不要依賴 generatedString
暫存訊息保留 headless 模式下,主動掃描產生的暫存 HTTP 訊息預設不再保留 對取報告流程無影響

沒有破壞性的 API 端點移除,主線呼叫序列可以沿用。

舊碼參考:前同事的 auto-pentest 專案

位置 ~/Documents/Projects/Jedicogy/auto-pentest/(DDD 分層 + MinIO 儲存)。它的價值不在架構,而在踩過坑才整理得出的那幾份知識

可直接搬的知識 必須丟棄的部分 必須修的問題
API 呼叫主線序列auto_pentest/domain/zap/service/zap_domain_service.py):ajaxSpider.scan → 輪詢 ajaxSpider.status(回字串 'running')→ ascan.scan → 輪詢 ascan.status()(回百分比數字)→ 取報告(舊碼用 reports.generate;本案依 D4 改版改用 core.htmlreport / core.jsonreport 取 bytes)。⚠️ 兩種輪詢的回傳型態不同,這個細節在 2.17.0 沒有改變 整個 DDD 分層:domain service / app service / entity / MinIO storage adapter 全不需要——我們的 connector 是扁平單一類別,上傳走 task_executor 既有 blob 通道,沒有 storage adapter 的位置 硬編憑證(紅線):zap_auth.pyzap_domain_service.py 內有寫死的帳號密碼與固定 IP。憑證禁入版控是本專案紅線,一律改走 self.credentials(FR-056 D9 解密下發)與 self.params
12 種報告模板 + sections / themes 相容矩陣auto_pentest/common/enums/zap_report/report_enum.pyTEMPLATE_CONFIGS)。⚠️ D4 改版後主線不再走 reports.generate 模板體系(改用 core.htmlreport / core.jsonreport 直接取 bytes),此矩陣降為背景知識,不再搬去當 FE 下拉選項來源 自起 Docker 容器ZAPContainerDomainService):ZAP 應視為「客戶已安裝好的服務」,connector 只負責連上去(D3) 沒有取消機制、沒有 timeout 上限while status == 'running' 是無限等待。必須支援 cancel_event 與 timeout 上限。另外 print 當 log 要換成 logger
四種登入認證設定方式auto_pentest/domain/zap/service/zap_auth.py):form-based / json-based / script-based / Selenium-based,加上 set_logged_in_indicator登入後掃描是整個 ZAP 接入最難的部分,這裡有完整的 API 序列可參考邏輯 報告落地檔案系統再上傳reports.generate(reportdir=...) 會寫檔到磁碟——而且寫的是 ZAP daemon 主機的磁碟,遠端部署模型下 agent 根本取不到(D4 改版的直接動因,issue #7821)。本案改用 core.htmlreport / core.jsonreport 直接取回 bytes,記憶體轉手不落地 Oracle Nashorn script engine:JDK 15 之後已移除,ZAP 新版預設改用 Graal.js。舊碼的 upload_script() 已改用 Graal.js,但 java_script() 還停在 Nashorn——是會踩到的雷

connector 實作要點(evidence-agent/core/task_executor_connectors/zap.py

  • run():依 scan_mode 分支(被動 / 主動 / 登入後主動)。登入後主動要先設定 context + 登入認證 + logged_in_indicator,再跑 ajaxSpider 與 ascan。
  • 取報告:雙格式(D4,2026-07-30 改版)——core.htmlreport() 回 bytes 上傳當證據(檔名 ZAP掃描報告_<target>_<date>.html);core.jsonreport() 回 bytes 只在記憶體解析出 summary,不上傳。不走 reports.generate(只寫 daemon 主機磁碟、無回傳內容端點)。
  • probe():只驗「連得到 ZAP daemon 且 API key 正確」,不建立任何掃描資源(與 base.py 的 probe() 語意一致)。
  • cancel_event 與 timeout:輪詢迴圈每圈檢查 cancel_event,命中就 raise ScanCancelledError;另設 timeout 上限,避免無限等待。
  • setup_guide:寫 daemon 啟動指令與 API key 設定說明(客戶自備,D3)。

4.3 FR-058.2 InSpec / CINC Auditor(獨立,可與 .0 並行)

身分與要解的問題

組態合規檢測引擎,用 Ruby DSL 撰寫檢測規則(profile)。本案接入它是為了解決 Windows 主機組態檢測這個既有缺口——FR-057 的 OpenSCAP connector 是 SSH 加 Linux 寫死的(/usr/bin/oscap/etc/os-release、sudoers),無法處理 Windows 主機。

吸收 Notion CM-953(「FR-057 後續:異質 OS 環境(Windows 等非 Linux 主機)的檢測支援方案」,狀態=討論,https://app.notion.com/p/3ac346da4cd0819390d2c5b85c754bf8),不另開平行卡。CM-953 提的三個選項中,選項 1(OpenSCAP Windows 走 WinRM)已被證實不可行——OpenSCAP 官方放棄 Windows 支援,且引擎沒有 WinRM transport、沒有 Windows OVAL probe,無論怎麼配置都掃不到 Windows 目標。InSpec / CINC 屬選項 2 的具體答案。CM-953 定的「一任務一工具、靠多任務處理異質環境」模型本案沿用,不需改架構。本子需求落地時一併結案 CM-953。

為什麼是 CINC 而非其他 SCAP 系工具

候選 判定
OpenSCAP on Windows 死路。官方 docs/windows.md 明文標示 no longer usable,最後一個勉強可用的版本是 1.3.4 且不再修 bug
「Linux 掃描機遠端掃 Windows」 引擎做不到——不是設定問題,是能力缺失(無 WinRM transport、無 Windows OVAL probe)
CIS-CAT Pro 唯一能做「單機遠端 WinRM 掃多台 Windows」的 SCAP 系工具,但需要 CIS SecureSuite 付費會員資格
SCC 免費但遠端能力弱,偏「目標本機安裝 agent」模式,與我們 agentless 的既有模型不合
CINC Auditor 採用。 agentless(目標主機不必安裝掃描器,與 OpenSCAP-SSH 同一種遠端模型);SSH 與 WinRM 同一套 CLI 與 profile 格式--reporter json 統一輸出,connector 不必寫兩套解析;Apache 2.0 免費;一個 connector 供三條線用(Windows 組態檢測 + Linux GCB + Windows GCB)

授權關鍵:必須用 CINC Auditor(Apache 2.0 自由重建版,Progress 官方認可的 drop-in 替代品),不要用官方 InSpec 6+ 商業 binary(需接受 Chef EULA 並持有 license key)。這一點在採購與部署文件上都要寫明(D5)。

架構事實:CINC 裝在 Agent 本機,與 OpenSCAP 相反(T-2.2 實作後補記,2026-07-31)

這一點與 OpenSCAP 的方向完全相反,後續實作者務必先看清楚:

OpenSCAP(FR-057) CINC Auditor(本案)
掃描引擎裝在哪 目標主機(客戶自行安裝 oscap Agent 本機(隨 agent image 安裝)
誰發起連線 connector 用 paramiko 自組 SSH 指令進目標主機執行 cinc-auditor 自己連出去;connector 只是 shell out 呼叫本機執行檔
目標主機的前提 需安裝 oscap + content + sudoers 不需安裝任何掃描程式(agentless),只需開放 SSH / WinRM
connector 處理 transport 的方式 自己寫 SSH 連線邏輯 交給 cinc-auditor-t ssh://… / -t winrm://…

這正是 D6「多做一個 transport 的邊際成本很低」的真正原因——transport 差異被 CINC 自己吸收了,connector 兩邊共用同一套指令組裝與 --reporter json 解析。原設計文件只有推論,T-2.2(CM-973,evidence-agent dcedb4f)實作後已有實證。

因此 openscap.py 只有「多目標逐台處理」與「host_failures 逐台回報」兩個形狀可借鏡,連線那一層完全不同,不可照抄。 本文件先前「對照 openscap.py 的 SSH 模式」之類的描述已依此更正(§4.0 參考檔案座標)。

對 FR-058.3 GCB 的直接影響:T-3.2 的 content 外部載入接口是建立在這個架構上的——profile content 由 agent 本機讀取後交給 cinc-auditor,不需要、也不應該送到目標主機。若誤以為 content 要先傳到被掃描的機器上,接口會設計錯,而那要等到端到端驗收才會發現。

T-2.2 建立的三個實作約定(後續棒次延續)

  1. 憑證不進 argv。 cinc-auditor --password 會讓密碼出現在 Agent 主機ps 輸出,而證據 Agent 是長駐服務——任何能看到該主機行程的人都讀得到客戶的稽核帳號密碼。故改走官方 --config <JSON 檔>credentials 區塊,命令列只帶 -t <transport>://<連線名>。config 檔與私鑰一律寫 /dev/shm(tmpfs,不落實體磁碟)、權限 0600try/finally 用完即刪(含例外與取消路徑)。
  2. 授權紅線以測試釘住。 指令刻意不帶任何 license 相關參數——CINC 沒有 license 機制,而官方 InSpec 6+ 未接受 EULA 會 exit 172。若日後有人把 Dockerfile 換成官方 InSpec,掃描會整批硬失敗,那個失敗是刻意留的路障,不該靠加 --chef-license accept 之類的參數「修好」(那等於預設替客戶接受商業授權條款)。已有測試釘住此約定。
  3. probe 的檢查順序:憑證缺漏在前、引擎檢查在後。 兩組 transport 憑證都是非必填(設定當下無從得知任務會選哪一種),這個取捨成立的前提就是缺漏必須在 probe() 講清楚。順序若對調,憑證不齊的使用者會先看到「Agent 沒裝檢測引擎」——把客戶端設定問題誤報成我方部署問題,會把人導向完全錯誤的排查方向。已有測試釘住此順序。

設計要點

  • 雙 transport(D6):SSH(Linux)與 WinRM(Windows)。目標 transport 的選擇透過任務參數或設定欄位決定;detection_tools.connection_type 需涵蓋這兩種型態(SSH 已存在,WinRM 為新值)。實作落點為任務參數 params.transport(同租戶的 Linux / Windows 目標分屬不同任務,沿用 CM-953「一任務一工具」模型),憑證則兩組並存於租戶層(ssh_* / winrm_* 前綴)。
  • 結果解析統一走 --reporter json:兩個 transport 輸出格式一致,connector 只需一套解析。
  • 證據格式:html2 上傳、json 只算 summary(2026-07-31 實測後定案,對齊 D4 ZAP / D9 Nmap 的雙格式精神):一次執行帶兩個 reporter,json 走 stdout 只在記憶體算 summary 不上傳html2:<檔案> 寫出的 HTML 上傳當證據。兩份等價內容都進證據池是 D4 已排除過的走法。
    • html2 而非內建的 html:後者是 RSpec 風格測試輸出,實測 grep 控制編號命中 0 次,無控制標題與嚴重程度,對稽核判讀無用;官方文件亦說明 html 僅為向下相容保留、「unaware of profiles or controls」。html2 三者皆有,且產出的 HTML 不引用任何外部 CDN 資源(自包含、離線可開)——證據要能長期保存後再開啟,這是必要條件而非加分項。
    • 不自行從 JSON 手刻 HTML:上游已有結構完整且自包含的產出,手刻只是多一份要維護的模板。
  • 多目標處理沿用 FR-057 模式(僅此一項):逐台掃、每台一個證據檔,host_failures 逐台失敗回報通道直接沿用(避免「靜默假成功」)。連線那一層不沿用——見上方架構事實表。
  • 憑證:沿用租戶層 tenant_detection_tool_configs 加密鏈(Linux SSH 帳號/金鑰、Windows WinRM 帳號密碼)。若日後需要每台主機不同帳密,走 FR-058.0 的任務層敏感參數。
  • agent image:加入 CINC Auditor。

4.4 FR-058.3 GCB(依賴 FR-058.2)

身分與範圍

政府組態基準(TWGCB),我國公務機關資通設備的組態安全基準,屬合規稽核情境的高需求項目。

本案範圍已由 D7 收斂:只做引擎(直接複用 .2 的 CINC Auditor,不另立引擎、不另寫 connector)與一份 Windows 最小 Demo profile,證明端到端鏈路可運作即可。

面向 Linux GCB Windows GCB
引擎 統一走 FR-058.2 的 CINC Auditor(Linux 用 SSH transport、Windows 用 WinRM transport,同一套 CLI 與 profile 格式) 同左
引擎層工作量 已由 .2 承擔,GCB 本身不需要新的 connector 同左
本案 content 不做 一份最小 Demo profile,僅用於驗證鏈路
content 載入方式 一律從外部載入,不硬編進 connector、不打包進 agent image(本案設計約束) 同左
後續 content 產製 列為後續,與後台 content 更新機制一併另案評估 同左

為何 Demo 選 Windows(2026-07-30 官方資源查訪的結論)

資料來源為 NICS 兩個官方頁面(GCB 說明文件頁標示 2026/6/26 更新、GCB 部署資源頁標示 2026/3/30 更新)。本次查訪最重要的發現:Linux 與 macOS 在部署資源頁完全沒有任何機器可讀檔案。

平台 可用來源 產製方式 自動化程度
Windows(Win11 / Server 2016·2019·2022 + Chrome / Edge) GPO backup ZIP(含 registry.pol + GptTmpl.inf 可寫產生器解析產出骨架,再人工補判定邏輯 約七成
Linux(RHEL 8 / RHEL 9 伺服器·工作站 / Ubuntu 22.04) 只有 DOCX / PDF 逐條人工閱讀文件、手寫檢查規則 幾乎零
macOS 15 只有 DOCX / PDF 同 Linux;且現有 connector 未曾處理 macOS 幾乎零(且已由 D7 排除出本案)

說明文件頁有完整的 RHEL 8 / RHEL 9 / Ubuntu 22.04 / macOS 15 基準,但沒有任何對應的機器可讀部署檔——連 shell script 都沒有。這證實並擴大了 NICS FAQ v1.6 §7.1「未提供 RHEL8 等平台自動化檢測工具」的說法。

Windows 有 GPO 檔這個結構化來源可直接參考,寫一個最小 Demo profile 的成本最低;Linux 只有 DOCX,連一條 Demo 規則都要先人工翻譯文件敘述。這是選 Demo 平台的理由,不是要在本案做 Windows content 工程。

常見誤解澄清:「是不是要把官方 PDF 轉成 content」——Windows 不需要(有 GPO 檔可解析);Linux 需要,但那不是「轉檔」,而是逐條人工把文件敘述翻譯成檢查邏輯。content 工程已不在本案範圍,此處僅為背景說明。

本案的設計約束:content 必須可外部載入

content 不可硬編進 connector 或 agent image,必須設計成可從外部載入。理由:content 本質上是會持續變動的資料,不該綁在程式版本裡。日後要接後台更新機制時,若當初把 profile 打包進 agent,等於每次更新 content 都要重新發版並更新所有客戶站點的 agent這個接口現在留成本極低,事後補要改動 connector 結構。

D7 約束的達成方式:不另做抽象層(T-3.2 定案,2026-07-31)

結論:約束已滿足,且不需要寫任何 code。 T-3.2 原規劃要做一層「content 外部載入接口」,實作前確認後裁示不做這層抽象——CINC 原生就支援三種 content 來源,而 params.profile直接進 cinc-auditor exec 的 argv,外部載入能力本來就在。再包一層只是把 CINC 的能力用我們的詞彙重新命名一次,徒增一個要維護、要測試、且會限縮上游能力的中間層。

content 來源 形式 取得時機
網址(Git repo 或壓縮檔) https://github.com/<帳號>/<專案>/archive/refs/heads/master.tar.gz 執行時由 cinc-auditor 自行下載
Agent 主機上的本機路徑 目錄或壓縮檔路徑 執行時本機讀取
Supermarket 上的 Profile 名稱 profile 名稱字串 執行時自 Supermarket 取得

D7 約束的逐條驗收

  • 不硬編進 connector ✅ — inspec.py 對 profile 的處理只有「取 params.profile、缺就報錯、有就原樣放進 argv」,connector 內不含任何 content 內容或路徑常數。
  • 不打包進 agent image ✅ — Dockerfile 只裝引擎(cinc-auditor)與其相依(gitxsltproc),無任何 profile 檔。
  • 更換 content 不需重建 image、不需改 connector ✅ — 換一份 content=任務參數換一個字串,agent 端零改動。

架構前提再強調一次(§4.3 架構事實):content 由 Agent 本機讀取後交給 cinc-auditor不送到目標主機。CINC 裝在 agent 端、由它自己連出去掃,目標主機不需要安裝任何東西、也不需要拿到 profile。若誤以為 content 要先傳到被掃描的機器上,接口會設計錯,而那要到端到端驗收才會發現。

實跑佐證(2026-07-31,DEV agent 0.2.17 / 192.168.50.123

① 兩份公開 content 對兩種 transport 各跑通一次(這同時是 T-2.3 的雙 transport 驗收):

content 目標 transport 結果
dev-sec/linux-baseline 192.168.50.151 SSH pass 131 / fail 60 / error 0 / n/a 2
dev-sec/windows-baseline 192.168.50.160 WinRM pass 300 / fail 472 / error 0 / n/a 122

error 0 是這裡的重點——引擎確實把 content 取到、看懂、跑完,不是連上去之後靜默空轉。

② 裸 GitHub 網址走的是 git fetcher,不是 tarball 下載(踩過才知道的事實):CINC 對裸 GitHub 網址會 shell out 呼叫 git 去查 default branch。agent image 原本沒裝 git,首次實跑因此在還沒連上目標主機之前就炸在 profile 解析階段(Errno::ENOENT)。已於 agent commit 6d1a4fc 補裝 git,DEV 現行 0.2.17 容器內為 git 2.47.3

連帶的錯誤分類修正:當時所有失敗都被歸類成「連線目標失敗」,把 agent 端的 profile 取得問題誤報成客戶的網路/憑證問題。inspec.py 已補上 profile 階段的 marker 比對,並刻意排在連線判定之前(同 T-2.2 第 3 點的精神:分類籠統比分類錯更貴)。

③ tarball 形式對 git 零相依(T-3.1 為決定 profile 下拉預設值而做的對照實驗):把容器內 /usr/bin/git 移走後重跑同兩個網址——

形式 正常容器 移走 git
.../archive/refs/heads/master.tar.gz ✅ Valid、431 條控制 照常 Valid、431 條控制
https://github.com/dev-sec/windows-baseline ✅ Valid、431 條控制 Ruby stack trace 直接失敗

耗時無實質差異(84s / 88s)。裸網址今日之所以可用,完全建立在 6d1a4fc 補裝的 git;tarball 形式對此無相依。故 T-3.1 的 profile 下拉預設值一律採 tarball 形式(少一個外部相依與故障點),自行輸入的彈性不減。

兩個已知限制(本案不做,已裁示開後續卡)

「不做抽象層」的取捨有代價,兩者都是能力缺口而非 bug,且都不影響本案驗收(Demo 走公開網址)。記在此處是為了讓日後接後台 content 更新機制的人不必重新踩一次:

# 限制 成因 後果
封閉網路客戶只能用本機路徑,但目前沒有把 profile 檔送進 agent container 的機制 agent 跑在 Docker container 內,有獨立檔案系統。deploy/docker-compose.yml 現有的 bind-mount 只有 ./filedata(證據落地)、./certs(mTLS 憑證)與三個 host 識別碼唯讀掛載,沒有任何 profile / content 用途的掛載點。客戶把 profile 放在主機上,容器內看不到 agent 不能連外時,三種來源全部不可用(網址與 Supermarket 需連外,本機路徑指不到)。與 D9 Nmap 「容器看不到主機上的 nmap」是同一個結構性問題
私有版本庫無憑證路徑 params.profileparam_schemasecret: false 的純文字欄位,值原樣進 argv。CINC 的 git fetcher 要認證需要 token 或 SSH key,而任務參數沒有地方放 secret——FR-058.0 的任務層敏感參數雖已具備此能力(標 secret: true 即走 Fernet 鏈),但 profile 是「位置」不是「憑證」,把 token 塞進位置字串(https://<token>@github.com/...)會讓憑證出現在 FE 執行紀錄與稽核 log 客戶的 content 放私有 repo 時無法取用,只能改放公開位置或本機路徑(而本機路徑受限制 ① 影響)

兩者的解法都指向同一件事:後台 content 管理機制(上傳/版更/套用),由平台把 content 送到 agent 可讀的位置,順帶消掉私有 repo 的認證需求。該機制 D7 已排除出本案,兩個限制併入其評估範圍。

Demo profile 的注意事項

必須避開 tattoo settings 陷阱:規則若讀取非 HKLM\Software\Policies 路徑,可能讀到上次殘留的舊值而誤判為通過,實際上政策根本沒生效(詳見 §3.3 風險表)。Demo 選題時就要挑正規政策區的項目。

4.5 FR-058.4 Nmap(完全獨立)

2026-07-30 改案:本節原為 connection_type='CLI'(agent 本機子行程)設計,已改為 SSH 型。改案的完整推導、NPSL 條款原文與三個被排除方案見 §2 的 D9

身分與整合形態

網路埠與服務探測工具。在合規情境對應「開放服務盤點」「非必要服務關閉」這類控制項的證據。技術上是四個工具裡最單純的一個。

connection_type='SSH',與 OpenSCAP 完全同構:nmap 由客戶安裝在自己指定的主機上,agent 透過 SSH 登入該主機執行 nmap -oX 並取回 XML 再解析。agent 本身不安裝 nmap、不在容器內執行它。

之所以不走原定的 CLI(agent 本機執行),有法律與技術兩個各自獨立成立的理由(詳見 D9):法律上,NPSL v0.95 §3 的衍生作品定義明文涵蓋「專門執行本軟體並解析其結果」的程式,我們的 connector 正中此條,因此不能 bundle;同條款末段又明說不主張控制「執行使用者早已安裝在自己系統上的 nmap」的軟體,SSH 型正落在這個出口內。技術上,agent 跑在 Docker container 內,看不到主機上安裝的 nmap,原 CLI 設計的「客戶自備」在容器化部署下根本無法成立。

連帶結果:Nmap 需要 SSH 憑證requires_credentials 為 TRUE),不再是零憑證工具。connection_type='CLI' 因此至今仍無任何實際使用者(FR-056 定義後未被用過)。

前提

授權:不要打包進我們的 image。 Nmap 採 NPSL(非標準開源授權,基於 GPLv2 但加了額外限制),不可 bundle 進我們發布的 agent image,也不可要求客戶把它裝進我們的容器。做法是客戶在自己指定的主機上安裝 nmap(apt install nmap 之類,無任何 docker 操作),agent 走 SSH 遠端呼叫。setup_guide 要寫清楚:在哪台主機安裝、各主流發行版的安裝與驗證指令、SSH 連線與帳號權限前提,以及埠掃描的授權警語(比照 ZAP)。

D9 原列的第二個前提「零憑證會被平台誤擋」在改案後不再適用於 Nmap(它現在需要 SSH 憑證)。但該平台能力(detection_tools 的「是否需要憑證」宣告欄位 + start_execution() 放行)仍然正確且保留,供日後真正零憑證的工具使用。

connector 實作要點(evidence-agent/core/task_executor_connectors/nmap.py

主要對照範本是 openscap.py(SSH 型),不是本機子行程模式。 SSH 連線處理、私鑰暫存與用完清除、多目標主機逐台執行、host_failures 逐台失敗回報這些形狀直接複用。

  • run():SSH 登入指定主機 → 執行 nmap -oX → 取回 XML → 解析產出 summary(開放埠數、服務清單等)→ XML 或轉出的人可讀報告上傳當證據。多台執行主機時逐台一份證據,某台失敗以 host_failures 明確回報(不靜默假成功)。
  • probe():四段式錯誤分類,各段回各自明確的訊息——① SSH 連得上嗎(網路 / port)→ ② 認證過嗎(帳密 / 私鑰)→ ③ 該主機上有沒有 nmap 且可執行command -v nmap + nmap --version)→ ④ 執行權限夠不夠(部分掃描類型需要提權)。第 ③ 段的訊息要讓管理員一看就知道「要去那台主機裝 nmap」,而不是丟原始的 exit code 127。
  • cancel_event 與 timeout:取消時要中止遠端執行中的 nmap 並清理 SSH channel,raise ScanCancelledError;照 openscap.py 的遠端子行程取消形狀處理,不要留下對端的孤兒行程。
  • param_schema:掃描目標、掃描類型、埠範圍等(皆非敏感,不涉及 FR-058.0)。SSH 憑證屬租戶層設定tenant_detection_tool_configs,走既有 Fernet 鏈),不放任務參數。

4.6 FR-058.5 SonarQube(2026-07-31 追加;獨立,不依賴其他子需求)

決策脈絡見 D13–D16。原規劃期裁示「本期不做」移出(D10 因此成為 tombstone 空編號),2026-07-31 決策者裁示重新納入為第五個子需求。

身分與整合形態

SAST(靜態應用程式安全測試)——分析原始碼找出 bug、弱點與程式碼異味,在合規情境對應「安全開發流程」「程式碼安全檢測」這類控制項的證據。

平台第一個「拉取快照(pull-snapshot)」型工具。 連線形態歸 §1.1 的 ①(連遠端服務 API),但語意與 OpenVAS / ZAP 不同——連的是 API 卻不下掃描指令。根本原因:SonarQube Server 沒有任何觸發掃描的 Web API,掃描由 sonar-scanner 在有原始碼的環境(客戶 CI)執行後把結果推上 server,Web API 只能讀取既有結果(唯一例外是 SonarQube Cloud SaaS 的 Automatic Analysis,與 self-hosted 無關)。因此任務執行=連 server 拉指定專案最近一次分析結果、組成 HTML 報告當證據(D13)。

租戶層設定(config_field_schema)

[
  {"key": "base_url", "label": "SonarQube 服務位址", "type": "text",     "required": true, "secret": false},
  {"key": "token",    "label": "User Token",        "type": "password", "required": true, "secret": true}
]

token 必須是 user tokensqu_ 前綴),HTTP 用 Authorization: Bearer header 送出;token 所屬帳號只需目標專案的 Browse 權限。不可用 sqp_ / sqa_ analysis token——那兩種只能執行分析、不能呼叫讀取 API(D16),setup_guide 要寫明這個區別。

任務層參數(param_schema v1)

key label type required 備註
project_key 專案 key text SonarQube 專案識別碼
branch 分支 text label 註明付費版(Developer Edition 起)才支援多分支;Community Build 只有單一 branch。不填=不送 branch 參數(Community 相容);填了但 server 不支援時 connector 回明確錯誤訊息(D14)

皆非敏感參數,不涉及 FR-058.0。

API 呼叫序列

endpoint 用途 備註
api/project_analyses/search?project=<key> 取最新分析時間(最新一筆 date,進報告供稽核員判斷資料新鮮度) 回空=該專案尚無分析結果,raise 明確錯誤「請先於客戶端執行 sonar-scanner」——不可繼續往下組空報告
api/qualitygates/project_status?projectKey=<key> Quality Gate 狀態(OK / ERROR)+各 condition
api/measures/component?component=<key>&metricKeys=bugs,vulnerabilities,code_smells,security_hotspots,coverage,duplicated_lines_density,ncloc,alert_status 核心度量 summary 數字的唯一來源,繞開 MQR severity 語意差異(D15)
api/issues/search?components=<key>&ps=500 + facets(severities / types) 分佈統計用 facets、issue 清單取前 200 條(severity 由高至低) 10,000 筆硬上限(第 21 頁起報錯),故絕不逐頁撈全量;統計一律用 facets 與 measures 取全量
api/hotspots/search?project=<key> Security Hotspots

全部只需 Browse 權限;branch 參數僅在使用者有填時附加到各呼叫。

probe 設計

兩段式:

  1. api/system/status——免認證,驗連通與服務狀態(server 是否 UP)。
  2. Authorization: Bearer 呼叫 api/authentication/validate——驗 token 有效性。陷阱:匿名呼叫此端點也回 valid:true(它驗的是「當前身分是否有效」,匿名也是一種有效身分),必須帶認證 header 呼叫、結果才有意義(D16)。

probe 是租戶層動作、當下沒有 project_key專案層權限問題不在 probe 驗——留到 run() 時以明確訊息回報(例如專案不存在或無 Browse 權限)。

connector 實作要點(evidence-agent/core/task_executor_connectors/sonarqube.py

與既有 API 型 connector(OpenVAS / ZAP)的關鍵差異:沒有輪詢迴圈——拉取型不需要等掃描完成,run() 是一串順序 API 呼叫。

  • 取消:逐 API 呼叫之間檢查 cancel_event,命中 raise ScanCancelledError
  • timeout:每個 HTTP 呼叫設 60 秒 timeout(不是整體輪詢上限,是單呼叫上限)。
  • HTTP client:用 agent 既有的 httpx(FR-039 心跳已用),不加新相依
  • issue 解析跨版本相容(D16):impacts[] 優先(MQR 語意,10.2+ 回應就有),缺席時 fallback 舊 type + severity,相容 9.9 LTS 至 2026.x LTA。
  • 報告(D15):拉 JSON 自行 render 自包含 HTML(無外部 CDN 資源)上傳當證據,檔名 SonarQube掃描報告_<project_key>_<date>.htmlcontent_type="text/html"。內容=Quality Gate 狀態+核心度量+severity 分佈+issue 清單(前 200 條,註明「僅列前 200 條/總數 N」)+Security Hotspots+分析時間。這是必做項不是 fallback——Community Build 沒有任何報告匯出功能(付費版才有 PDF)。
  • summary dict{"findings": <issues 總數>, "bugs": …, "vulnerabilities": …, "code_smells": …, "security_hotspots": …, "quality_gate": "OK"/"ERROR"}——數字取自 measures API,不從 issue 清單自行加總。

factory 註冊

_CONNECTOR_BUILDERS"sonarqube": _build_sonarqube(lazy import,3 行 builder 形狀比照 _build_zap)。絕不動 _LEGACY_TOOL_ID_TO_CODE——表內既有的 3: "sonarqube" 是 legacy fallback(D11 解耦後的過渡相容表),保持原樣。

seed 特殊點(易踩雷,T-5.1 必讀)

DB 已有 FR-056 留下的 id=3 sonarqube 佔位列status='coming_soon'config_field_schema=[]、無 param_schema)。因此——

  • seed 是 UPDATE 既有列 + INSERT param_schema,不是 INSERT 新工具。照 ZAP seed 範本的 INSERT ... ON CONFLICT (code) DO NOTHING 寫,會被既有列擋下靜默跳過,看起來成功實際什麼都沒改。
  • UPDATE 內容:config_field_schema(上方兩欄)+ status → 'available'setup_guide + description 只拿掉句尾「此工具尚在規劃中、目前無法使用,敬請期待。」其餘不動
  • 依環境異動鐵律,開發期間只套 DEV

驗收環境

公司自有 SonarQube(http://192.168.50.171:9000,專案 guidant-ai-backend / guidant-ai-fe)直接當 DEV 端到端驗收目標。token 為 user token——不寫進任何文件,執行者自查本機環境

FE 接入點

純 schema 驅動、零新元件。僅兩處微調(T-5.3):

  • JobExecutionDrawer.vue_PRIMARY_PARAM_KEYS'project_key'
  • i18n my-tasks.json(zh-tw + en,比照既有 summary_* key 所在檔)補 summary_bugs / summary_vulnerabilities / summary_code_smells / summary_security_hotspots / summary_quality_gate

已知限制

# 限制 說明
不觸發掃描 資料新鮮度取決於客戶端掃描頻率;報告內的分析時間是稽核員判斷依據(D13)
issue 清單僅前 200 條 統計數字為全量(facets / measures),僅明細清單截斷並在報告註明(D15)
證據可下載、不可預覽 同 ZAP 第一批出貨標準(證據池預覽鏈不吃 HTML)
Community 無多 branch branch 留空即可;填了而 server 不支援時回明確錯誤(D14)

4.7 FR-058.6 SonarQube 主動掃描模式(2026-07-31 追加;雛形,依賴 FR-058.5 已落地的拉取鏈路)

決策脈絡見 D17–D24;討論稿:discussion-sonarqube-scan-mode.html

⚠️ 定位:這是雛形,不是完整產品。 本節每一處取捨都朝「最小可行」傾斜。遇到「這裡是不是應該做得更周全」的疑問時,先翻本節末的「後續強化」——所有被砍掉的東西都在那裡列了一句話說明為什麼這階段不做。決策者原話:「先用公開 repo 可以下載的那種,我們先做一個雛形,後面再慢慢加強」。

需求背景:為何 pull 不夠

FR-058.5 做的是拉取快照——連客戶的 SonarQube,把「最近一次分析結果」拉回來組報告,平台不觸發掃描。決策者看到實際畫面後指出:

流程好像不太對,我想要的是 user 有提供一版源碼,可能上傳 或是提供 server 位置,然後透過 sonarqube cli 去掃描並且產出結果報告 — 決策者,2026-07-31

這句話包含三個要素:① 使用者提供一版源碼 → ② 平台透過 sonar-scanner CLI 執行掃描 → ③ 產出結果報告。上線的 FR-058.5 只有 ③,而且那個「結果」不是平台掃出來的,是客戶自己先掃好放在 server 上的。

這是新需求不是 bug。 FR-058.5 的 D13 明確裁示了「拉取最近一次分析結果組報告、不觸發掃描」,當時就把「agent 端自跑 sonar-scanner」列為被排除的選項,理由是「agent 沒有原始碼,也不可能備齊各語言 build 環境」。那個判斷在當時的前提下是對的——當時沒有「使用者提供源碼」這個輸入。本案正是補上那個前提(把源碼送進 agent),而「不可能備齊各語言 build 環境」則靠收斂語言範圍解決(D18 只做不需要 build 環境的 A 類)。

為什麼要把這段軌跡寫進文件:設計文件上會留下「D13 排除了 agent 自跑 scanner,D17–D24 又把它做出來」這種看似矛盾的紀錄。寫明是為了讓未來的人知道這不是反覆,是前提變了——輸入從「沒有源碼」變成「使用者會提供源碼」,被排除的理由就不再成立。

兩種模式的差異(=本案工作量所在)

面向 拉取快照(FR-058.5,已上線 DEV) 主動掃描(本案 FR-058.6)
誰發動掃描 客戶自己的 CI 流程 / 開發者機器 平台(agent 執行 sonar-scanner)
源碼在哪 平台從來沒碰過源碼 必須送到 agent(本案=Git clone)
報告新鮮度 取決於客戶上次掃描的時間,可能是三個月前 就是現在——稽核當下的源碼狀態
對客戶 SonarQube 的動作 唯讀(只呼叫查詢類 API) 寫入——推分析結果,專案不存在時自動建立
agent 需要什麼 只要連得到 SonarQube 的 HTTP sonar-scanner 執行檔、源碼落地空間、記憶體
執行時間量級 秒級(幾個 API 呼叫) 分鐘到數十分鐘——掃描 + server 端排隊處理
適用客戶 已有成熟 CI、SonarQube 天天在跑,只要把結果收進證據池 沒有 CI 整合、或稽核當下才要看某個特定版本的源碼

兩模式並存,拉取模式不移除(D17)。理由:① 對「客戶已有成熟 CI」的情境,拉取模式是正確且更輕量的答案,主動掃描反而多餘(會在他們的 server 上多建一個專案、多跑一次分析);② 成本已付掉(connector / seed / setup_guide 都已完成並上 DEV);③ C# / VB.NET 這類技術上做不了主動掃描的語言,拉取模式是唯一的路。

一個容易誤會的地方:兩種模式都需要客戶有 SonarQube server——因為 sonar-scanner 沒有 dry-run,掃完必須把結果推到 server 才讀得到。差別只在「誰發動掃描」,不在「要不要有 server」。

相同的部分(可直接複用):都連客戶自架的 SonarQube、憑證都是 base_url + token、最後都組成自包含 HTML 進證據池、取消 / timeout 的行為標準一致。

端到端流程

使用者建任務:選 SonarQube、scan_mode=原始碼掃描(預設)、填 Git repo URL + 專案代碼後綴
  → BE 派工(既有鏈路,本案無敏感參數:公開 repo 不需憑證)
  → agent 領回 task → 步驟一:git clone 到磁碟型 tmp 目錄(不可用 /dev/shm,D22)
  → 步驟二:Popen 起 sonar-scanner(輪詢式取消,stdout 走檔案)
     → scanner 分析(分鐘~數十分鐘)→ 推送結果到客戶 SonarQube(寫入!專案不存在會自動建立)
     → scanner 產出 report-task.txt
  → 步驟三:解析 report-task.txt 取 ceTaskId → 輪詢 GET /api/ce/task?id=<id> 直到 status 脫離 IN_PROGRESS
  → 步驟四:拉 quality gate / measures / issues / hotspots → render 自包含 HTML(複用 FR-058.5 既有程式碼)
  → 報告 bytes 走既有 blob 通道上傳 → 進證據池
  → finally:刪除 tmp 目錄(掃完即刪,D22)

步驟一到三是全新的;步驟四可直接複用已上線的拉取模式程式碼。

流程上兩個最容易被忽略的坑(官方文件明確,非推論)

坑一:沒有 dry-run。 sonar-scanner 沒有「只分析不推送」的模式——每一次掃描都是對客戶 SonarQube 的寫入,且專案不存在時會自動建立(provision),等於污染客戶的專案清單。管理員想擋,只能移除 token 擁有者的 "Create Projects" 全域權限,沒有專門的開關。D21 的 Guidant-AI- 前綴解決「撞號」與「無法辨識來源」,但「每掃一次多一筆專案」本身沒有解。

坑二:掃完不等於能讀。 scanner 印 EXECUTION SUCCESS 只代表上傳完成,server 端是非同步序列佇列。必須解析 scanner 產出的 report-task.txt 取得 ceTaskId,再輪詢 GET /api/ce/task?id=<ceTaskId> 直到 task.status 脫離 IN_PROGRESS 才能拉結果。如果掃完就直接去拉,很可能拉到上一次的舊資料——而且拉得到、不會報錯。

⚠️ 補充陷阱:CE 介面顯示的「處理時間」不含排隊時間,不能拿它當 timeout 的依據。實際等待時間取決於客戶 server 當下的佇列長度,這是我們控制不了的變數。逾時錯誤訊息建議能區分兩段——「掃描超過 N 分鐘未完成」與「掃描已完成但貴公司 SonarQube 佇列超過 N 分鐘未消化」是兩種完全不同的客戶端問題,混在一起客戶不知道要查什麼。

租戶層設定(config_field_schema)— 一個字都不用改

FR-058.5 已 seed 的兩欄(base_url text required / token password secret required)兩模式完全共用。原因見 D17 更正說明:squ_ User Token 推得上去也讀得回來(只有 sqp_ / sqa_ analysis token 是只能推不能讀),且 D19 定案只支援公開 repo → 沒有 Git 憑證這回事。

這代表本案不需要動 config_field_schema,也不需要 FR-058.0 的任務層敏感參數能力(公開 repo 無憑證可加密)。

任務層參數(param_schema v2)

key 型別 顯示條件 說明
scan_mode select・required・**預設 scan 永遠顯示 兩個選項:pull(讀取既有結果)/ scan(原始碼掃描)。預設 scan**(D24),配套見下
project_key text conditionpull 既有欄位,語意不變
branch text・選填 conditionpull 既有欄位。付費版才支援多分支(D14)
Git repo URL text・required conditionscan 新增。公開 repo,不需憑證(D19)
專案代碼後綴 text・required conditionscan 新增。實際推上去的是 Guidant-AI-<此欄>(D21)

形狀直接照 ZAP seed 的既有寫法({"key":"...","condition":{"field":"scan_mode","value":"..."}}),FE 走現成 visible computed,不可見欄位跳過必填驗證——因此 pull 模式不會被 scan 的必填欄位誤擋,反之亦然。

setup_guide 必須改寫(D24 配套②③):現行 setup_guide 開頭寫的是「本平台不會觸發程式碼掃描」——加入 scan 模式後這句話變成錯的,必須改寫為分模式說明,並明講「預設為原始碼掃描模式,執行時會在貴公司的 SonarQube 建立 / 更新專案(key 為 Guidant-AI-<識別>)」+ 支援語言清單(D18 配套①)+ 收緊模式做法(預先建好專案 + 移除 "Create Projects" 權限)。

connector 實作要點(evidence-agent/core/task_executor_connectors/sonarqube.py

模式分流:在 run() 入口依 scan_mode 分兩條路。pull 分支=現況原封不動,scan 分支新增。

⚠️ 模式值非法必須明確 raise,絕不靜默降級。 直接沿用 zap.py:164-171 _resolve_scan_mode() 的既有立場——那段註解的理由是:使用者選了「主動」卻因拼錯被降級成「被動」,會拿到一份看起來正常但實際沒掃的報告,那比直接失敗更糟。本案同理且更嚴重:選了 scan 卻退回 pull,使用者會拿到舊的快照卻以為是剛掃出來的,而稽核報告上會標示分析時間——一份「時間是三個月前但使用者以為是今天」的證據,是會誤導稽核結論的。

子行程執行:直接照抄 inspec.py 的既有骨架,兩個設計要點必須一起帶走

  • :685-722 _run_with_cancelPopen + 輪詢)——不用 subprocess.run(timeout),因為它無法中途取消;
  • stdout 走檔案不走 PIPE——大輸出會在 OS 管線緩衝區填滿時死鎖,scanner 輸出正是大輸出
  • :657-683 _terminate(SIGTERM → 寬限 → SIGKILL)處理取消與逾時。

暫存與清理(D22):clone 目錄開在容器自身 /tmp(磁碟型),不可用 /dev/shm(tmpfs 記憶體型,既有 inspec.py:197-200 慣例是為小憑證檔設計的)。清理走 finally / contextmanager,掃完即刪——含失敗與取消路徑。

輪詢 CE 佇列:解析 scanner 產出的 report-task.txtceTaskId → 輪詢 GET /api/ce/task?id=<id> 直到 task.status 脫離 IN_PROGRESS;逾時訊息區分「掃描未完成」與「佇列未消化」兩段(見坑二)。

結果拉取與報告完全複用 FR-058.5 既有的 _fetch_*_render_report——這是兩模式共用的部分,不另寫一套。

FE 接入點

檔案 改什麼 為什麼
JobExecutionDrawer.vue:308 _PRIMARY_PARAM_KEYS = ['profile', 'project_key'] 硬編需一併調整 scan 模式下 project_key 是使用者填的後綴、不是最能辨識這筆任務掃了什麼的欄位——Git repo URL 才是。若不處理,執行紀錄的主要區會顯示一個沒什麼資訊量的後綴,真正的識別(掃了哪個 repo)反而被收進摺疊的技術細節區。該檔既有註解已說明分區原則是「執行前需要知道的參數 vs 技術細節」,Git repo URL 明確屬於前者
scan 模式警語 D24 配套① 預設值指向不可回復的一邊,事前告知從「最好有」升級為「必須有」。hint 文字不夠(使用者會略過),具體形式列實作建議

scan_mode 的 select 與條件欄位本身走現成 schema 驅動渲染,零新元件

後續強化(本階段刻意不做,全數為裁示排除非遺漏)

項目 為什麼這階段不做 日後撿起來時的已知結論
檔案上傳取源碼 要動三層(FE 元件 / upload 中繼 / 派工鏈路),雛形先求打通。→ 2026-08-01 更新:已由 FR-058.7 立案並設計定案(D25–D31,§4.8),本列保留為歷史紀錄 雲端→agent 通道已存在且成熟infra/upload_file/remote_agent_adapter.py:54-102save_file()(SHA-256 雙邊對帳 + mTLS/JWT),agent 端接收在 evidence-agent/api/blob/routes/blob_route.py:44-66。兩個坑:① param_schema 沒有 file 型別且未知型別靜默不渲染;② RemoteAgentAdapter 只在 storage_type=REMOTE_AGENT 的租戶存在,MINIO 租戶建不出來。
FR-058.7 落地時的走法與此欄當年預估不同:不做 file 型 schema 欄位(改為執行時輸入,D26)、不走 BE 推檔給 agent 的 /blob 通道(改為 agent 主動 mTLS 拉取,D25)
私有 repo 認證 公開 repo 已足以驗證整條鏈路,且平台現況本來就只支援公開 repo。→ 2026-08-01 註記:FR-058.7 上傳模式已部分涵蓋此情境(客戶把私有源碼打包上傳即可,不用把 git 憑證交給平台);真正的 git 憑證管理仍列後續強化 三個選項:token 內嵌 URL(會進 argv、可能落 log)、credential helper 檔案化(建議走這個)——inspec.py:44-49 已有一模一樣的前例(憑證寫進設定檔、命令列只帶引用、密碼不進 argv)、SSH deploy key(安全性佳但要處理 known_hosts
**Java(B 類) 需要客戶另外交出編譯產出,打包規則要有真實客戶樣本才定得準 sonar.java.binaries required,值必須是目錄**、war/jar 不能直接餵、不支援萬用字元;libraries 選填支援 **/*.jar。兩種失敗模式:全缺 → 直接失敗;部分缺 → 繼續跑但降級"Class 'XXX' is not accessible through the ClassLoader")。【推論待驗】 war 解開後 WEB-INF/classes→binaries、WEB-INF/lib/*.jar→libraries
**C / C++ 技術上可行,但雛形階段收斂語言範圍 AutoConfig 是 scanner 預設模式(官方 "enabled by default"),agent 端無分歧邏輯,複雜度只在使用者說明。代價是官方明說的「in rare instances, may result in some issues being overlooked」。Objective-C 不支援 AutoConfig**,不能一起納入
Scala 官方文件完全沒表態是否需要 bytecode 傾向 A 類但必須實測——不可憑推論寫進使用者說明
C# / VB.NET 技術上真的做不到(不是取捨,是不可行) SonarScanner CLI 明文 "unsupported";begin/build/end 是硬性架構,分析發生在編譯器內部(Roslyn analyzer 隨編譯執行),sonar.sources 對 .NET scanner 也 "not supported"。預先編譯好的 DLL 無法補做分析。永遠只能走 pull 模式
磁碟 / 記憶體控管 決策者裁示只做逾時(D22) agent 容器目前完全沒有資源限制(兩份 compose 皆無 mem_limit / cpus / ulimits,無 k8s manifest)。已知 OOM 熱點:JS/TS 與 Kotlin analyzerSONAR_SCANNER_JAVA_OPTS 可調。詳見下方 R2
coverage 紅燈處理 決策者裁示不處理(D23,「主要掃品質就好」) SonarQube 自己不產生 coverage,只匯入第三方報告;我們不跑測試 → 報告必然無 coverage。Go 特例:無 coverage 資訊時視為「全部未覆蓋」而非「無資料」。詳見下方 R3
語言版本參數 雛形階段不加欄位 Python 不設 sonar.python.version 時,分析器自動靜音部分 issue 以避免誤報——從稽核角度就是漏報。日後若加,建議做成選填並在報告上標註「未指定語言版本,部分檢查已略過」

風險與已知限制

# 風險 說明與處置
R1 使用者送錯語言(縮範圍後的主要風險) Java → 硬失敗(沒有 .class,scanner 直接中止,難看但安全)。C# → 靜默淺掃(CLI 不支援但不會中止,很可能產出看起來正常、實際幾乎沒分析到東西的報告,稽核員可能把「沒掃到」誤讀成「很乾淨」)。配套已列為必做(見 D18 右欄三件事)
R2 資源控管缺席(已裁示本案不做) agent 容器完全沒有記憶體 / CPU 限制,而 OOM 風險真實存在——官方已知兩個熱點 JS/TS analyzer(純 JS 無 tsconfig 時全部檔案進單一 program)與 Kotlin analyzer正好都在 D18 的 A 類清單裡。一旦吃爆,影響的是整台 agent 主機而不只是這個任務。
⚠️ 資源量級:官方零基準數據。 文件唯一出現過的數字是 SONAR_SCANNER_JAVA_OPTS="-Xmx512m",而那只是語法示例不是建議值。調研中推得的「至少 2GB、JS/TS 配 4GB」是推論不是文件,不可寫進任何對外文件,必須實測。日後若要補,最小一步是在 compose 加 mem_limit——讓 OOM 只殺容器、不拖垮宿主機
R3 報告必然沒有 coverage(已裁示本案不處理) 我們不跑測試 → 無覆蓋率 → 客戶 Quality Gate 若含覆蓋率條件會亮紅燈,而那個紅燈不代表程式碼有問題。Go 更糟:無 coverage 資訊時視為「全部未覆蓋」,顯示 0%。已裁示照實顯示(D23),但實作時仍建議在 setup_guide 提一句
R4 對客戶 SonarQube 的寫入副作用 沒有 dry-run,每次掃描都是寫入,且專案不存在時自動 provision。D21 前綴解決「撞號」與「無法辨識來源」,但「每掃一次多一筆專案」沒有解。UI 必須事前告知這是寫入行為;收緊模式(預先建好專案 + 移除 "Create Projects" 全域權限)寫進 setup_guide——SonarQube 沒有專門開關,只有這個辦法
R5 等待時間有一段我們控制不了 結果進客戶 server 的佇列序列處理,必須輪詢 ce/task 直到脫離 IN_PROGRESS。排隊多久取決於客戶 server 當下負載。⚠️ CE 顯示的「處理時間」不含排隊時間,不能當 timeout 依據。 逾時訊息建議區分「掃描未完成」與「佇列未消化」兩段
R6 兩個 JS/TS 的靜默降級 node_modules → 型別推導精度降低,不報錯tsconfig extends node_modules 內的 base config 而解析不到 → 官方描述為 "unexpected analysis results"——看起來有跑,結果是錯的。建議把 scanner 的警告輸出呈現在報告上,不要只顯示 issue 清單。與 R1 同一原則:「執行降級」不能假裝成「執行完整」

落地前必須實測的項目

  1. scanner 打包後的 image 體積增量——交付走 docker save,體積直接影響客戶體驗。
  2. 逾時秒數的合理值——現在填任何數字都是猜的,官方零基準數據
  3. 各 A 類語言的實際記憶體用量——尤其 JS/TS 與 Kotlin 兩個 OOM 熱點。
  4. 端到端驗收目標:公司自有 SonarQube(192.168.50.171:9000)+ 一個公開 repo。

4.8 FR-058.7 SonarQube 上傳檔案掃描(2026-08-01 追加;scan 模式的取源擴充,依賴 FR-058.6 已落地的 scan 鏈路)

決策脈絡見 D25–D32;討論稿:discussion-fr058.7-upload-scan.html(v4,決策者已全數過稿)。

定位:scan 模式的取源擴充,不是新模式。 FR-058.6 scan 模式的管線是:取源(git clone)→ sonar-scanner 掃描 → 推上 SonarQube server → CE 等待 → 報告回收 → 品質門檻渲染。本案只把第一段的取源方式多加一種:從 tenant 儲存空間下載上傳的壓縮包並安全解壓。後面整條掃描管線全部複用,一行不動。新造的東西只有四塊:取源通道(D25)、執行時輸入的傳遞(D26)、檔案生命週期(D28 / D29)、解壓額度檢查(D30)。

需求背景:為何 scan 的 Git URL 不夠

決策者原話(2026-08-01):

還是要可以上傳檔案做檢測,有辦法是上傳到 tenant 設定的儲存空間後,由 agent 拉下來,做掃描,掃完就刪,如果硬碟不夠就跳錯誤就好

宏觀流向由這句話定調成五段:① 使用者上傳源碼壓縮包 → ② 落地在 tenant 設定的儲存空間system_configsSTORAGE_CONFIG,支援 local / minio / remote_agent 三後端——走既有機制,不另闢儲存路徑)→ ③ agent 把檔案拉下來執行掃描 → ④ 掃完就刪 → ⑤ agent 磁碟不夠直接報錯。後續討論釐清後,「掃完就刪」的適用範圍是 agent 端 workspace(tenant 儲存空間上的壓縮包保留供重掃沿用,D29 定案)。

FR-058.6 的 scan 模式取源只有公開 Git repo URL(D19 的刻意最小化取捨),但實際客戶情境裡「源碼不在公開 repo」才是常態:私有 repo(客戶不願交 git 憑證)、源碼根本不在 git(委外交付源碼包、歷史版本快照)、稽核要特定一版(客戶打包當下狀態最直接)。上傳模式一次涵蓋三種情境,並部分涵蓋原列 scope 外的「私有 repo 認證」需求(客戶打包上傳即可,完全不用把 git 憑證交給平台)。

上傳時機:任務抽屜發起執行時,不是任務設定(決策者拍板)

初版設計曾把上傳欄位放在任務設定表單(param_schema 加 file 型欄位),決策者審後指出流程不對:上傳的壓縮包是「這一次要掃的東西」——特定時點的源碼快照,不該混進設定表單。放設定表單的兩個問題:① 每掃新版都要改設定再執行(兩段式繞路,且任務設定僅專案管理者可改,執行者不見得改得動);② 語意錯位(設定表單是「這個任務怎麼掃」,源碼包該在「要執行了」的當下提供)。

修正後的分工:任務設定表單只留 scan_mode 的第三選項 upload;上傳動作發生在任務抽屜發起執行時——scan_mode=upload 的任務,執行對話框出現上傳區,首次執行必須上傳(無檔不給按執行),檔案參照隨「發起執行」請求送出。參照落點是任務綁定層的 sticky 狀態(供之後重掃沿用,D29),每筆執行的 scan_params 快照同時各存一份(承接「這筆掃的是哪個檔」的稽核回溯)。附帶兩個好處:孤兒檔視窗大幅縮小(上傳與執行是同一個動作);FE 參數表單元件完全不用動(DetectionConfigField 不需要新型別分支)。

端到端流程

任務設定已存 scan_mode=upload(設定表單裡沒有檔案欄位)
使用者在任務抽屜發起執行:
  執行對話框出現上傳區——已有 sticky 檔案時顯示「目前檔案:xxx.zip(上傳於 M/D)」可直接沿用
  → (換新版時)POST /file/upload 上傳壓縮包(驗副檔名與大小上限)
     → upload_files_for_tenant() 存入 tenant 儲存空間 → 回 file uid
  → POST /detection-tools/jobs/<uid>/execute(body 帶 source_file.uid;沿用舊檔則不帶 body)
  → BE start_execution:帶新 uid 則覆寫綁定的 sticky 參照、不帶則沿用、首次無檔 400
     從 upload_files 權威取 file_name / size / sha256(不信 FE,D31)
     寫執行紀錄(scan_params 快照含 source_file: {uid, file_name})
     + agent_tasks(params 夾 _source_file 完整參照,含 size / sha256 / limits)
  → agent 心跳(既有 mTLS 通道)領回任務 payload(含 source_file 參照)
  → agent 取源段(本案新造):
     步驟一:shutil.disk_usage 磁碟檢查——不足直接 fail「agent 磁碟空間不足」(D28)
     步驟二:GET /api/1.0/agents/files/<uid>(mTLS 串流下載;BE 依 storage_type 讀出、
             minio / remote_agent 後端串流轉發不落地)
     步驟三:sha256 對帳(防竄改)
     步驟四:標準庫安全解壓進 scan workspace(zipfile / tarfile filter="data";
             累計大小 / 檔數超過 D27 限額即中止清理,D30)
  → 以下全複用 FR-058.6 scan 管線:sonar-scanner 掃描(_run_with_cancel)
     → 推客戶 SonarQube → 輪詢 CE → 拉 issues / 品質門檻 → 組自包含 HTML 報告
  → 報告上傳進證據池 → 回報 result(既有 mTLS 通道)
  → finally:刪 agent workspace(既有機制+五路徑測試鎖約)
  → tenant 儲存上的壓縮包保留供重掃沿用(D29 定案,不做終態刪檔;清理機制列 follow-up)
  → FE 執行紀錄顯示檔名(非裸 UUID,D31)

流程上兩個關鍵設計點:① 磁碟檢查在下載之前——不是解壓失敗才發現,呼應本 arc 教訓「錯誤要在最早能發現的地方被發現」;錯誤分類為 agent 環境問題,不是掃描失敗。② BE relay 不落地——tenant 儲存為 minio / remote_agent 時,BE 用串流轉發,遵守 FR-039 不變式「雲端任何路徑不得把 binary 寫進雲端磁碟」。

FR-039 三條不變式對照(本設計不破壞任何一條):agent 不連雲端 DB(檔案參照由心跳 payload 下發,agent 只打 BE 的 HTTP 端點);雲端不落地 binary(串流轉發);只有 agent 對外連出、無對內連入(下載是 agent 主動發起的出站請求,方向與 heartbeat / result 一致)。

任務層參數(param_schema v3)

版本策略照既有規範:INSERT 新 version=3 + v2 is_current=FALSE,不原地 UPDATE。v3 的變更縮小到只有 scan_mode 一欄——不新增型別、不新增欄位,source_file 不進 param_schema(它是執行時輸入)。

key 型別 顯示條件 說明
profile select (同 v2) 沿用 v2 不變
scan_mode select・required 永遠顯示 唯一變更:選項由 pull / scan 增為 pull / scan / upload(D26),附 hint「上傳模式於發起執行時提供源碼壓縮包」。預設值沿用 D24 裁示
server_url、token 等連線欄位 (同 v2) (同 v2) 沿用 v2 不變(secret 欄位加密 envelope 機制照舊)
repo_url text・required conditionscan 沿用 v2;condition 維持只在 scan 出現——upload 模式下不出現、不必填
project_key_suffix text・選填 scan 與 upload 皆出現 沿用(D26)。既有 condition 是單層等值比對,無 OR——實作上可能要出兩條同 key 的 condition 欄位或擴充 condition 語法,實作時定案,屬 T-7.1 細節

執行時輸入與 params 形狀(T-7.1 / T-7.2 並行前必須凍結的 key 契約)

source_file 不進 param_schema(設定表單沒有檔案欄位),但參照落點在任務綁定層的 tool_params(sticky 狀態,供重掃沿用,D29):execute body 帶新 uid 時覆寫、不帶時沿用現值。每筆執行的 scan_params 快照另存一份完整資訊(含檔名,稽核回溯用)。

// 任務綁定 tool_params(schema 欄位 + sticky 檔案參照;後者不是 schema 欄位、FE 設定表單看不到)
{
  "profile": "...",
  "scan_mode": "upload",
  "project_key_suffix": "release-1.2",
  "_source_file": { "uid": "<file-uid>" }   // sticky:execute 帶新 uid 覆寫、不帶沿用
}

// 每筆執行的 scan_params 快照(每筆各存一份;file_name 等由 BE 從 upload_files 權威取,D31)
{
  "...任務設定各欄...",
  "source_file": { "uid": "<file-uid>", "file_name": "my-project-src.zip" }
}

// compliance.agent_tasks.params(心跳下發給 agent 的 payload,BE 注入內部 key)
{
  "...同上...",
  "_task_uid": "...",               // 既有夾帶慣例
  "_credentials": { ... },          // 既有:心跳時當場解密注入
  "_source_file": {                 // 新:agent 取源所需的完整參照
    "uid": "<file-uid>",
    "file_name": "my-project-src.zip",
    "size_bytes": 52428800,
    "sha256": "<上傳時算好的摘要>",   // agent 下載後對帳防竄改
    "limits": { ... }               // D27:BE 設定的限額隨 payload 下發(與 credentials 下發同構)
  }
}

註:_ 前綴 key 回 FE 時由 _resolve_scan_params()detection_orchestration_service.py:385-400)剝除——既有機制自動涵蓋 _source_file,FE 只看得到 source_file{uid, file_name}:執行紀錄顯示檔名、不顯示裸 UUID(D31)。

端點契約草案

A. 執行端點改造(既有端點加 request body)

項目 內容
路徑 POST /detection-tools/jobs/<uid>/execute(既有)——目前不收 body,純快照綁定任務設定
改造 改為可帶當次輸入:{"source_file": {"uid": "..."}}——FE 只送 uid,檔名不由 FE 送(D31)。scan_mode=upload 時:帶 uid → 驗證後覆寫綁定的 sticky 參照;不帶 → 沿用綁定現值;綁定也沒有(首次)→ 400「請先上傳源碼包」。BE 驗證:uid 存在且屬本 tenant(RLS)、副檔名在允許清單(.zip / .tar / .tar.gz / .tgz,D27)、大小在 D27 限額內;通過後從 upload_files權威取回 file_name / size / sha256,寫進 scan_params 快照與 agent_tasks.params_source_file
相容性 不帶 body 的既有呼叫(pull / scan 模式)行為完全不變

B. 新端點:agent 檔案下載

項目 內容
路徑 GET /api/1.0/agents/files/<uid>(命名比照既有 agent 控制面端點)
認證 純 mTLS、不掛 jwt——與 tasks ack / result 同形狀(api/remote_agent/__init__.py:34-38 先例)
授權(綁定驗證) 由 client 憑證識別 agent 身分後,驗證該 uid 必須屬於該 agent 名下 pending / running 任務的 source_file——防任意檔案讀取。任務終態後參照即失效
回應 串流回傳壓縮包 binary(Content-Length + 建議帶 X-Checksum-Sha256 header);BE 對 minio / remote_agent 後端串流轉發不落地
錯誤 uid 不存在或不屬於該 agent 任務 → 404(不區分兩種情況,避免探測);檔案已被清理刪除 → 404。error code 依規範新增 GRC_404xxx 序號

connector 實作要點(agent 端取源段)

  • 磁碟檢查(D28):_scan_workspace() 建目錄前 shutil.disk_usage("/tmp"),free < (壓縮包大小 × 12 + 1GB)即 fail,係數與底值吃 payload 下發的 limits。
  • 下載 client:比照 core/task_executor.py:148-153 _post() 的 httpx mTLS client 形狀加 _get_stream(),串流寫進 workspace,下載後 sha256 對帳。
  • 安全解壓(D30):zip 走 zipfile(extractall 內建路徑消毒)、tar 系走 tarfile filter="data"(Python 3.11.4+,agent 基底 python:3.11-slim 足夠);兩家族共用約二十行額度檢查層(解壓前加總宣告總大小與檔數、超過 D27 限額即拒絕;逐檔解時看 cancel_event)。
  • 取源之後:接進既有 scan 管線(取代 _clone_repo() 分支),_run_scanner() 起整段原樣沿用;workspace 清理走既有 finally: shutil.rmtree(五路徑測試鎖約,test/test_sonarqube_connector.py:1132-1180)。

自動派發與 upload 模式(D33,2026-08-01 驗收發現後追加)

專案規劃頁「開始執行任務」在發佈時會對 detection_tool 任務自動派第一次掃描(FR-056.5 #5:TaskExecutionService.start_task_execution_auto_dispatch_detection_scansapp/grc/service/task_execution_service.py:90-133,冪等條件=該任務查無任何 detection_executions 紀錄)。自動路徑不帶檔案,對 scan_mode=upload 的任務必然打中「首次無檔 400」(DETECTION_TOOLS_400005),且被批次派發的 try/except 吞掉只留 warning log——形成 PM 看到成功 toast、執行者無感、且每按一次就再失敗一次的靜默失敗迴圈。D33 定案:upload 任務在挑選階段就排除——get_detection_jobs_without_executioninfra/grc/repository/task_execution_query.py:134-159)的查詢加上排除 tool_params->>'scan_mode'='upload' 綁定的條件,upload 任務一律由執行者在任務抽屜上傳檔案後手動執行(抽屜已有完整前置擋門),pull / scan 自動派發不變。FE 同步在批次發佈 confirm 文案補一句提示。實作為 T-7.5(§5.12)。

掃描完成通知(D34,2026-08-01 驗收期間提出後追加)

現況兩缺陷:① 成功通知內容過簡——只有「專案 X 的檢測工具掃描已完成」一句,且只發 Email(平台其他通知已是 Email/Discord/Telegram 三管標準);② 失敗完全不通知——on_scan_failed 只寫 detection_executions,使用者要自己去任務抽屜看才知道掃描失敗。

九項資料鏈:通知內容擴為九項——專案(升級走 round 鏈取代現行 ORDER BY id LIMIT 1)、控制項群組、控制項、AO、任務名稱、工具名稱、掃描設定、時間(started/finished)、成功或失敗;另把 handler 現成卻沒用的 evidence 參數用起來(報告檔名/份數,零額外查詢)。掃描設定複用 _resolve_scan_params() 現成兩層剝除(_ 前綴內部欄位+secret envelope),profile UUID 轉譯成名稱。新查詢走 8 層 join 鏈(job→workflow_execution_control_mapping→round→project→extension→ssp→profile_imports→catalog_controls/groups/parts),加在 infra/detection_tools/repository/detection_job_notify_query.py(天然擴充點)新增 get_notify_context(job_uid) 一次 SQL 取齊。失敗路徑:on_scan_failed 補查 job/binding(抄 on_scan_succeeded :472-475 pattern)後發通知,含 error_message,收件人與成功相同(指派人+專案 manager+reviewer)。hosts(目標主機 IP)照出(D34 ③)。文案準備 HTML(email 表格)+純文字(Discord/Telegram)兩版,i18n 6 個 msgid(兩語系 messages.po+pybabel compile)。

兩坑(探查已實證,實作必守):① catalog_controls.control_id 跨 catalog 不唯一(DEV 實測同 control_id 41 筆)——join 時必須帶 catalog_id = profile_imports.source_catalog_id 過濾,否則會撈到別的 catalog 的同名控制項;② AO 的 title 對 CMMC 全 NULL——用 title or prose or part_id fallback(job_export_query.py:145 前例)。

範圍外(本段不做,另案):站內鈴鐺通知——平台無 notifications 表,屬平台級新能力;收件人 locale 切換——通知在 agent request context 發、Accept-Language 是 agent 的,一律 zh_Hant_TW(既有限制,非本案引入)。

無 schema migration、無 DI 變更(_profile_domain_tool_domain_notify_query 全已注入)。實作為 T-7.6(§5.12)。

known limitations 與 scope 外(v1 接受,出貨時要知道)

限制 說明
remote_agent 同台繞圈 tenant 儲存為 remote_agent 且掃描 agent 為同一台時,檔案走 agent→BE→agent 一圈(D25 附註)。罕見組態,正確性不受影響,只是低效
檔案只增不減(v1 無清理機制) D29 定案保留沿用+歷史不刪,孤兒檔(原 D32)與替換下來的歷史版本都留在 tenant 儲存空間,v1 統一不清理、列 follow-up。未來清理機制必須以「綁定仍參照中的檔案」為保留名單、不能只看檔案年齡(D29 ④)
上傳無進度條 BaseService.post 不支援 onUploadProgress;100MB 內先接受轉圈等待,列 follow-up
語言限制同 scan 模式 C# / VB.NET 永遠只能走 pull(MSBuild 架構限制);Java 需 .class 檔的限制照舊——上傳的壓縮包是純源碼時 Java 專案掃不出 bytecode 相關規則

scope 外清單(本案不做,有需求另立案):7z / rar 等其他格式(已收 .zip / .tar / .tar.gz / .tgz,再擴充等實際需求出現);私有 repo 認證(上傳模式已部分涵蓋,真正的 git 憑證管理仍列 §4.7 後續強化);上傳檔案掃毒 / 內容檢查(源碼包長存於 tenant 設定的儲存空間;平台任何環節都不執行壓縮包內容——agent 只解壓後交給 sonar-scanner 做靜態分析;BE relay 串流不落 BE 自身磁碟。防毒屬防毒層議題,有需求另議);檔案清理機制(D29 ④ follow-up);agent 容器資源限制(部署面議題,本案只做應用層磁碟檢查);多檔上傳 / 資料夾上傳(一次任務一個壓縮包,多模組專案由客戶自行打成一包)。


5. 拆分(1 橫向項 + 8 子需求;FR-058.5 / FR-058.6 為 2026-07-31 追加,含 T-5.1〜T-5.3 與 T-6.1〜T-6.3;FR-058.7 為 2026-08-01 追加,含 T-7.1〜T-7.6,見 §5.12)

5.1 依賴關係與排程

兩條真依賴(不可並行)

  • FR-058.0 任務層敏感參數FR-058.1 ZAP:ZAP 要收「被掃網站的登入帳密」屬任務層敏感資料。.0 未完成,ZAP 登入後掃描只能把帳密明文存進 tool_params,等於白做一次要重改。
  • FR-058.2 InSpec/CINC connectorFR-058.3 GCB:GCB 不另立引擎,跑的就是 .2 的 connector。.2 沒有,.3 沒東西可跑。
  • FR-058.5 SonarQube 拉取FR-058.6 SonarQube 主動掃描:同一個工具的兩種模式(D17),.6 的 seed 是在 .5param_schema 上加 scan_mode 與條件欄位、connector 是在 .5 的檔案內加分支,且步驟四(拉結果組報告)直接複用 .5 的程式碼.5 沒有,.6 無處可加。

可同時跑的線(原三條;2026-07-31 追加線 D 後為四條)

橫向 FR-058.X:_TOOL_ID_TO_CODE 解耦(建議最先單獨做完)
     ↓
線 A:FR-058.0 敏感參數 → FR-058.1 ZAP
線 B:FR-058.2 InSpec/CINC → FR-058.3 GCB
線 C:FR-058.4 Nmap(獨立)
線 D:FR-058.5 SonarQube 拉取(T-5.1 → T-5.2 → T-5.3)
       → FR-058.6 SonarQube 主動掃描(T-6.1 → T-6.2 → T-6.3)
       → FR-058.7 SonarQube 上傳檔案掃描(T-7.1〜T-7.4,2026-08-01 追加;
         T-7.1 BE 與 T-7.2 agent 可並行,但並行前必須先凍結欄位 key 契約,見 §5.12)

線 D 的 UI 驗收合併(2026-07-31 裁示):FR-058.5 的三支子任務程式碼皆已完成並 commit,但端到端 UI 驗收尚未做(DEV detection_executions 內 sonarqube 仍 0 筆,只有 probe 通過與容器內手動驗證)。決策者裁示「那版基本不能用了,先不測試,要等這版」——因此 T-5.3 的 UI 驗收併入 T-6.3 一起做,一輪走完 pull 與 scan 兩模式(詳見 §5.9 與 §6 第 11 項)。→ 2026-08-01 已完成:pull 與 scan 兩模式端到端通過(detection_executions sonarqube 2 筆皆 succeeded,171 上長出 Guidant-AI-soybean-ui 專案),本段保留為歷史紀錄。

兩個實務但書(排程建議,不是純技術描述)

  1. 橫向的 ID 解耦建議最先單獨做完,再開三條線。 三條線都會動到 agent 這個共用 repo:各自新增一支 connector 檔案彼此不衝突,但 _TOOL_ID_TO_CODE 那個 factory 檔三條線都要改,同時開工會反覆衝突。ID 解耦工作量很小(改一個 factory + 派工 payload 加一個欄位),先做完三條線就不會撞同一個檔。
  2. 線 A 與線 B 的第一棒技能需求不重疊.0 是 BE + FE 的加解密與欄位渲染,.2 是 agent 端的 WinRM 連線與 profile 執行,適合分給不同 session 平行跑。

5.2 FR-058.X 橫向前置:_TOOL_ID_TO_CODE 解耦 — 依賴:無(所有工具的共同前置)

# 子任務 驗收 依賴 Repo
T-X.1 BE:_collect_pending_tasks() 派工 payload 加 detection_tool_code 欄位 心跳回應含 code 欄位;既有 openvas / openscap 派工不受影響 BE
T-X.2 agent:get_connector() 改依 code 取 connector,detection_tool_id 降為 fallback(含 warning log),移除硬編 id 表 新舊 payload 都能正確取到 connector;openvas / openscap 既有派工迴歸綠 T-X.1 evidence-agent

5.3 FR-058.0 任務層敏感參數(平台前置) — 依賴:無(可與 .2 / .4 並行)

# 子任務 驗收 依賴 Repo
T-0.1 BE:param_schema 支援 secret: true 語意;寫入 tool_params 前對 secret key Fernet 加密(複用既有加密鏈,依任務綁定的 schema 版本判定) 帶 secret 欄位的任務參數落庫為密文;非 secret 欄位維持明文;schema 改版後舊任務仍解得開 BE
T-0.2 BE:派工組裝時解密 secret key(與租戶層 credentials 同一時機下發) agent 收到的 pending_tasks 內敏感參數為明文;DB 內仍為密文 T-0.1 BE
T-0.3 BE:讀取供 FE 顯示與稽核 log 寫入時,secret key 一律剝除(整個 key 不出現,非遮罩) 執行紀錄 API response 無敏感 key;system_logs 事件內容無明文敏感值 T-0.1 BE
T-0.4 FE:secret 欄位渲染(走現成 password 型態)+ 執行紀錄顯示驗證(確認 _PRIMARY_PARAM_KEYS 等硬編邏輯不會因 key 缺席出錯) 任務參數表單可填敏感欄位;執行紀錄畫面不顯示敏感值且不出現異常空白/原始 key T-0.3 FE

5.4 FR-058.1 ZAP — 依賴:FR-058.0(+ 建議 FR-058.X 先完成)

# 子任務 驗收 依賴 Repo
T-1.1 migration:seed ZAP(connection_type='API' + config_field_schema:服務位址 / API key)+ param_schema(§4.2 六欄,含 login_passwordsecret: truescan_mode 預設被動)+ setup_guide(daemon 啟動與 API key 設定);三環境套用 設定頁清單出現 ZAP(available);租戶可完成設定並測試連線;任務可選 ZAP 並依 scan_mode 條件顯示登入四欄 T-0.1 BE
T-1.2 agent:zap.py 骨架 + probe()(只驗連得到 daemon 且 API key 正確,不建立掃描資源)+ zaproxy 0.6.0 相依 測試連線在「連不到 / API key 錯 / 正常」三情境回清楚訊息 T-1.1、T-X.2 evidence-agent
T-1.3 agent:run() 被動與主動模式 + 雙格式取報告(core.htmlreport HTML 上傳 / core.jsonreport 僅記憶體解析 summary)+ cancel_event + timeout 上限;輪詢兩種回傳型態正確處理(ajaxSpider.status 回字串、ascan.status 回百分比) 手塞派工掃真實目標 → HTML 進證據池(可下載)、summary 有數字、取消可即時中止、逾時不無限等待 T-1.2 evidence-agent
T-1.4 agent:run() 登入後主動模式(context + 登入認證 + logged_in_indicator),帳密取自任務層敏感參數 對需登入的測試站台掃描,報告內容顯示已進入登入後頁面;帳密全程不落地、不出現在 log T-1.3、T-0.2 evidence-agent

5.5 FR-058.2 InSpec / CINC Auditor — 依賴:無(可與 .0 並行;建議 FR-058.X 先完成)

# 子任務 驗收 依賴 Repo
T-2.1 migration:seed InSpec/CINC(connection_type 涵蓋 SSH 與 WinRM)+ config_field_schema(雙 transport 憑證,含條件顯示)+ param_schema + setup_guide(含「必須用 CINC Auditor 而非官方 InSpec」的授權說明與目標主機 WinRM 啟用前提);三環境套用 設定頁出現 InSpec/CINC(available);雙 transport 憑證可分別設定並落庫為密文 BE
T-2.2 agent:Dockerfile 加入 CINC Auditor + inspec.py 骨架(雙 transport 連線參數解析)+ probe()(連線 / 認證 / CINC 可執行三段檢查與錯誤分類) image build 過;測試連線在 SSH 與 WinRM 兩側各自回清楚訊息 T-2.1、T-X.2 evidence-agent
T-2.3 agent:run() 執行 profile + --reporter json 統一解析 + 逐台多檔證據 + host_failures 逐台失敗回報 + cancel_event / timeout 手塞派工掃 Linux(SSH)與 Windows(WinRM)各一台 → 每台一份報告進證據池;其中一台失敗時另一台仍成功且失敗被明確回報 T-2.2 evidence-agent
T-2.4 結案 Notion CM-953(本 FR 吸收,於此子任務完成時標記) CM-953 狀態更新並註明由 FR-058.2 落地 T-2.3 —(Notion)

5.6 FR-058.3 GCB — 依賴:FR-058.2

驗收條件是「鏈路可運作」,不是「涵蓋多少規則」(D7 明確定案)。這不是 content 工程。

# 子任務 驗收 依賴 Repo
T-3.1 migration:seed GCB 條目(引擎指向 .2 的 CINC Auditor,Windows 與 Linux 共用)+ param_schema(含 content 來源指定欄位)+ setup_guide;三環境套用。連帶:agent factory 加 code='gcb'InspecConnector 映射(GCB 不另立 connector,D7);一併把 InSpec 的 param_schema.profile 改為 select_or_text 設定頁出現 GCB;任務可選 GCB 並指定目標與 content T-2.1 BE(+ evidence-agent factory 一行)
T-3.2 (2026-07-31 縮成「驗證 + 寫文件」,不寫 code) 原規劃的 content 外部載入接口裁示不做——CINC 原生支援三種來源、params.profile 直接進 argv,能力本來就在,再包一層只是把上游能力重新命名一次。改為:確認 D7 三條約束皆已滿足並留下實跑佐證,一併記錄兩個已知限制(封閉網路無法送檔進 container、私有 repo 無憑證路徑)。架構前提:content 由 agent 本機讀取後交給 cinc-auditor,不送到目標主機(見 §4.3 架構事實) 更換 content 不需重建 agent image、不需改 connector 程式碼(已驗證,見 §4.4) T-2.3 —(文件)
T-3.3 一份 Windows 最小 Demo profile(取最單純的一個基準項目,且必須落在 HKLM\Software\Policies 正規政策區以避開 tattoo settings 陷阱) 端到端鏈路跑通:WinRM 連線 → 認證 → profile 執行 → 結果解析 → 證據上傳,證據進證據池且 summary 有數字。不追求規則覆蓋量 T-3.2 evidence-agent

5.7 FR-058.4 Nmap — 依賴:無(完全獨立;建議 FR-058.X 先完成)

# 子任務 驗收 依賴 Repo
T-4.1 BE + migration:detection_tools 加「是否需要憑證」宣告欄位;start_execution() 放行零憑證工具(D9);三環境套用。(2026-07-30 改案後 Nmap 不再是此能力的使用者,但這一棒仍要做——它是平台級能力) 零憑證工具可正常開始執行,不再誤回 DETECTION_TOOL_CONFIG_NOT_FOUND;需憑證的既有工具檢查行為不變 BE
T-4.2 migration:seed Nmap(connection_type='SSH'requires_credentials=TRUE——2026-07-30 改案,原定 CLI + 零憑證)+ config_field_schema(SSH 憑證,對照 OpenSCAP)+ param_schema(掃描目標 / 掃描類型 / 埠範圍)+ setup_guide客戶在自己指定的主機安裝 nmap、NPSL 授權說明、SSH 連線前提、埠掃描授權警語);三環境套用 設定頁出現 Nmap(available) 並可設定 SSH 憑證;任務可選 Nmap 並填參數發佈 T-4.1 BE
T-4.3 agent:nmap.pySSH 登入執行主機nmap -oX → XML 解析 summary + 報告上傳,逐台 host_failures 回報)+ probe()(SSH 連線 / 認證 / 該主機 nmap 存在可執行 / 權限四段分類)+ cancel_event / timeout;不 bundle 進 image主要對照 openscap.py(SSH 型)而非本機子行程模式 手塞派工掃真實網段 → 報告進證據池、summary 有開放埠數;執行主機未安裝 nmap 時 probe 回明確訊息 T-4.2、T-X.2 evidence-agent

5.8 FR-058.5 SonarQube — 依賴:無(獨立;2026-07-31 追加)

詳細設計見 §4.6;決策見 D13–D16。pull-snapshot 型,不觸發掃描。

# 子任務 驗收 依賴 Repo
T-5.1 BE seed:UPDATE 既有 id=3 sonarqube 佔位列config_field_schema 兩欄 + status → available + description 去尾句「此工具尚在規劃中…」+ setup_guide)+ INSERT param_schema v1(project_key 必填 / branch 選填)——一支 SQL migration。注意不是 INSERT 新工具,照 ZAP 範本的 ON CONFLICT DO NOTHING 會被既有列擋下靜默跳過(§4.6 seed 特殊點)。依環境異動鐵律只套 DEV DEV SELECT 確認佔位列已更新且 param_schema 存在;FE 工具卡由 coming_soon 變可設定 BE
T-5.2 agent:sonarqube.py connector——probe()system/status + 帶 Bearer 的 authentication/validate)+ run()(拉快照 → render 自包含 HTML + summary,逐呼叫檢查 cancel_event、單呼叫 timeout 60s)+ factory 註冊(_CONNECTOR_BUILDERS"sonarqube"不動 _LEGACY_TOOL_ID_TO_CODE 單元測試綠(mock HTTP,比照 zap / nmap connector 測試慣例);對 192.168.50.171:9000 實測 probe 與 run T-5.1(schema 定形) evidence-agent
T-5.3 FE 微調+端到端驗收:JobExecutionDrawer.vue _PRIMARY_PARAM_KEYS'project_key'+i18n my-tasks.json(zh-tw + en)補五個 summary_* key;用 171 兩專案(guidant-ai-backend / guidant-ai-fe)完整走一輪 測試連線 → 發任務 → 證據進池 → summary 對數全通。驗收需 DEV agent image 重建部署(只動 DEV 123,STG / POC agent 不動)。⚠️ 2026-07-31 裁示:程式碼部分已完成 commit,UI 端到端驗收併入 T-6.3 一起做(見 §5.1 但書)→ 2026-08-01 已完成 T-5.1、T-5.2 FE + evidence-agent

5.9 FR-058.6 SonarQube 主動掃描 — 依賴:FR-058.5(2026-07-31 追加;雛形)

詳細設計見 §4.7;決策見 D17–D24。定位是雛形——驗收標準是「端到端鏈路可運作」,不是「掃描品質有多好」或「語言涵蓋多廣」。

# 子任務 驗收 依賴 Repo
T-6.1 BE seed(一支 SQL migration,檔名接續既有序號 → 2026-XX-XX-fr058-19-sonarqube-scan-mode.sql):param_schema 升 v2——加 scan_mode(select、required、預設 scan、兩選項 pull / scan)+ scan 模式兩個新欄位(Git repo URL required / 專案代碼後綴 required,皆 conditionscan)+ 既有 project_keybranchconditionpullsetup_guide 改寫(現行開頭「本平台不會觸發程式碼掃描」加入 scan 模式後變成錯的,必須改為分模式說明;明講預設為原始碼掃描、會在客戶 SonarQube 建立 / 更新 Guidant-AI-<識別> 專案;補支援語言清單與收緊模式做法)。版本策略照 2026-07-27-fr056-5 範本:先把舊版 is_current=FALSE 再 INSERT v2,不是覆蓋 v1。依環境異動鐵律只套 DEV DEV SELECT 確認 v2 為 is_current、v1 保留;FE 建任務頁選 SonarQube 時預設顯示 scan 模式的兩個欄位,切到 pull 改顯示 project_key / branch切換模式時隱藏欄位不觸發必填驗證setup_guide 內無「本平台不會觸發掃描」這類與 scan 模式矛盾的敘述 T-5.1 BE
T-6.2 agent:① Dockerfile 裝 sonar-scanner CLI(平台專屬 zip 自帶 JRE,不裝 JDK;D20);② sonarqube.py 加 scan 模式分支——git clone磁碟型 tmp不可用 /dev/shm,D22)→ Popen 起 scanner(照抄 inspec.py:685-722 _run_with_cancel:657-683 _terminatestdout 走檔案不走 PIPE)→ 解析 report-task.txtceTaskId → 輪詢 GET /api/ce/task?id=<id> 直到脫離 IN_PROGRESS複用既有 _fetch_* / _render_report 組報告;③ 逾時上限+finally 刪 tmp(含失敗與取消路徑);④ 模式值非法明確 raise 不靜默降級(照 zap.py:164-171 立場);⑤ 單元測試(mock HTTP + mock 子行程,比照既有 connector 測試慣例) 單元測試綠,且必含「非法 scan_mode 值 raise」與「tmp 目錄在失敗路徑仍被清掉」兩條;image build 過並記錄體積增量;容器內對一個公開 repo 手動跑通全鏈路(clone → scan → 推 171 → 輪詢 → 拉結果 → HTML);逾時訊息可區分「掃描未完成」與「佇列未消化」 T-6.1(schema 定形) evidence-agent
T-6.3 FE 微調+兩模式合併端到端驗收:① JobExecutionDrawer.vue:308 _PRIMARY_PARAM_KEYS 調整——scan 模式下 Git repo URL 才是主要識別(見 §4.7 FE 接入點);② scan 模式警語(D24 配套①,hint 文字不夠);③ DEV 端到端一次驗完 pull 與 scan 兩模式——含 FR-058.5 尚未做的 UI 驗收(§5.1 但書) scan:建任務填公開 repo URL + 後綴 → 執行 → 171 上出現 Guidant-AI-<後綴> 專案 → HTML 報告進證據池、summary 有數字、執行紀錄主要區顯示 repo URL。pull(補做 T-5.3 驗收):填 project_key → 報告進池、summary 與 server 畫面對數。共同:取消可即時中止、逾時不無限等待。驗收需 DEV agent image 重建部署(只動 DEV 123,STG / POC agent 不動 T-6.1、T-6.2、T-5.2 FE + evidence-agent

5.10 收尾(本案完成後)

# 子任務 驗收 Repo
T-9.1 清掉 WorkflowSetupEditor.vue 的硬編死碼工具清單 toolsMenu(與 detection_tools 完全脫鉤;本案加入 ZAP 與 Nmap 後畫面會出現兩處 Nmap,其中一處是死的) 畫面不再出現與工具目錄脫鉤的工具名稱 FE
T-9.2 docs/claude/database-schema.md 的 config schema 與 detection 三表記載(目前 grep 零命中) 三表結構與 FK 關係有記載 BE

5.11 後續優化(本案不做,全案驗收後另議)— InSpec 報告呈現

2026-07-31 決策者實地比對兩份報告樣本後的結論:本輪照現行 html2 出貨驗收,以下列為後續優化案,最後再做。

背景:實測比對結果

同一次掃描(dev-sec/linux-baseline,59 條控制)產出的兩份報告:

現行 html2 MITRE SAF hdf2html
檔案大小 167 KB 1.9 MB(Executive 808 KB / Manager 1.8 MB)
內嵌 JS 2.6 KB 702 KB(含 Chart.js)
本質 CINC 順手產的靜態報表 打包成單檔的前端應用(Heimdall 檢視器)
控制涵蓋 / 標題 / 描述 / 原始檢測碼 ✓(相同)
嚴重程度 Impact: 1.0(數字) Critical(文字)
合規統計與圖表 (Passed 17 / Failed 3 + 百分比)
外部 CDN 依賴 0 0(皆離線可開)

關鍵澄清:兩者原始資料完全相同。 SAF 的優勢不是「內容較多」,而是把已有資料加工成結論:① 控制層狀態彙總(原始 JSON 只有逐檢查點的 pass/fail,沒有「這條控制算不算過」)② impact 數字依 InSpec 慣例轉 severity 文字 ③ 圖表。SAF 報告可讀文字量看似多 4 倍,主因是同一條控制重複呈現三次(總表/明細/Result Details),非資料量差異。

亦查明:SAF 的 800-53 Controls & CCIs 欄位對本 profile 是空的——該欄需 profile 自身帶 tags / refs 標記,dev-sec baseline 未帶(實測 59 條控制 tags 與 refs 全空)。若改用 MITRE 維護的 STIG profile 才會有值。

待議項目

# 項目 內容與代價
SAF CLI 導入評估 產出品質確實較佳且離線可用--network none 實測通過)。代價:agent image 目前 921 MB、base python:3.11-slim無 Node;SAF 官方 image 自身 711 MB,裝 Node + npm i -g @mitre/saf+300~400 MB。另:本次實測環境為 arm64(@mitre/saf 1.6.0 / node 22),上線需 amd64 重驗。三種裝法(直裝 Node/multi-stage 複製/呼叫獨立容器)各有取捨,其中「呼叫獨立容器」需 docker socket,與 D9「客戶端負擔最小」抵觸
summary 語意檢討(逐檢查點 vs 逐控制) 現行 connector summary 為逐檢查點計數(同一份資料:pass=60 fail=6 notapplicable=39);控制層彙總則是 Passed 17 / Failed 3 / Skipped 39兩個數字都正確、只是語意不同——稽核員的心智模型通常是「3 條控制未通過」而非「6 個檢查點失敗」。不建議單獨改 InSpec_merge_summaries() 多台加總與 FE i18n key(pass / fail / notapplicable / error)為跨工具共用,OpenSCAP 是逐規則計數,只改一邊會讓同一顆數字在兩個工具間語意不一致,反而更糟。要改就跨工具一起定義。

本輪不處理的理由:① 屬純呈現優化,現行 html2 已跨過「人可讀」門檻(控制編號/標題/描述/原始檢測碼齊全),非不可出貨狀態;且全案驗收進行中,變動 image 相依結構會讓「驗的是哪個版本」複雜化。② 非正確性缺陷——證據 HTML 內逐條控制狀態俱在,稽核員點開報告看得到真相;且牽動跨工具一致性,需一併定義。

5.12 FR-058.7 SonarQube 上傳檔案掃描 — 依賴:FR-058.6(2026-08-01 追加;✅ 全數完成,2026-08-01 決策者驗收通過

詳細設計見 §4.8;決策見 D25–D34(D33 為 2026-08-01 驗收發現後追加,對應 T-7.5;D34 為同日驗收期間提出的通知加厚,對應 T-7.6)。T-7.1 與 T-7.2 可並行,但並行前必須先凍結欄位 key 契約——承 FR-058.6 教訓:並行實作各自取名會對不上,FR-058.6 是用契約表避開的,本案照辦。契約項:source_file / _source_file 的形狀(§4.8「執行時輸入與 params 形狀」)、下載端點路徑、sha256 header 名。

完成標記(2026-08-01 收尾補記,commit hash 皆經 git log 核實)

  • T-7.1 ✅ BE fc3cbbe5(+404 修正 fdfbb091)——param_schema 實為 v4 非設計時寫的 v3:v3 已被同日稍早的 fr058-20(scan 選項標籤精簡,e78f1fd9)佔用,故 fr058-21 為 v3 下架+INSERT v4;execute sticky 參照/agent 下載端點(可插拔 resolver 清單,後被 FR-059 1d9e43e4 復用)皆落地
  • T-7.2 ✅ agent 13af0b9(bump 0.2.24 64e41d9)+契約對齊修正 8a444d3(limits key 名稱+X-Agent-Uid header,bump 0.2.25 fd2572f)——mTLS 串流下載+sha256 對帳+磁碟檢查+兩格式家族安全解壓
  • T-7.3 ✅ FE a424511——任務抽屜上傳原始碼壓縮包+執行紀錄顯示檔名
  • T-7.4 ✅ 端到端驗收通過(2026-08-01 決策者親驗)。四個未實測項裁示:磁碟不足模擬與解壓炸彈接受單元測試涵蓋、環境變數調限額不另實測、nginx client_max_body_size 列上版必驗項(DEV 直連 BE 驗不到)
  • T-7.5 ✅ BE 648d3370(挑選查詢排除 upload 綁定)+FE f6a26d8(批次發佈 confirm 提示)
  • T-7.6 ✅ BE 85d9ca9f——get_notify_context() 8 層 join、九項內容+報告檔名、失敗通知、三管道(Email/Discord/Telegram)、i18n 6 msgid
# 子任務 驗收 依賴 Repo
T-7.1 ✅ BE:① param_schema 升 v3 migration(INSERT 新版+v2 is_current=FALSE,版本策略照舊;僅 scan_mode options 加 upload,D26);② 限額 config(config class 屬性+環境變數覆寫,D27 定案:上傳 100MB / 解壓後 500MB / 50,000 檔 / 磁碟預留係數,比照 DRIVE_FILE_SIZE_LIMIT_MB pattern);③ execute 端點改造(收 body 帶 source_file.uid——帶則覆寫綁定 sticky 參照、不帶則沿用、首次無檔 400;BE 從 upload_files 權威取回 file_name / size / sha256 寫進 scan_params 快照與 agent_tasks.params,D31);④ 新 upload_type 分類(如 DETECTION_SOURCE);⑤ agent 檔案下載端點 GET /api/1.0/agents/files/<uid>(純 mTLS+任務綁定驗證+串流轉發不落地,D25);⑥ 心跳下發注入 _source_file 參照(含 size / sha256 / limits,D27)。migration 依環境異動鐵律只套 DEV DEV SELECT 確認 v3 為 is_current、v2 保留;execute 帶 uid 覆寫 / 不帶沿用 / 首次 400 三路徑正確;下載端點對非本 agent 任務的 uid 回 404(不區分不存在與無權限);不帶 body 的既有呼叫(pull / scan)行為完全不變 D25、D26 已定案(本次全定);與 T-7.2 並行前凍結 key 契約 BE
T-7.2 ✅ agent:① mTLS 下載 client(比照 _post()_get_stream(),串流下載+sha256 對帳);② 磁碟檢查(D28:下載前 shutil.disk_usage,free < 壓縮包×12+1GB 即 fail,係數吃 payload limits,錯誤標「agent 磁碟空間不足」歸類 agent 環境問題);③ 標準庫安全解壓——zip 走 zipfile、tar 系走 tarfile filter="data"——+兩家族共用約二十行額度檢查(解壓炸彈計量 / cancel,D30);④ 接進既有 scan 管線(取代 _clone_repo() 分支,_run_scanner() 起原樣沿用);⑤ 單元測試(含五路徑 workspace 清理斷言擴充、兩格式家族解壓案例、路徑逃逸 / 超額壓縮包被擋案例) 單元測試綠;image build 過;容器內對一個實際壓縮包手動跑通全鏈路(下載 → 對帳 → 解壓 → scan → 推 171 → 拉結果 → HTML);磁碟不足與超額壓縮包兩條負向路徑皆明確 fail 且 workspace 已清 同左;key 契約凍結後可與 T-7.1 並行 evidence-agent
T-7.3 ✅ FE:JobExecutionDrawer.vue 執行流程加上傳區(scan_mode=upload 時顯示;accept 限 .zip / .tar / .tar.gz / .tgz;移植 SurveyPreview.vue fileupload pattern:先打 POST /file/upload 拿 uid、execute 只送 uid;已有 sticky 檔案時顯示「目前檔案:xxx.zip(上傳於 M/D)」可直接執行沿用、上傳新檔即替換;首次無檔不給按執行);紀錄顯示:_PRIMARY_PARAM_KEYSsource_fileparamValueLabel 加物件型分支取 file_name(D31)。DetectionConfigField.vue 不用動 首次執行未上傳時執行鈕不可按;上傳後可執行;重掃對話框顯示目前檔案並可沿用 / 替換;執行紀錄顯示檔名而非裸 UUID T-7.1 的 v3 schema 與 execute 端點落 DEV 後開工 FE
T-7.4 ✅ 端到端驗收:抽屜發起執行時上傳 zip → 掃描 succeeded → 171 出現專案 → agent workspace 已刪 → tenant 儲存壓縮包保留且重掃沿用可用(D29 定案)→ 不上傳直接重掃=沿用舊檔跑通 → 上傳新檔替換後舊檔仍在(歷史保留)→ 上傳 .tar.gz 跑通(tar 系格式,D27)→ 磁碟不足模擬跳「agent 磁碟空間不足」錯誤 → 超額壓縮包(解壓炸彈計量)被擋 → 環境變數調限額生效(D27 設定化)。另含部署驗證:上版環境 nginx client_max_body_size(D27 待辦) 驗收清單全項通過;驗收需 DEV agent image 重建部署(只動 DEV,STG / POC agent 不動 T-7.1–7.3 全數完成 BE + FE + evidence-agent
T-7.5 ✅ upload 任務不自動派發(D33,驗收發現):① BE——get_detection_jobs_without_executioninfra/grc/repository/task_execution_query.py:134-159)查詢排除 tool_params->>'scan_mode'='upload' 的綁定,upload 任務不進「開始執行任務」自動派發清單;② FE——專案規劃頁批次發佈 confirm 文案補一句「上傳型掃描任務需由執行者上傳檔案後手動執行」(ProjectPlanningView.vueProjectAuditorOverview.vue 同款 confirm 兩處同步) upload 任務按「開始執行任務」後不產生掃描執行紀錄與 warning log(挑選階段即排除,非派了才失敗);pull / scan 任務自動派發行為完全不變;confirm 文案含上傳型任務提示 T-7.1(upload 綁定已可建立)後開工 BE + FE
T-7.6 ✅ 檢測掃描通知加厚(D34):① BE——infra/detection_tools/repository/detection_job_notify_query.py 新增 get_notify_context(job_uid)(8 層 join 鏈一次 SQL;⚠️ 必帶 catalog_id=profile_imports.source_catalog_id 過濾、AO 用 title→prose→part_id fallback);② app/detection_tools/service/detection_orchestration_service.py——成功通知內容擴為九項+報告檔名(複用 _resolve_scan_params() 剝除、evidence 參數用起來),on_scan_failed 補查 job/binding 後發失敗通知(含 error_message,收件人同成功)、管道補齊 Discord/Telegram(HTML+純文字兩版文案);③ i18n——兩語系 messages.po 加 6 個 msgid+pybabel compile;④ 測試——test_detection_orchestration.py 補成功/失敗通知案例。無 migration、無 DI 變更 成功通知含九項+報告檔名;失敗通知會發且含 error_message;三管道(Email/Discord/Telegram)皆發;secret/內部欄位(_ 前綴)不出現在任何管道;pybabel compile 完成 無(可獨立開工;與 T-7.1〜7.5 無依賴) BE

6. 端到端驗收

  1. 平台敏感參數:任務參數含敏感欄位時,DB 內為密文、agent 收到明文、FE 執行紀錄與 system_logs 皆不出現該值。
  2. ZAP:管理員設定 ZAP 服務位址與 API key → 測試連線通過 → 任務選 ZAP、填 target_url、選「登入後主動」並填測試帳密 → 開始執行 → HTML 報告進證據池(可下載;不可預覽為第一批出貨標準)、summary 有 alert 統計、報告內容顯示已進入登入後頁面。
  3. ZAP 預設安全:任務未主動選擇時 scan_mode 為被動;選主動類時 FE 顯示攻擊流量警語。
  4. InSpec / CINC:對 Linux(SSH)與 Windows(WinRM)各一台目標執行 → 每台一份人可讀 HTML 報告進證據池(html2 reporter;JSON 僅在記憶體算 summary 不上傳,見 §4.3「證據格式」);其中一台連不上時另一台仍成功,且失敗主機被明確回報(非靜默假成功)。
  5. GCB:以一份 Windows 最小 Demo profile 跑通 WinRM 連線 → 認證 → profile 執行 → 結果解析 → 證據上傳整條鏈路;更換 content 不需重建 agent image。
  6. Nmap(2026-07-30 改案為 SSH 型):設定 nmap 執行主機的 SSH 憑證 → 測試連線通過 → 任務選 Nmap 填掃描目標 → 開始執行 → 報告進證據池、summary 有開放埠統計;執行主機未安裝 nmap 時 probe() 回明確訊息(可辨識是「沒裝 nmap」而非「連不上」或「認證失敗」)。
    零憑證放行能力(T-4.1,平台級):以宣告為零憑證的工具驗證可正常開始執行、不再誤回 DETECTION_TOOL_CONFIG_NOT_FOUND,且需憑證的既有工具(含改案後的 Nmap)檢查行為不變。
  7. D11 解耦:agent 依派工夾帶的 code 取 connector;三環境 seed id 不一致時不再造成派工錯配。
  8. 既有工具迴歸:OpenVAS 與 OpenSCAP 全鏈不受影響(factory 取 connector / 憑證檢查 / 多檔回收 / FE 動態渲染)。
  9. 取消與逾時:四個新工具在執行中皆可被取消(cancel_event),且皆有 timeout 上限,不會無限等待。
  10. SonarQube(2026-07-31 追加,FR-058.5):管理員設定 base_url 與 user token → 測試連線通過(system/status + 帶認證的 authentication/validate)→ 任務選 SonarQube、填 project_key → 開始執行 → 對公司自有 SonarQube(192.168.50.171:9000guidant-ai-backend / guidant-ai-fe 兩專案)拉取快照 → 自包含 HTML 報告進證據池(可下載、不可預覽)、summary 數字(bugs / vulnerabilities / code_smells / security_hotspots / quality_gate)與 server 畫面對數;專案尚無分析結果時回明確錯誤「請先於客戶端執行 sonar-scanner」。取消與單呼叫 timeout(60 秒)行為同第 9 項標準。
    ⚠️ 2026-07-31 裁示:本項 UI 驗收與第 11 項合併一次做完(程式碼已完成,detection_executions 內 sonarqube 仍 0 筆,見 §5.1 但書)。→ 2026-08-01 已完成驗收
  11. SonarQube 主動掃描(2026-07-31 追加,FR-058.6):建任務時 scan_mode 預設為原始碼掃描(D24)→ 填公開 Git repo URL + 專案代碼後綴 → 開始執行 → agent clone 源碼、跑 sonar-scanner、推上公司自有 SonarQube(192.168.50.171:9000)→ server 上出現 Guidant-AI-<後綴> 專案(D21 前綴生效)→ 輪詢 ce/task 至處理完成 → 自包含 HTML 報告進證據池、summary 有數字。
    同一輪一併驗完 pull 模式(補做第 10 項):切 scan_mode 為讀取既有結果 → 條件欄位改顯示 project_key / branch隱藏欄位不觸發必填驗證)→ 報告進池、summary 與 server 畫面對數。
    負向驗收scan_mode 帶非法值時明確失敗不靜默降級(絕不退回 pull 給出舊快照);掃描逾時與佇列逾時訊息可區分;任務失敗或取消後 agent 容器內 tmp 目錄已清除docker exec 確認無殘留源碼)。
    執行紀錄:scan 模式下主要參數區顯示 Git repo URL(非後綴),pull 模式顯示 project_key