---
title: U13a 檢查結果：套件接線九支＋DI 模組清單（找「接錯」不是找「沒接」）
---

# U13a 檢查結果：套件接線九支＋DI 模組清單

> 檢查日期 2026-09-25｜對應卡片 CM-2166（母卡 CM-2152）｜檢查範圍 10 個檔案 1,844 行｜工具 run ID `wf_5b4ba820-2db`

## 🔴 一句話結論

**這十支檔案都有把零件接上，但有兩處接上的守門問錯了問題。** 這兩處都不是全新的洞，是舊洞裡先前沒有人說清楚的一角：

1. **AI 儀表板只問「這家公司有沒有買」，不問「這個人能不能用」**（淨新增，低）。產品替 AI 儀表板定了一個權限點 `ai-dashboard.read`，選單也照它決定要不要顯示。但生成端點本身只檢查「有登入」和「公司有買這個模組」。所以角色沒有這個權限點的人（DEV 上的「稽核人員」「IT執行人員」就是）選單看不到，直接打網址照樣能用，每用一次公司就付兩次 AI 費用。這正是 U4 那條教訓：「問你有沒有買」不等於「問這是不是你的」。
2. **共用的原廠檔案（例如法規框架 PDF），任何一家公司的登入者都能把實體檔刪掉，全體客戶一起壞**（淨新增，中，建議首腦判斷是否併入第 37 項）。資料庫那道牆（第 34／47 項的修正）讓大家以為跨公司刪除已經擋住了。實際上刪除的順序是「先刪儲存空間裡的實體檔、再刪資料庫紀錄」，資料庫只擋住了第二步，而且擋的時候不會報錯。結果是紀錄還在、檔案沒了，API 還回「成功」。

工具這一棒有 4 條候選，三人面板 3:0 通過 3 條、3:0 否決 1 條。**通過的 3 條全是舊帳**（第 34／37／40 項、第 11 項、第 36 項），不另計。

卡片要我回答的三件事：

| 卡片問的 | 答案 |
|---|---|
| 交給套件的守門零件，問的問題對不對 | 九支裡七支問對了。**問錯的兩處**是上面兩條：AI 儀表板用「公司有沒有買」代替「人能不能用」；檔案端點用「有沒有登入或有沒有通行證」代替「這是不是你的」（後者是第 34 項舊帳） |
| 套件拿到空值時是拒絕還是放行 | 登入、權限點、商務授權這類「門」一律**拒絕掛載**，六支套件都寫死了。**會放行的只有兩種**：①公告拿不到「你是哪個部門」時看全部（第 1 項舊帳）；②修正線的檔案套件拿不到「歸屬名冊」時只記一行警告、照樣掛上（主專案修正線已經接上名冊，現況不會觸發） |
| DI 模組清單的載入順序，會不會讓守門在被用到時還沒接上 | **不會**。所有守門零件都是「被呼叫的那一刻」才去找 DI 容器，而容器在迴圈之前就放好了。`di_modules.py` 也沒有漏列任何用到注入的模組（逐支比對過） |

## 這一棒在檢查什麼

系統啟動時，主專案要把 21 支外部套件一支一支「接上」：交給它們資料服務、登入檢查、權限檢查、商務授權檢查等零件。這十支檔案就是這份接線表：

| 檔案 | 行數 | 接的是什麼 |
|---|---:|---|
| `core/plugins/identity.py` | 679 | 登入、帳號、角色、租戶（jedi-iam）。全產品的守門零件從這裡交出去 |
| `config/di_modules.py` | 225 | 決定哪些程式要「自動拿到零件」。漏列一支，那支的注入全部落空而且不報錯 |
| `core/plugins/bulletin.py` | 220 | 公告 |
| `core/plugins/file_upload.py` | 145 | 檔案上傳、下載、預覽、刪除 |
| `core/plugins/ai_bot.py` | 135 | AI 聊天小幫手 |
| `core/plugins/ai_dashboard.py` | 98 | AI 動態儀表板 |
| `core/plugins/integrity.py` | 93 | 防竄改（出貨程式有沒有被改過） |
| `core/plugins/__init__.py` | 83 | 21 支套件的清單與掛載順序 |
| `core/plugins/flow_engine.py` | 83 | 稽核流程引擎（只當函式庫用，不掛網址） |
| `core/plugins/notification.py` | 83 | 寄信、測試信 |

DI 比對腳本（`di-wiring-audit.md`）已經確認這十支「零缺接」，所以這一棒不找「沒接」，專找「接錯」。

## 掃到什麼（總覽）

