# FR-177 B6b 掃描報告：主系統通道、設定拉取、心跳狀態、工具 schema

> ⚠️ **歷史紀錄**：本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線（修正卡 CM-3559～3564、3573～3576、3584，驗收紀錄見母卡 CM-3507）。文中的檔名與行號是掃描當下（commit `bab2aa3`）的版本，修正後已變動，請勿拿來定位現行程式。

## 🟢 一句話結論

**這一棒沒有要立刻處理的問題。** 工具找到 0 個問題，「重點看什麼」7 項我逐條人工查過，也沒有成立的漏洞。但這不等於這塊完全乾淨：工具一條候選都沒提出，檢查員面板（三個獨立檢查員投票的那一關）沒東西可投。另外有 3 件「設計上就信任主系統」的事，寫在文末「留意事項」，給決策者知道邊界在哪，不算漏洞。

## 這一棒在檢查什麼

- Agent 連主系統的唯一出口 `cloud_http.py`：TLS 驗證（確認對方真的是主系統的那道檢查）、逾時、DPoP 標頭（每個請求現簽的「持有私鑰證明」，綁這次的網址，防止被重送）；
- `cloud_channel.py`：派工回報、上傳、串流下載都走這條；
- 設定通道 `config_pull.py`：Agent 定期向主系統拉設定，逐類交給「套用者」（applier，負責把某一類設定寫進本機）；
- 自報狀態 `agent_status.py`：Agent 回報給主系統的狀態欄位；
- 工具表單定義 `tool_schemas.py`、顯示名 `agent_name.py`、下載加密開關 `file_protect_sync.py`、金鑰頁 `keys_view.py`、主控台票券入口 `console_ticket.py`；
- sync 模組的組裝點 `sync/__init__.py`。

範圍共 10 檔、約 1006 行。為了查證呼叫端，我有翻範圍外的 `sync/app/enroll.py`、`identity/__init__.py`、`console/api/__init__.py`、`scan/app/secret_form.py`、`platform/ops.py`、`jedi_agent_protocol/events.py`。這些檔只為查證而讀，不當作發現。

## 掃到什麼：總覽表

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 嚴重度 |
|---|---|---|---|---|---|---|
| — | 工具沒有提出任何候選問題 | — | — | — | — | — |

沒有 HIGH／CRITICAL，所以不需要逐條開檔核對；「重點看什麼」的人工查證見下一節。

## 「重點看什麼」逐條人工查證

1. **`cloud_http.py`：能不能關掉驗證、逾時多少、會不會跟著跳轉、DPoP 綁什麼**
   - `request`（`:49-55`）和 `stream`（`:71-77`）建 `httpx.Client` 時，`verify` 寫死為 `cloud_tls_verify(cfg)`。呼叫端多帶的 `**kwargs` 只會傳給 `client.request(...)`，而 httpx 的 `request` 不收 `verify`，所以沒有任何參數能覆寫驗證。
   - 逾時：`request` 預設 30 秒；`stream` 規定呼叫端一定要給（`cloud_channel.get_stream` 給的是 connect 30／read 120 秒）。
   - 跳轉：`httpx.Client` 預設 `follow_redirects=False`。全 repo grep 過，範圍內四個呼叫點（`config_pull.py:140`、`enroll.py:116`、`cloud_channel.py:67`、`:105`）都沒有開它。3xx 不會被跟著導到 http 或別的主機。
   - DPoP：`auth_headers(method, url, ...)` 的 `url` 是完整網址 `cfg.cloud_endpoint + path`（`:52`、`:73`），不只是 path。query 另外交給 httpx 接上，不進簽章範圍，這是刻意設計（`cloud_channel.py:63` 有註明）。
   - 小留意：`request` 的 `**kwargs` 理論上可以帶 `follow_redirects=True`，目前沒有任何呼叫端這麼做。
   - 結論：**不成立**。

