---
title: U13b 檢查結果：主專案組裝表七支（di_containers/）
---

# U13b 檢查結果：主專案組裝表七支

> 檢查日期 2026-09-26｜對應卡片 CM-2167（母卡 CM-2152）｜檢查範圍 7 個檔案 996 行｜工具 run ID `wf_21bd999c-982`（報告目錄 `CLAUDE-SECURITY-20260926-024832`）｜驗證章 `verified`

## 🔴 一句話結論

**這七支組裝表沒有漏接任何安全檢查零件，淨新增 0 條。** 工具報了 4 條，三人面板都是 3:0 通過，但**全是總表已經登記過的舊帳**（第 188、192、65 項，以及 CM-1631 金鑰外洩）。四條沒有一條是組裝表本身寫錯：研究員順著組裝表追進它組出來的服務，在服務裡撿到的。

卡片要我回答的三件事：

| 卡片問的 | 答案 |
|---|---|
| 總組裝表（`containers.py`）有沒有漏登記哪個容器 | **沒有漏**。全專案 36 個子組裝表逐一比對，36 個都有登記；每個子組裝表向外要的零件（共 107 個插槽）只缺 1 個，是一個宣告了卻從來沒人用的空插槽，無害 |
| 登記清單（哪些程式要自動拿零件）有沒有漏掛，漏掛的網址會怎樣 | **沒有漏掛**。全部 62 支用到「自動拿零件」的程式，60 支在清單上，另外 2 支是自己在說明文字裡寫了範例、實際不需要掛。就算真的漏掛，實測結果是**一呼叫就報錯（500）**，不會靜默改用別的東西 |
| 建構子有「預設是空」的守門零件，組裝表沒接時是拒絕還是放行 | 七支範圍內**只有摘要報告的兩支服務屬於「沒接就放行」的寫法**，但組裝表**有接**，我把組裝表實際建起來，確認拿到的是真零件。比對腳本在這七支標出的 6 處「可選未接」，逐一核過**全都不是守門零件** |

## 這一棒在檢查什麼

系統啟動時，主專案要把幾百個「零件」（資料服務、權限檢查、授權檢查……）組起來，交給每個網址入口使用。負責「誰拿到哪個零件」的就是這批組裝表（DI 容器：Dependency Injection，依賴注入，意思是程式不自己建零件，由組裝表統一分配）。

這類問題的危險在於**安靜**：組裝表少接一個安全檢查零件，服務不會報錯，只會把那道檢查跳過去。之前在合規稽核套件、SSP 套件都發生過。

| 檔案 | 行數 | 組裝的是什麼 |
|---|---:|---|
| `di_containers/containers.py` | 397 | **總組裝表**。所有子組裝表在這裡登記，並互相接線 |
| `di_containers/cloud_integration/cloud_integration_containers.py` | 310 | 雲端硬碟整合（U4～U6 那三棒的服務） |
| `di_containers/license/license_containers.py` | 84 | 商務授權檢查（U1 的 `license.py` 從這裡拿零件） |
| `di_containers/support/support_containers.py` | 82 | 系統診斷包（U10／U11） |
| `di_containers/project/project_summary_report_containers.py` | 48 | 專案摘要報告（U9） |
| `di_containers/setup/setup_containers.py` | 41 | 首次安裝精靈（U3） |
| `di_containers/identity/identity_containers.py` | 34 | 審計欄位補暱稱用的名冊 |

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

### 1. 總組裝表有沒有漏登記容器

**沒有。** 我寫了一支小腳本，把每個子組裝表宣告「我需要外面給我哪些零件」的插槽，和總組裝表實際塞進去的東西逐一配對（包含後面那些「回頭補接」的寫法，因為有些容器互相需要，只能先登記一個再回頭補）。

- 全專案 `di_containers/` 底下 36 個子組裝表，**36 個都在總組裝表登記**，沒有孤兒。
- 子組裝表向外要的插槽共 107 個，**只有 1 個沒接**：`di_containers/associations/associations_containers.py:45` 的 `job_execution_container`。全專案搜尋，**沒有任何地方用到這個插槽**，是一個宣告了卻從沒用過的殘骸，無害（順手刪掉即可，不必開卡）。
- 補充：沒接的插槽如果真的被用到，實測會**直接報錯**（「Dependency ... is not defined」），不會悄悄給空值。所以「漏登記」在這套寫法下會變成一眼就看得到的錯，不是安靜的缺線。