| # | 嚴重度 | 一句話 | 來源 | 算不算新的 |
|---|---|---|---|---|
| U13a-1 | 🟡 中 | 共用原廠檔案可被任何公司刪掉實體檔，全體客戶一起壞 | 工具 F1 的一段（面板 3:0）＋runner 開檔與 DEV 唯讀查證 | **淨新增**（待首腦判是否併第 37 項） |
| U13a-2 | 🟢 低 | AI 儀表板生成端點不檢查 `ai-dashboard.read` | 工具 F2 的一句（面板 3:0 提到）＋runner 手做實測 | **淨新增** |
| 工具 F1 其餘 | 中 | 檔案下載／預覽／刪除不問「是不是你的」 | 面板 3:0 | 舊帳：**第 34／37／40 項**，不另計 |
| 工具 F2 其餘 | 中 | AI 儀表板 26 支查詢沒有逐支權限檢查 | 面板 3:0 | 舊帳：**第 11 項**（修正分支 CM-2038 已補），不另計 |
| 工具 F3 | 中 | 上傳網頁檔在預覽時被當程式執行（儲存型 XSS） | 面板 3:0 | 舊帳：**第 36 項**（總表記高），不另計 |
| 工具候選 C4 | — | 測試信把存好的 SMTP 密碼送到呼叫者指定的主機 | 面板 **0:3 否決** | 舊帳：CM-1605（FR-078 當時記 HIGH），見下方「面板與前次判定不同」 |

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - U13a-1（共用原廠檔被刪實體檔）＝總表第 217 項，✅ 已修（CM-2228，套件 `5ec9b617`：原廠共用檔禁刪、刪除順序改先刪紀錄再刪檔，1.21.0 出貨）。
> - U13a-2（AI 儀表板生成缺能力點）＝總表第 218 項，✅ 已修（CM-2220，commit `80dedfbb5`／套件 `fb271590`，1.21.0 出貨）。
> - 候選 C4＝CM-1605（測試信送存好的 SMTP 密碼）：🚫 裁定不修（權限面由客戶自行決定）；程式面已加強（位址或連接埠不同就不沿用存著的密碼，CM-2111，M16 第 1 條）。

### U13a-1 — 共用原廠檔案，任何一家公司的登入者都能把實體檔刪掉

- **嚴重度**：🟡 中
- **位置（該補檢查的地方）**：`app/upload_file/service/managed_file_upload_service.py:385`（`delete_file`）。選儲存空間的那段在 `:177-182`（`_adapter_for_uid`）。接線落點在 `core/plugins/file_upload.py:92`（沒交歸屬名冊）。實際刪檔的是套件 `jedi_file_upload/infra/adapter/minio/minio_adapter.py:134`（先刪實體檔）→ `:137`（再刪紀錄），版本是出貨釘住的 1.2.0。
- **白話說明**：系統裡有一類檔案是「原廠共用」的（`storage_scope='system'`，例如法規框架的 PDF），所有客戶都讀得到，DEV 上有 18 支。資料庫的規則是：這類紀錄**每家公司都能讀**，但**只有平台管理員能刪**。刪檔 API 的流程是：先用檔案編號讀出紀錄（讀得到，因為共用）→ 到原廠儲存空間把實體檔刪掉 → 再刪資料庫紀錄。第三步被資料庫規則擋下，但它**擋的方式是「當作沒有這筆」、不報錯**，所以 API 照樣回成功。
- **出事會怎樣**：這份法規框架 PDF 對**所有客戶**都打不開了（紀錄還在、實體檔不見）。被刪的人不會收到任何通知，要等有人開檔時才發現。
- **觸發前提**：任何一家公司的任何登入帳號，加上知道一支原廠檔案的編號（原廠檔案在資源庫頁面上每家都看得到）。
- **為什麼說是新的**：第 37 項當初寫的是「別家客戶的證據會被刪」。後來第 34／47 項補了資料庫那道牆，看起來像是跨公司刪除已經擋住。這條說明**對原廠共用檔案，牆只擋住了紀錄，實體檔照樣被刪**。修第 37 項時如果只驗「刪別家客戶的檔會失敗」，這條不會被測到。
- **建議修法**：①`delete_file` 對 `storage_scope='system'` 的檔案，非平台管理員一律拒絕，而且要在刪實體檔**之前**拒絕；②刪除順序改成「先刪紀錄、刪成功（影響筆數 = 1）才刪實體檔」，這樣任何被資料庫擋下的刪除都不會留下半套結果。建議跟第 37 項同一張卡，驗收時加一條「一般帳號刪原廠檔要失敗、實體檔要還在」。
- **打折處**：沒有對正在運行的服務實刪（會真的弄壞 DEV 的共用檔）。證據是開檔讀 1.2.0 wheel 的刪除順序＋讀 migration 的刪除規則＋DEV 唯讀查到 18 支原廠檔（2026-09-25 18:15 +08，`BEGIN READ ONLY … ROLLBACK`）。另外，修正分支 `fix/security-b1` 已經接上歸屬名冊（`core/plugins/file_upload.py:355-356`），但**原廠檔的「刪除」會不會被名冊擋下，我沒有驗**。修的人請在修正線上補測這一條。

