# FR-058 第三批（Nmap）實作 session 派工交接（2026-07-30）

| 項目 | 內容 |
|------|------|
| 緣由 | FR-058 分批出貨。第一批（ID 解耦 + 任務層敏感參數 + ZAP，CM-958〜970）實作已完成、agent 已 bump 0.2.13。本文是**第三批（Nmap）實作 session 的派工單** |
| Branch | BE：`feature/FR-058`（不切 branch）；evidence-agent：當下 branch（第一批 HEAD `8185893` bump 0.2.13） |
| **本棒角色** | **實作者。** 完成 FR-058.4（Nmap），依 T-4.1 → T-4.2 → T-4.3 順序做，每個子任務完成即 commit + 回寫 Notion 卡 |
| **與第二批的關係** | **完全獨立、可並行。** 第二批（InSpec/CINC → GCB，CM-971〜979）與本批無任何依賴：不碰 `inspec.py`、不碰第二批的 seed 檔，各自新增檔案不衝突。**但 evidence-agent 有三處共用檔會撞**（factory `__init__.py` / `Dockerfile` / 版號），處理方式見 §7 |
| **⚠️ 改案（2026-07-30，本文已全份更新）** | **D9 改案：Nmap 由 `connection_type='CLI'`（agent 本機執行）改為 `'SSH'`**——nmap 由客戶安裝在自己指定的主機上，agent 透過 SSH 登入該主機執行，與 OpenSCAP 完全同構。觸發點是「客戶自備」在容器化部署下無法成立（agent 在容器內看不到主機的 nmap），查證 NPSL v0.95 §3 後確認不能 bundle 且同條款末段的自留出口讓 SSH 型成立。連帶：Nmap **需要 SSH 憑證**、T-4.1 零憑證平台能力保留但 Nmap 不再是其使用者、`connection_type='CLI'` 至今仍無實際使用者。完整推導見 design.md §2 D9，本文摘要見 §2.1〜§2.3 |
| 接手前必讀 | 本文全份 + design.md §2（**D9 含 2026-07-30 改案段，務必讀原文**）+ §4.5（Nmap 詳細設計，已改為 SSH 型）+ §5.7（子任務表）+ §3.2 元件盤點的 `connection_type` / `start_execution()` 兩列 |
| 上游文件 | `docs/features/FR-058-2607-detection-tools-expansion/handoff/2026-07-30-fr058-planning-complete-handoff.md`（全案大圖與需求演變；本文已摘實作所需部分，卡住時才回頭讀） |

## §0 讀序

1. 本文 §1（範圍）→ **§2（Nmap 的特殊性，本批最重要一節）** → §3（第一批帶來的新事實）
2. `design.md` §4.5（FR-058.4 Nmap 詳細設計）全讀 + §5.7（T-4.1〜T-4.3 子任務表與驗收）
3. `design.md` §2 的 **D9 定案原文，含 2026-07-30 改案段**（本文 §2.1〜§2.3 是摘要，原文含 NPSL 條款原文、改案的完整推導與三個被排除方案的理由）
4. `design.md` §3.2 元件盤點表的兩列：`config.detection_tools`（「加是否需要憑證宣告欄位」）與 `start_execution()` 憑證檢查（「放行零憑證工具」）——這兩列是 T-4.1 的範圍界線
5. §4 的範本檔案——動手寫每個子任務前先開對應範本照形狀寫

---

## §1 任務範圍與 Notion 卡

### 1.1 範圍

第三批 = **FR-058.4（Nmap）**，共 3 個子任務，序列執行（T-4.2 依賴 T-4.1 的欄位、T-4.3 依賴 T-4.2 的 seed）。

| # | 子任務 | Repo | 一句話 |
|---|--------|------|--------|
| T-4.1 | `detection_tools` 加「是否需要憑證」宣告欄位（schema 異動 + ORM model + migration）；`start_execution()` 依該欄位放行零憑證工具（D9）；三環境套用 | BE | **本批唯一動到平台本體的一棒，迴歸風險最高**。改案後 Nmap 已不是此能力的使用者，但**這一棒照做**——它是平台級能力（見 §2.2） |
| T-4.2 | seed Nmap（**`connection_type='SSH'`、`requires_credentials=TRUE`**）+ `config_field_schema`（SSH 憑證，對照 OpenSCAP）+ `param_schema`（掃描目標 / 掃描類型 / 埠範圍）+ `setup_guide`（**客戶在自己指定的主機安裝 nmap**、NPSL 授權說明、SSH 前提、主動掃描授權警語）；三環境套用 | BE | migration |
| T-4.3 | `nmap.py`：**SSH 登入執行主機** → `nmap -oX` → XML 解析 summary + 報告上傳 + `probe()`（SSH 連線 / 認證 / 該主機 nmap 可執行 / 權限四段分類）+ `cancel_event` / timeout；**不 bundle 進 image**；factory 依 code 註冊 | evidence-agent | connector（**主要對照 `openscap.py`**） |

**驗收（design.md §5.7 原文）**：

- T-4.1：零憑證工具可正常開始執行，不再誤回 `DETECTION_TOOL_CONFIG_NOT_FOUND`；**需憑證的既有工具檢查行為不變**（← 迴歸紅線，見 §2.2）
- T-4.2：設定頁出現 Nmap(available) **並可設定 SSH 憑證**；任務可選 Nmap 並填參數發佈
- T-4.3：報告進證據池、summary 有開放埠數；**執行主機**未安裝 nmap 時 `probe()` 回明確訊息（且可與「連不上」「認證失敗」區分）
  （其中「手塞派工掃真實網段」屬端到端驗收，依 §5 測試紀律**延後到全案最後**）

### 1.2 Notion 卡對照（完成後逐卡回寫，母案 CM-957：`https://app.notion.com/p/3ad346da4cd081859d2ed8b21ca2697c`）

