---
title: module_frame 掃描盤點——範圍界定與切棒
---

# module_frame 掃描盤點：範圍界定與切棒

> 盤點日期 2026-09-22（2026-09-22 依首腦回饋修訂：B1 由水平切改縱切、補 2 支懸案檔、標 B4/B6 同 runner 接縫）｜這份文件只盤點、不掃描——切好棒等首腦開卡派工。

---

## 這批最該先看的兩支

在看範圍界定跟切棒之前，先點名這批裡訊號最強的兩支檔案——兩支都是**整支抓不到任何權限關鍵字**，而且都涉及「刪除」或「下載／匯出」這兩種master 總表已經證實最容易出事的路徑：

- **`app/module_frame/service/module_frame_reference_document_service.py`（B2，257 行）**：8 個對外方法、0 個權限關鍵字命中，其中含一支**刪除**路徑。
- **`app/module_frame/service/module_frame_template_import_service.py`（B5，1,011 行）**：6 個對外方法、0 個權限關鍵字命中，其中含兩支**下載／匯出**路徑，且涉及批次寫入。

**為什麼這兩支要優先看**：master 總表第 128 項（下載／匯出，跨公司下載 SSP Excel）跟第 133 項（刪除，刪框架沒查用量）已經證實「刪除」跟「下載／匯出」是這批程式碼裡最容易漏掉歸屬檢查的兩種路徑形狀；而「整支檔案 grep 不到任何權限關鍵字」，訊號強度跟 O2 當初鎖定、後來證實真的有洞的 `resource_library_app_service.py` 一模一樣——O2 就是靠同樣的訊號抓到真正的高風險漏洞。這兩支已經分別排進 B2、B5 的重點清單（見第 3 節），這裡先整段列出來，讓首腦一眼看到本次盤點裡風險訊號最集中的地方。

---

## 為什麼要另開一棒

`module_frame` 是「合規範本／資源庫」那塊程式碼——客戶自己建的合規範本、原廠給大家用的公版範本，都放在這裡。FR-113 原本盤點時判斷這裡「入口層的守門看起來比較齊全」（25 支路由檔裡有 15 支已經用了 `require_capability` 這種角色權限檢查），所以沒有排進原本的十一棒，準備另外處理。

**這個判斷後來被推翻了。** O5 那一棒本來是去查 SSP 底下六類附屬資料（人員、設備清單等）的權限有沒有守好，查完結論是「六組全部守齊，乾淨」。但**餵資料給這六組的那支「把整份系統安全計畫匯出成 Excel 範本」的功能，剛好就是 module_frame 的程式碼，而它完全沒有檢查『這份計畫是不是你的』**——這條被列進總表第 128 項，判定「高風險」：任何一個登入的人，只要知道別人的計畫編號，就能把對方整份系統安全計畫（人名、email、電話、地址、設備 IP、每條控制項的實施狀況）下載走。

這件事說明一個道理：**「有守門」不等於「守對了門」。** module_frame 的路由層確實比較常見 `require_capability` 這種「你的角色能不能用這個功能」的檢查，但這種檢查只問「你是誰」，不問「這筆資料是不是你的」——真正防止跨公司資料外洩的第二道檢查（「這份範本／這份計畫是你們公司的嗎」），module_frame 到目前為止**沒有一支檔案被完整讀過、確認過有沒有這道檢查**。O2、O3a、O4、O5 四棒陸續在 module_frame 或它緊鄰的檔案裡各撞到一次同一種病（「隔壁端點有守，這支沒守」），這是第三、第四次同一種病出現在同一塊地——值得專門派一棒把 module_frame 自己的程式碼從頭到尾看一遍。

---

## 1. 範圍界定（Scope）

### 1.1 起手：從 `git ls-files` 抓檔名

```bash
git ls-files -- api/module_frame app/module_frame domain/module_frame infra/module_frame di_containers/module_frame | grep '\.py$'
```

共 **83 支 `.py` 檔**。扣掉 15 支空的或近乎空的 `__init__.py`（0～1 行，完全空白或只有 1 行，見下方「不列入切棒的空檔」），剩下 **68 支有實質內容的檔案，合計 11,065 行**——這裡面還含 2 支「只有 docstring、沒有邏輯」的 `__init__.py`（`app/module_frame/__init__.py` 7 行、`infra/module_frame/models/__init__.py` 4 行），同樣不派工，实際要切棒的是 **66 支邏輯檔、11,054 行**。

**這個數字還要再加上兩支「檔名／路徑抓不到、但判定要納入切棒」的檔案**：`api/oscal/routes/module_frame_template_ssp_route.py`（38 行，掛在 `api/oscal/routes/` 底下，原本因為列在 O2 宣告範圍內而排除，**首腦回饋後改判定「O2 沒真的查過，要納入」**，見下方 1.4）、`di_containers/dashboard_apis/module_frame.py`（22 行，掛在 `di_containers/dashboard_apis/` 底下，原本判斷風險低不排棒，**首腦回饋後改判定「DI 漏接會讓守門靜默失效，22 行成本低，要納入」**，見下方 1.2）。

**最終要切棒的範圍：68 支檔、11,114 行**（66 支邏輯檔 11,054 行 + 2 支補入檔 60 行），跟第 2 節「切棒」逐棒行數加總、扣掉跨棒重複的接縫檔之後的數字一致。

### 1.2 光靠檔名抓不到的檔案

按規定不能只信任「檔名有沒有 `module_frame` 字樣」，另外做了兩件事：

**① 反查誰在 import module_frame 的東西**：

```bash
grep -rl "module_frame\|ModuleFrame" --include="*.py" api app domain infra di_containers common config core
```

抓出 46 支檔案提到 `module_frame`。逐一檢查後，**沒有找到任何一支「檔名不含 module_frame、但骨子裡其實是 module_frame 業務邏輯」的檔**——這 46 支全部屬於別的模組（`oscal`／`flow_control`／`auth`／`project` 等），只是**呼叫或參照** module_frame 的東西（例如 import 一個 enum、呼叫一個服務方法、在註解裡提到對齊哪支檔的欄位命名慣例）。這些檔案本身的守門邏輯屬於它們自己所在的模組，不屬於 module_frame，所以**不納入這次的範圍**，但下面「跟別的模組共用的介面」會列出幾支比較值得注意的。

**② 特別檢查 DI 接線目錄**：`di_containers/dashboard_apis/module_frame.py`（22 行）—— 這支檔案掛在 `di_containers/dashboard_apis/` 底下、**不在 `di_containers/module_frame/` 這個路徑裡，靠檔名抓不到**。內容是把 `ModuleFrameService.get_module_frame_list_for_dashboard` 這個唯讀查詢方法註冊給 AI 儀表板呼叫。