2. **`config_pull.py`：主系統能遠端改哪些設定**
   - 全部 appliers 共 13 類：sync 9 類在 `enroll.py:252-262`，identity 4 類在 `identity/__init__.py:56-59`。
   - 會動到門禁的有這幾類：
     - `allowed_targets`／`allowed_target_names`：決定 Agent 能連誰（B6a 在看）；
     - `console_allow_cidr`：只能收窄（A7 已核）；
     - `offline_operators`、`offline_login_days`：離線期間誰能登入、能登幾天（A7）；
     - `ldap_config`：LDAP 連線設定；
     - `trust_anchors`：Agent 信任哪幾把主系統公鑰（`cloud_trust.apply_trust_anchors`，範圍外）；
     - `file_protect`：見第 6 項。
   - 其餘 `error_tracking`、`vulndb`、`profiles`、`tool_schemas`、`agent_name` 不直接開門。
   - 遠端**改不到**：`cloud_endpoint`（只從 env `CLOUD_ENDPOINT` 讀，`config/config.py:167`）和 CIDR 安裝底線。
   - 套用失敗的處理：逐類各自處理（`:109`、`:121-134`）。一類失敗只記 log、不記那一類的版本，下次重給；其餘類照樣套用。所以不是整批回滾，而是「一類一類各自生效」。檔頭有寫明，這是 SR-04.6 要求的「各 observer 互不影響」。
   - 回放舊版：`versions` 只是 Agent 告訴主系統「我手上有哪版」，Agent 不檢查新收到的版本有沒有比較新。但要回放，得先能冒充主系統的 TLS 回應，而通道有 TLS 驗證加 DPoP。主系統本身若被攻下，本來就能直接下新設定，不需要回放。
   - 結論：**不成立**（主系統被攻下時的放大效應，見「留意事項」①）。

3. **`agent_status.py`：心跳送出哪些狀態欄位，有沒有洩漏內網拓撲**
   - 送出的欄位受契約 `AGENT_STATUS_KEYS` 限制（`jedi_agent_protocol/events.py:39-43`），契約外的欄位在 `snapshot()`（`:36-42`）會被丟掉。
   - 契約內共 15 個欄位：`vulndb_date`、`credential_store`、`configured_tools`、`storage`、`browser_port`、`outbox_size`、`local_admin_set`、`local_admin_password_set_at`、`ldap_source`、`sync_state`、`last_restart_reason`、`console_allow_cidr_installed`、`name`、`name_updated_at`。
   - 沒有內網 IP、主機名或磁碟路徑這類欄位。`name` 只在使用者從主控台改過名、還沒回報時才帶（`agent_name.py:88-93`），不會自動送出主機名。
   - 沒逐支追的：`storage`、`console_allow_cidr_installed` 的內容由範圍外的模組產生，沒有逐支追到底。依欄位名稱推，分別是儲存統計與安裝時設定的 CIDR 白名單，後者本來就是主系統要知道的設定值。
   - 結論：**不成立**（`storage` 欄的實際內容留給 files 模組那一棒確認）。

4. **`tool_schemas.py`：工具表單定義有沒有簽章、`default`／`options` 會不會被當成可信值**
   - 沒有簽章，只靠通道（TLS＋DPoP）保護。`apply`（`:69-77`）只檢查形狀（是 dict、`tools` 是 list），就原子寫進檔案。
   - 使用端在 `scan/app/secret_form.py`（範圍外）：
     - `options` 只用來限制下拉選單能選的值（`:65-66`）；
     - `default` 在使用者沒填、而且不是祕密欄時，當成欄位值存起來（`:89-90`）。也就是說，主系統給的 `default` 會一路流到 connector 的參數。
   - 能不能拼進指令要看各 connector，屬於第三期 connectors 的範圍，這裡不判。
   - 結論：**不成立**（不是這一層的漏洞；風險轉交第三期，見「留意事項」②）。

5. **`keys_view.py:38`：不驗簽解通行證，會不會把 token 本體回給主控台**
   - `_pass` 只回 `not_after`（從 `exp` 換算）和 `issuer`（`iss`），不回 token 原文。
   - `build` 回的欄位：
     - `certificate`：只有到期日與換證狀態；
     - `trust_anchors`：`ring.describe()`，依檔頭寫法只有 kid／用途／指紋；
     - `enrollment`：`token_configured` 是布林值，不是 token 本身；
     - `main_system`：主系統網址。
   - 不驗簽只用在畫面顯示，沒有拿來當授權依據。
   - 結論：**不成立**。

