---
title: D1 檢查結果：檢測工具目錄、租戶憑證與守門殼（jedi-detection）
---

# D1 檢查結果：檢測工具目錄、租戶憑證與守門殼（jedi-detection）

> 檢查日期 2026-09-17｜對應卡片 CM-1873｜檢查範圍 36 個檔案

## 🔴 一句話結論

**找到 4 條發現、其中 1 條高風險。** 高風險那條是「測試連線」這個按鈕背後的端點忘了檢查權限——**客戶公司裡任何一個登得進系統的人，都能叫系統把存好的掃描工具帳號密碼解密，送到他自己指定的一台電腦去**，等於把客戶整批機器的維運帳密拱手送人。另外三條中風險分別是：那個端點的目標主機完全照使用者填的走（把客戶內網的代理程式變成任意連線跳板）、資料庫的客戶隔離規則寫反了方向（子公司反而看得到母公司的資料）、以及掃描歷史紀錄少了專案參與者檢查。**高風險那條修法很小，就是補一行權限宣告，建議優先處理。**

## 這一棒在檢查什麼

客戶要用系統做弱點掃描之前，得先在設定頁填好掃描工具的帳號密碼（例如 SonarQube 的權杖、連 Linux 主機的 SSH 帳密、連 Windows 的管理員密碼），系統會加密存起來。這一棒看的就是這批帳密的整條路：**存進去、讀出來、送到畫面上、寫進系統日誌**，每一段有沒有漏；「測試連線」這個功能會不會被人拿來當跳板；誰能改別家客戶的設定；以及整支套件的守門機制與資料庫隔離規則。

掃描目標 `/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-detection`，revision `6119da51d488`（branch `feature/review`，工作區乾淨），mode `scan`，scope 36 個檔案（檢測工具字典、租戶工具設定與憑證、任務層敏感參數、守門殼與路由表、五支資料庫變更腳本、插件組裝層），effort `low`，focus `attack-surface`。

## Coverage

`low` 強度＋設定了 focus：派出兩位研究員（一位通盤讀完 36 檔，一位專做密鑰專項），未做元件盤點、未做威脅建模，`completenessCheckOutcome` 為 `not-applicable`（低強度本就不跑盤點）。兩位研究員共提報 8 個候選，去重後 6 個，三位檢查員各自獨立投票共 18 票、全數投出，沒有候選漏投、沒有候選遺失、沒有嚴重度被降低。**6 個候選中 5 個通過、1 個被三位檢查員一致駁回**（詳見下方「被駁回的候選」）。通過的 5 條中兩條（F4／F5）是同一個問題的兩個角度，已依決策者指示併為一條，故本報告呈現 4 條。

工具沒有實際執行任何程式碼：沒跑測試、沒發請求、沒示範攻擊，所有發現都是讀原始碼推出來的。

**工具只碰到卡片列的八個重點裡的三個（①③⑤）。** ②④⑥⑦⑧ 五個重點工具完全沒有提出任何候選，全部由首腦回頭開檔查證補上，詳見下方「卡片重點逐項人工查證」段。

🔴 **兩點必須先講明白（本報告的打折處）：**

1. **工具的正式產物目錄沒有留下來。** 這次掃描跑到一半時，前一支 D1 session 被系統中止，該 session 在收尾時誤刪了報告目錄 `CLAUDE-SECURITY-20260917-014935/`，連帶帶走工具自己產的報告檔與驗證章。**這不是平行掃描造成的**（同時段另一支 D3 掃描有自己獨立的目錄，互不干擾）。本報告的發現內容與票數取自工作流實際回傳的結果，完整且未經改寫，但**沒有工具蓋的驗證章可供對照**。
2. **掃描期間程式碼有往前跑。** 開掃時 HEAD 是 `6119da5`，掃描結束時已是 `82d78bc`（中間三筆是分類容器的 commit，屬別線工作、不在本次掃描範圍）。**本報告描述的是 `6119da5` 這個版本**，行號請以該版本為準。

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - F1（測試連線無權限檢查）＝M03 第 2 條，✅ 已修（CM-2042）。
> - F2（測試連線目標主機照單全收）＝M03 第 6 條，✅ 已修（CM-2058）。
> - F3（客戶隔離規則方向寫反）＝M03 第 7 條，✅ 已修（CM-2271，BE `6c74ff0e3`／套件 `6e0d0d4b`，1.21.0 出貨）。
> - F4（掃描歷史缺參與者檢查）＝M03 第 12 條，✅ 已修（CM-2040）。