**判斷（已依首腦回饋修正）**：盤點員原判斷「只是把既有服務的一個唯讀方法登記給另一個系統用，沒有新的商業邏輯，風險低，不需要排棒」——**這個判斷被推翻**。理由：FR-113 O9b 那一棒的發現是「依賴注入漏接會讓守門靜默失效」——DI 的 Factory 是 lazy 建構，接線對不對只有在那個端點真的被打到時才會現形，平時看起來一切正常。這支檔案就是一個「換一條路徑接出去」的 DI 接線，跟主線（`api/module_frame/routes/module_frame_route.py` 那條走 `require_capability` 守門的路徑）是不是同一套邏輯、同一套租戶隔離，沒有實際驗證過。22 行的成本近乎零，**改判定納入 B1（縱切後的主體 CRUD 批次），見第 2 節**，審查重點是「它接出去的東西跟主線是不是同一套守門」。

### 1.3 跟別的模組共用介面、但不屬於這次範圍的檔案（僅供參考，不用看）

以下檔案會 import 或提到 module_frame，但邏輯屬於別的模組，**已經在 FR-113 別棒的範圍內或屬於別的模組**，這裡列出來只是讓人知道「查到過、判斷過不用重複看」：

- `app/oscal/service/ssp_*_app_service.py`（六支，components/party/inventory/leveraged/resources/system_characteristic）—— OSCAL 側的 SSP 子物件服務，會 import module_frame 對應服務的欄位命名常數來保持一致（例如 `ModuleFramePartyService as _PmMap`），**已經是 FR-113 O5 的範圍，O5 判定「六組全部守齊」**。
- `domain/oscal/service/ssp_context_resolver.py`——已經是 O5 範圍內的檔案。
- `app/oscal/service/resource_library_app_service.py`——O2 範圍，見下方排除清單。
- `app/project/service/project_start_app_service.py`——專案啟動時「複製一份範本」的呼叫端，只是呼叫 module_frame 的既有方法，不在這次範圍，但值得留意：**呼叫端要是也繞過了守門，即使 module_frame 自己補好了洞，這裡沒補一樣有事**——這點寫進第 4 節的風險提示。
- `core/plugins/evidence_classification.py`——證據分類外掛程式，讀取 `module_frame_id → module_frames.oscal_framework_version_uid` 做唯讀查詢，不在範圍內。
- `app/auth/service/user_import_template_app_service.py`、`app/oscal/service/excel_parser/sheet_handlers.py`——這兩支是 `excel_template` 這個共用套件（在範圍內，見批次 B6）的**外部消費者**，屬於 auth／oscal 模組自己的範圍，不用看，但代表 B6 這個套件被別的模組共用，改動時要小心波及。

### 1.4 排除清單（已經掃過、真的查過確認過，不重複掃）

以下檔案**明確排除**，不列入這次切棒：

| 檔案 | 行數 | 哪一棒掃過 | 備註 |
|---|---|---|---|
| `app/oscal/service/resource_library_app_service.py` | 530 | **FR-113 O2** | O2 找到的高風險就在這支：改範本的「適用控制項」清單時沒檢查是不是自己公司的範本 |

**`api/oscal/routes/module_frame_template_ssp_route.py`（38 行）不再排除，已改判定納入 B3**（依首腦回饋修正）。這支原本因為「列在 O2 的 11 檔範圍表內」被當成已掃過而排除，但 O2 報告在「這棒沒有回答的事」段落明確寫「掃描沒有針對這支提出發現」——**這句話的意思是『工具沒有針對這支報出發現』，不是『查過、確認沒事』**。這支路由只有 `@jwt_required()`、完全沒有角色權限檢查，**O2 排除它是因為工具沒報、不是查過沒事，這次要把它當成沒查過的檔來看**，見第 2 節 B3、第 3 節審查重點。

除了 `resource_library_app_service.py`，FR-113 十一棒（O1～O9，另加 O3a／O3b／O9a／O9b 這種細分）的宣告範圍**沒有任何一支落在 `api/module_frame/`、`app/module_frame/`、`domain/module_frame/`、`infra/module_frame/`、`di_containers/module_frame/` 這五個目錄底下**——O5 掃的六組附屬資料 CRUD 全部是 `api/oscal/routes/ssp/*` 和 `app/oscal/service/ssp_*`，物理上不在 module_frame 目錄裡，即使找到的洞（`ssp_import_template_app_service.py`）剛好是 module_frame 的檔案。這點已經對照 `scan-inventory.md` 全部十一棒的檔案清單逐項確認過。

### 1.5 不列入切棒的空檔（15 支，0～1 行）

這些檔案只有 `__init__.py` 的模組宣告或一行 docstring，沒有任何邏輯，不派工也不用看：

```
api/module_frame/routes/__init__.py
api/module_frame/serializers/__init__.py
app/module_frame/code/__init__.py
app/module_frame/dto/__init__.py
app/module_frame/service/__init__.py
di_containers/module_frame/__init__.py
domain/module_frame/__init__.py
domain/module_frame/code/__init__.py
domain/module_frame/entity/__init__.py
domain/module_frame/repository/__init__.py（1 行）
domain/module_frame/service/__init__.py（1 行）
infra/module_frame/__init__.py
infra/module_frame/adapter/__init__.py
infra/module_frame/mapper/__init__.py
infra/module_frame/repository/__init__.py
```

另外 2 支雖然 `wc -l` > 1，但整支只有 docstring、沒有任何邏輯，同樣不派工：`app/module_frame/__init__.py`（7 行）、`infra/module_frame/models/__init__.py`（4 行）。

### 1.6 最終要掃的範圍

**68 支檔、11,114 行**（83 支 `git ls-files` 抓到的檔 − 15 支空檔 − 2 支純 docstring 檔 = 66 支邏輯檔、11,054 行；加上 2 支路徑/檔名抓不到但改判定納入的檔案 `api/oscal/routes/module_frame_template_ssp_route.py`（38 行）與 `di_containers/dashboard_apis/module_frame.py`（22 行），共 68 支、11,114 行）。

驗證指令（首腦可以直接跑，跟本文件第 2 節的批次總數對得起來；已實跑驗證輸出「68 支檔、11114 行」）：

```bash
{
  git ls-files -- api/module_frame app/module_frame domain/module_frame infra/module_frame di_containers/module_frame | grep '\.py$'
  echo api/oscal/routes/module_frame_template_ssp_route.py
  echo di_containers/dashboard_apis/module_frame.py
} | while IFS= read -r f; do
    n=$(wc -l < "$f")
    if [ "$n" -gt 1 ] && [ "$f" != "app/module_frame/__init__.py" ] && [ "$f" != "infra/module_frame/models/__init__.py" ]; then
      echo "$n $f"
    fi
  done | awk '{s+=$1; c+=1} END {print c" 支檔、"s" 行"}'
```