### 2. 登記清單有沒有漏掛程式（`config/di_modules.py` 那份清單）

總組裝表另外有一份「哪些程式要自動拿零件」的清單。漏掛的程式不會在啟動時報錯，會拿到一個**佔位物件**。

- 我用清單本身的產生邏輯跑出 77 支，再搜尋全專案 `api/ app/ common/ core/ config/` 底下真的用到「自動拿零件」的程式 62 支。**只有 2 支不在清單上**：
  - `common/authz/license.py`：只在檔頭說明裡寫了一段範例，自己不需要自動拿零件（它是在呼叫當下從容器直接取，`:191`、`:197`）。
  - `config/socketio_namespaces.py`：同樣只是說明文字提到。
- **漏掛會發生什麼（實測）**：做一個沒掛上的假入口去呼叫，拿到的是佔位物件，一使用就 `AttributeError`（使用者看到 500）。**不會靜默改用預設值、也不會跳過檢查。**
- U13a 已經從「清單順序」那面核過同一份檔案，結論一致。

### 3. 授權檢查的零件對不對得上（`license_containers.py`）

**對得上。** 組裝表給 `LicenseGuard` 的三個零件，就是它建構子要的三個（`common/authz/license.py:153-154`），沒有多、沒有少、沒有預設值可以偷懶。

卡片擔心的「兩條接線路徑只給一邊」也核過：租戶目錄（用來判斷「子公司繼承母公司的授權」）在組裝表這邊有給（`license_containers.py:59`），另一條網址用的接線在 `core/plugins/license.py:334` 也有給，**兩邊都接上了**。套件那端如果沒拿到租戶目錄，會記一行警告並退回「只看自己的授權」（`jedi-license-runtime` `license_tenant_resolver.py:44`），所以這個零件漏接屬於安靜型缺線，但目前兩邊都有接。

另外兩個「預設是空」的參數也看過：套件裡授權服務的「停權紀錄」零件預設是空、空的時候當作「沒人被停權」（套件原始碼自己註明是刻意不擋），但組裝表有接（`license_containers.py:74`），不觸發。

### 4. 雲端硬碟、診斷包、摘要報告、安裝精靈四支，對照各業務棒的發現

- **雲端硬碟（U4～U6）**：我把組裝表實際載入、逐一核對每個零件的建構子，沒接的只有三個資料存取物件的 `session` 參數。那是留給測試塞假連線用的，正式執行時從目前的交易自動取，**不是缺線**。U5／U6 查到的「初始化資料夾不檢查專案歸屬」（第 192 項）是**服務本身就沒寫檢查**，不是組裝表沒接；同一張表裡需要檢查的 `drive_project_verify_service`，專案成員零件有接（`cloud_integration_containers.py:308`）。加密金鑰（`:109`）沒設的話，建構時直接報錯（`fernet_crypto.py:8-11`），不會退回沒加密。
- **診斷包（U10／U11）**：三支服務的零件全部有接。兩支對外服務的守門是**寫在程式裡、不靠零件**（`require_platform_admin()`，`diag_bundle_export_service.py:74`、`recent_error_app_service.py:46`），組裝表沒辦法讓它們放行。U10 已確認這組容器只接了兩條網址，與我看到的一致。
- **摘要報告（U9）**：這是七支範圍裡**唯一一處「零件沒接就放行」的寫法**（下一節詳述），但有接。
- **安裝精靈（U3）**：`SetupWizardService` 的七個零件**全部是必填**，沒有預設值，組裝表七個都有給，少一個啟動時就會報錯。

### 5. 帶著 DI 比對腳本的結果來看

比對腳本（`di-wiring-audit.md`）標紅的 3 處，**都不在這七支裡**（在 `auth_containers.py` 與 `oscal_containers.py`），屬於 U13a／首腦串接縫的範圍。我還是順手開了套件原始碼看一眼，給首腦參考：

| 腳本標紅 | 我看到的 | 判斷 |
|---|---|---|
| `TenantProvisioningService.default_role_name`（`auth_containers.py:269`） | 預設值是字串 `"System Manager"`（`jedi-iam` `tenant_provisioning_service.py:47`），是「新公司預設建哪個角色名稱」 | **不成立**。參數名含 role 被腳本誤判，其實是設定值，不是守門零件 |
| `MetadataCloneService.role_repo`（`oscal_containers.py:177`） | 沒給時套件自己建一個（`metadata_clone_service.py:54`，`role_repo or RoleRepoImpl()`），用途是複製 OSCAL 文件裡的「角色」欄位資料 | **不成立**。這是資料存取物件，不是權限檢查；沒接也不會變空 |
| `OscalIoService.role_repo`（`oscal_containers.py:206`） | 同上（`oscal_io_service.py:199`） | **不成立**，理由同上 |

