# W3 掃描報告 — 檢測的接線本體（CM-2092）

> 範圍：12 檔／1,457 行（`core/plugins/detection.py`、`core/plugins/_host.py`、檢測三支 DI 容器檔、`infra/readmodel/detection/` 兩支查詢、生命週期接縫、任務型別申報、三支轉手檔）。`common/code/detection_tools_error_code.py` 依首腦裁定不列入。
> 掃描工具：Claude Code 官方 `claude-security` plugin，effort low，focus 生產程式碼。
> 掃描基準 commit：`aa23a63554ce`（工作區有其他 session 的未提交改動）。
> 驗證章：**verified**（0 候選，面板 0 票）。只掃不修。

---

## 1. 一句話結論

**卡片說的「上游防線」——`detection.py` 第 337～340 行那組解壓上限——其實是沒接上的死設定：套件收下之後一次都沒讀過。** 真正擋住上傳檔案的，是 DI 容器另外直接讀 `Config` 組出來的那一份；而網址那條路（總表第 116 項）在修正分支上補的上限，用的是套件寫死的數字，也不讀宿主設定。所以同一件事現在有三套數字，運維調環境變數只會改到其中一條路。這不是可以直接打的洞，但它就是「守了一半」會長出來的樣子（見 W3-1）。

工具這一棒 0 條候選。我開檔追卡片重點時另外查到一條：**母公司刪除自己分享給子公司的檢測基準時，系統判斷「還有沒有人在用」會漏看子公司的引用**，於是子公司正在用的基準可以被刪掉。根因是總表已登記的「資料庫隔離規則方向寫反」，這裡是它的一個新後果，**不另計新項**，但要列進那張修正卡的驗收項（W3-2）。

卡片其餘四點都查過了，都不成立，第 5 節逐條寫了理由。

---

## 2. 這一棒在檢查什麼

弱點檢測功能本身寫在 `jedi-detection` 套件裡，FR-108 十四棒已經掃完。這一棒看的是**主專案把套件接上產品的那一層**：套件需要「登入檢查、權限檢查、加解密、查任務、寄通知、寫證據」這些東西，都由主專案遞給它。要問的不是「哪裡沒有權限檢查」，而是：**主專案遞過去的守門零件對不對、齊不齊；套件以為主專案會守的地方，主專案真的有守嗎。**

`core/plugins/detection.py` 被 FR-108 五棒引用過，但都只是查某一點時打開看一行。這一棒是第一次從頭讀到尾。

---

## 3. 掃到什麼：總覽

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才會發生 | 在哪裡 | 嚴重度 | 來源 |
|---|---|---|---|---|---|---|
| W3-1 | **壓縮檔上限有三套數字，卡片點名的那一套根本沒接上** | 運維以為調了上限、實際只改到上傳那一條路；網址那條路永遠是寫死的數字。調緊的時候，會變成一條路緊、一條路鬆 | 有人去改 `DETECTION_PROFILE_*` 環境變數 | `core/plugins/detection.py:322`（`build_config`，死設定）<br>`di_containers/detection_tools/detection_tools_containers.py:152`（實際生效的那一份） | **不是漏洞**：設定分岔，會長成漏洞的地方 | runner 開檔，未經投票 |
| W3-2 | **母公司刪自己分享出去的基準時，看不到子公司正在用** | 子公司任務綁的基準版本被刪掉，下次派工失敗，稽核軌跡裡的那份基準也跟著消失 | ① 母公司把基準設成「分享給子樹」② 子公司的任務綁了它 ③ 母公司管理員按刪除 | `infra/readmodel/detection/detection_profile_usage_query.py:128`（`find_refs`）；根因在資料庫隔離規則 | **低**：沒有洩漏，是資料完整性；而且刪的人本來就是基準的擁有者 | runner 開檔＋DEV 唯讀查證，未經投票 |

---

## 4. 每條發現的詳述

### 4.1 W3-1 壓縮檔上限有三套數字，卡片點名的那一套沒接上

**白話說明**

客戶可以上傳一包檢測規則（壓縮檔），也可以填一個網址讓系統去抓。為了防止有人丟「解壓炸彈」（很小的壓縮檔，解開後幾十 GB，把伺服器記憶體吃光），系統對壓縮檔設了三道上限：最多幾個檔、解開後最多多大、壓縮比最多幾倍。

我追了這組數字從哪裡來、被誰用，結果有**三套**：