## Findings

### F1 — 「測試連線」端點沒有權限檢查，任何登入者都能叫系統把工具帳密解密送往他指定的主機（HIGH，confidence high）

**這是什麼問題。** 產品的檢測工具設定頁有個「測試連線」按鈕，用來確認填的帳密能不能連上。這個按鈕背後的 API **只檢查「有沒有登入」，沒有檢查「有沒有管理檢測工具的權限」**——而同一個檔案裡的新增、修改、重置三個功能都有檢查。更麻煩的是，這個 API 允許呼叫的人**自己指定要連到哪一台主機**，系統就會把加密存著的帳密解開、交給客戶內網的代理程式，由它拿著這組帳密去連那台主機。

**出事會怎樣。** 客戶公司裡任何一個登得進系統的人（包含權限被刻意限制到最小的唯讀稽核員、約聘人員），可以：先打清單 API 拿到所有工具設定的識別碼（那支也沒有權限檢查），再打測試連線 API 並把目標主機填成自己控制的一台機器。系統會**解密這家客戶的掃描帳密並送過去**——包含 SSH 密碼或私鑰、Windows 管理員密碼、OpenVAS／SonarQube／ZAP 的 API 權杖。攻擊者在自己那台機器上架個服務就能把明文密碼記下來，而**那組帳密正是用來掃描這家公司整批機器的**，等於一次拿到全公司維運通行證。此外還能任意覆寫別人設定上的「上次測試結果」欄位。

**要先有什麼才打得到。**
- 攻擊者持有這家客戶的任何一個帳號（**不需要**「管理檢測工具」這個權限），且該客戶的授權含 `plugin` 模組
- 這家客戶至少設定過一個帶帳密的檢測工具，且有一台上線中的代理程式
- 要拿到明文密碼的話，該工具需是走 SSH／WinRM 的類型（OpenSCAP／nmap／InSpec／GCB）且用密碼認證，並且代理程式連得到攻擊者那台機器

**在哪裡。** `jedi_detection/api/routes/detection_tool_route.py:126`（`TenantDetectionToolConfigTestConnectionRoute.post`）只掛了 `@require_license("plugin")`（這是「這家客戶有沒有買這個模組」的授權檢查，不是「這個人有沒有這個權限」）；對照同檔的 `:67` 新增、`:88` 修改、`:105` 重置，三支都多掛了一行 `@capability_required(lambda rt: rt.config.plugin_update_capability)`。解密與外送的實際位置在 `jedi_detection/app/service/detection_tool_service.py:158` 起的 `test_connection()`。同檔 `:53` 的清單端點也沒有權限檢查，是攻擊者取得識別碼的那一步。

**怎麼修。** 在 `TenantDetectionToolConfigTestConnectionRoute.post` 補上與三個兄弟端點一模一樣的那行 `@capability_required(lambda rt: rt.config.plugin_update_capability)`。清單端點 `TenantDetectionToolConfigsRoute.get`（會吐出設定識別碼與 `field_values`）建議一併補，但那屬產品決策（讀取端要不要守門），可一起裁。

**驗證。** 3/3 三位檢查員（可達性／影響／既有防護）一致確認成立，嚴重度維持 HIGH。

**首腦核對註記。** **屬實，逐檔開過行號全部對得上。** 這正是卡片重點①預判的那一點，而且坐實了它會解密憑證往外送，比卡片原先「回應會不會洩漏憑證」的疑慮更嚴重——洩漏不是透過回應，是透過連線行為本身。

### F2 — 測試連線的目標主機照單全收，把客戶內網的代理程式變成任意連線跳板（MEDIUM，confidence medium）

**這是什麼問題。** 上一條說的那個 API，目標主機是呼叫者在請求裡直接填的，系統**完全不檢查這台主機是不是這家客戶登記過的掃描目標**，就把它和解密後的帳密一起交給代理程式去連。而代理程式依設計就裝在客戶的內部網路裡。

