# 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.html)（四工具母案）、[`discussion-sonarqube-scan-mode.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`](./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`](./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 則證據池不是人可讀。兩份都上傳——排除：在證據池塞入兩份等價內容。<br>**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 不另立引擎、不另寫 connector**。**content 層**：本案只做**一份 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 的整合型態與零憑證放行<br>（2026-07-30 改案） | **（改案後）Nmap 走 `connection_type='SSH'`**——nmap 由客戶安裝在自己指定的主機上，agent 透過 SSH 登入該主機執行，與 OpenSCAP 完全同構。法律依據：NPSL v0.95 §3 末段明文不主張控制「執行使用者早已安裝在自己系統上的 nmap」的軟體（原文見右欄），SSH 型正落在此描述內。連帶：nmap **需要 SSH 憑證**（`requires_credentials=TRUE`）。<br><br>**（原定案，平台能力部分保留）放寬 `start_execution` 的憑證檢查**——在 `detection_tools` 增加「是否需要憑證」的宣告欄位，讓平台知道零憑證是一種**合法狀態**。此為**平台級能力，改案後仍然正確且保留**，只是 nmap 不再是它的第一個使用者，日後其他真正零憑證的工具會用到。 | 為 Nmap 建立一筆空的租戶設定當佔位——排除：會留下一筆語意不明的資料，日後看到的人無法判斷那是刻意還是遺漏。<br><br>**2026-07-30 改案（原定案為 `connection_type='CLI'`、agent 本機執行、不 bundle 理由僅記「NPSL 授權」）**：<br>**① 觸發點**：實作前發現「客戶自備」在容器化部署下有實務障礙——agent 跑在 Docker container 內，**看不到主機上安裝的 nmap**（容器有獨立檔案系統）。原 CLI 設計隱含假設 nmap 在 agent 可執行範圍內，但「不 bundle」又「不能裝進容器」，兩個條件互相矛盾。<br>**② 查證 NPSL 後確認不能 bundle，且理由比原記載更強。** NPSL v0.95 §3（出處 `https://svn.nmap.org/nmap/LICENSE`）明文列舉的「衍生作品」定義包含這一條：<br>`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).`<br>我們的 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/`）。<br>**③ NPSL §3 最後一段自己留了出口**：軟體若解析的是使用者提供的結果、或執行的是「使用者早已安裝在自己系統上的 nmap」，授權方不主張控制（`Licensor does not purport to control through this license any software which does not require the rights granted herein`）。**SSH 型態正好落在這個描述裡**——nmap 是客戶裝在自己主機上的，我們只是遠端下指令。<br>**④ 評估過並排除的方案**：<br>　• **掛載主機 binary 進容器**（`-v /usr/bin/nmap:/usr/bin/nmap:ro` 加上共享函式庫目錄）——排除：nmap 非靜態連結，依賴 libssl / libpcap / liblua 等一串共享函式庫，掛主機的 lib 目錄進容器會與容器內 glibc 版本賭運氣，且可能蓋掉容器自身函式庫把 agent 弄壞；架構也綁死 x86_64。把「客戶自備」變成一個需要客戶理解共享函式庫的任務，故障時難以遠端診斷。<br>　• **客戶自建一層 image**（以我方 image 為 FROM，自行 `apt install nmap` 後 build）——技術上可行且乾淨（函式庫由套件管理員解決），法律上也成立（nmap 由客戶裝入，非我方散布）。排除理由：客戶每次 agent 升版都要改 Dockerfile 版號並重新 build，是持續性負擔；且「誰把它裝進去」雖在法律上是關鍵，技術結果與我方 bundle 完全相同，此種形式上的區隔在交付溝通上彆扭。<br>　• **購買 Nmap OEM 授權**——本案不採，未評估成本；若日後產品策略需要零客戶設定的 nmap 整合，這是唯一能 bundle 的合法途徑，列為備查。<br>**⑤ 拍板走 SSH 型的理由**：「最沒爭議」——法律上落在 NPSL 自留出口內、技術上與已上線的 OpenSCAP 同構（connector 可複用 SSH 連線處理、私鑰暫存、`host_failures` 逐台回報）、客戶只需在指定主機 `apt install nmap`，無任何 docker 操作。<br>**⑥ 連帶影響**：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 執行），而且**這種錯誤不會拋例外、只會安靜地跑錯工具**。<br>**界線說明**：這項改動**不會讓新增工具完全免於動 agent**——connector 實作本身仍必須裝進 agent，因此每加一個全新工具，agent 仍需更新一次。D11 消除的是另一個更脆弱的問題：額外維護一份 id 對照表、且三個環境的 seed id 必須完全一致。 |
| **D12** | ZAP 登入後掃描的 SPA 支援範圍<br>（2026-07-30 追加，實測後補列） | **本案（第一批）只支援傳統表單登入**（form-based），`param_schema` 的 `login_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 會誤判未登入並反覆重跑登入。<br>**實測後果更正（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 Session` → `ZAP 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 證據進證據池、任務層敏感參數平台能力）**不依賴登入後掃描**，被動與主動掃描已實測可交付。<br>② **只做 json-based 認證、session 管理留待日後**——排除：**沒有中間地帶**。認證送出去了但 token 帶不進後續請求，ZAP 一樣掃不進登入後頁面，結果與完全不做相同，卻已付出一次工程成本並在 UI 上留下「看起來支援了」的誤導。<br>③ **改用 `logged_in_indicator` 以外的判定繞過**——排除：那只解第三個原因，前兩個原因未解則掃描本來就進不去，判定準不準沒有意義。 |
| **D13** | SonarQube 整合模式（pull-snapshot）<br>（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 任務參數<br>（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 證據格式<br>（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 認證與跨版本相容<br>（2026-07-31 追加，FR-058.5） | **憑證＝`base_url` + user token**（`squ_` 前綴，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 兩模式的共存形式<br>（2026-07-31 追加，FR-058.6） | **ZAP 式——同一個 `sonarqube` 工具 ＋ `scan_mode` 參數分流。** 工具清單只有一張卡，租戶層憑證設定一次、兩模式共用，模式差異只出現在「建任務時填什麼參數」。四個理由：① **概念一致**——使用者心裡想的是「我要用 SonarQube」，不是「SonarQube-讀取版 / SonarQube-掃描版」，出現兩張同名卡等於把內部實作差異推給使用者；② **設定只做一次**——server 位址與 token 填一次兩模式都能用，走兩張卡要填兩次，換 token 時漏改一邊會變成「一個模式能用一個不能用」的怪問題；③ **FE 零改動**——`condition:{field,value}` 是現成機制（`DetectionConfigField.vue:23-27` 的 `visible` computed、`:31-34` 對不可見欄位跳過必填驗證），ZAP 已實證；④ **憑證共用**（見右欄更正）。 | **GCB 式（另立 tool code、工具清單兩張卡）——排除**：它唯一的優勢是「憑證 schema 各自乾淨」，而**該優勢在本案不存在**，只剩「工具清單多一張同名卡」的代價。<br>**重要更正 · 規劃初期的「憑證分流痛點」在本案不成立**：曾以為兩模式需要兩組憑證（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` 兩欄，**兩模式完全共用、一個字都不用改**。<br>InSpec 雙 transport 那個「probe 第一段檢查憑證完整性」的先例仍是有效的一般性技術參考，但**不是本案的痛點**——記錄於此以免日後誤讀。 |
| **D18** | 主動掃描的語言範圍<br>（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 模式。<br>**縮範圍後浮現的必要配套（不是決策項，是配套）**：使用者不知道範圍而送錯語言時，兩種失敗方式嚴重性差很多——**Java 是硬失敗**（沒有 `.class`，scanner 直接報錯中止，難看但安全）；**C# 是靜默淺掃**（CLI 不支援但不會中止，很可能產出一份看起來正常、實際幾乎沒分析到東西的報告，稽核員可能把「沒掃到」誤讀成「很乾淨」）。三件事必須一起做：① 事前——UI 與 `setup_guide` 明列支援語言並寫清楚「清單外請改用拉取模式」；② 事中——可考慮讓使用者宣告主要語言，非清單內直接擋下不送進 scanner；③ 事後——報告產出時若分析檔案數 / 程式碼行數異常偏低，明確標示而非靜靜輸出（「零發現」與「沒掃到」必須看得出來是兩件事）。具體形式（擋在哪一層、訊息怎麼寫）留到實作計畫階段定。 |
| **D19** | 主動掃描的源碼來源<br>（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 後沒有任何下載檔案的邏輯，兩端都要補）。額外還要自訂大小上限、暫存空間與解壓炸彈防護。<br>**私有 repo 認證——排除**：雛形階段先求打通，且平台現況本來就只支援公開 repo（CINC/InSpec 的 Git URL 用法同樣只支援公開 repo，私有認證在本平台完全沒做過）。<br>決策者原話兩種都提了（「可能上傳 或是提供 server 位置」），但成本量級差一個數量級——雛形階段選成本低的那條先把鏈路打通。 |
| **D20** | sonar-scanner 怎麼進 agent<br>（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-8`、`nmap.py:1-6`）。⚠️ **易誤判**：`poetry.lock` 內的 `docker 7.2.0` 是 testcontainers 經 jedi-common 拉進來的**測試相依洩漏**，不是既有能力，不可據此以為 agent 能操作 docker。<br>**執行時下載 scanner——排除**：客戶 agent 常在不能連外的內網。 |
| **D21** | 推上客戶 server 的 projectKey 命名<br>（2026-07-31 追加，FR-058.6） | **一律加前綴 `Guidant-AI-<使用者輸入>`**——使用者只填後半段，前綴由平台組上。 | **直接用使用者輸入——排除**：① 可能撞到客戶既有專案 key，把一次性稽核掃描結果**覆蓋到他們正式的專案歷史上**；② 客戶無法辨識哪些專案是本平台建的。前綴同時是 R4 寫入副作用的緩解手段——「撞號」與「無法辨識來源」兩個問題由它解決，但「每掃一次多一筆專案」本身沒有解（SonarQube 無專門開關，想擋只能移除 token 擁有者的 "Create Projects" 全域權限，這點寫進 `setup_guide`）。 |
| **D22** | 資源保護與暫存位置<br>（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。<br>⚠️ **`/dev/shm`——排除且必須明確避開**：既有暫存慣例是 `inspec.py:197-200` 的 `_SHM_DIR = "/dev/shm"`（0600 ＋ contextmanager 必刪），但**那是 tmpfs 記憶體型**，是為「小的憑證檔」設計的。解壓一個幾百 MB 的源碼包到記憶體會直接吃掉容器的 RAM 配額，**不可無腦沿用**。 |
| **D23** | coverage 紅燈的處理<br>（2026-07-31 追加，FR-058.6） | **照實顯示，不濾除、不加特別說明。** 決策者原話：「主要掃品質就好」。 | **濾除 coverage 條件後再判定 Quality Gate——排除**：等於我們自己改寫客戶的判定標準。**加醒目說明——排除**：雛形階段不做。<br>事實記錄（§4.7 R3）：SonarQube 自己不產生 coverage、只匯入第三方報告，而我們不跑測試 → 報告**必然**無 coverage，客戶 Quality Gate 若含覆蓋率條件會亮紅燈，**而那個紅燈不代表程式碼有問題**。Go 更糟——無 coverage 資訊時視為「全部未覆蓋」而非「無資料」，會顯示 0%。實作時仍建議在 `setup_guide` 提一句讓管理員事前知道。 |
| **D24** | `scan_mode` 的預設值<br>（2026-07-31 追加，FR-058.6） | **預設 `scan`（原始碼掃描）。** 決策者原話：「預設不要，預設一定是 scan mode」。理由：主動掃描是本需求的**主線意圖**，pull 只是先落地的那一個；預設值應指向主要使用情境，讓使用者少一步操作。<br>**連帶要求——事前告知從「最好有」升級為「必須有」**（三項配套實作必做）：① 建立任務時的提示必須顯眼，hint 文字不夠（使用者會略過），建議在 scan 模式的參數區塊做明顯標示或送出前給明確提示（具體形式列實作建議，不在此定死）；② `setup_guide` 必須明講「本工具預設為原始碼掃描模式，執行時會在貴公司的 SonarQube 建立 / 更新專案（key 為 `Guidant-AI-<識別>`）」，讓管理員在設定階段就知道，而非看到專案清單多東西才發現；③ 收緊模式寫進 `setup_guide`（預先建好專案 ＋ 移除 token 擁有者的 "Create Projects" 全域權限）。 | **預設 pull——排除**：雖然 pull 純唯讀可回復、scan 會寫入客戶系統不可回復，但那是配套要解決的問題，不該讓預設值偏離主線意圖。<br>⚠️ **與 ZAP D2 取捨方向相反是刻意的，不是疏漏**。D2 預設被動的理由正是「預設值選錯的代價不對等」，本案結論方向相反，差別在**代價的量級**：<br>　• **ZAP D2** 誤觸＝對目標送出**真實攻擊流量**，影響**第三方外部系統**——可能觸發對方告警甚至造成服務中斷，**不是我們能收拾的**。<br>　• **本案 D24** 誤觸＝在**客戶自有**的 SonarQube 建立一個專案——一筆專案紀錄、客戶可自行刪除，且有 `Guidant-AI-` 前綴可辨識來源（D21）。<br>兩者都是「預設值有副作用」，但一個是對外部系統送攻擊流量、一個是在客戶自有系統留一筆可刪除的紀錄，量級不同故結論不同。 |
| **D25** | 上傳檔案的取源通道：agent 如何拿到源碼包<br>（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 資料面混用，授權語意混濁。<br>**推檔給 agent（派工時 BE 主動 `POST /blob`）——排除**：推檔時機與任務生命週期耦合（派工瞬間 agent 可能離線）；`POST /blob` 走 FileUploadService **永久落地**（寫 agent 自己的 upload_files 表），與 /tmp 即用即棄語意不同，agent 端要另管清理；且 tenant 儲存為 remote_agent 時檔案本來就在 agent 上，會變成「推自己」。<br>附註：tenant 儲存為 remote_agent 且掃描 agent 恰為同一台時，BE relay 會繞一圈（agent→BE→agent）。v1 接受此低效（罕見組態），列 §4.8 known limitation。 |
| **D26** | 上傳掃描的參數模型：scan_mode 三選項 ＋ 執行時輸入<br>（2026-08-01 追加，FR-058.7） | **`scan_mode` 增 `upload` 選項——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` 完全不用動（不需新型別分支）、孤兒檔視窗大幅縮小（上傳與執行幾乎是同一個動作）。<br>**scan 選項內做 repo_url / source_file 互斥——排除**：condition 機制是單層 `{"field","value"}` 等值比對，表達不了「二欄擇一」；required 驗證也會亂（兩欄都掛 required 就都必填，都不掛就都可空）。硬做要改 condition 語法引擎，成本遠超過多一個選項。 |
| **D27** | 上傳檔案格式與限額<br>（2026-08-01 追加並定案，FR-058.7） | **格式**：收 `.zip` / `.tar` / `.tar.gz` / `.tgz`；**裸 `.gz`（單檔 gzip）不收**——解開只有一個檔，對源碼掃描無意義。<br>**限額設定化，不寫死**：四個限額——上傳上限 MB / 解壓後總量 MB / 解壓檔案數 / 磁碟預留公式（D28 的係數與底值）——做成 **BE config class 屬性 ＋ 環境變數可覆寫**（現成前例 `DRIVE_FILE_SIZE_LIMIT_MB`，`config/config.py:166` 同 pattern）。**預設值**：上傳 **100MB** / 解壓後 **500MB** / **50,000** 檔（皆可由環境變數調整）——100MB 壓縮包對純源碼專案已相當大（源碼壓縮比高，約當數十萬行等級），不夠再調。<br>**agent 不自己配置**：BE 派工時把限額隨任務 payload 下發（與 credentials 下發同構），agent 只做執行時 enforcement——**單一真相在 BE**，不違反 FR-039「agent 不連雲端 DB」不變式。<br>待辦：驗證上版環境 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** | 磁碟防護的具體規則<br>（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 端全保留 ＋ 重掃沿用 ＋ 歷史不刪<br>（2026-08-01 追加並定案，FR-058.7；**推翻討論過程 v2 的「終態即刪」中間版本**） | **「掃完即刪」只適用 agent 端 workspace**（既有 `finally: shutil.rmtree` 機制＋五路徑測試鎖約，零新工作，不變）；**tenant 儲存空間上的壓縮包保留**。完整模型四點：<br>**① 重掃沿用**——`scan_mode=upload` 的任務，抽屜執行對話框顯示「目前檔案：xxx.zip（上傳於 M/D）」，直接按執行＝沿用舊檔；要換版就上傳新檔替換。**首次執行仍必須上傳**（無檔不給執行）。<br>**② 檔案參照落點在任務綁定層**（sticky 狀態供沿用，形狀見 §4.8）。注意與被否決的 D26 初版方案不同：**上傳動作仍在抽屜發起執行時，流程不變**，只是參照回寫進綁定；初版被否決的主因「終態刪檔→參照懸掛」在保留檔案後自然消失。<br>**③ 替換不刪舊檔，歷史版本全保留**（決策者明示：留歷史，萬一未來 user 要找舊檔，還有容錯修正空間）。每筆執行紀錄 `scan_params` 快照的 `{uid, file_name}` 都能對回實體檔案，稽核可回溯「那次掃的是哪一包」。<br>**④ 清理機制列 follow-up（v1 不做）**——孤兒檔（上傳未執行）、歷史版本、殘檔統一由未來的清理機制管（**原 D32 併入本項**）。給未來清理機制的約束先立在此：**必須以「綁定仍參照中的檔案」為保留名單，不能只看檔案年齡**——否則會清掉三個月沒重掃、但仍要沿用的 zip。 | **終態即刪（討論過程 v2 曾定案，後推翻）**：其理由「掃完不留平台」**不成立**——tenant 儲存空間本來就是客戶自己設定的位置（local / minio / 客戶自家 agent），保留不構成「平台持有客戶源碼」問題；且終態即刪會造成「重掃必重傳」與「執行紀錄參照懸掛（快照指向已刪檔案）」兩個實際代價。特此記錄推翻軌跡，避免日後誤讀為反覆——是理由被檢驗後不成立，不是需求搖擺。 |
| **D30** | 解壓實作：標準庫 zipfile ＋ tarfile ＋ 共用額度檢查層<br>（2026-08-01 追加，FR-058.7） | **兩個格式家族都用標準庫**：**zip 走 `zipfile`**——`extractall()` **內建路徑消毒**（剝絕對路徑與 `..`、不建 symlink），zip-slip 已由標準庫擋掉；**tar 系（.tar / .tar.gz / .tgz）走 `tarfile` ＋ `filter="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）。<br>**第三方解壓套件——排除**：標準庫已涵蓋兩個家族的需求，agent 依賴保持精簡，不為二十行的事引套件。 |
| **D31** | 顯示與紀錄：完全複用既有上傳機制 ＋ 兩點收緊<br>（2026-08-01 追加，FR-058.7） | **完全複用既有上傳機制**：FE 上傳統一打 `POST /file/upload`（`API.UPLOAD_FILE`），新 upload_type 分類（如 `DETECTION_SOURCE`）；白拿清單（零新工作）——sha256（agent 下載後對帳防竄改）、RLS 租戶隔離、三種 storage backend 全支援。兩點收緊：<br>**① FE 只送 uid，檔名不由 FE 送**——BE 收到 execute 請求後從 `upload_files` 表**權威取回** file_name / size / sha256 快照進執行紀錄；顯示歷史不依賴檔案仍存在，也不信任前端送來的檔名。BE 要補的驗證：uid 存在且屬本 tenant（RLS）、副檔名在允許清單（D27）、大小在 D27 限額內。<br>**② 快照存 `{uid, file_name}` 物件**——執行紀錄抽屜 `paramValueLabel` 對非 select 型參數原樣輸出，快照若只存 file uid 畫面會顯示裸 UUID；故 `scan_params` 快照存 `source_file: {uid, file_name}`，FE 顯示 `file_name`；`JobExecutionDrawer.vue` 的 `_PRIMARY_PARAM_KEYS`（:315）加 `source_file`、`paramValueLabel` 加物件型分支（取 `file_name`）。 | **信任 FE 送來的檔案 metadata——排除**：檔名 / 大小由前端自報則快照可被竄改，稽核回溯失去意義；權威來源只有 `upload_files` 表。<br>**另立新上傳通道——排除**：既有 `POST /file/upload` ＋ `upload_files_for_tenant()` 已涵蓋三後端與 RLS，另立通道違反「新增前先找既有能力」鐵則。 |
| **D32** | 孤兒檔處理<br>（2026-08-01，FR-058.7） | **併入 D29 第 ④ 點，不獨立成案。** 上傳後放棄執行的孤兒檔、替換後的歷史版本、其他殘檔，統一由未來的清理機制管理（v1 不做），完整說明與「以綁定參照為保留名單」的約束見 D29 ④。上傳時機移到發起執行時後，孤兒檔視窗已大幅縮小（上傳與執行幾乎是同一個動作）。 | — |
| **D33** | `scan_mode=upload` 與「開始執行任務」自動派發的關係<br>（2026-08-01 追加，FR-058.7 驗收發現） | **upload 任務不進自動派發清單——在挑選查詢排除。** 背景：專案規劃頁「開始執行任務」會自動派發檢測掃描（FR-056.5 #5，`TaskExecutionService.start_task_execution` → `_auto_dispatch_detection_scans`，`app/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_execution`（`infra/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 任務根本不該出現在待派清單裡，而不是進了清單再被擋。<br>**② 維持現狀讓它失敗——排除**：靜默失敗會讓 PM 反覆觸發且無人察覺，是要修的洞不是可接受行為。<br>背景註記：自動路徑「悄悄沿用舊檔」的疑慮**不會發生**——自動派發只挑「從未執行過」的任務（冪等條件），已執行過的 upload 任務（有 sticky 檔案可沿用者）本來就不在清單內，故本修正只涉及首次執行段。 |
| **D34** | 檢測掃描通知加厚：九項內容＋失敗通知＋三管道<br>（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——既有限制另案）。 | **① 失敗只發指派人（避噪音）——排除**：決策者裁示失敗更需要管理層知道，收件人與成功通知相同。<br>**② hosts 遮蔽——排除**：客戶自己的信箱、掃描目標是核心資訊，遮蔽只會降低通知可用性。<br>**③ 只修 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.py` → `start_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.py` → `heartbeat()` / `_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.py` → `dispatch_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_schema`、`param_schema`、`schema_migrations` 收尾。四個新工具的 seed 都照這支寫 |
| connector 介面 | `evidence-agent/core/task_executor_connectors/base.py`（`DetectionConnector` ABC、`ScanResult`、`ScanCancelledError`、`cancel_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: true` 且 `type: 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.py` 與 `zap_domain_service.py` 內有寫死的帳號密碼與固定 IP。憑證禁入版控是本專案紅線，一律改走 `self.credentials`（FR-056 D9 解密下發）與 `self.params` |
| **12 種報告模板 + sections / themes 相容矩陣**（`auto_pentest/common/enums/zap_report/report_enum.py` 的 `TEMPLATE_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，不落實體磁碟）、權限 `0600`、`try/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`）與其相依（`git`、`xsltproc`），無任何 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.profile` 是 `param_schema` 裡 `secret: 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）

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