| 哪一套 | 數字（檔數／解開後大小／壓縮比） | 誰在用 |
|---|---|---|
| ① 主專案設定檔 `config/config.py:194-200`（讀環境變數） | 10,000／500MB／200 倍 | **上傳檔案那條路**（DI 容器 `detection_tools_containers.py:152-158` 直接讀它，組出驗證器交給套件） |
| ② `core/plugins/detection.py:337-340` 遞給套件的 `DetectionConfig` | 讀①，讀不到才用 20,000／500MB／100 倍 | **沒有人**。套件全部原始碼只有宣告這四個欄位的那一處（`plugin/contract.py:82-85`），沒有任何讀取點 |
| ③ 套件內建預設值 `common/archive_limits.py:23-25`（修正分支） | 10,000／500MB／200 倍，寫死 | **網址那條路**（修正分支 `profile_extractor/inspec.py:404` 的 `ArchiveBombGuard(archive_size=len(raw))` 沒傳上限，直接吃預設值） |

**卡片問的三件事，答案如下：**

- **「Config 沒設時用預設值，預設值合不合理？」** ②的後備數字永遠用不到：`Config` 是類別屬性，就算環境變數沒設，`getattr` 也一定拿得到值（`os.getenv` 自己就帶預設值）。真正會生效的預設值是①的 10,000／500MB／200 倍，設定檔註解說明了取值理由（現有規則包約 123KB、解開 876KB），這組數字是合理的。只是②的後備值（20,000、100 倍）跟①③都不一樣，同一件事就有三組數字。
- **「是 detection 專用還是全域？」** 是 **detection 專用**。變數名是 `DETECTION_PROFILE_*`，設定檔註解寫明它刻意跟「源碼包上傳」那組上限（`DETECTION_UPLOAD_*`，100MB／五萬檔）分開，因為兩者大小差兩個數量級。
- **FR-113 O3b 建議 Excel 那條路「直接沿用 `detection.py:338-340`」**：這個指路指錯了地方，那幾行是死設定。要沿用的話，來源是 `config/config.py:194-200`。但這組數字是照「一份檢測規則包」的大小訂的，拿去管 Excel 合不合適，要由開 Excel 修正卡的人自己判斷，不是可以直接照抄的全域常數。

**為什麼算「會長成漏洞的地方」**

現在三套的數值剛好一致（①跟③都是 10,000／500MB／200），所以目前兩條路擋得一樣緊。問題出在有人調整的時候：

- 運維把 `DETECTION_PROFILE_MAX_ENTRIES` 調緊成 2,000 → 上傳那條路變 2,000，**網址那條路還是 10,000**。調緊的人會以為全擋住了，其實只擋了一半。
- 開發者看到 `detection.py` 的 `build_config()`，以為改這裡就能調上限 → 改了沒有任何效果，也不會有錯誤訊息。

這跟總表第 116 項（網址那條路原本完全沒有上限）是同一個病的延續：第 116 項的修法是把上限補到網址那條路上，但補的是寫死的數字，沒有接到宿主的設定。

⚠️ **兩份套件副本現況不同**：`feature/review` 這邊的套件副本（`jedi-python-package/jedi-detection`），網址那條路還是完全沒有上限（`inspec.py:361-369` 沒有任何防炸彈檢查），也就是第 116 項在這個分支還沒修。有接上限的是修正分支 `fix/security-b1` 的副本。本機 venv 目前以 editable 模式指向修正分支的那一份。

**建議修法**（只建議、不動手）

1. 套件把 `DetectionConfig` 的四個上限真的用起來：`register()` 用它組出 `ArchiveLimits`，上傳驗證器跟網址解析器都吃同一份，不再各讀各的。
2. 主專案刪掉 `detection_tools_containers.py:152-158` 另外組驗證器的那段，也刪掉 `detection.py:337-340` 的 `getattr` 後備值（直接讀 `Config.DETECTION_PROFILE_*`），整件事只剩一份數字。
3. 驗收時兩條路各測一次：調緊環境變數後，上傳跟網址都要被擋。

### 4.2 W3-2 母公司刪自己分享的基準時，看不到子公司正在用

**場景**

母公司（例如集團總部）的管理員在「檢測基準庫」建了一份基準，設成「分享給子樹」（FR-094 的 `SHARED`），底下的子公司就能拿來用。某家子公司的顧問建了一個檢測任務，綁上這份基準。之後母公司管理員覺得這份基準過時了，在基準庫按「刪除」。

