# FR-057 手測驗收棒 — 換 session 交接（2026-07-29）

| 項目 | 內容 |
|------|------|
| 緣由 | FR-057 八張子任務卡全部完成（修正待驗證），三 repo 程式碼已就位、單元測試綠、主 session 已做獨立抽查；**尚未經 user 手測驗收**。本棒接手做端到端手測與問題修正 |
| Branch | BE `feature/instance-agent`（10 commits 未 push）／FE `feature/instance-agent`（2 commits 未 push）／agent repo（4 commits，working tree 乾淨）|
| 本棒角色 | **手測驗收 + 修問題**，不做收尾（spec/SUMMARY/進版一律等 user 下令）|
| 接手前必讀 | 本文 §0 讀序 |
| 預估時間 | 討論 + 升 image 約 1h；端到端手測視問題量 |

## 🧭 原始需求 / WHY

**FR-057 = 在 FR-056 檢測工具整合平台上接入第二個掃描工具 OpenSCAP（CIS/STIG 組態合規稽核），並開出 connector 第三種連線型態 SSH。**

大圖：FR-056（已隨 v1.11.0 上線）讓租戶設定檢測工具 → 任務按「開始執行」→ 客戶端 Agent（evidence-agent，Docker）心跳領派工 → 掃描 → 報告自動回收成任務證據。第一版只有 OpenVAS（API 型）。本案價值：①客戶多了組態合規稽核能力（不只弱點掃描）②connector 架構證明可擴（SSH 型開出後，未來 Lynis 等 CLI/SSH 工具同軌）。

關鍵設計事實（討論中 user 反覆確認過的）：
- OpenSCAP 是純 CLI 無 API、無 daemon。組態稽核必須以登入身分在目標 OS 上執行——物理限制，全業界一致。**目標主機需一次性裝 `openscap-scanner` + SCAP content（SSG）**，這是官方正規用法（Red Hat Satellite 同模式），不是我們的妥協。Agent 維持 Docker 形態不變，只當掃描發起端。
- 憑證＝**租戶層一組共用稽核帳號**（Tenable 業界標準），金鑰/密碼二擇一推薦金鑰 + `use_sudo`，沿用 FR-056 既有 Fernet 加密鏈與 D9 派工下發。
- SCAP content 用**目標主機自帶的 SSG**（D3），不由平台推送 → 免維護版本矩陣。
- 證據＝**每台一份 HTML 原樣上傳**（D4/D5）→ 牽動 result 回收鏈由單檔擴多檔（FR-057.3 的由來）。
- **D3 實作澄清（2026-07-29，commit `3a67e938`）**：官方 `oscap-ssh` 腳本語意是「本機 content 推送到遠端」，與 D3 矛盾 → 定案 connector **自組 SSH 指令（paramiko）**直接在目標上執行 oscap 引用目標本地 content。D3 核心價值不變。

## §0 接手讀序

1. 🔒 **先懂需求 gate**：本文「🧭 原始需求」全讀 + `docs/features/FR-057-2607-openscap-ssh-connector/design.md` §1–§2（背景 + D1–D7 + 變更紀錄的 D3 澄清）全讀
2. 本文 §1（現況）→ §2（connector 實際行為）→ §3（本棒工作順序）
3. 需要細節才開：design.md §4（詳細設計）／`~/Projects/Billows/Audit-Manager/evidence-agent/core/task_executor_connectors/openscap.py`（458 行，含大量設計註解）
4. Notion 母案：https://app.notion.com/p/3ab346da4cd081af9cfac78397f7acfb（data source `collection://23c346da-4cd0-8041-955e-000bb6976dd2`）

**冷接自檢**：①FR-057 解決什麼使用者問題？②為什麼不用官方 `oscap-ssh` 而自組 SSH 指令？③為什麼每台一份 HTML 而不是合併單檔？④本棒該做什麼、不該做什麼？——答不出回去讀 §0.1。

## §1 現況：八張卡全完成（修正待驗證），未手測

| 子需求 | 卡號 / 子任務 | Repo | commit |
|--------|--------------|------|--------|
| FR-057.1 BE/FE 地基 | CM-942 T-1.1 migration seed | BE | `768aa289` + `3e30d99d`（required 語意修正）+ `de979c30`（setup_guide 落地鏈路）|
| | CM-943 T-1.2 FE 設定頁欄位型態 | FE | `d3dc91a` |
| | CM-944 T-1.3 FE 任務參數 select_or_text | FE | `e77d303` |
| FR-057.2 Agent connector | CM-945 T-2.1 骨架 | agent | `2ec3c5b` |
| | CM-946 T-2.2 `run()` | agent | `21e157b` |
| | CM-947 T-2.3 `probe()` | agent | `b628b1c` |
| FR-057.3 多檔回收鏈 | CM-948 T-3.1 agent 多檔上傳 | agent | `a14ab28` |
| | CM-949 T-3.2 BE 多檔取回 | BE | `c20b22a2` |