| 卡 | 對應 | Notion URL |
|----|------|-----------|
| **CM-980**（FR-058.4 母卡） | Nmap | `https://app.notion.com/p/3ad346da4cd081159daae65d4b08c80d` |
| CM-981 | T-4.1 | 從母卡 CM-980 子卡清單進入 |
| CM-982 | T-4.2 | 同上 |
| CM-983 | T-4.3 | 同上 |

---

## §2 Nmap 的特殊性（本批核心，動手前先讀透）

> ⚠️ **2026-07-30 D9 改案（本節已依此改寫）**：Nmap 由原定的 `connection_type='CLI'`（agent 本機執行）改為 **`connection_type='SSH'`**——nmap 由客戶安裝在自己指定的主機上，agent 透過 SSH 登入該主機執行，**與 OpenSCAP 完全同構**。改案的完整推導、NPSL 條款原文與三個被排除方案見 `design.md` §2 的 **D9**（本節 §2.1〜§2.3 是摘要，**動手前務必讀 D9 原文**）。

Nmap 在四個工具裡「技術上最單純」（呼叫 CLI + XML 解析）。改案前它是「唯一一個逼平台改自己的工具」（零憑證），改案後**它變成 OpenSCAP 的同構複製**，實質工作量從「改平台」轉為「照抄 SSH 型範本」。本節四點動手前逐點讀完。

### 2.1 走 SSH 型、與 OpenSCAP 同構（`CLI` 型態至今仍無使用者）

`detection_tools.connection_type` 在 FR-056 就定義了三種型態（`API` / `CLI` / `SSH`），至今**只有 `API`（OpenVAS、ZAP）與 `SSH`（OpenSCAP）被真正用過**。原本 Nmap 要當 `CLI` 的首個實際使用者，**改案後這件事沒有發生**——`CLI` 這一格從 FR-056 定義出來到現在**仍然沒有任何工具走過**，本批不會改變這個事實。

**這對本批的意義是好消息**：走 SSH 就是走**已被 OpenSCAP 實戰驗證過的路徑**，BE 派工鏈、設定頁憑證渲染、測試連線端點、agent 的 SSH 連線處理全部有現成可照抄的形狀，原本「未經驗證路徑可能藏著缺口」的風險**不復存在**。

**因此實作方向是「照抄 OpenSCAP」而不是「開新路」**：

| 對照點 | 照誰寫 |
|--------|--------|
| `detection_tools` seed（`connection_type` / `config_field_schema` / `requires_credentials`） | OpenSCAP 那一列 + `scripts/sql/2026-07-28-fr057-1-openscap-seed.sql` |
| 設定頁 SSH 憑證欄位（host / port / 帳號 / 密碼或私鑰） | OpenSCAP 的 `config_field_schema`，**不需要新欄位型態** |
| agent connector 的 SSH 連線 / 私鑰暫存 / 多目標逐台 / `host_failures` | `openscap.py`（見 §4.3） |
| 測試連線（`probe()`）的分段錯誤分類 | `openscap.py` 的 probe，**再加一段「該主機有沒有 nmap」**（§2.3） |

**唯一要留意的新事實**：Nmap 現在**需要 SSH 憑證**（`requires_credentials=TRUE`），設定頁必須先設定憑證才能執行——與 OpenSCAP 行為一致，不再是「無需填憑證」。

**發現與 OpenSCAP 形狀不一致而需要偏離時的處置**：**先停下回報**再決定——這屬 D1–D12 沒涵蓋的新決策，依 §6 規範不自行開新方向。

### 2.2 D9 零憑證放行：本批唯一動到平台本體的部分（迴歸風險最高）

> ⚠️ **改案後的定位（先讀這段再往下）**：Nmap 改為 SSH 型後**需要 SSH 憑證**，**它自己不再是零憑證工具**，也就不會被 `start_execution()` 的憑證檢查誤擋。<br>**但 T-4.1 這一棒照做、範圍不變**——「是否需要憑證」宣告欄位 + 放行邏輯是**平台級能力**（design.md §1.2 已把它列為平台結構性補強），日後真正零憑證的工具會用到，只是**第一個使用者不再是 Nmap**。<br>**對本批的具體差異只有兩點**：① T-4.2 seed Nmap 時 `requires_credentials` 要填 **TRUE**（不是 FALSE）；② T-4.1 的驗收要另找方式驗（見本節末的「改案後怎麼驗 T-4.1」）。下面的現況分析與紅線**全部仍然適用**。

**現況（已查證，`app/detection_tools/service/detection_orchestration_service.py:105-110`）**：

```python
creds = self._resolve_credentials(
    binding.tenant_config_id, tenant_id, binding.detection_tool_id
)
if not creds:
    # 空憑證派下去 agent 必炸（缺 host/帳密），且錯在雲端卻只能從 agent log 看到
    # → 提前擋下並講清楚要去設定工具憑證。
    raise BadRequestError(DetectionToolsErrorCode.DETECTION_TOOL_CONFIG_NOT_FOUND)
```

（**改案前**的問題敘述：Nmap 不需要任何帳號密碼，走到這裡必被誤擋。**改案後 Nmap 有 SSH 憑證，不會走到這個死路**——但下面的平台能力仍要補上，理由見本節開頭的框。）

**D9 定案做法**：**在 `detection_tools` 增加「是否需要憑證」的宣告欄位**，讓平台知道零憑證是一種**合法狀態**，`start_execution()` 依該宣告放行。

**D9 明確排除的做法**：為 Nmap 建立一筆空的租戶設定當佔位——**不要這樣做**。理由：會留下一筆語意不明的資料，日後看到的人無法判斷那是刻意還是遺漏。

**這一棒的風險與紅線**：