**出事會怎樣。** 代理程式變成一支「代打任意連線」的工具：外面的人可以指使它去連內網任何位址。更細的問題是，回傳的訊息會分辨「連線被拒」「找不到路由」「認證失敗」「工具沒安裝」這幾種不同結果——**這等於一台內網掃描器**，攻擊者靠回應內容就能摸清內網有哪些主機、開了哪些服務。而且主機欄位可以一次填多台（用逗號分隔），一個請求就能探測一整段。

**要先有什麼才打得到。**
- 打得到測試連線這個 API（依 F1，不需要任何額外權限）
- 這家客戶有上線中的代理程式
- 該工具是會用到目標主機參數的類型（OpenSCAP／nmap／InSpec／GCB）

**在哪裡。** `jedi_detection/app/service/detection_tool_service.py:224`（`DetectionToolService.test_connection` 裡逐台發送 probe 的那段）；主機字串來自 route `:132` 的 `payload.get("host")`，中間只做了用逗號／空白／分號切開，沒有任何比對或過濾。

**怎麼修。** 把目標主機限制住，而不是照單全收：逐台比對這家客戶已登記的掃描目標（或明確的白名單），拒絕不合理的內外網跨越，限制一次最多幾台，並且把逐台的細部結果收斂成單一的成功／失敗，讓回應不再能當內網探測器用。

**驗證。** 3/3 三位檢查員一致確認成立，嚴重度 MEDIUM。

**首腦核對註記。** 屬實。與 F1 是同一個端點上的兩個獨立問題：F1 是「誰可以按」，F2 是「按下去可以打到哪」。**補 F1 的權限檢查並不會讓 F2 消失**——有管理權限的人依然能拿代理程式當跳板，只是門檻從「任何登入者」升到「工具管理員」。兩條要分開修。

### F3 — 資料庫的客戶隔離規則方向寫反，子公司反而拿得到母公司的資料（MEDIUM，confidence medium）

**這是什麼問題。** 系統用資料庫層的規則來隔離不同客戶的資料，判斷依據是「這個人屬於哪個組織路徑」。但這五張表的規則把路徑（例如 `/1/102/`）從中間切開，得到 `[1, 102]` 這串編號當作可存取清單——**於是屬於 102 的人，連編號 1（也就是它的母公司／平台根）的資料也一起拿到了**。正確的做法應該是「我自己和我底下的單位」，同一個檔案裡的基準庫相關規則用的就是正確寫法（`app_tenant_allowed_for_session()`），只有這幾張表用了切路徑的寫法。

**出事會怎樣。** 子單位的一般成員，對母單位（或平台根）的檢測工具設定與掃描執行紀錄，**可以查、可以改、可以刪**。搭配 F1 那條，還能把母單位的帳密解密後從自己這邊的代理程式送出去。

**要先有什麼才打得到。**
- 部署上存在母子關係（平台根加客戶，或客戶底下再分子單位）
- 攻擊者是子單位的一般成員
- 攻擊者取得母單位某一列的識別碼——`TenantDetectionToolConfigDomainService.verify_config_is_exist()` 只用識別碼查、完全依賴資料庫規則把關，應用層沒有另一道客戶檢查

**在哪裡。** `jedi_detection/migrations/002-detection-rls-grants.sql`：`:34`（detection_execution_groups）、`:39`（detection_executions）、`:86`（job_execution_detection_tool_agents）、`:91`（job_execution_detection_tools）、`:96`（tenant_detection_tool_configs）——五條都是 `FOR ALL`（查改刪一次全包）且都用 `string_to_array(...)` 切路徑，且**都沒有 `WITH CHECK`**。同檔 `:44`-`:82` 的基準庫八條規則則是正確寫法，對照明顯。

🔴 **範圍註記（決策者指定）：同樣寫法的規則在出貨基線裡共 9 條，除本套件這 5 條外，另含 `agent_tasks` 與三張授權相關表。** 這不是本套件獨有的寫法，修法要一次看完 9 條、統一改成正確的子樹語意，不能只改這五條——否則同一個缺口會留在另外四張表上。