系統刪除前會先查「這份基準還有沒有人在用」，有人用就回 409、不給刪。**但這次它回「沒人在用」，就刪下去了。** 子公司任務綁的那一版連同控制項一起消失，子公司下一次按「開始掃描」會拿到「找不到基準」；如果這個任務已經掃過，稽核軌跡裡指向的那份基準也不在了。

**為什麼會查不到**

「還有沒有人在用」這件事，是 `detection_profile_usage_query.py` 用一支 SQL 查三張表：任務綁定表 `config.job_execution_detection_tools`、派工單 `compliance.agent_tasks`、執行紀錄 `compliance.detection_executions`。這支查詢用的是**當前操作者的身分**，所以受資料庫隔離規則（RLS）限制。

這三張表的隔離規則，正是總表已登記的那條「**方向寫反**」（FR-108 D1 F3，模組報告 M03 第 7 條）：它把登入者的公司路徑 `/1/102/` 切成 `{1, 102}`，只認「路徑上的祖先」，不認「底下的子公司」。所以母公司（102）的 session **看不到子公司（152）的綁定紀錄**。

我在 DEV 開了唯讀交易、切成 `cm_app` 身分核對（最後 ROLLBACK，沒有寫入任何東西）：

- 以母公司 102 的身分：任務綁定表的規則判斷「子公司 152 的列看不看得到」→ **看不到**；同一時間，產品正規的子樹判斷函式 `app_tenant_allowed_for_session(152)` → **看得到**。兩者方向相反。
- 以子公司 152 的身分：看得到母公司 102 的 58 筆任務綁定（這就是 D1 F3 已經報過的那一半）。

**卡片原本擔心的情境不成立**：卡片擔心「平台管理員刪公版基準時，以為沒人在用」。平台管理員是最上層公司（路徑只有一段 `/1/`）的成員，資料庫會給他超級管理員旗標，三張表的規則第一個條件就直接放行，所以他看得到全站的引用。公版（`SYSTEM`）的刪除只有平台管理員做得到，因此公版這條路沒有問題。**真正漏掉的是「母公司刪自己的 `SHARED` 基準」這條路**，這條路在 FR-094 加入分享功能之後才出現。

**為什麼判「低」、而不是「中」**

- 沒有任何資料外洩，是資料完整性問題：子公司的任務斷掉、稽核軌跡少了一份基準。
- 按刪除的是基準的合法擁有者，不是攻擊者拿到了原本沒有的權限。
- 根因已經在總表登記過，修好那條（三張表的規則改用 `app_tenant_allowed_for_session`），這條會跟著消失。

**處理建議**

- **不另外登記新項次**，併進「隔離規則方向寫反」那張修正卡，在驗收項加一條：「母公司刪除自己分享出去、且子公司任務已綁定的基準版本，要回 409 並列出子公司的專案名」。
- 另外要注意：修完之後，母公司在刪除錯誤訊息裡會看到**子公司的專案名稱**（`_in_use_message` 會列出專案名）。母公司本來就看得到子樹的專案（`projects` 表用的是正規的子樹規則），所以這不是新的洩漏，但修的人應該知道會有這個變化。

**沒有實際做的事**：我沒有在 DEV 真的建一份 `SHARED` 基準、讓子公司綁定、再從母公司刪除。上面的推論是「讀程式＋讀資料庫規則＋唯讀驗證規則的判斷結果」得出的。手測步驟見第 8 節。

---

## 5. 卡片點名、查證後不成立的（排除也是結論）