1. **既有四個工具都需要憑證，不可因這次改動被放行**。新欄位的預設值必須讓既有列維持「需要憑證」語意（`NOT NULL DEFAULT TRUE` 之類），migration 不可讓既有四列變成零憑證。驗收的第二句「需憑證的既有工具檢查行為不變」就是在講這件事。
2. **改動範圍是三處，缺一不可**：
   - DB schema：`config.detection_tools` 加欄位（migration，三環境都套）
   - ORM model：`infra/detection_tools/model/detection_tool.py`（32 行，加 `Mapped[bool]` 欄位 + `comment=`；本專案 ORM `comment=` 是 DB COMMENT 的單一來源）
   - domain entity / mapper：`domain/detection_tools/entity/detection_tool_entity.py` + `infra/detection_tools/mapper/detection_tool_mapper.py`（欄位要能傳到 service 層才判斷得了）
   - service 邏輯：`start_execution()` 的 `if not creds` 改為「若該工具宣告需要憑證才擋」
3. **判斷資料要從哪裡拿**：`start_execution()` 目前手上只有 `binding.detection_tool_id`，需要取到該工具的宣告欄位。**依 DDD 層級規範，走 detection tool 的 domain service 取，不要在 app service 直接 import ORM model 查**（既有 `self._jedt_domain` / `self._exec_domain` 的注入形狀可照抄；缺 domain service 就在 `__init__` 加參數並到 `di_containers/` 對應 container wiring）。
4. **欄位命名建議**：`requires_credentials`（BOOLEAN NOT NULL DEFAULT TRUE）——語意正向、預設安全（新工具沒宣告就是需要憑證，往嚴的方向失敗）。若採別的命名，理由寫進 migration 檔頭註解。
5. **FE 影響要一併確認**：設定頁對 `requires_credentials=false` 的工具應該不顯示憑證表單 / 不要求「先設定才能執行」。若 FE 需要這個欄位，**API response 要一併帶出**（`detection_tools` 列表端點的 schema）——這點在 T-4.1 就要決定，不要留到 T-4.2 才發現前端拿不到。<br>（**改案後**：Nmap 是 `requires_credentials=true`，走的是既有的「顯示憑證表單」路徑，與 OpenSCAP 一致，因此本批不會實地踩到 false 分支的 FE 行為。仍建議在 T-4.1 把 false 分支的 FE 行為想清楚並寫進 commit message，避免日後第一個真正零憑證的工具進來時才發現前端沒處理。）

**改案後怎麼驗 T-4.1**（原驗收假設「用 Nmap 驗零憑證放行」已不成立）：

- **放行邏輯本身**：不必為了驗證而 seed 一個假工具。可用**既有工具暫時把 `requires_credentials` 設 false 跑一次**（DEV 上驗完改回），或在 BE 加一個針對 `start_execution()` 憑證分支的單元測試（mock 工具宣告的兩種值各跑一次）——**後者較乾淨且可留下迴歸保護，優先選它**。
- **迴歸紅線不變且更重要**：既有四個工具**加上改案後的 Nmap** 在憑證為空時都仍要被擋。Nmap 現在也在這個名單裡。

### 2.3 Nmap 不 bundle 進 agent image（NPSL 授權）→ 這正是改走 SSH 型的原因

Nmap 採 **NPSL**（Nmap Public Source License，基於 GPLv2 但加了額外限制），**不可打包進我們發布的 agent image**。

**法律依據（2026-07-30 查證，比原記載的「NPSL 授權」四個字強得多）**——NPSL v0.95 §3（出處 `https://svn.nmap.org/nmap/LICENSE`）列舉的「衍生作品」定義明文包含這一條：

```
Is designed specifically to execute Covered Software and parse the results
(as opposed to typical shell or execution-menu apps, which will execute
anything you tell them to).
```