6. **`console_ticket.py`、`file_protect_sync.py`**
   - 票券入口：`console_ticket` 整支模組直接別名到 `agent_auth`。呼叫端 `console/api/__init__.py:106-108` 傳的 `agent_uid` 是 `state.agent_uid`，也就是本機註冊狀態，不是從請求讀來的，攻擊者控制不了。
   - 下載加密能被關掉嗎：**能**。主系統送 `{"enabled": false}`，主控台下載就改給明文（`:49-66`）。這是 FR-169 D6 的設計：開關本來就由主系統決定，會記 log「關閉（主控台下載改給明文）」。
   - 會不會不小心變成不加密：不會。檔案不存在、壞掉、形狀不對，一律當作開啟（`:44-46`），不會因為格式錯誤而誤關。
   - 結論：**不成立**（主系統能遠端關加密，見「留意事項」③）。

7. **`sync/__init__.py`：模組註冊、`services()` 持有什麼、背景元件誰起停**
   - `SyncServices` 只持有 4 把鎖（`:19-30`），沒有帳密或其他狀態。
   - `config_pull.COMPONENT` 和 `agent_status.COMPONENT` 在 `enroll.py:376-380` 接好設定、交進元件清單，由 platform 統一呼叫 `start()`／`stop()`。
   - `ConfigPull` 自己開的保底計時器 thread，會在 `stop()` 裡收掉（`config_pull.py:72-77`）。
   - `clear_tenant_files`（`:113-119`）用的路徑來自 env `AGENT_CERT_DIR` 加固定檔名，外部輸入控制不了。
   - 結論：**不成立**。

## 留意事項（設計上的邊界，不是漏洞）

這三件都屬於「主系統是信任根」的既定設計。列出來是讓決策者清楚：主系統一旦被攻下，影響會放大到哪裡。

- ① **主系統被攻下時，影響會擴及所有 Agent。** 設定通道能改授權清冊、離線操作者、LDAP 設定、信任公鑰（trust anchors）、下載加密開關。改不到的是 `cloud_endpoint` 和 CIDR 底線，而且 CIDR 只能收窄。其中「信任公鑰也能由主系統下發」最值得注意：主系統能換掉 Agent 信任的簽章鑰。這部分在範圍外的 `cloud_trust.py`，建議下一期專門看。
- ② **工具表單定義沒有簽章**，`default` 值會流進 connector 的參數。第三期掃 connectors 時要對照：connector 有沒有把參數放進參數陣列、有沒有拼成字串。
- ③ **主系統能遠端關掉主控台下載加密**（FR-169 D6 的設計），關掉時有 log 可查。

## 這份結果可信到什麼程度

- **「找到的存在嗎？」** 不適用，因為工具沒找到任何東西。
- **「只有這些嗎？」** 不能保證。這次是低強度掃描：只派一位研究員、他沒提出候選，面板 0 票。「沒東西可投」不等於乾淨。人工也只照「重點看什麼」7 項查證，結論全部來自讀程式碼，沒有跑測試、沒有實際打過。

## 執行概況

| 項目 | 值 |
|---|---|
| run ID | `wf_8e2e3ef2-e8f` |
| 掃描版本 | `63334f464215c18fa89c334b360a86f31679c1f8`（工作目錄乾淨） |
| 強度與範圍 | low，10 檔 |
| verification.status | `verified`（reason_kind 為 null） |
| candidates（候選問題） | 0：沒東西可投，不等於乾淨 |
| panel_votes（檢查員投票） | 0 |
| researchers_dispatched / returned（派出／回來的研究員） | 1 / 1 |
| 耗時 | 約 7.6 分鐘，研究員約 8.4 萬 token |
| 原始產物 | `docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-174550/` |