母案 CM-938 與三張子需求卡 CM-939/940/941 仍 Not started——**收線時才動**（等 user 下令）。

**主 session 2026-07-29 獨立抽查結果**（已實查，非 runner 自報）：
- agent repo working tree 乾淨，四個 commit 都在
- 測試綠：`cd ~/Projects/Billows/Audit-Manager/evidence-agent && PYTHONPATH=. poetry run pytest test/ -q` → **122 passed, 1 skipped**
- factory `core/task_executor_connectors/__init__.py` 的 `_TOOL_ID_TO_CODE` 已加 `4: "openscap"`，且 `.2` 沒動 `core/task_executor.py`（檔案邊界正確）
- 實作有落實 D3 裁決：自組 SSH 指令（paramiko）非 oscap-ssh、exit code (0,2) 視為成功、遠端 mktemp 暫存 + finally 清理且等 `recv_exit_status()`、`shlex.quote` 全參數、私鑰 `/dev/shm` 暫存
- ⚠️ **已知小問題（不擋事，列 follow-up）**：agent repo 缺 `pytest.ini`/`conftest.py`，裸跑 `pytest test/` 會 `ModuleNotFoundError`，必須帶 `PYTHONPATH=.`

## §2 connector 實際行為（讀 openscap.py 後寫實，供討論掃描指令用）

實際送到目標主機的掃描指令（`_scan_one_host`，所有參數過 `shlex.quote`）：

```
sudo /usr/bin/oscap xccdf eval --profile '<profile id>' --report '<遠端 mktemp .html>' '<content 路徑>'
```

- `sudo` 前綴只在 `use_sudo=true`（預設 true）時加；`/usr/bin/oscap` 是寫死常數，對齊 setup_guide 腳本的 sudoers 白名單 `NOPASSWD: /usr/bin/oscap`
- 報告先在目標主機 `mktemp --suffix=.html` 建暫存（由**登入帳號**建立，oscap 以 root 開檔寫入不改擁有者，故登入者可讀回）；掃完 SFTP `stat` 顯式驗可讀性（讀不到給「sudo 情境下可能是暫存檔擁有者問題」明確診斷）→ 讀 bytes → `finally` 一律 `rm -f` 並 `recv_exit_status()` 等清理跑完才關連線
- **content 路徑解析**（`_resolve_content_path`）：路徑以 `.xml` 結尾 → **直接用**；否則視為目錄（含預設 `/usr/share/xml/scap/ssg/content/`）→ 遠端 `cat /etc/os-release` 取 `ID`/`VERSION_ID` → 組 `<目錄>ssg-<id><ver>-ds.xml`。版號格式：`ubuntu` 去點（`22.04`→`2204`），`debian/rhel/centos/rocky/almalinux/ol/fedora` 只取主版號。**ID 不在表內或缺 VERSION_ID → 明確 raise 叫使用者直接填完整檔案路徑，不靜默猜檔名**
- **exit code**：`0`（全 pass）/`2`（有 fail 但掃描成功）視為成功；其餘 raise 並附 stdout 末 500 字
- **取消/逾時**（`_exec_with_cancel`）：邊跑邊排空 stdout/stderr 避免 paramiko 管線死鎖（stderr 讀完丟棄不解析），每 0.5s 輪詢 `cancel_event` 與 deadline，命中則 `channel.close()` 並 raise `ScanCancelledError`/`TimeoutError`
- **summary**（`_parse_stdout_summary`）：先把 `\r` 去掉再 splitlines（否則 `Result\r\tpass` 會被拆行破壞比對），取 `Result` 開頭行的值，`pass/fail/notapplicable` 各自計數、其餘（unknown/error/notchecked/…）一律進 `error` 桶；解析失敗回全零不拖垮派工。**每筆 ScanResult 只放該台統計**（`findings=fail`、`hosts_scanned=1`），多台加總由 `task_executor._merge_summaries()` 做——connector 不可自己加總，否則 N 倍虛報
- **多台策略**：hosts 以 `[,;\s]+` 正規化；單台失敗（連線/認證/執行/逾時）記 warning 續掃下一台，只有 `ScanCancelledError` 中止整個 run()；**全部失敗才整體 raise**（不靜默回空）
- **檔名**：`OpenSCAP掃描報告_<host>_<YYYYMMDD>.html`
- **私鑰**：`tempfile.mkstemp` 寫進 `/dev/shm`（不存在才 fallback 系統 tmp）+ 顯式 `chmod 0600` + try/finally 刪；paramiko `look_for_keys=False`/`allow_agent=False`（不撿本機其他身分）、`AutoAddPolicy`（第一版接受首次連線風險）
- **`probe()`**：需 `params.host`（**單數**，只測一台）→ ①②連線+認證（`AuthenticationException` → 「認證失敗」；`SSHException`/`OSError` → 「無法連線」）③ `command -v /usr/bin/oscap` 空 → 「未安裝 openscap-scanner」④ `test -s <解析後 content>` 非 OK → 「缺少 SCAP content」。四段任一失敗即 raise 不續探