**怎麼修。** 把這幾條規則的判斷式換成 `app_tenant_allowed_for_session(tenant_id)`（即「我自己和我底下的單位」，與檔案其餘部分一致），並在 `verify_config_is_exist()` 補一個明確的客戶條件，讓應用層不再單靠資料庫規則把關。**動到出貨基線的規則屬決策者裁示範圍，runner 不自行處理。**

**驗證。** 2/3 通過：可達性與影響兩位確認成立，既有防護那位持保留。異議在「實際部署有沒有這種母子結構」，不在規則寫法本身——寫法方向寫反是三位都同意的事實。

**首腦核對註記。** **規則寫法屬實，五條全部開檔核對過，一字不差。**

🔵 **首腦已補做 DEV 唯讀實查（2026-09-17 13:30），前提成立**：

- **母子單位結構真的存在**：組織樹上查得到 `/1/102/`、`/1/102/152/153/` 這種「母客戶底下掛子單位」的路徑，不是假設。
- **資料全部落在子單位**：檢測工具設定 7 筆、掃描執行紀錄 76 筆，**全部屬於客戶 102（子單位），客戶 1（母單位）一筆都沒有**。
- **所以兩邊都已經錯了**：資安面——子單位 102 的成員看得到（也改得動、刪得掉）母單位 1 的資料；功能面——母單位的管理者**看不到自己底下子單位的掃描結果**，這已經是一個現在就在發作的功能錯誤，不只是理論風險。
- **與 F1 疊起來更糟**：子單位的成員可以對母單位登記的工具設定按「測試連線」，把母單位的帳密送到自己指定的主機（`verify_config_is_exist` 只用設定編號查，完全沒有客戶條件）。

**急迫度不必再等修正棒查，已經確定：方向寫反是事實，實際部署也踩得到。**

### F4 — 掃描歷史紀錄少了「你是不是這個專案的參與者」檢查（MEDIUM，confidence medium）

> 本條由工具的 F4／F5 兩個候選合併而成（決策者指示）。兩者是同一個缺口的兩個角度：F4 從 API 入口看，F5 從服務層看，指向同一條缺失的檢查。

**這是什麼問題。** 查詢某個任務的掃描執行紀錄時，系統只確認「你跟這個任務同一家客戶」，**沒有確認「你是不是這個專案的參與者」**。而同一個服務裡其他八個吃任務識別碼的功能（啟動、取消、重跑、刪除⋯⋯）**每一個都做了這道檢查**，只有查詢這支沒做。

**出事會怎樣。** 同一家客戶裡、沒被指派到某專案的人，可以讀到那個專案的完整掃描歷史：**被掃描的主機位址與網段範圍**、由哪台代理程式執行、錯誤訊息、是誰發動的、以及產出的弱點報告檔識別碼。而報告下載端點是「知道識別碼就能下載」的設計，所以把識別碼交出去，實質上等於把**尚未修補的弱點清單**交出去。限制是要先有一個有效的任務識別碼（UUID，無法從這支 API 枚舉）。

**要先有什麼才打得到。**
- 攻擊者持有這家客戶的任何帳號，客戶授權含 `plugin` 模組
- 攻擊者取得一個他沒參與的專案的任務識別碼（例如從截圖、分享連結，或曾被指派後又被撤銷）

**在哪裡。** API 入口 `jedi_detection/api/routes/detection_tool_route.py:340`（`DetectionToolJobExecutionsRoute.get`）；實際缺失在 `jedi_detection/app/service/detection_orchestration_service.py:1667`（`list_executions`）。對照同檔有做檢查的八處：`:196`／`:871`／`:925`／`:1007`／`:1032`／`:1097`／`:1127`／`:1470`，每一支都先呼叫 `self._wf_svc.assert_project_participant(job.workflow_execution_id)`。

**怎麼修。** 在 `list_executions` 開頭用 `self._task_lookup.get_task(job_uid)` 取出任務，再呼叫 `assert_project_participant(job.workflow_execution_id)`，形狀與那八支完全一致；任務不存在時回 404，避免這支變成「這個識別碼存不存在」的探測管道。