我們的 connector 正是「專門呼叫 nmap 並解析其 XML 輸出」，**正中此條**。因此「只用子行程呼叫、不連結函式庫」這個 GPL 慣用的免責理由**在 NPSL 底下不成立**——該條款是特意加來堵掉這個論點的。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/`）。

**但 §3 最後一段留了出口**：軟體若執行的是「使用者早已安裝在自己系統上的 nmap」，授權方不主張控制（`Licensor does not purport to control through this license any software which does not require the rights granted herein`）。**SSH 型正好落在這個描述裡**——nmap 是客戶裝在自己主機上的，我們只是遠端下指令。

**原 CLI 型為何撐不住**：實作前發現「客戶自備」在容器化部署下無法成立——**agent 跑在 Docker container 內，看不到主機上安裝的 nmap**。原設計隱含假設 nmap 在 agent 可執行範圍內，但「不 bundle」又「不能裝進容器」互相矛盾。被評估並排除的三個方案（掛載主機 binary 進容器 / 客戶自建一層 image / 購買 Nmap OEM 授權）各自的理由見 `design.md` §2 D9 的改案段——**若你在實作中想走其中任何一條，先讀那段再停下回報**。

**對實作的四個具體要求**：

1. **`Dockerfile` 不加 nmap**。（對照組：第二批要在 Dockerfile 加 CINC Auditor，本批**不動 Dockerfile 的套件安裝**——若你發現自己在改 Dockerfile 裝 nmap，停下來重讀這段。**也不可用 volume 掛載主機 binary 的方式繞過**，那是 D9 明確排除的方案之一。）
2. **`probe()` 必須能在「執行主機」未安裝 nmap 時回明確訊息**，且要與「SSH 連不上」「認證失敗」明確區分（四段式分類，見 §4.3）。這是驗收條件之一，不是選配。遠端 `command -v nmap` / `nmap --version` 失敗時的訊息要讓管理員一看就知道「要去那台執行主機裝 nmap」，而不是丟一個 `exit code 127` 的原始輸出。
3. **`setup_guide` 要寫清楚「裝在哪裡、怎麼裝」**：在**客戶自己指定的執行主機**上安裝（各主流發行版的安裝指令 + 驗證指令），**無任何 docker 操作**；SSH 連線與帳號權限前提（部分掃描類型需要提權）；並註明「本平台不隨附 nmap，需由貴公司在指定主機自行安裝」與 NPSL 授權說明。
4. **SSH 憑證屬租戶層設定**（`tenant_detection_tool_configs`，走既有 Fernet 鏈，對照 OpenSCAP 的 `config_field_schema`），**不放任務參數**、不涉及 FR-058.0 的任務層敏感參數機制。

### 2.4 主動掃描性質：`setup_guide` 需比照 ZAP 加授權警語

埠掃描會**對目標送出真實探測流量**——不是被動觀察，而是主動連線嘗試。在客戶網路環境中，這可能觸發 IDS/IPS 告警、被誤判為攻擊行為，或違反目標方的使用條款。

**要求**：`setup_guide` 比照 ZAP 的寫法加上授權警語。ZAP seed 的原句可直接照抄改寫：

> ⚠️ **主動掃描會對目標網站送出真實的攻擊測試流量。** 請務必確認已取得目標方授權，並優先在測試環境執行。

Nmap 版的措辭要對上埠掃描的實際行為（送出探測封包、可能觸發資安設備告警），並提醒「掃描網段前確認該網段屬於受稽核範圍且已獲授權」。

**param_schema 的掃描類型欄位若含侵入性較高的選項**（例如版本偵測 `-sV`、OS 偵測 `-O`、腳本掃描 `-sC`），**預設值要選保守的那一個**——這條沿用 D2 的判斷原則：預設值選錯的代價不對等，預設保守最多是掃得淺，預設侵入則可能在使用者未察覺下對正式環境送出大量探測流量。

---

## §3 第一批帶來的新事實（第三批必須遵守 / 直接受益）

### 3.1 factory 已改依 code 取 connector（agent commit `f17a311`，D11）

- `get_connector()` 現在**優先吃 `detection_tool_code`**，`detection_tool_id` 降為 fallback（走 fallback 記 warning log）。
- 新 connector 的註冊方式：在 `core/task_executor_connectors/__init__.py` 加一個 `_build_nmap()` builder（**import 留在 builder 內部**，維持 lazy import 慣例——某支 connector 的第三方相依缺失時只有該工具不可用，不會讓整個 factory import 失敗），並在 `_CONNECTOR_BUILDERS` dict 加 `"nmap": _build_nmap`（實際 code 以 T-4.2 seed 的 `detection_tools.code` 為準）。
- **絕不把新工具加進 `_LEGACY_TOOL_ID_TO_CODE`**——檔頭註解已明訂該表只涵蓋 FR-056/057 已出貨四筆（`{1: openvas, 2: nessus, 3: sonarqube, 4: openscap}`），新工具加入等於把剛移除的脆弱性種回去。ZAP 已照此模式（參考 `_build_zap`）。
- factory 迴歸測試在 `test/test_connector_factory.py`，新 code 註冊後補對應 case。

### 3.2 BE 派工 payload 已夾帶 `detection_tool_code`（BE commit `5151fb2c` + `082a02d8`，T-X.1 已上線）

`_collect_pending_tasks()` 已加欄位；`082a02d8` 補齊了心跳以外的第二條派工路徑（測試連線 probe payload）。第三批**不需要動 BE 派工鏈的 code 夾帶部分**——seed 進 `detection_tools` 後派工自然帶 code，agent 端照 §3.1 註冊即可接上。

（注意：T-4.1 仍要動 `start_execution()`，但那是憑證檢查的分支，與 code 夾帶無關。）

### 3.3 任務層敏感參數平台能力已就緒（同 commit `5151fb2c`，T-0.1〜0.3）

param_schema 欄位標 `secret: true` 即自動走 Fernet 加密落庫 / 派工解密下發 / FE 與稽核 log 剝除（實作在 `common/util/detection_secret_params.py`）。

**Nmap 本批預設用不到**（埠掃描不需要憑證，這正是本批要解的問題）。但若日後 Nmap 要做**認證掃描**（例如需要目標主機帳密的服務探測），直接在 param_schema 標 `secret: true` 即可，不需自建機制——這條在 design.md §1.2 已被列為此能力的預期受益者之一。

### 3.4 ⚠️ FE 必填驗證的已知邊界（設計 param_schema 時要避開）

第一批已在 FE 補上任務參數的必填驗證（FE commit `82208529`，helper `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/utils/detectionFieldValidation.js`）：任務存檔時會擋 condition-aware 的必填欄位（condition 不成立的欄位不渲染也不驗證）。

**已知邊界（helper 檔頭註解已載明）**：空值判斷刻意沿用 falsy（`!value`），與 `DetectionConfigField.showError` 同源。當時的前提是「有 required 的欄位型態不含 number / boolean」，所以 falsy 判斷不會誤殺 `0` 或 `false`。

**對 Nmap 的直接影響**：若 Nmap 的 param_schema 出現 **`type: "number"` 或 `type: "boolean"` 且 `required: true`** 的欄位（很可能——例如埠號範圍起點、逾時秒數、某個布林開關），使用者填 `0` 或 `false` 時**會被誤判為缺漏而擋下存檔**。

**處置（依序）**：

1. **優先避開**：把這類欄位設計成 `required: false` + 給 `default`（timeout 類本來就該這樣，ZAP 的 `timeout_sec` 即是 `required:false` + `default:3600`），或改用 `text` / `select` 型態（埠範圍本來就常寫成 `"1-1024"` 這種字串）。
2. **真的必須是必填 number / boolean** → **停下回報**，說明需要一併修 helper（把空值判斷改成明確的 `value === undefined || value === null || value === ''`），不要自己在 seed 裡繞過或默默改 FE。

### 3.5 D4 教訓通則化：報告取得方式必須先確認拿得到「內容本身」

ZAP 原定用 `reports.generate` 產 PDF，實作時發現該 API **只把檔案寫進 ZAP daemon 主機的磁碟、回傳路徑字串**，官方沒有回傳檔案內容的端點（zaproxy issue #7821）——遠端部署模型下 agent 拿不到那個路徑的檔案，被迫改案（改 `core.htmlreport` 取 bytes）。

**通則**：任何「工具端產檔」的設計，動手前都必須先驗證取得管道回的是**內容本體**而不是「對方主機上的路徑」。

**Nmap 原則上沒有此問題，但改案為 SSH 型後這條要多留意一層**——`nmap -oX` 在**遠端執行主機**上跑，輸出必須真的傳回 agent。最直接的做法是 `-oX -` 讓 XML 走 stdout，SSH 直接收到 bytes（省掉遠端暫存檔的清理問題）；若改用遠端暫存檔，就必須自己把檔案抓回來並清掉遠端殘檔。寫 `run()` 前確認一次：確定拿到的是 XML **bytes** 而不是「遠端主機上的路徑字串」、確定遠端沒留下暫存檔。`openscap.py` 已經解過同一題，照它的形狀。

### 3.6 agent 版號

第一批已出到 **0.2.13**（DEV 已部署；HEAD `8185893`，含 ZAP 主動掃描修正）。第三批出貨版 bump **0.2.14**——**但兩批可能同時進行**，若第二批先出 0.2.14 就順延到 0.2.15。**commit 前先 `git log --oneline -5` + 看 `pyproject.toml` 的 `version` 確認目前版號**，不要拿本文的數字當現況。

bump pattern 照 evidence-agent commit `c775fc8`——**四處版號同步**：`pyproject.toml` / `config/config.py`（`AGENT_VERSION` 預設值）/ `deploy/docker-compose.yml`（`AGENT_IMAGE` + `AGENT_VERSION` 兩處）/ `rebuild.sh`（註解範例）。依 §5 測試紀律，開發期間**不重建 image、不部署**，bump commit 做完留著等全案端到端才出貨。

### 3.7 第一批可照抄的實作水準基準

- **seed SQL**：`scripts/sql/2026-07-30-fr058-1-zap-seed.sql`（見 §4.1）——檔頭把決策背景寫成註解、`ON CONFLICT (code) DO NOTHING`、用 `nextval` 自然拿 id 且註明「code 派工後 id 不影響正確性」、`schema_migrations` 收尾。
- **mock 單元測試**：`test/test_zap_connector.py`（485 行，47 項全 mock + 九輪突變測試驗斷言真的會咬）——Nmap connector 測試照此水準寫 `test/test_nmap_connector.py`。Nmap 的 XML 解析特別適合這種寫法：準備幾份假 XML fixture（正常多埠 / 全關閉 / 主機不可達 / 格式殘缺），對解析邏輯跑純 mock 測試，完全不需要真的掃任何東西。

---

## §4 實作範本座標（照形狀寫，不重新發明）

### 4.1 BE seed / migration 範本（T-4.1 / T-4.2）

| 檔案 | 用法 |
|------|------|
| `/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-07-30-fr058-1-zap-seed.sql` | **SQL 寫法的最新範本**——nextval 拿 id + 「code 派工後 id 不影響正確性」註解、`ON CONFLICT (code) DO NOTHING`、param_schema 的 `condition` / `secret` / `default` 寫法、`setup_guide` 的 `E''` 多行 markdown 寫法（含表格與程式碼區塊）、`detection_tool_param_schemas` 的 `is_current` 寫法、`schema_migrations` 收尾 |
| `/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-07-28-fr057-1-openscap-seed.sql` | ⭐ **內容形狀的主要對照（2026-07-30 改案後）**——Nmap 與 OpenSCAP 同為 SSH 型，`connection_type='SSH'` 與 **`config_field_schema` 的 SSH 憑證欄位**（host / port / 帳號 / 密碼或私鑰）直接照它的形狀寫，不要自創欄位。SQL 寫法本身仍以上面的 ZAP 檔為準（較新） |
| `/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-07-30-fr058-2-fix-zap-login-required-flags.sql` | **既有資料補正檔的範本**——T-4.1 的欄位新增若要同時回填既有四列，形狀照它（改既有列 + 檔頭寫清楚為何要補、新環境走 seed 檔不必再跑此檔） |

**T-4.1 的 SQL 特別注意**：本批是 FR-058 唯一有 **ALTER TABLE** 的一棒（其餘都是 INSERT seed）。CLAUDE.md SQL 鐵則照舊（檔頭 `-- Date:`、每語句加日期註解、收尾 `INSERT public.schema_migrations`、`psql --single-transaction -v ON_ERROR_STOP=1`）；本批只加欄位不建表，故不涉及 `GRANT ... TO cm_app`（既有表的授權不變）。

### 4.2 BE 程式碼座標（T-4.1）

| 檔案 | 用途 |
|------|------|
| `app/detection_tools/service/detection_orchestration_service.py` → `start_execution()`（憑證檢查在 105-110 行附近） | D9 放行邏輯的落點；同檔可看到 `self._jedt_domain` / `self._exec_domain` / `self._agent_task_domain` 的 DI 注入形狀 |
| `infra/detection_tools/model/detection_tool.py`（32 行） | ORM model 加欄位（`Mapped[bool]` + `comment=`；`comment=` 是 DB COMMENT 單一來源，見 memory `project_erd_tooling_convergence`） |
| `domain/detection_tools/entity/detection_tool_entity.py` + `infra/detection_tools/mapper/detection_tool_mapper.py` | entity 欄位 + mapper 對映，缺一則 service 層拿不到值 |
| `infra/detection_tools/repository/detection_tool_repo_impl.py` | 若需依新欄位查詢時看這裡（預期不必改） |
| `common/code/detection_tools_error_code.py`（`DETECTION_TOOL_CONFIG_NOT_FOUND = DETECTION_TOOLS_404002`） | 放行後這個 error code 對零憑證工具不再觸發；**不要刪除它**（既有工具仍在用） |

### 4.3 agent connector 範本（T-4.3）

| 檔案 | 用法 |
|------|------|
| `~/Projects/Billows/Audit-Manager/evidence-agent/core/task_executor_connectors/openscap.py`（578 行） | ⭐ **主要對照範本（2026-07-30 改案後）**——Nmap 與 OpenSCAP 同為 SSH 型，形狀幾乎可整套照搬：**SSH 連線建立與認證、私鑰暫存與用完清除、遠端指令執行與輸出取回、多目標主機逐台執行、`host_failures` 逐台失敗回報、遠端子行程的取消與清理、`probe()` 的分段錯誤分類**。改案前這裡只列為「多目標處理的參考」，現在它是第一順位 |
| `~/Projects/Billows/Audit-Manager/evidence-agent/core/task_executor_connectors/zap.py`（631 行，第一批最新出爐） | **次要對照（通用紀律）**：cancel_event / timeout 輪詢寫法、`_check_cancelled()` 的「停止呼叫失敗不可蓋掉取消語意」處理、`_probe_failure_message()` 錯誤訊息組裝、回傳值驗證（錯誤碼偽裝成正常回傳的防護）、summary 解析容錯、**log 紀律（憑證絕不進 log）**——這些與連線型態無關的形狀照抄 |
| `~/Projects/Billows/Audit-Manager/evidence-agent/core/task_executor_connectors/base.py`（63 行） | `DetectionConnector` ABC、`ScanResult`（content bytes / filename / content_type / summary）、`ScanCancelledError`、注入點 `cancel_event` / `host_failures` / `content_mismatch_hosts`——**介面不動** |

**Nmap 特有的實作要點**（design.md §4.5，已依 2026-07-30 D9 改案更新）：

- `run()`：**SSH 登入客戶指定的執行主機** → 執行 `nmap -oX` → 取回 XML → 解析產出 summary（開放埠數、服務清單等）→ XML 或轉出的人可讀報告上傳當證據。多台執行主機時逐台一份證據，某台失敗以 `host_failures` 明確回報（不靜默假成功）——形狀照 `openscap.py`
- `probe()`：**四段式錯誤分類**，各段回各自明確的訊息——① SSH 連得上嗎（網路 / port）→ ② 認證過嗎（帳密 / 私鑰）→ ③ **該主機上有沒有 nmap 且可執行**（遠端 `command -v nmap` + `nmap --version`）→ ④ 執行權限夠不夠（部分掃描類型需要提權）。第 ③ 段的訊息是驗收條件（§2.3 第 2 點）
- `cancel_event` 與 timeout：取消時要**中止遠端執行中的 nmap 並清理 SSH channel**，raise `ScanCancelledError`。照 `openscap.py` 的遠端子行程取消形狀處理，**不要留下對端的孤兒行程**。（注意：這與改案前規劃的「本機 `subprocess` terminate」不同，也與 ZAP 的 HTTP 輪詢不同）
- 證據檔格式：XML 原檔或轉出的人可讀報告擇一上傳。**選定前先想清楚證據池的可讀性**——ZAP 的 D4 教訓是「證據池要人可讀」（原文見 design.md §2 D4）；若上傳 XML，考慮同時在 summary 帶足夠的統計數字，或轉一份人可讀格式。**此處若要偏離 design.md §4.5 的描述，停下回報**

### 4.4 factory 註冊（T-4.3 的一部分）

`~/Projects/Billows/Audit-Manager/evidence-agent/core/task_executor_connectors/__init__.py`（82 行）——`f17a311` 之後改依 code 註冊（見 §3.1）。照 `_build_zap` 的形狀加 builder + `_CONNECTOR_BUILDERS` 一列，**不動 `_LEGACY_TOOL_ID_TO_CODE`**。

⚠️ **此檔第二批也會改**（加 `_build_inspec`），是 §7 三個衝突點之一。

### 4.5 其他座標

| 檔案 | 用途 |
|------|------|
| `~/Projects/Billows/Audit-Manager/evidence-agent/Dockerfile` | **本批不加任何套件**（§2.3 第 1 點）；只有第二批會動它 |
| `~/Projects/Billows/Audit-Manager/evidence-agent/test/`（跑法 `PYTHONPATH=. poetry run pytest test/ -q`，repo 缺 conftest.py 必帶 PYTHONPATH） | 單元測試落點 |
| `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/utils/detectionFieldValidation.js` | §3.4 的必填驗證 helper（設計 param_schema 前先讀檔頭註解） |
| `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/.../DetectionConfigField.vue` | FE 七種欄位型態 + `condition` 條件顯示；設計 param_schema 時確認型態在支援清單內 |

---

## §5 測試紀律

> 決策者 2026-07-30 裁示，**取代**原「批次結束測一次」。與第二批派工單完全一致。

### 5.1 端到端驗收整批延後

**端到端驗收延後到全案最後一次做**（與第一批、第二批合併驗收）。開發期間：

- **不重建 agent image**
- **不部署**（121 / 122 / 123 三台都不動）
- **不跑真實掃描**（不對真實網段 / 主機發動 nmap 掃描）

版號 bump 的 commit 照做（§3.6），但不出貨。

### 5.2 做中持續做的便宜檢查（每個子任務完成前必過）

| 檢查 | 做法 |
|------|------|
| import / 語法檢查 | agent：`PYTHONPATH=. poetry run python -c "from core.task_executor_connectors.nmap import NmapConnector"`；BE：改完 model / entity / service 後起服務或跑既有測試確認 import 鏈完整 |
| **mock 單元測試** | 全 mock、不碰網路與真實主機（**SSH client 一併 mock**）。**Nmap 特別適合**：準備假 XML fixture（正常多埠 / 全部關閉 / 主機不可達 / 格式殘缺）+ 假的遠端執行結果，對解析邏輯與 `probe()` **四段錯誤分類**（SSH 連不上 / 認證失敗 / 該主機沒裝 nmap / 權限不足，四種要能各自回不同訊息）跑測試。水準對齊第一批 `test/test_zap_connector.py`（47 項全 mock + **突變測試驗斷言真咬**——刻意改壞被測邏輯確認測試會紅，避免寫出永遠綠的假測試）。`openscap.py` 若已有對應測試檔，mock SSH 的形狀照它 |
| factory 迴歸 | `test/test_connector_factory.py` 補 nmap code 的 case，確認既有四個工具取 connector 行為不變 |
| **T-4.1 的 BE 迴歸（本批特有，不可省）** | 放行邏輯改完後，確認**需憑證的既有工具（openvas / openscap / zap）在憑證為空時仍被擋**——這是驗收明文要求。用既有 BE 測試或手動觸發皆可，但要有實際執行紀錄，不能只是「看程式碼覺得沒問題」 |
| SQL 套 DEV 後 SELECT 驗證 | T-4.1：`SELECT id, code, connection_type, requires_credentials, status FROM config.detection_tools ORDER BY id`（確認既有四列仍是「需要憑證」）；T-4.2：同一查詢確認 Nmap 進去且 **`connection_type='SSH'`**、`status='available'`、**`requires_credentials=true`**（← 2026-07-30 改案，原為 `CLI` + false），再查 `config.detection_tool_param_schemas` 確認 schema 進去且 `is_current` 正確，並確認 `config_field_schema` 有 SSH 憑證欄位（對照 OpenSCAP 那一列） |

### 5.3 seed / migration 三環境都要套

DEV → STG → POC 三環境（指令照 CLAUDE.md「DB 環境清單」段；帳號 `cmmgr`，**密碼請查 `.env` 的 `DB_SECRET` 或部署文件**，禁入任何版控檔案）：

```bash
# DEV
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  --single-transaction -v ON_ERROR_STOP=1 -f scripts/sql/<本批 SQL 檔>.sql
# STG（同台不同 DB）
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_stg \
  --single-transaction -v ON_ERROR_STOP=1 -f <同上>