---

## 開卡結果（2026-09-22）

母卡與 8 張子卡已一次建完，Case No 連號。

**母卡：[CM-2073](https://app.notion.com/p/module_frame-8-68-11-114-3e3346da4cd081b98646fd60bca7dd4c)**

| 棒 | Case No | 範圍 | 派工順序 |
|---|---|---|---|
| B5 | [CM-2074](https://app.notion.com/p/module_frame-B5-Excel-3-1-376-Opus-5-1M-effort-low-3e3346da4cd0816e9138e9c999b93cec) | 3 檔／1,376 行 | ① 全批最優先 |
| B2 | [CM-2075](https://app.notion.com/p/module_frame-B2-12-1-642-Opus-5-1M-effort-low-3e3346da4cd081b9bc8af134fdacc734) | 12 檔／1,642 行 | ② 次優先 |
| B4 | [CM-2076](https://app.notion.com/p/module_frame-B4-SSP-Excel-2-1-669-B6-runner-Opus-5-1M-effort-low-3e3346da4cd081568ebec187d0ca79fa) | 2 檔／1,669 行 | ③ 接 B6，同一 runner |
| B6 | [CM-2077](https://app.notion.com/p/module_frame-B6-excel_template-Excel-7-1-656-B4-runner-Opus-5-1M-3e3346da4cd0812f9c33ee2dc6282850) | 7 檔／1,656 行 | ③ 承 B4，同一 runner |
| B1 | [CM-2078](https://app.notion.com/p/module_frame-B1-19-1-787-Opus-5-1M-effort-low-3e3346da4cd081d0886bde0bdbb837b4) | 19 檔／1,787 行 | ④ |
| B1-item | [CM-2079](https://app.notion.com/p/module_frame-B1-item-4-675-B1c-runner-Opus-5-1M-effort-low-3e3346da4cd081feadb8c41e2fa22a05) | 4 檔／675 行 | ⑤ 接 B1c，建議同一 runner |
| B1c | [CM-2080](https://app.notion.com/p/module_frame-B1c-YAML-8-925-B1-item-runner-Opus-5-1M-effort-low-3e3346da4cd081f78f41c8b28dca3fea) | 8 檔／925 行 | ⑤ 承 B1-item，建議同一 runner |
| B3 | [CM-2081](https://app.notion.com/p/module_frame-B3-SSP-15-1-904-Opus-5-1M-effort-low-3e3346da4cd081a1bee6dff5400bfb4a) | 15 檔／1,904 行 | ⑥ |

**八棒 scope 檔數已全數實跑 `git ls-files` 驗證**，與本文件各棒寫的數字完全對上（19／4／8／12／15／2／3／7），沒有一棒對不上。

**開卡時實查補進 B1-item 卡的一件事**（本文件第 3 節原本沒有）：`app/module_frame/service/module_frame_item_service.py` 的守門只出現在一處（`:7` import `assert_scope_writable`、`:65` 呼叫一次），但整支檔有 **5 支公開方法**（`:29` get／`:46` update_xml／`:89` add／`:159` update／`:227` delete）——一支有守、四支沒有出現守門呼叫，是本 arc 反覆抓到的「同一支檔裡這條路守了、那條路沒守」形狀。此事實已寫進 CM-2079。

---

## 2. 切棒（8 棒，每棒 ≤2,000 行、≤30 檔）

切法原則：**按業務路徑垂直切**（同一組功能的 route→app→domain→infra→repo→ORM model 放一棒），不按 DDD 分層水平切——權限漏洞活在層與層的接縫上，只給 route 或只給 service 兩邊都看不出來。行數是逐檔 `wc -l` 實算，不是估的。

**這節在首腦回饋後重切過一次**：原本的 B1a（route＋app 層）／B1b（domain＋infra 層）是按 DDD 分層水平切，違反上面這條原則，即使把 `module_frame_service.py` 重複列在兩棒當接縫，也只補了一個點、整條路徑還是斷的。改法是**實際追過呼叫關係**，按「範本主體」跟「範本子項」這兩條獨立業務路徑重新縱切，見下面 B1、B1-item。追完之後確認：**主體跟子項並沒有落入「domain／infra 幾乎完全共用、縱切會讓兩棒重複八成」的狀況**——子項（`module_frame_item_service.py`）本身完全沒有直接碰 module_frame 的 domain／infra，唯一一次跨進主體地盤是呼叫 `module_frame_service.get_module_frame()` 這一行，所以子項這棒是「route＋serializer＋app service＋一支接縫檔」的乾淨小棒，不需要重複整組 domain／infra，兩棒之間沒有大量重複的問題。

### B1 — ModuleFrame 主體 CRUD（route→app→domain→infra→DI 全路徑）

**派工卡：[CM-2078](https://app.notion.com/p/module_frame-B1-19-1-787-Opus-5-1M-effort-low-3e3346da4cd081d0886bde0bdbb837b4)**

範本本身的新增／查詢／修改／刪除／複製，完整一條路徑，不按層拆。**追過呼叫關係確認**：`module_frame_service.py` 只 import `domain.module_frame.entity.*`、`domain.module_frame.service.module_frame_domain_service`，不 import `domain.module_frame.dto`／`action_type_enum`（那兩支只有 YAML 匯入路徑在用，歸進 B1c）；`module_frame_domain_service.py` 只 import `domain.module_frame.repository.module_frame`／`entity.*`，往下到 `module_frame_repo_impl.py`／`infra/module_frame/models/module_frame*.py`／`mapper/module_frame*.py` 是唯一路徑，沒有分岔。`di_containers/dashboard_apis/module_frame.py`（22 行）依首腦回饋納入這棒——它註冊的 `get_module_frame_list_for_dashboard` 是 `module_frame_service.py` 自己的方法，屬於主體這條路徑的另一個入口。

```
api/module_frame/__init__.py                                  252   ← Blueprint 路由總表，看得到全部 URL
api/module_frame/routes/module_frame_route.py                 102
api/module_frame/serializers/module_frame.py                  153
app/module_frame/service/module_frame_service.py              260   ← 主體 CRUD 邏輯
app/module_frame/dto/module_frame_dto.py                       70
app/module_frame/dto/module_frame_menu_dto.py                  34
app/module_frame/code/module_frame_error_code.py                32
domain/module_frame/service/module_frame_domain_service.py      76
domain/module_frame/entity/module_frame_entity.py              113
domain/module_frame/entity/module_frame_query_filter.py         37
domain/module_frame/entity/module_frame_menu_entity.py          23
domain/module_frame/repository/module_frame.py                  33
infra/module_frame/repository/module_frame_repo_impl.py        136
infra/module_frame/models/module_frame.py                       91
infra/module_frame/models/module_frame_trans.py                 29
infra/module_frame/mapper/module_frame_mapper.py                41
infra/module_frame/mapper/module_frame_menu_mapper.py            23
di_containers/module_frame/module_frame_containers.py           260   ← 全模組 DI 接線，看得到所有 service 怎麼被組起來
di_containers/dashboard_apis/module_frame.py                     22   ← 依首腦回饋納入：檢查它接出去的東西跟主線是不是同一套守門
```
共 **19 檔／1,787 行**。驗證：`git ls-files -- api/module_frame/__init__.py api/module_frame/routes/module_frame_route.py api/module_frame/serializers/module_frame.py app/module_frame/service/module_frame_service.py app/module_frame/dto/module_frame_dto.py app/module_frame/dto/module_frame_menu_dto.py app/module_frame/code/module_frame_error_code.py domain/module_frame/service/module_frame_domain_service.py domain/module_frame/entity/module_frame_entity.py domain/module_frame/entity/module_frame_query_filter.py domain/module_frame/entity/module_frame_menu_entity.py domain/module_frame/repository/module_frame.py infra/module_frame/repository/module_frame_repo_impl.py infra/module_frame/models/module_frame.py infra/module_frame/models/module_frame_trans.py infra/module_frame/mapper/module_frame_mapper.py infra/module_frame/mapper/module_frame_menu_mapper.py di_containers/module_frame/module_frame_containers.py di_containers/dashboard_apis/module_frame.py | xargs wc -l | tail -1`

### B1-item — ModuleFrame 子項 CRUD（route→app→接縫，全路徑）

**派工卡：[CM-2079](https://app.notion.com/p/module_frame-B1-item-4-675-B1c-runner-Opus-5-1M-effort-low-3e3346da4cd081feadb8c41e2fa22a05)**

範本底下「子項」（流程樣板 Call Activity 相關）的新增／查詢／修改／刪除。**追過呼叫關係確認：這條路徑幾乎沒有自己的 domain／infra**——`module_frame_item_service.py` 全文只在 `delete_module_frame_item` 一支方法裡呼叫一次 `self.module_frame_service.get_module_frame(...)` 跨進主體地盤，其餘邏輯全部委派給外部套件 `jedi_flow_engine` 的 `WorkflowTemplateService`（管流程範本 XML），不直接碰 `domain.module_frame` 或 `infra.module_frame` 任何一支檔案，DI 接線上也印證了這點（`module_frame_item_service` 的 Factory 只依賴 `module_frame_service`，沒有另外接 domain/infra）。**這不是「共用太多切不開」——是這條路徑本來就沒有自己的 domain/infra 層**，所以縱切自然乾淨：這棒只需要它自己的 route／serializer／service，加上 `module_frame_service.py` 當接縫檔（重複自 B1，看得出子項改動有沒有真的經過主體那層的檢查）。

```
api/module_frame/routes/module_frame_item_route.py             128
api/module_frame/serializers/module_frame_item.py                38
app/module_frame/service/module_frame_item_service.py           249   ← 唯一跨進 B1 地盤的呼叫在 delete_module_frame_item()，呼叫 module_frame_service.get_module_frame()
app/module_frame/service/module_frame_service.py                260   ← 接縫檔，重複自 B1
```
共 **4 檔／675 行**。驗證：`git ls-files -- api/module_frame/routes/module_frame_item_route.py api/module_frame/serializers/module_frame_item.py app/module_frame/service/module_frame_item_service.py app/module_frame/service/module_frame_service.py | xargs wc -l | tail -1`

### B1c — YAML 匯入（獨立小路徑，順便併在這棒問）

**派工卡：[CM-2080](https://app.notion.com/p/module_frame-B1c-YAML-8-925-B1-item-runner-Opus-5-1M-effort-low-3e3346da4cd081f78f41c8b28dca3fea)**

範本可以整批用 YAML 檔匯入，是獨立的一條路，體積小，附在這裡不用單獨開棒。**`domain/module_frame/dto/module_frame_dto.py`／`domain/module_frame/code/module_frame_action_type_enum.py` 這兩支確認只有這條路徑在用**（`module_frame_import_service.py` import 這兩支，B1 的 `module_frame_service.py`／`module_frame_domain_service.py` 都不 import），歸進這棒。這條路徑也會呼叫 `module_frame_item_service.add_module_frame_item`／`update_module_frame_item`（批次建子項），所以邏輯上跟 B1-item 也有一條接縫，但因為 `module_frame_item_service.py` 完整重複進來會讓這棒過重，改成文字提醒：**要追「YAML 匯入建出來的子項有沒有跟手動建立走同一套檢查」，去 B1-item 對照 `module_frame_item_service.py`**。

```
api/module_frame/routes/module_frame_import_route.py              95
app/module_frame/service/module_frame_import_service.py          415
infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py  65
infra/module_frame/adapter/parser_adapter_factory.py               17
infra/module_frame/ports/parser_port.py                             9
domain/module_frame/dto/module_frame_dto.py                        49
domain/module_frame/code/module_frame_action_type_enum.py          15
app/module_frame/service/module_frame_service.py                  260   ← 接縫檔，重複自 B1
```
共 **8 檔／925 行**。驗證：`git ls-files -- api/module_frame/routes/module_frame_import_route.py app/module_frame/service/module_frame_import_service.py infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py infra/module_frame/adapter/parser_adapter_factory.py infra/module_frame/ports/parser_port.py domain/module_frame/dto/module_frame_dto.py domain/module_frame/code/module_frame_action_type_enum.py app/module_frame/service/module_frame_service.py | xargs wc -l | tail -1`

**B1／B1-item／B1c 三棒都在 2,000 行門檻內，可以個別派工，也可以視首腦人力安排合併派（例如 B1-item＋B1c 合併約 1,600 行，仍在門檻內）——這點留給首腦視實際派工資源決定，這裡只保證每棒單獨派工時本身是完整路徑、不必再靠其他棒補完。**

### B2 — 控制項預設清單／稽核目標（AO）預設清單／參考文件池

**派工卡：[CM-2075](https://app.notion.com/p/module_frame-B2-12-1-642-Opus-5-1M-effort-low-3e3346da4cd081b9bc8af134fdacc734)**

範本底下另外三組獨立的清單型資料：這套範本要求哪些控制項要做（control-defaults）、每條控制項底下要驗哪些稽核目標（objective-defaults）、範本可以掛哪些參考文件（reference-documents）。

```
api/module_frame/routes/module_frame_control_default_route.py           139
api/module_frame/routes/module_frame_control_objective_default_route.py 147
api/module_frame/routes/module_frame_reference_document_route.py        281
api/module_frame/serializers/module_frame_control_default.py             46
api/module_frame/serializers/module_frame_control_objective_default.py   49
api/module_frame/serializers/module_frame_reference_document.py          61
app/module_frame/service/module_frame_control_default_service.py        240
app/module_frame/service/module_frame_control_objective_default_service.py 269
app/module_frame/service/module_frame_reference_document_service.py     257
app/module_frame/dto/module_frame_control_default_dto.py                 49
app/module_frame/dto/module_frame_control_objective_default_dto.py       47
app/module_frame/dto/module_frame_reference_document_dto.py              57
```
共 **12 檔／1,642 行**。驗證：`git ls-files -- api/module_frame/routes/module_frame_control_default_route.py api/module_frame/routes/module_frame_control_objective_default_route.py api/module_frame/routes/module_frame_reference_document_route.py api/module_frame/serializers/module_frame_control_default.py api/module_frame/serializers/module_frame_control_objective_default.py api/module_frame/serializers/module_frame_reference_document.py app/module_frame/service/module_frame_control_default_service.py app/module_frame/service/module_frame_control_objective_default_service.py app/module_frame/service/module_frame_reference_document_service.py app/module_frame/dto/module_frame_control_default_dto.py app/module_frame/dto/module_frame_control_objective_default_dto.py app/module_frame/dto/module_frame_reference_document_dto.py | xargs wc -l | tail -1`

### B3 — SSP 六類附屬資料在範本側的鏡射 CRUD ＋ 範本匯出

**派工卡：[CM-2081](https://app.notion.com/p/module_frame-B3-SSP-15-1-904-Opus-5-1M-effort-low-3e3346da4cd081a1bee6dff5400bfb4a)**

O5 查過 OSCAL 那一側（真正專案的 SSP）的六類附屬資料（元件／人員／設備清單／繼承授權／參考資料／系統特性），結論是「全部守齊」。**module_frame 這邊有一份幾乎一模一樣的鏡射版本**——範本本身也可以編輯這六類資料（作為範本的預設內容），程式邏輯是複製過去改的，**O5 沒有看過這一份**。這批也包含範本專屬的 SSP 匯出入口（`mf_ssp_export_route.py`，匯出 docx/pdf/odt，底層呼叫已經是 O2 範圍內的 `ssp_export_app_service.py`，這裡只看 module_frame 自己擁有的路由檔）。**依首腦回饋納入 `module_frame_template_ssp_route.py`（38 行）**——O2 排除它是因為工具沒報、不是查過沒事，這次要把它當成沒查過的檔來看：這支只掛 `@jwt_required()`、沒有 `@require_capability`，呼叫 `resource_library_app_service.get_template_ssp_ref(mf_uid)`（唯讀），要確認這個唯讀查詢本身有沒有做歸屬檢查。

```
api/module_frame/routes/module_frame_system_characteristic_route.py    56
api/module_frame/routes/module_frame_components_route.py               78
api/module_frame/routes/module_frame_inventory_route.py                78
api/module_frame/routes/module_frame_leveraged_route.py                91
api/module_frame/routes/module_frame_party_route.py                    95
api/module_frame/routes/module_frame_ssp_resources_route.py            99
api/module_frame/routes/mf_ssp_export_route.py                         71   ← 路由本身已核對過守門齊全（@jwt_required + require_capability("module-frame.read")），底層服務在 O2 範圍，這裡只需確認路由本身沒問題即可，不用往下追
api/oscal/routes/module_frame_template_ssp_route.py                    38   ← 依首腦回饋納入：O2 沒報不代表查過沒事，只有 @jwt_required，沒有 require_capability，要查 get_template_ssp_ref() 有沒有歸屬檢查
api/module_frame/serializers/module_frame_party.py                     43
app/module_frame/service/module_frame_system_characteristic_service.py 186
app/module_frame/service/module_frame_components_service.py            211
app/module_frame/service/module_frame_inventory_service.py             220
app/module_frame/service/module_frame_leveraged_service.py             214
app/module_frame/service/module_frame_party_service.py                 323
app/module_frame/service/module_frame_ssp_resources_service.py         101
```
共 **15 檔／1,904 行**。驗證：`git ls-files -- api/module_frame/routes/module_frame_system_characteristic_route.py api/module_frame/routes/module_frame_components_route.py api/module_frame/routes/module_frame_inventory_route.py api/module_frame/routes/module_frame_leveraged_route.py api/module_frame/routes/module_frame_party_route.py api/module_frame/routes/module_frame_ssp_resources_route.py api/module_frame/routes/mf_ssp_export_route.py api/oscal/routes/module_frame_template_ssp_route.py api/module_frame/serializers/module_frame_party.py app/module_frame/service/module_frame_system_characteristic_service.py app/module_frame/service/module_frame_components_service.py app/module_frame/service/module_frame_inventory_service.py app/module_frame/service/module_frame_leveraged_service.py app/module_frame/service/module_frame_party_service.py app/module_frame/service/module_frame_ssp_resources_service.py | xargs wc -l | tail -1`

### B4 — 🔴 已知高風險現場：SSP 匯出 Excel 範本（master 總表第 128 項）

**派工卡：[CM-2076](https://app.notion.com/p/module_frame-B4-SSP-Excel-2-1-669-B6-runner-Opus-5-1M-effort-low-3e3346da4cd081568ebec187d0ca79fa)**

**這棒不是去「找」問題，是去確認總表第 128 項這個已知洞、以及它旁邁有沒有同類的其他洞。**

```
api/module_frame/routes/ssp_import_template_route.py               169   ← 3 個入口：MF-範本版、SSP 專屬版（已知洞在這支的呼叫目標）、框架版本版
app/module_frame/service/ssp_import_template_app_service.py       1500   ← 3 個公開方法：generate（MF範本）、generate_for_ssp（已知洞）、generate_by_framework_version
```
共 **2 檔／1,669 行**。刻意做成單檔大棒——1,500 行的檔案不拆成好幾份丟給不同人看，因為漏洞就是「檔案裡有 3 個公開方法，只有其中一個被查過」，拆開反而容易漏掉另外兩個。驗證：`git ls-files -- api/module_frame/routes/ssp_import_template_route.py app/module_frame/service/ssp_import_template_app_service.py | xargs wc -l | tail -1`

**接縫提醒**：這支服務 import 了 B6 批次裡的 `generator.py`（第 77 行）和 `sheet_definitions.py`（第 81 行）來產生實際的 Excel 檔案內容——如果要追「使用者填的文字有沒有被安全處理」這類問題，要跨到 B6 去看，不是這支檔案自己能回答的。

**🔴 本棒與 B4／B6 必須由同一個 runner 連續跑，不可分開派給不同人或隔開時間——兩棒有 import 接縫且無物理重複檔案，分開派會出現責任真空。**（B4 匯出邏輯呼叫 B6 產生 Excel，B6 沒有自己的歸屬判斷邏輯，兩邊各自只看得到半條路徑，只有同一人接續看才能確認「使用者填的文字」這條線從產生到寫入儲存格全程有沒有被安全處理。）

### B5 — 範本控制項清單的 Excel 匯出／匯入（跟 B4 是同一種風險形狀、不同資料）

**派工卡：[CM-2074](https://app.notion.com/p/module_frame-B5-Excel-3-1-376-Opus-5-1M-effort-low-3e3346da4cd0816e9138e9c999b93cec)**

跟 B4 很像但服務不同資料：B4 匯出「整份 SSP」，這裡匯出／匯入的是「範本的控制項清單與 AO 清單」本身（下載空白範本、上傳填好的 Excel、驗證、存檔）。**這支檔案是這次盤點裡少數「整支抓不到任何權限檢查關鍵字」的服務層檔案之一**（見下方風險提示），值得跟 B4 對照著看。

```
api/module_frame/routes/module_frame_template_import_route.py     260   ← 6 個入口：下載範本、下載匯出檔、上傳範本、驗證上傳的檔、驗證資料、存檔
api/module_frame/serializers/module_frame_template_import.py      105
app/module_frame/service/module_frame_template_import_service.py 1011
```
共 **3 檔／1,376 行**。驗證：`git ls-files -- api/module_frame/routes/module_frame_template_import_route.py api/module_frame/serializers/module_frame_template_import.py app/module_frame/service/module_frame_template_import_service.py | xargs wc -l | tail -1`

### B6 — excel_template 共用套件（Excel 檔案產生的底層機制）

**派工卡：[CM-2077](https://app.notion.com/p/module_frame-B6-excel_template-Excel-7-1-656-B4-runner-Opus-5-1M-3e3346da4cd0812f9c33ee2dc6282850)**

B4 跟 B5 各自匯出/匯入不同的資料，但兩邊產生 Excel 檔案時共用同一套底層機制——這套機制本身也要單獨看一次，因為：（a）B4 直接 import 這裡的兩支主檔；（b）O5 已經在這個套件裡找到一條中風險（存進去的文字會被 Excel 當公式執行，見 `generator.py:578`），那條在 B4 的範圍外、B6 才是它真正的家，值得順便看這個洞有沒有別的姊妹洞；（c）這個套件還被 `auth` 模組和 `oscal` 模組的 Excel 解析器共用，改動或新發現要考慮波及面。

```
app/module_frame/excel_template/__init__.py                11
app/module_frame/excel_template/generator.py               652   ← O5 已知中風險在第 578 行（Excel 公式注入）
app/module_frame/excel_template/sheet_definitions.py        617
app/module_frame/excel_template/styles.py                    38
app/module_frame/excel_template/data_validation_builder.py   54
app/module_frame/excel_template/lookup_builder.py            108
app/module_frame/excel_template/header_i18n.py               176
```
共 **7 檔／1,656 行**。驗證：`git ls-files -- app/module_frame/excel_template | grep '\.py$' | xargs wc -l | tail -1`

**🔴 本棒與 B4 必須由同一個 runner 連續跑，不可分開派給不同人或隔開時間——兩棒有 import 接縫且無物理重複檔案，分開派會出現責任真空。**（B6 本身不含歸屬判斷邏輯、只負責產生 Excel，B4 呼叫這裡的 `generator.py`／`sheet_definitions.py`；已知的公式注入洞在 `generator.py:578`，同支檔案其他寫入儲存格的地方要不要一起算進同一批修法，只有接續看過 B4 呼叫端的人才判斷得準。）

---

## 3. 每棒重點看什麼

除了下面各棒特別點名的重點，**每一棒開工前都先做這四件事**（跟 FR-113 前幾棒查出來的問題形狀直接對應）：

**① 算「守門次數」對「對外方法數」的落差**。對每支 app service 檔案跑：
```bash
grep -c "require_\|authz\|permission\|Forbidden\|Permission" <檔案>
grep -c "^    def [a-zA-Z]" <檔案>
```
兩個數字差很多（尤其是守門次數是 0，或遠少於對外方法數）就是要優先盯的檔案。這次盤點先跑過一輪，**兩支檔案是 0 命中**：`app/module_frame/service/module_frame_reference_document_service.py`（8 個對外方法，0 命中）、`app/module_frame/service/module_frame_template_import_service.py`（6 個對外方法，0 命中）——這兩支已知的 0 命中要在 B2、B5 優先確認清楚（見第 5 節）。

**② 找「一個方法裡好幾條分支，只有部分分支有守」的形狀**。O2 的洞就是這樣長出來的：`module_frame_service.py` 裡一個 `update` 方法，改「分享身分」那個分支有守衛（`assert_scope_writable`），改「適用控制項清單」那個分支完全沒有守衛。**任何一個 update／save 類方法，只要裡面有 `if` 分岔去處理不同欄位，就要每個分支分開檢查。**

**③ 找「檢查 A、動手改 B」的形狀**。也是 O2 的洞——資料庫層的防護設在 `module_frames` 這張表本身，但實際被改被刪的是另外幾張表（`profile_imports`、`ssp_implemented_requirements` 之類），那幾張表根本沒有防護。**看到「查一個 uid 存不存在」之後，接下來的寫入／刪除動作有沒有精準對應到剛剛查的那個資源，還是動到了別的表／別的資料範圍。**

**④ 特別注意刪除與下載／匯出路徑**。master 總表第 128 項（下載）、第 133 項（刪除）都是這兩種形狀。**每一支 `delete` 開頭的方法、每一支回傳檔案（Excel／docx／pdf）的方法，都要能回答『呼叫者憑什麼有資格動這筆／看這筆資料』這句話，不能只回答『這筆資料存在嗎』。**

### 各棒個別重點

**B1（主體 CRUD 全路徑）**：`module_frame_route.py` 的 PUT／DELETE 有沒有像 O2 那樣「檢查 A、動手改 B」的形狀（改分享身分有守、改別的欄位沒守）；`module_frame_repo_impl.py` 的 `update`／`delete_by_uid`／`soft_delete_by_uid` 有沒有在 SQL 層面加上「只能改自己公司的」條件，還是完全信任呼叫端已經檢查過（如果是後者，代表這層沒有第二道防線，跟 O2 report 裡「資料庫沒擋下來」是同一個風險敘事，要點出來讓首腦知道防線只有一層）；`di_containers/dashboard_apis/module_frame.py`（依首腦回饋納入本棒）要檢查**它接出去的東西跟主線是不是同一套守門**——這支把 `module_frame_service.get_module_frame_list_for_dashboard` 註冊給 AI 儀表板消費，要確認這個 DI 接線暴露出去的方法本身有沒有做租戶過濾，不是只看「這是唯讀」就跳過（O9b 已證實 DI 漏接會讓守門靜默失效）。

**B1-item（子項 CRUD）**：子項的新增／修改／刪除大部分邏輯委派給 `jedi_flow_engine` 套件處理 BPMN XML，本身沒有直接查 module_frame 的 domain/infra，**重點反而是這支怎麼確認「這個子項屬於哪個範本」**——`delete_module_frame_item` 靠 `module_frame_service.get_module_frame(payload.get("moduleFrameUid"))` 取得範本再往下刪，這裡的 `moduleFrameUid` 是前端送來的欄位，要確認拿到的範本有沒有再核對「呼叫者是不是這個範本所屬公司的人」，還是單純信任 uid 查得到就往下動。

**B1c（YAML 匯入）**：YAML 整批匯入時建立的範本歸屬哪家公司是不是由伺服器端決定、不是信任前端送來的欄位；批次建立子項時呼叫 `module_frame_item_service.add_module_frame_item`／`update_module_frame_item`，跟 B1-item 對照確認走的是同一套檢查、沒有繞過。

**B2（三組清單型資料）**：`module_frame_reference_document_service.py` 是本節①提到的 0 命中檔案之一，**優先看它的 `delete_from_pool`／`update_meta`／`attach_to_control_default` 這幾支**——這些方法目前看到的模式是「拿 `module_frame_uid` 去查 `verify_module_frame_exists_by_uid`」，這支方法**只確認範本存在、不確認範本屬於誰**，是不是真的漏了歸屬檢查，還是靠路由層的 `require_capability` 加上別的機制頂著，要逐支方法讀完再下結論，不要看到 0 命中就直接斷定是洞。

**B3（六類附屬資料鏡射版）**：**逐一對照 O5 report 裡「六組都是：讀用『你要是這專案的人』，寫用『你要是這專案的負責人』」這個標準**，看 module_frame 這邊的鏡射版本是不是做到同樣的事，還是因為範本沒有「專案」這個概念，改用了另一套邏輯——如果用了另一套，那套邏輯本身有沒有把「這份範本是不是你家的」問清楚。`module_frame_party_service.py`（323 行，六組裡最大支）優先看。**`api/oscal/routes/module_frame_template_ssp_route.py`（38 行，依首腦回饋新納入）要當成沒查過的檔案完整讀**：只掛 `@jwt_required()`、沒有 `@require_capability`，呼叫 `resource_library_app_service.get_template_ssp_ref(mf_uid)`——確認這支唯讀查詢底層有沒有做歸屬檢查，不能因為是 GET／唯讀就跳過（跟 master 總表第 128 項一樣，讀取類端點一樣可能外洩別家公司資料）。

**B4（已知高風險＋兩個未查方法）**：`generate_for_ssp` 這個方法已經是確認的洞，不用重查，**重點是另外兩個方法 `generate`（MF 範本版）跟 `generate_by_framework_version`（框架版本版）有沒有同樣的問題**——`generate` 是對範本本身匯出，理論上路由層 `require_capability("module-frame.read")` 加上如果 service 層有查範本歸屬就夠了，但要實際讀過code確認；`generate_by_framework_version` 完全不掛在任何 module-frame uid 底下（`api/module_frame/__init__.py` 裡它的路由是 `/ssp-import-template`，沒有 MF context），要搞清楚它到底是「查全域框架目錄的唯讀操作」（風險低）還是也會碰到某份具體的客戶資料（風險高）。

**B5（範本 Excel 匯入匯出）**：這支也是 0 命中檔案，是本棒最重要的問題。**先確認一件事：`module_frame_template_import_route.py` 的 6 個入口是不是每個都掛了 `@require_capability`**（盤點時已經抽查過，6 個入口都有 `@jwt_required` + `@require_capability("module-frame.read"/"module-frame.create")`，是角色權限檢查）——**但角色權限檢查不能回答『這份範本是不是你家的』**，要往下追 `module_frame_template_import_service.py` 每一個方法有沒有在拿到 `module_frame_uid` 之後，用類似 `assert_scope_writable` 或「範本 tenant_id 對比呼叫者 tenant_id」的方式確認歸屬，還是像 B2 提到的 `verify_module_frame_exists_by_uid` 一樣只確認存在。**這支涉及寫入操作（`save_import`），比 B2 的純讀寫清單風險更高，因為它會批次改動範本底下的控制項實施記錄。**

**B6（excel_template 共用套件）**：已知洞在 `generator.py:578`（公式注入），**檢查同支檔案裡其他寫入儲存格的地方有沒有同樣的問題**（O5 report 已經點名 `generator.py:319` 直式工作表、`lookup_builder.py` 也要一起看）；另外確認這個套件本身**不含任何權限判斷邏輯是正常的**（它只負責把資料寫成 Excel 檔案格式，歸屬判斷應該在呼叫它的 B4／B5 那一層做），如果發現這裡面藏了任何跟資料歸屬有關的邏輯，代表判斷邏輯散落在不該散落的地方，要另外標記。

---

## 4. 切棒推導與風險提示

### 為什麼這樣切

- **B1 按業務路徑縱切成 B1（主體）／B1-item（子項）／B1c（YAML 匯入）三棒，不是按 DDD 分層切**：這節原本是水平切（route＋app 一棒、domain＋infra 一棒），首腦回饋指出這違反「垂直切一條完整路徑」的原則，即使把 `module_frame_service.py` 重複列在兩棒當接縫也只補了一個點。改法是**實際追過三條路徑各自的呼叫關係**：主體 CRUD（route→app→domain→infra→repo→ORM→DI，1,787 行）是一條完整路徑；子項 CRUD 追完發現它幾乎沒有自己的 domain/infra（子項邏輯委派給外部套件 `jedi_flow_engine` 處理 BPMN XML，只有 `delete_module_frame_item` 一行呼叫回主體的 `module_frame_service.get_module_frame()`），所以子項這棒天生就小（675 行），**不是「domain/infra 幾乎完全共用、縱切會讓兩棒重複八成」的escape-valve 情境**——子項路徑本來就沒有自己的 domain/infra 層可以重複，縱切下去自然乾淨；YAML 匯入是完全獨立的第三條小路徑（925 行），domain 層的 `module_frame_dto.py`／`action_type_enum.py` 追確認只有這條路徑在用。三棒之間的接縫（`module_frame_service.py`）依規則重複列在需要它的每一棒。

- **B4 只放 2 支檔案（1,669 行）**：這是刻意的單檔大棒——`ssp_import_template_app_service.py` 一支就 1,500 行，硬要跟其他檔案湊一棒只會讓研究員在多支檔案之間來回追，反而稀釋掉這支「已知有洞、還有兩個方法沒查過」檔案該得到的關注。也因為這樣，B4 跟 B6（excel_template 套件）之間留了一條**沒有物理重複、只用文字提醒的接縫**——B4 的檔案 import 了 B6 的 `generator.py`／`sheet_definitions.py`，但因為 B4 加上 B6 全部檔案會衝到 3,325 行（超過門檻兩倍），不適合硬塞，所以改成在 B4 的批次說明裡寫清楚「要往下追就去 B6」，B6 自己也是完整一棒、有自己的守備重點。**這個接縫需要首腦驗收時特別注意**：如果 B4 跟 B6 派給兩個不同的人／不同時間掃，「使用者填的文字最終有沒有被安全處理」這個問題可能兩邊都覺得是對方的責任，變成沒人真的查完整條路徑。

- **B2、B3、B5 各自維持「一組完整業務功能」不切開**：這三棒各自都在門檻之內（1,642／1,866／1,376 行），沒有切的必要，剛好也符合「一棒一條完整業務路徑」的精神。

- **B3 把 `mf_ssp_export_route.py` 放進「六類附屬資料」這棒，而不是跟 B4／B5 放一起**：雖然都是「匯出」，但 `mf_ssp_export_route.py` 匯出的是 docx/pdf/odt（底層服務在 O2 範圍），跟 B4／B5 的 Excel 匯出匯入是完全不同的程式路徑，沒有共用程式碼。放進 B3 是因為它服務的資料範圍（範本的六類附屬資料）跟 B3 其他檔案一致。

### 給首腦的風險提示

- **`app/project/service/project_start_app_service.py` 這個呼叫端不在範圍內，但邏輯上跟 module_frame 的守門結果連動**。專案啟動時會呼叫 module_frame 的「複製資源庫」功能，如果 module_frame 這邊之後補了歸屬檢查，這個呼叫端傳進去的資訊（呼叫者是誰、要複製哪份範本）要確保跟新加的檢查邏輯吻合，不然可能出現「補了檢查但呼叫端傳的參數本來就繞過了」的狀況。這個檔案本身沒有被掃描過，只是提醒這條連動關係存在。

- **B4／B6 之間留了一條沒有物理重複、只用文字＋🔴 標記提醒的接縫**：B4 的 `ssp_import_template_app_service.py` import 了 B6 的 `generator.py`／`sheet_definitions.py`，但兩棒合併會衝到 3,325 行（超過門檻兩倍），不適合硬塞成一棒，所以改成「同一個 runner 連續跑兩棒」（見 B4／B6 各自小節的 🔴 提示），不是完全獨立派工。

- **8 棒總計 68 支不重複檔案、11,114 行，逐一驗證加總**（`module_frame_service.py` 在 B1／B1-item／B1c 三棒間重複列出 3 次，是刻意的接縫檔，不是計算錯誤）：

```
B1（主體）      19 檔  1,787 行
B1-item（子項）  4 檔    675 行（含 1 支跟 B1 重複的接縫檔 module_frame_service.py）
B1c（YAML）      8 檔    925 行（含 1 支跟 B1 重複的接縫檔 module_frame_service.py）
B2              12 檔  1,642 行
B3              15 檔  1,904 行（含新納入的 module_frame_template_ssp_route.py）
B4               2 檔  1,669 行
B5               3 檔  1,376 行
B6               7 檔  1,656 行
------------------------------
逐棒加總（含重複計數） 70 檔、11,634 行
扣掉 module_frame_service.py 多算的 2 次（260 行 ×2＝520 行）
＝ 68 支不重複檔案、11,114 行
```

跟第 1.6 節最終範圍「68 支檔、11,114 行」核對一致，已用實際腳本跑過 `git ls-files` + `wc -l` 加總驗證無誤。

---

## 5. 盤點時發現、但需要首腦裁決的事

1. **`module_frame_reference_document_service.py`（B2）跟 `module_frame_template_import_service.py`（B5）逐支公開方法都抓不到任何權限或歸屬檢查關鍵字，比 O2 當初鎖定 `resource_library_app_service.py`（0 命中、確認有洞）的訊號還要明確一致。** 盤點員只做了關鍵字比對跟抽讀方法簽章，**沒有逐行讀完整支邏輯**，不確定這是真的漏洞還是有別的機制頂著（例如某個裝飾器、某個共用 base class 做了檢查但關鍵字沒對上）。**待首腦確認**：這兩支是不是要在派工卡裡明確標成「高懷疑」，要求研究員優先讀。

2. **`ssp_import_template_route.py` 裡的 `SspImportTemplateByFrameworkVersionResource`（框架版本版匯出）到底有沒有掛在任何客戶專屬資料上，盤點員沒有讀到底層邏輯，無法判斷風險等級。** 待首腦確認是否要在 B4 派工卡裡特別點名要求查清楚。

3. ~~`module_frame_template_ssp_route.py` 的懸案~~ **已裁決**：依首腦回饋納入 B3（見第 2、3 節），當成沒查過的檔案處理，不再是待裁決事項。

4. ~~`di_containers/dashboard_apis/module_frame.py` 要不要納入任何一棒~~ **已裁決**：依首腦回饋納入 B1（見第 2、3 節），理由是 O9b 已證實 DI 漏接會讓守門靜默失效，不再是待裁決事項。

---

## 附：完整檔案清單與批次對照（供首腦快速核對）

```bash
# 一次列出全部 module_frame 有效檔案（排除空檔／純 docstring 檔），加上依首腦回饋新納入的
# 2 支懸案檔（module_frame_template_ssp_route.py、dashboard_apis/module_frame.py），
# 核對總數應為 68 支、11,114 行（已實跑驗證過）
{
  git ls-files -- api/module_frame app/module_frame domain/module_frame infra/module_frame di_containers/module_frame | grep '\.py$'
  echo api/oscal/routes/module_frame_template_ssp_route.py
  echo di_containers/dashboard_apis/module_frame.py
} | while IFS= read -r f; do
    n=$(wc -l < "$f")
    if [ "$n" -gt 1 ] && [ "$f" != "app/module_frame/__init__.py" ] && [ "$f" != "infra/module_frame/models/__init__.py" ]; then
      echo "$n $f"
    fi
  done | awk '{s+=$1; c+=1} END {print c" 支檔、"s" 行"}'
```