**驗證。** 兩個候選各 3/3，三位檢查員一致確認成立，嚴重度均為 MEDIUM。

**首腦核對註記。** 屬實。⚠️ **注意修法落點跨棒**：缺失的那行在 `detection_orchestration_service.py`，**該檔屬 D3 範圍、不在本棒的 36 檔內**（本棒只掃到 API 入口那一半）。開修正卡時範圍要涵蓋 D3 那個檔，或與 D3 的發現一起收。

## 被駁回的候選

有一個候選被三位檢查員**一致駁回**，記在這裡以免之後重複提報：

**「解密後的掃描帳密會走未加密的 HTTP 送給代理程式」**——研究員指出 `agent_probe_client.py:83` 確實是明文 POST，而加密只在「代理程式認證有啟用」時才開。但三位檢查員各自查證後都確認**兩條觸發路徑都是關的**：① 唯一的使用端（主專案 `core/plugins/detection.py:318`）是無條件接線的，「沒接線」這個分支不存在；② 安裝腳本 `install.sh:1666` 會寫入認證模式的預設值，「環境變數沒設所以退成不認證」這個分支也被出貨預設關掉了。**判定為誤報，不列入發現。**

---

## 卡片重點逐項人工查證

🔴 **低強度掃描只跑兩位研究員，卡片列的八個重點只碰到三個（①③⑤）。其餘五項由首腦逐項開檔查證如下。**

### ① 租戶工具設定的守門（**工具已報＝F1／F2**）

工具報的兩條完整涵蓋。首腦另核對了卡片提到的「引用計數端點」（`:144`）：確認同樣沒有權限檢查，但它只回一個數字（有幾個任務在用這個設定），資訊量低，**不另列一條**，建議與 F1 一起補守門。

### ② 憑證三處防護逐處驗（**工具未報，人工查證**）

設計自陳「加密存取／畫面顯示剝除／日誌剝除，漏一處等於零」，逐處核對：

- **存**：`detection_tool_service.py:83`（新增）與 `:104`（修改）確實都走 `self._crypto.encrypt(json.dumps(creds))`。修改時是**合併而非整包覆蓋**（先解開舊的再蓋上新的），這是刻意的——前端對已設定的密碼欄位留空代表「沿用原值」，整包覆蓋會把沒重打的密碼洗掉。**寫法正確。**
- **讀回前端**：`api/serializers/detection_tool.py:29-46` 的回應格式**完全沒有 credentials 欄位**，只回一個 `has_credentials` 布林值。**卡片擔心的 `field_values` 欄位混進帳密**：核對 `_resolve_credential_group_params()`（`:122`）確認它只從資料庫的宣告組出參數、**不吃前端傳來的參數鍵值**，且 `field_values` 是非機敏設定值（服務網址、連接埠這類），機敏欄位一律走 `credentials` 那條獨立加密欄位。**沒有混入。**
- **日誌**：把 36 檔內所有日誌呼叫全數列出核對——**只有兩個檔案有日誌呼叫**，一個是 route 的 logger 宣告（宣告了但沒用），另一個是敏感參數模組的三行，內容只印**欄位名稱**不印值（`"剝除敏感參數 key=%s"`）。**沒有任何一行日誌會印出帳密、請求內容或設定內容。三處防護都到位。**

**任務層敏感參數**（`common/detection_secret_params.py`）另外查證兩件卡片點名的事：
- **合併語意會不會讓沒改的欄位以明文回寫**：不會。`encrypt_secret_params()` 的邏輯是「值本身已是加密信封 → 原樣保留不二次加密；值是空的 → 從舊資料沿用既有密文；值是新明文 → 加密」，三條路都不會產出明文。另外 crypto 沒注入時會**記警告後原樣回傳**而不是靜默落明文，設計上留了痕跡。
- **剝除函式是誰呼叫、有沒有漏**：`strip_secret_params` 只有一個呼叫點（`detection_orchestration_service.py:2298`），加密則在任務綁定處理器兩處（`:155`／`:428`）。
- **解密函式在哪裡被呼叫**：`decrypt_secret_params` 在本套件（jedi-detection）內找不到呼叫者，**但這是正常的、不是缺口**——呼叫它的是主專案（BE）那一側的接線程式 `infra/remote_agent/adapter/detection_task_payload_provider.py:60`，在組派工單時把加密信封解開。套件只負責提供能力，由誰呼叫是宿主（主專案）決定，所以只在套件 repo 裡搜尋當然搜不到。**「加密會做、解密沒人呼叫」這個結論不成立，這條路是通的。**