### U13a-2 — AI 儀表板只問「公司有沒有買」，不問「這個人能不能用」

- **嚴重度**：🟢 低
- **位置（該補檢查的地方）**：套件 `jedi_ai_dashboard/api/routes/ai_dashboard_route.py:99`（生成端點只掛 `@require_license`）。主專案接線 `core/plugins/ai_dashboard.py:52-61`（交出去的只有 `auth_required` 與 `license_guard`，沒有權限點守門）。
- **白話說明**：產品替每個功能訂兩道門：「公司買了這個模組沒」（商務授權）和「這個人的角色有沒有這個功能的權限點」。AI 儀表板在套件裡宣告了權限點 `ai-dashboard.read`，選單也靠它決定顯不顯示。但生成端點**只過第一道門**。這不是搬家時弄丟的：搬進套件前，主專案自己的端點也只掛了登入＋商務授權（git 查過 `1b27db20e` 之前的版本）。修正分支 `fix/security-b1` 的套件端點也一樣沒有。
- **DEV 實況**（2026-09-25 16:51 +08 唯讀）：公司 102 的「稽核人員」「IT執行人員」兩個角色沒有 `ai-dashboard.read`，其餘角色都有。
- **實測**：本機用套件的 `register()` 掛上端點，模擬「公司有買、角色沒有權限點」的使用者送出請求。商務授權檢查被呼叫了（參數 `ai-dashboard`），請求接著**直接進到生成服務**，途中沒有任何權限點檢查。
- **出事會怎樣**：①管理員以為把 AI 儀表板從某個角色拿掉了，實際上那個角色照樣能用，只是看不到選單；②每用一次公司就付兩次 AI 費用，而這個功能本來就沒有次數上限（第 11 項已記）。修正分支補了 26 支查詢各自的權限白名單（CM-2038）以後，這條**不會再多看到資料**，剩下的是「權限設定不生效」和費用。
- **觸發前提**：一個登入帳號，所屬公司有買 `ai-dashboard`，角色沒有 `ai-dashboard.read`。
- **建議修法**：在 `AiDashboardAdapters` 開一個 `capability_required` 欄位，主專案交 `require_capability`，生成端點加上 `ai-dashboard.read`。跟第 11 項同一個套件、同一個修正線，建議併在 CM-2038 的後續一起做。

## 卡片「重點看什麼」逐條回答