比對表「可選未接：預設 None」那節裡屬於這七支的 **6 處**，逐一核過：

| 位置 | 缺的參數 | 是什麼 | 判斷 |
|---|---|---|---|
| `cloud_integration_containers.py:97` | `TenantDriveIntegrationRepoImpl.session` | 測試用的假連線入口，正式執行自動取目前交易 | 不是缺線 |
| `cloud_integration_containers.py:131` | `DriveFolderMappingRepoImpl.session` | 同上 | 不是缺線 |
| `cloud_integration_containers.py:132` | `DriveSyncJobRepoImpl.session` | 同上 | 不是缺線 |
| `identity_containers.py:31` | `IdentityContext.tenant_resolver` | 把「公司編號」補成公司名稱，只影響顯示 | 不是守門；沒接時回空、畫面顯示編號（套件刻意設計） |
| `identity_containers.py:31` | `IdentityContext.org_unit_resolver` | 把「部門編號」補成部門名稱，只影響顯示 | 同上 |
| `support_containers.py:54` | `DiagLogReader.basename` | 日誌檔名，預設 `app.log` | 設定值，不是缺線 |

我另外用程式實際載入這六支子組裝表、逐一比對建構子（不靠靜態腳本），結果與腳本**完全一致**，沒有腳本漏掉的項目。

## 「零件沒接就放行」：摘要報告那一處

`project_summary_report_service.py:40` 的寫法是：專案成員檢查零件如果是空的，**直接放行**。同樣的寫法在 `project_summary_report_history_service.py:57` 也有。

- **現況**：組裝表有接（`project_summary_report_containers.py:38`、`:46`），我把容器實際建起來，兩支服務拿到的都是真零件。反過來把那個插槽拿掉，容器**直接報錯**，不會給空值。所以**今天不會觸發**。
- **為什麼還是寫出來**：這種寫法是「組裝表某天被改漏，檢查就安靜消失」的形狀，正是這一棒要找的東西。U9 報告第 112 行已經講過同一件事、結論相同。**我不另立項**，但建議修第 65 項時順手把 `is None` 改成「沒接就拒絕」，一行的事。
- 附帶：這支服務的全專案建構點只有組裝表這一處，沒有別的程式繞過組裝表自己 new 一個（全 repo 搜尋 `ProjectSummaryReportService(` 零命中，只有測試）。

## 工具報的逐條

四條都是 3:0 通過，**全部是舊帳，不另計**。

| 工具編號 | 嚴重度 | 說的是什麼（白話） | 位置 | 總表對應 |
|---|---|---|---|---|
| F1 | 高 | Google 硬碟授權完成的回呼頁，不用登入就能打，網址裡的錯誤訊息被原樣塞進頁面程式碼，可以插入攻擊程式偷走登入權杖 | `api/cloud_integration/routes/google_drive_integration_route.py:120` | **第 188 項**（U4 已記，建議先轉 FR-114） |
| F2 | 中 | Google 硬碟應用程式的密鑰，原文出現在 12 支已進版控的對話紀錄檔，而且跟目前 `.env` 用的是同一把 | `docs/conversation-history/2026-04-21-google-drive-integration/...part-05-of-14.md:8157` 等 12 檔 | **CM-1631**（檔數 12 與 FR-113 O2 重數相同） |
| F3 | 中 | 雲端硬碟「初始化資料夾」不檢查專案是不是你的，可以把別家公司的專案結構建進自己的硬碟，再反過來塞假證據 | `app/cloud_integration/service/drive_sync_admin_service.py:125` | **第 192 項**（U5／U6 已記，總表評高、決策者 09-25 已裁要修） |
| F4 | 中 | 摘要報告的單筆、清單、歷史四支讀取不檢查是不是專案成員，同公司任何人都讀得到 | `app/project_summary_report/service/project_summary_report_service.py:79` | **第 65 項**（主線 `:78-86` 仍未補，修正線 CM-2037 在處理） |

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - 本棒淨新增 0 條，工具報的 4 條全為舊帳（總表第 188／192／65 項與 Google 密鑰）：第 188 項 ✅ 已修（CM-2200）、第 192 項 ✅ 已修（CM-2201）、第 65 項 ✅ 已修（CM-2179）、Google 密鑰 ✅ 已修（CM-2051，原卡 CM-1631 作廢）。