### ③ 守門殼與路由表（**工具部分碰到，人工補完**）

卡片最擔心的是「宿主不給 `auth_required` 時 35 條路由全裸」。**查證結果：有攔截，這個擔心不成立。**

- `plugin/assembly.py:66` 在掛載路由**之前**先跑 `_assert_wiring(adapters)`，而 `auth_required` 列在 `plugin/contract.py:175` 的 `REQUIRED_WIRING` 必填名單裡（同名單還有三軸守門、加解密、任務查詢共六項）。任一項缺了直接 `RuntimeError` 拒絕掛載，錯誤訊息明講「掛上去會讓檢測工具設定失去認證，故拒絕掛載」。
- 卡片提到 `mount_routes(bp, auth_required=None)` 預設值是 None——確有其事（`api/routing.py:6`），且 `_guarded_factory(None)` 回傳的確實是不做任何事的 `lambda cls: cls`（`:172-173`）。**但走正規組裝路徑永遠到不了那裡**，因為 `assembly.py:79` 傳進去的值已經被必填檢查擋過一輪。這是「函式自己的預設值寬鬆、但唯一的呼叫者先把關」的形狀，**不是漏洞**。
- `common/guard.py` 的 `_guard()` 缺接線直接 `RuntimeError` ——如卡片所述是**正面案例**，確認做到，不列為漏洞。
- `require_license` 的兩顆資源字串（`plugin`／`detection-profile`）在 `api/guards.py` 有明確註記「字串值凍結，是對外契約」。**確認無誤。**

### ④ 工具字典與參數格式（**工具未報，人工查證**）

- **有沒有任何寫入路徑**：`DetectionToolRepoImpl` 與 `DetectionToolParamSchemaRepoImpl` **兩個檔案都只有建構子、沒有任何自訂方法**，領域服務那邊也只有 `get_tool_by_uid`／`get_tool_by_id`／`get_tools`／`verify_tool_is_exist`／`get_current_schema` 五個**純讀取**方法，**沒有 create／update／delete，沒有任何 session.add／merge／flush**。
- 結論：**這兩張表在應用層是唯讀的，唯一的寫入者是資料庫變更腳本（seed）**。依「有沒有主人」的判準，它們是碼表（8 支工具、版本化參數格式），沒開客戶隔離是正確的，且因為沒有寫入路徑，**沒有攻擊面**。
- **「誰能改參數格式裡的 `secret: true` 標記」**（這是決定哪些欄位要加密的唯一依據）：**只有能跑資料庫變更腳本的人**，也就是部署者。產品裡沒有任何 API 改得動。**這是安全的形狀。**

### ⑤ 任務綁定表與空條件（**工具未報該面向，人工查證**）

- 四個查詢條件物件（本棒範圍內的 `detection_tool`／`param_schema`／`job_execution_detection_tool`／`tenant_detection_tool_config`）**四個全部是每欄 `Optional[X] = None`＋`to_dict()` 只回有值的條件**，且每個檔案的註解都明寫「防幽靈 WHERE」。寫法一致、沒有例外。
- 送空條件確實會回「本客戶整表」（分頁內），**這與跨 arc 總表第 70 項是同一根因**（源頭在共用底層），**歸第 70 項不另計**。真正擋線的是資料庫隔離規則——而那正是 F3 指出寫反的地方。
- `tool_params` 這個欄位混雜明文與加密信封：確認設計如此，且判斷「要不要解密」是看**值本身長不長得像加密信封**，不是查參數格式表。這個設計讓格式改版後舊任務仍然解得開，**是正確的取捨**。

### ⑥ 錯誤碼與事件碼有沒有洩漏（**工具未報，人工查證**）