| 卡片點名 | 答案 |
|---|---|
| 🔴 `identity.py`：每個需要權限檢查的服務，這裡有沒有把守門零件接上 | **接上了，也問對了**。交給套件三道門：登入（`jwt_required()`）、權限點（`require_capability`，查「現在有效的角色指派」，停用、過期、跨租戶的角色都不算）、換發權杖（`jwt_required(refresh=True)`）。三道缺一個，套件會**拒絕掛載**（`jedi_iam/plugin/contract.py:42`）。其他幾件：人機驗證可以用 `TURNSTILE_ENABLED` 關掉（落地版內網連不到 Cloudflare，屬部署政策）；原廠帳號的密碼永不過期豁免，在判斷不出來時走一般政策（不放寬，`:252-269`）；新租戶開通時「無照＝不扣能力點」是刻意的放寬，真正的執法在每次請求（`:288-316`，理由寫在註解裡，與第 181 項的總開關同一條軸）。綁定 LDAP 帳號那支的包裝（`:496-516`）照單轉交 `user_uid`、不核是不是本人，這是 **CM-1562 舊帳**，不另計 |
| `di_modules.py`：有沒有漏列某個模組 | **沒有漏列**。用程式比對全 repo：用到 `@inject`／`Provide[...]` 的正式程式碼全部在清單內，清單外的命中都是註解、文件產生器或 `common/authz` 的說明文字。清單裡的 77 個模組都真的存在。另外核了兩件：①21 支套件的網址全部**不用** `@inject`（改成請求當下從 `app.extensions` 取），所以套件不需要進清單；②本機的靜態清單 `config/di_modules_static.py` 比動態掃描少了兩支、多了一支，**但這是本機舊產物**：該檔不入版控，出貨建置時先重產再用 `--check` 斷言零差異（`scripts/build/build_release.sh:308-311`），不會帶著舊清單出貨 |
| `bulletin.py`／`file_upload.py`：09-12 搬家後的新位置有沒有接對 | **公告接對了**：登入、權限點、登入帳號三件缺一個就拒絕掛載（套件 `assembly.py:69-82`），讀寫三個端點掛的權限點與 config 一致。「拿不到部門就看全部」是第 1 項舊帳。**檔案接的東西都在**，但守門問錯了問題：`file_access_guard=signed_token_or_jwt` 問的是「有沒有登入，或有沒有通行證」，不是「這是不是你的」，這是第 34 項舊帳；新找到的一角是 U13a-1 |
| `ai_bot.py`／`ai_dashboard.py`：之前沒被任何一棒碰過 | **AI 小幫手**：只有登入一道門（`auth_required` 是必填欄位，漏了建構就炸）。套件宣告的權限點是空的，產品授權表裡也沒有這個模組，所以「只驗登入」符合設計。金鑰每次對話才依「目前這家公司」解出來，公司沒設就用原廠那把（`app/system_config/service/ai_provider_key_resolver.py:145-151`）。這是刻意的後備順序，金鑰本身不會回給使用者。次數上限是 CM-1638 舊帳。**AI 儀表板**：見 U13a-2 |
| `integrity.py`：雜湊鏈驗證的零件有沒有接上 | **接上了**，而且啟動閘門和後續抽查用的是同一顆 context（`main.py:123` 建一次，傳進 `create_app()`，`integrity.py:84` 原樣交給套件）。沒傳進來時（測試或腳本）才自己補建。竄改事件寫資料庫那支沒註冊時是「放行、留缺口」，這是套件刻意的設計，已記在第 14 項（兩道鎖只做一道）。附帶觀察：`integrity_tamper_events` 表沒開資料庫隔離，一般應用帳號 `cm_app` 可以刪它（DEV 唯讀查權限）。這張表存的是整台機器的事件、不分公司，不另計，已補在第 14 項的脈絡裡提給首腦 |
| 帶著 DI 腳本比對結果去看：標紅的缺漏逐一回答 | 標紅 3 處都在 `di_containers/`（U13b 範圍）。我先開檔答了，給 U13b 參考：**①`TenantProvisioningService.default_role_name` 不成立**：預設值是 `"System Manager"` 這個角色名字串，不是守門零件，沒給就用預設名稱建預設角色；**②③`MetadataCloneService`／`OscalIoService.role_repo` 不成立**：這個 `role` 是 OSCAL 文件裡的「職責角色」資料表，不是權限角色，沒給時建構子自己補一支真的資料存取物件（`role_repo or RoleRepoImpl()`，套件 `metadata_clone_service.py:54`、`oscal_io_service.py:199`），不是空的 |

## 「套件拿到空值時」逐支對照

這張表回答派工說的「U1 核過 DataAPIService 缺 checker 時是拒絕，其他 plugin 逐支核」：

| 套件 | 必填的門（缺了就拒絕掛載） | 選填、缺了會怎樣 |
|---|---|---|
| jedi-iam | 登入、權限點、換發權杖 | 通知信沒接只產碼不寄；原廠帳號判斷不出來走一般政策（不放寬） |
| jedi-bulletin | 登入、權限點、登入帳號 | 拿不到部門 → **看全部**（放寬，第 1 項） |
| jedi-file-upload | 登入、檔案守門、通行證簽發 | 修正線：沒接歸屬名冊 → **只記警告、照樣掛上**（放寬）；出貨的 1.2.0 根本沒有這個欄位 |
| jedi-notification | 登入、權限點 | — |
| jedi-ai-dashboard | 登入、商務授權 | 修正線：沒接逐支權限檢查 → **全部拒絕**；1.2.0 沒有這個欄位；**端點本身沒有權限點這道門**（U13a-2） |
| jedi-ai-bot | 登入（必填欄位，漏了建構時就炸） | — |
| jedi-integrity | 三張驗證零件 | 資料庫落點沒接 → 留缺口、不擋關機（第 14 項） |
| jedi-flow-engine | 無 | 兩張 port 目前沒人讀 |

## 載入順序查了什麼