## §3 本棒工作順序

1. **與 user 討論掃描指令細節與預期問題**（user 明確要求的下一個議題）——素材見 §2，重點可能落在：profile id 是否對得上目標機實際 SSG 版本、`content_path` 用預設目錄推斷 vs 直接填檔案、sudo 白名單只涵蓋 `/usr/bin/oscap` 的邊界、逾時 3600s 是否合理
2. **升級 192.168.50.123 上的 agent image**（現行部署 `0.2.8` **不含** openscap connector）→ 見 §5，**版號 bump 與升級步驟未定，需與 user 確認**
3. **端到端手測**：設定頁填憑證 → 測試連線（probe）→ 建任務選 OpenSCAP + hosts + profile → 開始執行 → 驗證每台一份 HTML 報告進證據池
4. 發現問題 → 開 Notion case 修 → **全綠後才走收尾（spec/SUMMARY/進版），等 user 下令**

## §4 測試環境（user 2026-07-29 已備妥）

| 角色 | 主機 |
|------|------|
| 管理機 / Agent 主機 | `192.168.50.123`（agent 跑在該機 Docker）|
| 目標主機 ×5 | `192.168.50.123`（agent 宿主機自己）、`.151`、`.188`（DEV/STG DB）、`.171`、`.189`（POC DB）|

- 各台已建 `audit-scan` 帳號 + 佈公鑰 + sudoers 白名單（`NOPASSWD: /usr/bin/oscap`）+ 裝 `openscap-scanner` 與 SSG content
- 部署腳本：123 上的 `~/deploy-openscap.sh`（自包含、冪等可重跑；原始檔也在 user Mac 桌面）
- 稽核金鑰：123 上 `~/.ssh/openscap-audit-key`（私鑰，要貼進 Guidant 設定頁）／`.pub`（已佈五台）
- ⚠️ **已知環境事實**：`.151`/`.188`/`.171`/`.189` 四台**共用同一組 SSH host key 指紋**（同一 VM 映像克隆未重產）。connector 用 `AutoAddPolicy` 不會因此擋連線，但 CIS/STIG benchmark 會掃成 finding——**屬預期，不是 bug**

## §5 Pre-flight（可直接複製）

```bash
# ① BE：branch / 未 push commits / 服務
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git rev-parse --abbrev-ref HEAD          # 應為 feature/instance-agent
git log --oneline origin/feature/instance-agent..HEAD | cat   # 應 10 筆
lsof -i :8000 | head                      # BE 是否在跑（沒跑：python main_app.py）

# ② agent repo：測試（必須帶 PYTHONPATH=.，repo 缺 conftest.py）
cd ~/Projects/Billows/Audit-Manager/evidence-agent
git status --short && PYTHONPATH=. poetry run pytest test/ -q

# ③ DEV DB 查 openscap seed（密碼查 .env DB_SECRET，勿寫進任何檔案）
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "SELECT id,code,connection_type,status FROM config.detection_tools ORDER BY id"
# 預期：4 | openscap | SSH | available（id 必須是 4，agent factory 固定映射）

psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -c "SELECT dt.code, s.version, s.is_current FROM config.detection_tool_param_schemas s
      JOIN config.detection_tools dt ON dt.id = s.detection_tool_id WHERE dt.code='openscap'"

# ④ 目標主機就緒驗證（在 123 上跑，逐台換 IP）
for h in 192.168.50.123 192.168.50.151 192.168.50.188 192.168.50.171 192.168.50.189; do
  echo "=== $h ==="
  ssh -i ~/.ssh/openscap-audit-key -o StrictHostKeyChecking=no audit-scan@$h \
    'sudo /usr/bin/oscap --version | head -2; ls /usr/share/xml/scap/ssg/content/ | head -5; grep -E "^(ID|VERSION_ID)=" /etc/os-release'
done

# ⑤ agent 版本 / 容器狀態（在 123 上）
docker ps --filter name=agent --format '{{.Names}}\t{{.Image}}\t{{.Status}}'
```