**面板否決 1 條（0:3）**：雲端硬碟權杖的加密金鑰原文出現在 10 支對話紀錄檔。外洩是真的，但面板認為光有這把金鑰拿不到任何東西，還要有資料庫存取權。這把金鑰**已在 CM-1631 裡**（10 檔），而且總表已記下「跟 CM-1608 資料庫密碼串起來就能讀客戶硬碟」這條串接風險。所以面板否決不影響現有登記。

**嚴重度差異**：F3 工具評中、總表第 192 項評高。總表是 U6 在 DEV 實際跑通跨公司寫證據後定的，**維持總表的高**。

## 可信度分兩層

**第一層：工具正式報告（有三人面板背書）**
- 驗證章 `verified`：5 個候選、**15 票全投完、零漏投**，4 條 3:0 通過、1 條 0:3 否決。研究員 2 派 2 回，`failed` 0。
- 修訂章 `CLAUDE-SECURITY-REVISION-222a13f80868-dirty.json`。`-dirty` 是因為工作區有平行線未 commit 的改動，這批改動**都不在這七支檔裡**。
- 這是快篩檔（`low`），只有一位主研究員加一道密鑰掃描。工具擅長抓「寫錯的邏輯」，不擅長抓「應該有卻沒有的零件」。這一棒工具的四條產出全落在範圍外，**對組裝表本身沒有提供任何判斷**，所以本棒的主要結論靠第二層。

**第二層：runner 自己開檔與實測（未經三人面板）**
- 七支檔**逐支通讀**。
- 自寫腳本比對「子組裝表要的插槽 vs 總組裝表給的」（36 個容器、107 個插槽）。
- 自寫腳本比對「用到自動拿零件的程式 vs 登記清單」（62 支 vs 77 支）。
- 實際載入六支子組裝表，逐一比對建構子參數（結果與比對腳本一致）。
- **實測三件**：①摘要報告容器接上時拿到真零件、拿掉插槽時直接報錯；②沒掛清單的入口拿到佔位物件，一呼叫就 500；③比對腳本標紅三處都開了套件原始碼核對。
- **打折處**：總組裝表整張建不起來。本機 import 時 `flow_control_containers.py:33` 找不到 `jedi_compliance_audit.app.service.project_current_ssp_service`，因為目前虛擬環境指向修正線工作樹，套件版本與主線對不上。所以「授權檢查／診斷包／雲端硬碟在總組裝表裡拿到真零件」這一半是**讀碼加子容器單獨實測推出的，不是整張總表端到端實跑**。這件事跟 U5、U6 回報的「本機 BE 起不來」是同一類環境問題，不影響本棒結論，但請首腦知道。

## 範圍外讀了什麼（只讀不報）

`config/di_modules.py`、`core/app_factory.py:275-300`（組裝與登記的時機）、`common/authz/license.py`、`core/plugins/license.py`、全部 36 支子組裝表的插槽宣告、摘要報告／診斷包／安裝精靈／雲端硬碟相關服務的建構子，以及 `jedi-license-runtime`、`jedi-common`（`IdentityContext`）、`jedi-iam`（`TenantProvisioningService`）、`jedi-oscal-v2`（兩支 `role_repo`）的原始碼。

## 執行概況

| 項目 | 內容 |
|---|---|
| 範圍 | 7 檔／996 行（`git ls-files | xargs wc -l` 實測） |
| 工具 | `claude-security` 0.11.0，effort `low`、focus `attack-surface` |
| run ID | `wf_21bd999c-982`（報告目錄 `CLAUDE-SECURITY-20260926-024832`） |
| 掃描版本 | `222a13f8086847570815e247891453cc19dde1f2`（dirty，平行線改動不在範圍內） |
| 耗時 | 約 36 分鐘（02:48～03:25 UTC） |
| agent | 17 派 17 回，`failed` 0 |
| 候選／票數 | 5 候選、15 票全投，4 條 3:0 通過、1 條 0:3 否決 |
| 驗證章 | `verified` |
| 淨新增 | 0（工具 4 條全為舊帳：第 188、192、65 項、CM-1631） |
| 順帶觀察（不開卡） | `associations_containers.py:45` 有一個從沒用過的空插槽，可順手刪；摘要報告「零件沒接就放行」建議修第 65 項時一起改成拒絕 |