| 卡片重點 | 結論 | 怎麼查的 |
|---|---|---|
| `platform_admin_check`／`scope_writable_check` 走「模組級設定」，會不會在程序啟動時就被固定成某個人的身分 | **不會** | 啟動時交給套件的是**函式本身**（`detection.py:315-316`），套件存在模組變數裡（`common/guard.py:26-30`）。每次呼叫時，函式才去讀 `get_user_context()`（登入者身分存在「每個請求各自一份」的變數裡），`viewer_is_platform_admin()`（`jedi_iam/authz/platform.py:15`）與 `assert_scope_writable()`（`common/authz/sharing.py:25`）都是當下才讀身分。沒接上時套件會直接拋例外，不會預設放行（`guard.py:39-45`） |
| `CryptoAdapter` 沒有「誰能呼叫解密」的限制 | **設計如此**，不重報第 85 項 | 這是一個薄轉接（`detection.py:129-140`），只負責把宿主的 Fernet 金鑰物件遞給套件。「誰能觸發解密」由呼叫它的 service 決定，第 85 項（測試連線沒有守門）的缺口在套件的測試連線那一支，不在這個零件 |
| `detection_profile_usage_query.py` 會不會因為隔離規則只看得到自家引用，讓平台管理員刪公版時以為沒人在用 | **平台管理員這條不成立**；但查出母公司刪 `SHARED` 那條＝W3-2 | 見 4.2 |
| `detection_job_notify_query.py` 會不會把別家公司的人拼進收件人 | **不會** | 這支查詢只在遠端代理程式回報結果時執行，那時的身分是用 `tenant_context()` 設成**派工單所屬公司的機器身分**（`jedi_remote_agent/.../agent_task_service.py:113-120`，公司編號由伺服器從派工單查出，不信任請求內容）。收件人來源三張表都有隔離規則：`task_assignees`／`project_participants` 都要求「所屬專案看得到」，`projects` 用的是正規的子樹規則。唯一沒有隔離規則的 `workflow_execution_control_mapping` 只拿來串出 `project_id`，接著用那個 id 去查 `projects` 時還是會被規則過濾。這條路拼不進別家的人 |
| `detection_lifecycle_listener.py` | 不重報 FR-077 R3 | 37 行純轉呼叫，沒有判斷邏輯 |
| AI 儀表板那條路（④） | **不適用** | `di_containers/dashboard_apis/` 下沒有任何檢測相關的申報 |

---

## 6. 「套件以為宿主守、宿主以為套件守」逐處追查

| 註解或契約說…… | 追到另一側的結果 |
|---|---|
| `detection.py` 檔頭：「認證與三軸授權守門由宿主給」 | ✅ 給齊了。`auth_required=jwt_required()`（`:302`）；授權、能力點、平台管理員三支 adapter（`:303-305`）都呼叫 `common.authz` 的正式守門（實際 import 驗證五支函式都存在）。套件 `routing.py:166-180` 把 `auth_required` 包在每一條路由上；缺少必要接線時 `register()` 會拒絕掛載 |
| `detection.py:131-134`：「本 port 缺了就是明文落庫」 | ✅ 有給（`:306`），而且這一項被列在「缺了拒絕掛載」的必要接線裡 |
| `detection_orchestration_containers.py:45-47`：「`task_lookup` 缺了，派工三道守門全部無從進行」 | ✅ 有給。`TaskLookupAdapter.get_task` 查的 `job_executions` 有隔離規則；查無時照契約回 None，由套件決定回 404 |
| 套件編排服務（修正分支）：「任務被指派人或專案 manager 才可操作（`assert_job_operator`）」 | ⚠️ **這個方法在本分支的宿主上不存在**。修正分支的套件呼叫 `self._wf_svc.assert_job_operator(...)`（8 處），但 `feature/review` 的 `app/flow_engine/service/workflow_execution_service.py` 只有 `assert_project_participant`（`:126`），`assert_job_operator` 只存在於 BE 的修正分支 worktree。**這不是破口**，缺方法只會拋 AttributeError（回 500、不會放行）。但本機 venv 以 editable 指向修正分支的套件，`feature/review` 的 BE 在本機跑起來時，**檢測的「開始掃描」等 8 支操作應該會全部 500**。只要兩邊分支一起合併就沒事，這裡是提醒驗收環境要對齊 |
| `task_type_declaration.py` 啟動掛鉤「對檢測任務自動派第一次掃描」 | ✅ 守門在下一層。掛鉤呼叫 `orchestration.start_execution()`，那支方法一進去就先檢查任務操作權（`detection_orchestration_service.py:196`／修正分支 `:202`），權限檢查不會被掛鉤繞過 |

**同一套接線五支並排、有沒有哪支少了**（對照盤點檔 §3 ②的並排表）：檢測是唯一「逐項手寫」、不吃 `host_defaults()` 的一支，但登入、能力點、授權、身分四道門都有給，而且多給了平台管理員與分享守門，沒有少給的。

**守門次數對公開方法數**：

| 檔 | 守門字眼 | 公開方法 | 說明 |
|---|---|---|---|
| `core/plugins/detection.py` | 10 | 20 | 守門字眼集中在四支 guard adapter；其餘是資料轉接，守門在套件的 route／service 層 |
| `core/plugins/_host.py` | 3 | 5 | `host_defaults()` 給能力點守門；`lazy()`／`di()`／`_HostServiceProxy` 都是呼叫當下才解析，沒有「存下第一個實例」的問題 |
| 兩支 readmodel 查詢 | 0 | 各 2 | 查詢層本來就不該有守門，隔離靠資料庫規則（就是 W3-2 出事的那一層） |
| 生命週期接縫、任務型別申報 | 0 | 3／1 | 見上表 |
| 三支轉手檔 | 0 | 0 | 純 re-export，沒有邏輯 |