## §6 Agent image 升級（**待確認，以下為推測步驟**）

現行 123 部署的是 `guidant-ai-agent:0.2.8`，**不含** openscap connector（connector 是 0.2.8 之後才進的四個 commit）。推測路徑（開工前先與 user 確認版號與出貨方式）：

```bash
# 開發機 build（依賴 .secrets/nexus.env，帳密查部署文件，不入版控）
cd ~/Projects/Billows/Audit-Manager/evidence-agent
IMAGE=guidant-ai-agent:0.2.9 ./rebuild.sh

# 出貨到 agent 主機
docker save guidant-ai-agent:0.2.9 | ssh <user>@192.168.50.123 docker load

# 123 上改 deploy/.env 的 AGENT_IMAGE 後起服務（必指定 service 名，避免 recreate 同 compose 的 DB）
docker compose up -d file-agent
```

**待確認事項**：①是否 bump 到 0.2.9（版號要同步五處：`pyproject.toml`、`deploy/docker-compose.yml` ×2、`config/config.py` 預設值——前次 e59c9fc 就是補這個漏）②123 是否能直接從開發機 `docker save | ssh` ③agent 心跳間隔吃部署機 `agent.json` 快取，改環境變數可能無效（memory `reference_agent_heartbeat_interval_cache`）。

## §7 行為規範提醒

- **不切 branch**（三個 repo 都已在 `feature/instance-agent`，branch 不對停下問 user）
- **不自動 push**（BE 10 / FE 2 commits 未 push，等 user 明示）
- **顯式 `git add <檔名>`，禁用 `-am`**
- **憑證絕不進版控**：私鑰、DB 密碼、Nexus 帳密一律「查 `.env` / 部署文件」
- **改 agent code 要 rebuild image 才生效**；改 BE service code 要重啟 BE（Claude 負責重啟，FE 由 user 自理）
- **BE 出錯先看 log**：`tail -200 log/app.log | grep -A 30 -i 'openscap\|detection\|Traceback\|ERROR'`，不先問 user
- **收尾等命令**：手測全綠後不自動寫 spec/SUMMARY/改 Notion 母案狀態/進版

## §8 不在本棒 scope

- install.sh 安裝精靈（user 明示最後再做）
- Nessus / SonarQube connector、ARF findings 結構化解析、content 平台管理、host inventory 表、Push 模式（D7 明列第一版不做）
- FR-057 全案收尾文件（spec / SUMMARY / 進版）——手測全綠 + user 下令才做
- agent repo 補 `pytest.ini`/`conftest.py`（列 follow-up，不在本棒動）

## §9 未 push commits 清單

**BE `feature/instance-agent`（10 筆）**：`3a67e938` D3 實作澄清／`c20b22a2` T-3.2 多檔取回／`3e30d99d` T-1.1 required 語意修正／`7e93be34` 前一棒 handoff／`de979c30` setup_guide 落地鏈路／`92b28070` big-feature-workflow skill／`768aa289` T-1.1 migration seed／`8c17aefa`+`81301ee1` mermaid 修復／`3e39607e` design 初版

**FE `feature/instance-agent`（2 筆）**：`e77d303` T-1.3 select_or_text／`d3dc91a` T-1.2 設定頁欄位型態

**agent（4 筆，working tree 乾淨）**：`b628b1c` T-2.3 probe／`21e157b` T-2.2 run／`2ec3c5b` T-2.1 骨架／`a14ab28` T-3.1 多檔上傳

## §10 給 fresh session 的超短 prompt

```
請讀 ~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-057-2607-openscap-ssh-connector/handoff/2026-07-29-fr057-manual-test-handoff.md，
先過「🧭 原始需求」與 §0 冷接自檢（答不出先回去讀 design.md §1-§2），
再讀 §2（connector 實際掃描指令行為）與 §5 pre-flight。
你是手測驗收棒：先跟我討論掃描指令細節與預期問題，再升 123 上的 agent image，然後端到端手測。
不做收尾（spec/SUMMARY/進版/Notion 母案）除非我明確下令；不 push、不切 branch。
```