# POC（另一台）
psql -h 192.168.50.189 -p 25432 -U cmmgr -d guidant_ai_poc \
  --single-transaction -v ON_ERROR_STOP=1 -f <同上>
```

⚠️ **T-4.1 是 ALTER TABLE，三環境不同步的後果比 seed 嚴重**（BE 程式碼會讀新欄位，某環境沒套就整個 detection tools 查詢炸掉）。T-4.1 套完三環境確認無誤，才開始 T-4.2。

SQL 撰寫鐵則：檔頭 `-- Date:`、每個語句加日期註解、收尾 `INSERT public.schema_migrations`。

---

## §6 行為規範（實作 session 會踩到的，逐條遵守）

- **不切 branch**：任何 `git checkout <branch>` / `git switch <branch>` 一律不執行。BE 永遠在 `feature/FR-058` 工作；發現 branch 不對**停下回報**，不自己 fix（「為了在正確 branch commit 而切 branch」也不允許）
- **push 永遠等決策者明示**，不自動 push；階段性 commit（子任務完成後）直接做，毋需停下問
- **顯式 git add 檔名，禁用 `-am`**——避免誤 add 其他 arc 留下的 untracked / modified 檔案（BE working tree 現有一批與 FR-058 無關的既有檔案，不歸本批管、不要清理）
- **跨 repo 各自分開 commit**：BE 與 evidence-agent 的改動不混在一個 commit 敘事裡
- **憑證禁入版控**：密碼、token、API key 絕不寫進任何會 commit 的檔案（含 SQL migration、docs、handoff）。連線資訊只寫帳號 / host / port / db 名，密碼一律寫「請查 `.env` 或部署文件」
- **DDD 層級規範**（T-4.1 會踩到）：route 層不做 DB 查詢；app service 不直接 import ORM model，走 domain service → repository；新增 domain service 依賴時在 `__init__` 加參數並到 `di_containers/` 對應 container wiring
- **收尾類動作等命令**：SPEC / SUMMARY / memory / Notion 母案收尾一律等決策者明確下令，子任務 commit 完只回報 status，不往收尾跑
- **Notion 卡回寫格式（每個子任務完成後）**：狀態改「**修正待驗證**」+ 補一段**白話說明**（做了什麼、怎麼驗證的）+ **commit hash** + **執行紀錄**（跑了哪些檢查、結果）。格式參考第一批 CM-967〜970 的回寫
- **BE 出錯先看 log**：`tail -200 log/app.log | grep -A 30 -i 'Traceback\|ERROR'`，log 沒線索才回報
- **遇到 D1–D12 沒涵蓋的新決策**：**停下來回報，不要自己開新方向**（判斷原則見 design.md §2 各條的「被排除方案與原因」欄）
- **不要晶晶體**（中英夾雜語法），不用「速贏」一詞

---

## §7 與第二批的協調注意（兩批可能同時進行）

第二批（InSpec/CINC → GCB，CM-971〜979，派工單 `2026-07-30-fr058-batch2-dispatch.md`）與本批**無功能依賴、可同時進行**，但兩批都會動 **evidence-agent** 這個共用 repo。

### 7.1 不會衝突的部分

| 項目 | 為何不衝突 |
|------|-----------|
| connector 檔本體 | 第二批新增 `inspec.py`，本批新增 `nmap.py`——不同檔案 |
| 單元測試檔 | `test_inspec_connector.py` vs `test_nmap_connector.py`——不同檔案 |
| BE seed SQL | 各自新檔（檔名帶 `fr058-<序>-<工具>`），內容互不相干 |
| BE 平台邏輯 | 本批動 `start_execution()` 憑證檢查與 `detection_tools` schema；第二批只 seed 不動 BE 程式碼 |

### 7.2 ⚠️ 三處會撞的共用檔

| 檔案 | 兩批各自要做什麼 | 處置 |
|------|-----------------|------|
| `core/task_executor_connectors/__init__.py` | 第二批加 `_build_inspec` + `"inspec"` 一列；本批加 `_build_nmap` + `"nmap"` 一列 | 都是「加一個 builder 函式 + dict 加一列」，語意上不衝突，但**文字上會撞同一個區塊**。commit 前先 `git pull` / `git log --oneline -5` 看對方有沒有先動；若已被改，把自己的那一列加上去即可（**不要覆蓋對方的**） |
| `Dockerfile` | 第二批加 CINC Auditor；**本批不動**（Nmap 不 bundle，§2.3） | 本批不碰即無衝突。**若你發現自己在改 Dockerfile，先確認自己不是在裝 nmap** |
| 版號（四處同步） | 兩批各自要 bump 一版 | **先 `git log` + 看 `pyproject.toml` 的 `version` 確認目前版號再決定自己 bump 到多少**（第一批已到 0.2.13；先出貨者拿 0.2.14，後者順延 0.2.15）。不要拿本文的數字當現況 |

### 7.3 操作紀律

- **commit 前先 `git log --oneline -5`**（agent repo），確認對方的 commit 有沒有進來
- **顯式 `git add <檔名>`，絕不 `-am`**——`-am` 會把對方正在改的檔案一起帶走
- 若真的撞成 merge conflict：**兩邊的內容都要保留**（兩個 builder、兩列 dict），不要二選一
- BE repo 兩批共用 `feature/FR-058` branch，但改動範圍不重疊（本批動 detection_tools schema + orchestration service；第二批只加 SQL 檔），一樣照顯式 git add 原則

---

## §8 給 fresh session 的超短派工 prompt（直接複製貼上）

```
接手 FR-058 第三批（Nmap）的實作工作。