---

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

**工具那一層：「0 條」是真的 0 條，但只代表快速掃過一輪沒撿到東西。**
研究員與密鑰專項各派 1 支、都回來了，兩支都回報空清單，面板因此 0 票，驗證章是 `verified`。但研究員**沒有交代它讀了哪些檔**（工具的 `coverage.research` 是空的），所以不能說 12 支檔都讀完了。

**runner 那一層：W3-1 與 W3-2 沒有經過三人面板投票，是我自己開檔核對的。**
- W3-1：在兩份套件副本上 grep 過 `profile_max_*` 的所有出現處，確認只有宣告沒有讀取；三套數字都開檔逐行核對過。
- W3-2：DEV 查過 `pg_policies`，用唯讀交易以 `cm_app` 身分驗證規則的判斷結果（最後 ROLLBACK）；用 `pg_has_role` 確認 `cmmgr` 是 `cm_app` 的成員。**沒有實際跑一次「建分享基準→子公司綁定→母公司刪除」的完整流程。**
- 引用的套件行號：`feature/review` 副本與修正分支副本的行號不同，報告裡都標了是哪一份。

**不保證完整**：用的是最快的掃描檔位（effort low，沒有威脅建模和廣度掃描）。套件本體不在範圍內，只在追接線時讀了相關段落。

---

## 8. 手測清單（給驗收用）

**W3-2**
1. DEV 準備母公司 A、子公司 B（B 的路徑在 A 底下）。
2. A 的管理員建一份檢測基準，改成「分享給子樹」。
3. B 的顧問建一個檢測工具任務，綁上這份基準的目前版本（不用真的掃）。
4. A 的管理員先打「查這份基準的使用狀況」API → **預期現況**：回 `in_use: false`（應該要是 true）。
5. A 的管理員刪除這份基準 → **預期現況**：刪除成功（應該要 409）。B 的任務再按開始掃描 → 回「找不到基準」。

**W3-1**
1. 把 `DETECTION_PROFILE_MAX_ENTRIES` 設成 50，重啟 BE。
2. 上傳一份 60 個檔的規則包 → 被擋（上傳那條路有吃到設定）。
3. 用網址方式建立同一包 → **預期現況**：修正分支上會通過（網址那條路吃的是寫死的 10,000），`feature/review` 上完全沒有上限。

---

## 9. 執行概況（數字，給工程師看）

| 項目 | 數值 |
|---|---|
| 掃描範圍 | 12 檔／1,457 行 |
| 基準 commit | `aa23a63554ce`（工作區 dirty，有平行 session 改動） |
| 檔位 | effort low，focus 生產程式碼 |
| 研究員 | 派 2 支（研究 1＋密鑰專項 1），回 2 支，零失敗、零重跑 |
| 原始候選 | 0 條 |
| 投票 | 0 票（沒有候選） |
| 驗證章 | **verified**（`CLAUDE-SECURITY-REVISION-aa23a63554ce-dirty.json`） |
| 工具 run ID | `wf_1783d39b-c1f` |
| 耗時 | 約 16 分鐘（2 個 agent，約 20.6 萬 token） |
| 工具產出原始報告 | `CLAUDE-SECURITY-20260923-130633/`（未入版控） |
| runner 自行查證 | 兩份套件副本逐檔 grep、DEV 唯讀查 `pg_policies`／`tenants`／`pg_roles`、唯讀交易驗規則判斷 |

---

## 10. 待首腦裁決

1. **W3-1 要不要登記**：建議記進總表 §3.2（非資安程式錯誤，會長成漏洞的地方），修正卡跟第 116 項一起開，因為修法是同一件事：讓網址與上傳兩條路吃同一份、由宿主設定的上限。
2. **W3-2 併進「隔離規則方向寫反」那張修正卡**：建議不另外登記項次，只在那張卡加一條驗收項（見 4.2）。
3. **FR-113 O3b 的指路要不要更正**：它建議 Excel 修法「沿用 `detection.py:338-340`」，但那幾行是死設定。建議改指 `config/config.py:194-200`，並註明那組數字是照檢測規則包的大小訂的。
4. **本機驗收環境對齊**：`feature/review` 的 BE 搭修正分支的套件，檢測操作會因為缺 `assert_job_operator` 回 500（第 6 節）。驗收 FR-114 修正或手測本棒時，要注意兩邊分支要一致。