token 必須是 **user token**（`squ_` 前綴），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>.html`、`content_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`](./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 | `condition` 綁 `pull` | **既有欄位，語意不變** |
| `branch` | text・選填 | `condition` 綁 `pull` | **既有欄位**。付費版才支援多分支（D14） |
| Git repo URL | text・required | `condition` 綁 `scan` | **新增**。公開 repo，不需憑證（D19） |
| 專案代碼後綴 | text・required | `condition` 綁 `scan` | **新增**。實際推上去的是 `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_cancel`（`Popen` ＋ 輪詢）——**不用 `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.txt` 取 `ceTaskId` → 輪詢 `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-102` 的 `save_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 租戶建不出來。<br>**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 analyzer**。`SONAR_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 主機**而不只是這個任務。<br>⚠️ **資源量級：官方零基準數據。** 文件唯一出現過的數字是 `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`](./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_configs` 的 `STORAGE_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 | `condition` 綁 `scan` | 沿用 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_scans`，`app/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_execution`（`infra/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 connector` → `FR-058.3 GCB`：GCB 不另立引擎，跑的就是 `.2` 的 connector。`.2` 沒有，`.3` 沒東西可跑。
- `FR-058.5 SonarQube 拉取` → `FR-058.6 SonarQube 主動掃描`：同一個工具的兩種模式（D17），`.6` 的 seed 是在 `.5` 的 `param_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_password` 的 `secret: true`、`scan_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.py`（**SSH 登入執行主機** → `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，皆 `condition` 綁 `scan`）＋ **既有 `project_key`、`branch` 補 `condition` 綁 `pull`**；**`setup_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` `_terminate`，**stdout 走檔案不走 PIPE**）→ 解析 `report-task.txt` 取 `ceTaskId` → 輪詢 `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_KEYS` 加 `source_file`＋`paramValueLabel` 加物件型分支取 `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_execution`（`infra/grc/repository/task_execution_query.py:134-159`）查詢排除 `tool_params->>'scan_mode'='upload'` 的綁定，upload 任務不進「開始執行任務」自動派發清單；② FE——專案規劃頁批次發佈 confirm 文案補一句「上傳型掃描任務需由執行者上傳檔案後手動執行」（`ProjectPlanningView.vue` 與 `ProjectAuditorOverview.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」而非「連不上」或「認證失敗」）。<br>**零憑證放行能力**（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:9000`，`guidant-ai-backend` / `guidant-ai-fe` 兩專案）拉取快照 → 自包含 HTML 報告進證據池（可下載、不可預覽）、summary 數字（bugs / vulnerabilities / code_smells / security_hotspots / quality_gate）與 server 畫面對數；專案尚無分析結果時回明確錯誤「請先於客戶端執行 sonar-scanner」。取消與單呼叫 timeout（60 秒）行為同第 9 項標準。<br>**⚠️ 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 有數字。<br>**同一輪一併驗完 pull 模式**（補做第 10 項）：切 `scan_mode` 為讀取既有結果 → 條件欄位改顯示 `project_key` / `branch`（**隱藏欄位不觸發必填驗證**）→ 報告進池、summary 與 server 畫面對數。<br>**負向驗收**：`scan_mode` 帶非法值時**明確失敗不靜默降級**（絕不退回 pull 給出舊快照）；掃描逾時與佇列逾時**訊息可區分**；任務失敗或取消後 agent 容器內 tmp 目錄**已清除**（`docker exec` 確認無殘留源碼）。<br>**執行紀錄**：scan 模式下主要參數區顯示 **Git repo URL**（非後綴），pull 模式顯示 `project_key`。