- 三個檔案（錯誤碼 222 行、通用錯誤碼 38 行、事件碼 19 行）逐一核對訊息內容：**沒有任何一則訊息帶入檔案路徑、SQL 語句或第三方回應原文**。訊息都是固定的英文描述（例如「Source archive format is not supported」），沒有用格式化字串把外部內容拼進去。
- 唯一出現「path」字樣的兩處是**註解**在說明防護意圖（路徑穿越防護），不是訊息內容。**無洩漏。**

### ⑦ 八顆能力點的宣告與執法（**工具未報，人工查證**）

- `plugin/contract.py:87-90` 宣告四顆能力點設定槽：`plugin_update`、`profile_create`、`profile_update`、`profile_delete`。
- 實際執法：`plugin.update` 掛在檢測工具的新增／修改／重置**三支**；三顆 profile 能力點在 `detection_profile_route.py:54-56` **都有被使用**（該檔屬 D2 範圍，本棒只核對「有沒有被用」不深入）。
- 🔴 **卡片原先寫「`detection-profile.*` 一顆沒掛」——查證後不成立**，三顆都掛了。**卡片這一點的斷言有誤，以本查證為準。**
- **真正的缺口是 `plugin.update` 沒掛滿**：同一個資源上，新增／修改／重置有掛，**測試連線（F1）、清單、引用計數三支沒掛**。這與 FR-098 第 80 項「讀取端點不守門」是同款產品決策題，**但測試連線不是讀取端點**——它會解密憑證並發起對外連線，**不該比照讀取端點放行**，這是 F1 判 HIGH 的理由。

### ⑧ 五支資料庫變更腳本（**工具部分碰到 F3，其餘人工查證**）

- **`002` 的隔離規則**：五條 `FOR ALL` 且**都沒有 `WITH CHECK`**——這點卡片要求「確認 PostgreSQL 語意不憑印象」：`FOR ALL` 若只寫 `USING` 沒寫 `WITH CHECK`，PostgreSQL 會**把 `USING` 的條件同時當作 `WITH CHECK` 用**，所以新增／修改不會因此變成無限制。**缺 `WITH CHECK` 本身不是漏洞**，真正的問題是 `USING` 的判斷式方向寫反（即 F3）。
- **`003`／`004` 的寫入身分與冪等**：兩支 seed 都**不用 `ON CONFLICT`，而是用 `INSERT ... SELECT ... WHERE NOT EXISTS`** 做冪等（工具用 `code` 反查、參數格式用「工具代碼＋版本」反查），外鍵一律用代碼子查詢反查、**不寫死任何 id**。重複執行不會產生重複資料。寫入身分欄位（`created_user`／`updated_user`）一律填 `NULL`，代表系統寫入、非特定使用者假冒。**寫法正確。**
- **`005`（選單路由）**：用 `ON CONFLICT (route_id, capability_id) DO NOTHING` 做冪等。**正確。**
- **`plugin/migrations.py` 只提供不執行**：確認該檔只負責把腳本檔案清單交出去，**沒有任何實際執行 SQL 的程式碼**，套用的決定權在宿主。**這是正確的形狀**（套件不該自己動客戶的資料庫）。
- **九張零隔離表逐張判定**：本棒範圍內的兩張（`detection_tools`、`detection_tool_param_schemas`）依④的查證確認是**碼表、無應用層寫入路徑、零隔離正確**。其餘七張中 `detection_profile_taxonomies`／`detection_profile_controls` 屬 D2 範圍、`job_execution_comments` 等四張屬 flow-control 不在本套件，**本棒不判定**。
- **`DetectionAdapters` 有沒有靜默降級**：`contract.py:119-126` 明列「可省略（缺了優雅降級）」七項，逐項核對其降級後果——**`crypto` 不在可省略名單裡，它在必填名單**（`REQUIRED_WIRING`），所以卡片擔心的「`crypto=None` → 派工帶空憑證」**不成立，缺了會直接拒絕掛載**。可省略的七項降級後果都有明文記載且都是功能性降級（少個暱稱、少個下載識別碼、不產佐證），**沒有任何一項降級會導致守門失效**。

---

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

### 「工具報的這四條是真的嗎」→ ✅ 可信