完整讀序入口：
/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-058-2607-detection-tools-expansion/handoff/2026-07-30-fr058-batch3-nmap-dispatch.md
先讀它全份（特別是 §2 Nmap 的特殊性與 §3 第一批新事實），再照它的 §0 讀序開
design.md §4.5 + §5.7 + §2 的 D9。

範圍：T-4.1〜T-4.3（CM-981〜983，母卡 CM-980），序列執行。
與第二批（InSpec/CINC → GCB）完全獨立、可並行，但 evidence-agent 有三處共用檔會撞
（factory __init__.py / Dockerfile / 版號），處理方式見文件 §7。

硬紅線速記：Nmap 走 connection_type='SSH'（2026-07-30 D9 改案，原定 CLI 已作廢）——nmap 由
客戶裝在自己指定的主機上，agent 走 SSH 登入該主機執行，與 OpenSCAP 完全同構，connector 主要
對照 openscap.py 而非本機子行程模式；Nmap 需要 SSH 憑證，seed 時 requires_credentials=TRUE。
Nmap 不 bundle 進 agent image（NPSL v0.95 §3 的衍生作品條款明文涵蓋「專門執行本軟體並解析其
結果」的程式，正中我們的 connector），也不可用 volume 掛載主機 binary 繞過；probe() 要四段式
分類（SSH 連線 / 認證 / 該主機 nmap 可執行 / 權限），第三段訊息要能讓管理員知道「去那台主機裝
nmap」；setup_guide 要寫在哪台主機裝、怎麼裝（無 docker 操作）+ 埠掃描的授權警語。
T-4.1 的零憑證放行照做但 Nmap 已不是它的使用者——那是平台級能力，既有工具（含 Nmap）都必須
維持「憑證為空即擋」，新欄位預設往嚴的方向；不建空的租戶設定當佔位。
param_schema 避免出現必填的 number/boolean 欄位（FE 驗證用 falsy 判斷，0/false 會被誤判為
缺漏，詳見文件 §3.4）；新 connector 依 code 註冊，絕不加進 _LEGACY_TOOL_ID_TO_CODE。

注意 connection_type='CLI' 至今仍無任何實際使用者（改案後 Nmap 不再是它的首個使用者），
本批不會改變這個事實；走 SSH 是已被 OpenSCAP 實戰驗證過的路徑，照抄即可（文件 §2.1）。

測試紀律：端到端驗收整批延後到全案最後，開發期間不重建 image、不部署、不跑真實掃描；
只做 import 檢查 + 全 mock 單元測試（假 XML fixture）+ SQL 套 DEV 後 SELECT 驗證；
T-4.1 額外必做「既有需憑證工具仍被擋」的迴歸驗證；migration 三環境都要套，
且 T-4.1 是 ALTER TABLE，三環境套完確認無誤才開始 T-4.2。

BE 在 feature/FR-058 工作，不切 branch、不 push、顯式 git add 禁 -am；每個子任務完成即
commit 並回寫 Notion 卡（修正待驗證 + 白話說明 + commit hash + 執行紀錄）；收尾動作等我下令。
```

---

> 📌 **寫完自檢**：下個 session 只看這一份 + design.md §4.5 / §5.7，能不能不回頭問任何人就開工？——本文所有路徑 / 卡號 / commit hash / SQL 指令皆可直接使用；密碼類資訊一律指向 `.env`。