- `core/plugins/__init__.py` 的紅線只有兩條：identity 要早於 file_upload，api_log 要晚於 logging 設定。兩條都照做。
- **守門零件不會「被用到時還沒接上」**：`require_capability` 在請求當下才從 `app.extensions['di_container']` 找權限查詢服務（`jedi_iam/authz/capability.py:69-77`）；AI 儀表板的商務授權是請求當下才 `import`；公告、通知的守門是請求當下才去 `runtime()` 拿。`app.extensions['di_container']` 在掛載迴圈**之前**就放好了（`core/app_factory.py:313`）。
- 掛載當下就建實例的只有三處：公告的部門名冊與暱稱搜尋、身分的通知服務、全站唯一的身分名冊。它們內部的資料存取都是「用到才開連線」，不會把第一個請求的連線綁死，**也都不是守門零件**。

## 面板與前次判定不同（請首腦裁）

工具候選 C4 就是 **CM-1605**（FR-078 N2 F1，當時面板判 HIGH、決策者開過卡）：測試信 API 在「沒改密碼」時，把資料庫裡的真 SMTP 密碼配上呼叫者送來的主機位址去登入。這一棒三位檢查員 **0:3 否決**，理由一致：能打這支的人必須持有平台專屬的 `smtp-config.update`，他本來就能直接改掉 SMTP 設定，所以這支沒有給他新的能力。我沒有再查「存設定」那條路是否也會沿用舊密碼，所以無法判斷哪一次對。**兩次面板結論相反，請首腦決定 CM-1605 要維持還是降級。**

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

**第一層：工具正式報告（經三人面板驗證）**

- 驗證章：`verified`（stamp `CLAUDE-SECURITY-REVISION-256994ee2a34-dirty.json`）
- 候選 4 條、面板 12 票零漏投：3 條 3:0 通過、1 條 0:3 否決。研究員派出 2 位、交回 2 位（通讀全範圍 1 位、找寫死密碼專項 1 位，後者零發現），失敗 0，續跑 0。
- 通過的 3 條都錨在接線或釘版本的位置，病灶在套件或 `di_containers/`。這符合 DI 專掃的性質：接線檔本身幾乎沒有邏輯，問題都在「交出去的東西」。

**第二層：runner 自行開檔核對（未經三人面板投票）**

- 範圍 10 支逐支通讀；卡片「重點看什麼」六條、派工三個問題逐條答完。
- 跨讀（只讀不報）：六支套件的 `plugin/contract.py` 與 `assembly.py`（主線與修正線兩版）；`jedi_iam/authz/capability.py`、`middleware/context.py`；`common/authz/license.py`；`common/iam_ports.py`；`app/notification/service/*`；`app/system_config/service/ai_provider_key_resolver.py`；`core/app_factory.py` 的接線段；`main.py` 的防竄改 context；file-upload 1.2.0 wheel 的刪除順序；upload_files 的 migration 004；FE 的 AI 儀表板 API 常數。
- 實測：U13a-2 用套件真的 `register()` 掛端點、Flask 測試客戶端送請求，確認權限點那道門不存在。`di_modules.py` 用程式比對全 repo 的注入使用點。
- DEV 唯讀查證：角色與 `ai-dashboard.read` 對照（16:51）、upload_files 依儲存範圍計數（18:15）、`integrity_tamper_events` 隔離與權限。
- **打折處**：①U13a-1 沒有實刪（會弄壞 DEV 共用檔），是讀碼＋讀 migration 推論；修正線的歸屬名冊擋不擋得住，也沒驗。②本機 venv 有四支套件（file-upload／ai-dashboard／iam／ai-bot）指向**修正線**原始碼，不是出貨的釘住版本。U13a-2 的實測跑在修正線上，但我另外開檔確認主線 1.2.0 與搬家前的主專案端點也沒有權限點這道門，兩邊結論一致。③CM-1605 的面板分歧沒有追到底。

## 執行概況

| 項目 | 值 |
|---|---|
| 掃描目標 | BE repo `compliance-manager-be`，branch `feature/review` |
| 版本 | `256994ee2`（工作區有平行線未 commit 改動，範圍 10 支檔案本身無改動） |
| 工具 | Claude Code 官方 `claude-security` plugin 0.11.0 |
| 參數 | mode `scan`／effort `low`／focus `attack-surface`／scope 10 檔 |
| run ID | `wf_5b4ba820-2db` |
| 報告目錄 | `CLAUDE-SECURITY-20260925-083957/`（不入版控） |
| 耗時 | 約 92 分鐘（5,507 秒；研究員通讀約 64 分鐘，面板約 25 分鐘） |
| 研究員 | 2 派出／2 交回，`failed` 0 |
| 候選／面板票 | 4／12（3:0 通過 3、0:3 否決 1） |
| 驗證章 | `verified` |
| runner 淨新增 | 2 條（中 1、低 1），皆未經面板 |