18 票全投出、沒有漏投：F1／F2／F4（兩個候選）都是 3:0，F3 是 2:1 而異議在「實際部署有沒有母子結構」不在「規則寫法對不對」。另有一個候選被 3:0 一致駁回，顯示檢查員確實有在擋誤報而非照單全收。首腦逐條開檔核對（route 的守門宣告、服務層的解密位置、五條隔離規則的判斷式、八處對照的參與者檢查）全部對得上。

⚠️ **但沒有工具蓋的驗證章可對照**（產物目錄被前一支 session 誤刪，見 Coverage 段）。上述票數取自工作流回傳的結果，內容完整未經改寫，但無法出示工具自己產的那份簽章檔。

### 「只有這四條嗎」→ ❌ 不可信

最低強度快篩、兩位研究員、不做元件盤點不做威脅建模。卡片八個重點工具只碰到三個，其餘五個由首腦回頭開檔補查——人工查證的結論是**五項都沒有新的資安漏洞**（且推翻了卡片原先兩個斷言：profile 能力點沒掛、crypto 缺了會帶空憑證，兩者都不成立）。**這些是人工補的結論，不是工具給的。**

另外，本套件的程式碼註解極為詳盡且處處自我辯護（每個可疑設計都附一段說明為什麼安全）——這正是卡片預警「工具會被說服」的情境。首腦查證時一律以程式碼實際行為為準、不採信註解自陳，例如④是實際列出所有方法確認沒有寫入路徑，而非相信註解說它是碼表。

---

## 執行概況

| 項目 | 數字 |
|---|---|
| 檢查範圍 | 36 個檔案（工具字典＋租戶憑證＋守門殼＋五支資料庫腳本＋插件組裝） |
| 檢查強度 | 最低（`low`），focus 設為 `attack-surface` |
| 候選問題 → 去除重複 | 8 → 6 |
| 投票數 | 18（6 條候選 × 3 位檢查員），全數投出 |
| 通過 / 駁回 | 5 / 1 |
| F1 / F2 / F3 / F4（原 F4+F5） | 3:0（HIGH）/ 3:0（MEDIUM）/ 2:1（MEDIUM）/ 3:0 與 3:0（MEDIUM） |
| 沒投到票的 / 投票中斷的 / 被降低嚴重度的 | 0 / 0 / 0 |
| 研究員派出 / 回收 | 2 / 2 |
| 驗證章狀態 | **無法出示**（產物目錄被前一支 D1 session 誤刪，非平行掃描所致） |
| 掃描耗時 | 2541 秒（約 42 分），另含中斷後續跑 |
| 掃描當下的程式碼版本 | commit `6119da51d488`，branch `feature/review`，工作區乾淨 |
| 掃描期間的版本漂移 | 結束時 HEAD 已為 `82d78bc`（三筆別線 commit，不在掃描範圍） |
| scan_id / workflow run | `9ea230c3` 系列 / `wf_f074881c-329` |
| 首腦人工補查項目 | 卡片八個重點逐項查證，②④⑥⑦⑧ 為純人工補查 |
| 中斷與續跑 | 面板投票階段遭 session 中止一次，以 `resumeFromRunId` 續跑補完，研究員結果走快取未重跑 |

## 後續處理

- **F1 優先修**：補一行權限宣告，範圍在本套件，影響最大。
- **F2 與 F1 分開修**：補權限不會消除跳板問題。
- **F3 屬出貨基線範圍**：同寫法共 9 條（含 `agent_tasks` 與三張授權表），**要一次看完統一改**，屬決策者裁示，runner 不自行處理。**急迫度已由首腦 09-17 DEV 實查確認：母子單位結構存在、資料全在子單位，資安與功能兩面都已經在發作。**
- **F4 修法落在 D3 範圍的檔案**：開卡時範圍要跨棒，或與 D3 發現一起收。
- ~~**待決策者判斷**：`decrypt_secret_params` 無呼叫者~~ → **已排除**：呼叫者在主專案 `infra/remote_agent/adapter/detection_task_payload_provider.py:60`，不是功能缺口。

**修正卡本棒不開**（決策者指示）；後續統一派工，四條均已修（1.21.0 出貨）。
