---
title: FR-118 W2 掃描報告——主專案 CMMC 轉接器與中介格式
---

# W2 掃描報告 — 主專案 CMMC 轉接器與中介格式（CM-2130）

> 範圍：主專案 BE repo 4 檔／1,328 行：`domain/oscal/adapter/cmmc_ssp_adapter.py`、`domain/oscal/parser/ssp_intermediate.py`、`domain/oscal/adapter/i_ssp_docx_adapter.py`、`domain/oscal/adapter/adapter_registry.py`（原 FR-113 O7b 3 支＋補入 registry）。
> 掃描工具：Claude Code 官方 `claude-security` plugin，effort low，scoped 掃描。
> 基準 commit：`8f53ebb062c1`（`feature/review`，工作區有平行 session 的未提交改動，stamp 標 dirty；範圍內 4 檔乾淨）。
> 驗證章：**verified，1 條 3:0 通過**。只掃不修。

---

## 1. 一句話結論

**卡片最在意的 uuid 那條，答案是「轉接器根本不碰 uuid，uuid 全部由伺服器自己產生」。所以 V3 擔心的「Word 裡寫的人員 uuid 被照抄、同一個 uuid 出現在兩份 SSP」，走 Word 這條路不成立。**

資安問題方面，工具報了 1 條，面板 3:0 通過：轉接器切「標籤：值」的比對（regex，正則表達式）遇到一長串空白會三次方爆炸（W2-1，中）。這跟 W1 是同一種「幾 KB 的特製 Word 讓解析卡到逾時」，runner 用研究員給的字串實測重現：3,200 個空白跑 23 秒。

另外，runner 在同一支檔追到兩處同類的慢點（W2-2）：「在清單裡找自己排第幾」的平方級寫法，和一條「FCI…defined in」比對。建議併進 W1 那條中，一起修。

框架代號查不到時，不會落到預設轉接器（不成立）。

---

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

W1 從 Word 裡抽出段落和表格之後，W2 的**CMMC 轉接器**把它們認成「系統名稱、人員、外部服務、元件、設備」，組成**中介格式**（`ssp_intermediate.py` 的一組資料結構），存進解析單給使用者審閱。使用者按確認後，主專案另一層（`app/oscal/service/import_adapter/docx_to_oscal_ssp.py`，不在本棒範圍）才把中介格式組成 OSCAL 字典，交給套件的 `import_ssp` 寫進資料庫（那是 V3 的範圍）。

```
Word 檔 ─W1─→ 段落／表格 ─W2 轉接器─→ 中介格式（解析單）─確認─→ import_adapter ─→ OSCAL 字典 ─V3─→ import_ssp → 資料庫
```

本棒要回答三件事：

1. 字典裡的 uuid（人員、元件、實作項目）是轉接器自己產生，還是沿用 Word 內容。
2. 轉接器會不會輸出「別份 SSP 的元件 uuid」。
3. 框架代號查不到時，是回錯誤還是落到預設。

外加跟 W1 同一套的惡意檔案檢查：regex 卡死、迴圈爆掉。

---

## 3. 掃到什麼：總覽

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度＋為什麼 | 來源 |
|---|---|---|---|---|---|---|
| W2-1 | 切「標籤：值」的 regex `[:：]\s*(.+?)\s*$` 三次方爆炸 | 一段「冒號＋3,200 個空白＋換行」跑 23 秒（實測），約 4,500 個空白就過 120 秒逾時；四個請求讓整個 API 對所有客戶停擺 | 同 W1：同客戶登入帳號＋一個看得到的資源庫編號（或專案負責人、或 `module-frame.create`）＋框架選 CMMC | `cmmc_ssp_adapter.py:785-788`（`_after_colon` 改成「找第一個冒號、切掉、strip」）；呼叫前在 `:553-557`、`:509-515` 先截段落長度 | **中**（面板 3:0，三票都中）：理由同 W1——一般帳號、幾 KB 的檔、可重複打；不外洩、不竄改 | 工具 F1 |
| W2-2 | 同檔另兩處慢點：`paragraphs.index(p)` 平方級、`FCI.*defined in` 比對平方級 | 5,000 段「General Description:」跑 8.4 秒、2 萬段 46 秒；一段 8 萬字的「FCI FCI…」跑 8.5 秒 | 同上 | `cmmc_ssp_adapter.py:576`、`:627`（改用 `enumerate` 的索引）；`:853`（先截長度或改寫） | **併入 W1／W2-1 同一條中** | runner 自行開檔＋實測，**未經三人面板投票**（面板理由裡有提到，但沒單獨投票） |

---

## 4. 工具報的一條（經三人面板投票）

### 4.1 W2-1 切「標籤：值」三次方爆炸（F1，3:0）

**現況**：已修（M11-26，FR-114 CM-2182，commit `9edcf4de4`，1.21.0 出貨）

`_after_colon`（`:784-788`）用 `re.search(r"[:：]\s*(.+?)\s*$", text)` 取冒號後面的值。它被每一段內文呼叫（`_extract_system_characteristics` `:553-557`，**在判斷是不是標籤之前就先跑**），也被封面表格前三列的第一格呼叫（`:509-515`）。

三段 `\s*`、`.+?`、`\s*` 都能吃同一串空白。當後面還有換行（`$` 永遠對不上）時，引擎會把「空白怎麼分給三段」全試一遍。

實測（直接呼叫 `_after_colon`，輸入「`:` ＋ N 個空白 ＋ `a` ＋換行＋ `b`」）：

| 空白數 | 耗時 |
|---|---|
| 800 | 0.36 秒 |
| 1,600 | 2.9 秒 |
| 3,200 | **22.9 秒** |

每加倍約慢 8 倍，符合三次方。研究員原本只寫步數估算，runner 用同一組字串實測，結論一致。

**跟 W1-1 的關係**：W1-1 是抽取器裡「範本佔位字」判斷的 regex，W2-1 是轉接器裡的，兩支在同一次上傳裡都會跑。**兩個都修才算修好**，只修一個，另一個照樣卡。

---

## 5. 卡片點名的疑點，逐條回答（runner 自行開檔核對＋實測，未經三人面板投票）

### 5.1 🔴 uuid 是轉接器自產還是照抄 Word：兩者都不是——轉接器不碰 uuid；uuid 在確認那一步由伺服器全新產生

這條決定 V3「同一個人員 uuid 出現在兩份 SSP」成不成立。追了整條鏈：

**① 轉接器（本棒）完全不讀也不產 uuid。**

- `cmmc_ssp_adapter.py` 全檔 grep `uuid` 只命中一行 docstring（`:186`）。
- 它產出的 `ParsedParty`、`ParsedComponent`、`ParsedLeveragedAuthorization`、`ParsedInventoryItem` 等中介格式，**沒有任何 uuid 欄位**。只有 `name`、`title`、`email_address` 這類文字（`ssp_intermediate.py:53-80` 等）。
- 中介格式裡唯一像 uuid 的欄位是 `target_party_uid`、`matched_party_uuid`、`matched_user_uid`（`:73`、`:182`、`:212`、`:287`）。它們由伺服器端的對帳程式在**比對之後**填入，轉接器從不寫。
- W1 已確認抽取器也不讀 Word 裡的 uuid。所以 **Word 內容裡就算寫了 uuid，整條解析路上沒有任何一處會把它讀出來**。

**② 確認時組 OSCAL 字典的那一層，一律用伺服器產生新 uuid。**

- `docx_to_oscal_ssp.py:41`（人員）、`:69`（實作項目）、`:97`（實作敘述）都是 `_common.gen_uuid()`。
- `_common.py` 的元件、外部服務、設備也都是 `gen_uuid()`（`:238`、`:331`、`:402-404`、`:466`、`:497`、`:579`）。
- `gen_uuid()` 就是 `uuid.uuid4()`（`_common.py:24-25`），伺服器端隨機產生。

**③ 剩下兩處「讀既有 uuid」的地方，來源都是伺服器，不是使用者：**

- `_common.py:360` `component_uuid_by_title` 讀的是**同一次剛產生**的元件 uuid，用來把設備掛到元件上。
- `_common.py:519-520` 對 `implemented_components` 若已是 `{"component-uuid": ...}` 形式就原樣放行。但走 Word 這條，轉接器輸出的是**逗號分隔的標題字串**（`cmmc_ssp_adapter.py:325-329`），不會走到放行分支。
- `decision_merge.py:197`（使用者在預覽頁選「用 Word／保留現有」後的合併）讀的是**目標 SSP 從資料庫撈出的快照**的 uuid。
- 使用者在確認時能送的 `content_overrides`（`ssp_docx_import_app_service.py:689-708`）只能改「實作敘述」與「評估目標敘述」兩個文字欄位，**改不到任何 uuid**。

**④ 另一個可能讓同一個 uuid 出現在兩份 SSP 的來源——複製——也不會。** 套件 `metadata_clone_service.py:105`、`:121` 複製人員與資源時是 `uuid=_new_uuid()`，每份都換新的。

**結論**：走 Word 匯入與複製這兩條路，**同一個人員 uuid 不會出現在兩份 SSP**。

V3 的形狀（套件 `oscal_io_service.py:982` `uuid=p.get("uuid") or self._gen_uuid()`，給什麼 uuid 就存什麼）仍然存在，但要打得到，得有一條「使用者能直接給 OSCAL 字典（含 uuid）」的入口。W2 這邊可以確定 **Word 那條不是**。

Excel 那條（`excel_to_oscal_ssp.py:36`、`:51`、`:84`、`:98`）grep 起來也全是 `gen_uuid()`，但它不在本棒範圍，只抽看了組字典那一層，沒追完整。

### 5.2 轉接器會不會輸出「別份 SSP 的元件 uuid」：不會（下次不用重查）

同 5.1：轉接器不輸出任何 uuid。元件跟外部服務、設備跟元件的關聯都用**標題字串**（`leveraged_authorization_ref=la.title`、`implemented_component_refs`），到確認那一步才在**同一份新字典裡**用標題反查。查不到就略過（`_common.py:505-525`），不會跨到別份 SSP。

### 5.3 框架代號查不到會不會落到預設轉接器：不會（下次不用重查）

- `adapter_registry.py:20-21` 用 `dict.get`，查不到回 `None`。
- DI 只註冊了 `cmmc-l1`、`cmmc-l2` 兩個代號，都指向同一個 CMMC 轉接器（`di_containers/oscal/oscal_containers.py:476-483`）。
- 服務層拿到 `None` 時記 `adapter_status={"parties": "not_run"}` 就返回（`ssp_docx_import_app_service.py:618-621`），不會拿別的轉接器去跑。
- 代號在更前面還會經過 `docx_parser_core.parse` 的框架檢查（`docx_parser_core.py:207-213`）：完全不認得的代號會被擋成 `GRC_DOCX_FRAMEWORK_MISMATCH`；`nist-800-171`、`iso27001` 這兩個有編號樣式但沒註冊轉接器的代號能過這關，到轉接器這步就是上面的 `not_run`。

### 5.4 同檔其他 regex 與迴圈：兩處慢點成立（W2-2），其餘不成立

**現況**：W2-2 已修（M11-26，同上）

逐條量過本檔所有 regex，輸入是 2 萬字與 8 萬字的長行：

| 位置 | 用途 | 8 萬字耗時 | 判定 |
|---|---|---|---|
| `:787` `_after_colon` | 切「標籤：值」 | 見 4.1（三次方） | **W2-1** |
| `:853` `FCI.*defined in\|根據32 CFR` | 系統描述收集的停止條件 | **8.5 秒**（「FCI FCI…」重複；2 萬字 0.4 秒，平方級） | **W2-2**；只在「General Description」之後的段落跑 |
| `:747` `FCI involved.*includes` | 找 FCI 清單錨點 | 1.6 秒（「FCI involved」重複） | 平方級但係數小，併 W2-2 修 |
| `:737` `Port\s*\??\s*(\d+\|\?)` | 抽連接埠 | 0 秒 | 不成立（面板理由提到，實測不慢） |
| `:53-61` 九條角色標籤、`:67-71` 五條欄位標籤 | 認人員角色與欄位 | ≤0.1 秒 | 不成立 |
| `:795-805` `_normalize_blank` 四條 | 過濾範本佔位字 | 0 秒 | 不成立 |
| `:838-839` `_find_table_with_first_row` | 找表頭 | 0 秒 | 不成立 |

**迴圈**：`_extract_system_characteristics` 在 `:576` 用 `paragraphs.index(p)` 找自己排第幾。`_locate_role_labels` 在 `:627` 把 `paragraphs.index(p)` 當 `getattr` 的預設值，而 Python 會**不管用不用得到都先算**。兩處都是「每段都從頭找一遍」，平方級。

實測 5,000 段「General Description:」開頭的段落跑 8.4 秒、2 萬段 46 秒。這疊在 W1-3（逐段走訪平方級）之上，同一份上傳兩邊都會跑。修法：改用 `enumerate` 的索引。

### 5.5 整份通讀的其他觀察

- `ssp_intermediate.py`（369 行）、`i_ssp_docx_adapter.py`（78 行）是純資料結構與介面宣告，沒有判斷邏輯。
- 轉接器**不寫資料庫、不組 SQL**。對帳（把 Word 裡的人對到系統帳號）在服務層做（`ssp_docx_import_app_service.py:628-632`），不在範圍。
- `_extract_information_types`（`:743-779`）最多收 6 條、標題截 120 字；`_collect_description`（`:844-860`）最多收 6 段。這兩處有上限，是好的寫法，但沒有截**單段長度**。
- 人員欄位（姓名、職稱、email、電話、地址）與元件說明原樣保留，不中和 `=`、`+`、`-`、`@` 開頭。之後匯出 Excel 就是第 148／150 項的形狀，修在匯出端已涵蓋，不另計。

---

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

**「這幾條存在嗎」：高。**

- W2-1 面板 3 票全投、3:0，runner 用研究員的字串實測重現。
- W2-2 是 runner 實測。
- uuid 結論是逐檔 grep＋開檔讀到每一處產生 uuid 的那一行，有檔名行號可查。

**「只有這幾條嗎」：中偏高。**

- 4 支檔 runner 逐支通讀完，本檔所有 regex 逐條量過。
- uuid 鏈跨出範圍追到 `import_adapter/docx_to_oscal_ssp.py`、`_common.py`、`decision_merge.py`、服務層確認入口、套件複製服務。
- 沒做的：
  - `import_adapter/` 與 `import_diff/` 兩個目錄（FR-113 O9 範圍）沒有逐支通讀，只讀了跟 uuid 有關的行；
  - Excel 那條路的 uuid 只抽看了組字典那一層。

---

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

| 項目 | 值 |
|---|---|
| run ID | `wf_5381e8bf-6d6` |
| 報告目錄 | `CLAUDE-SECURITY-20260924-085329/`（不入版控） |
| stamp | `CLAUDE-SECURITY-REVISION-8f53ebb062c1-dirty.json` |
| verification.status | **verified**（`reason_kind` 無） |
| 形狀 | low：1 名研究員讀 4 檔＋1 次密鑰專項（focus=attack-surface） |
| 候選 → 通過 | 1 → 1（F1 3:0，三票皆中，未調降） |
| 面板票數 | 3 票，0 票反對 |
| agent 數／失敗 | 5／0（`journal.jsonl` 無 `failed`） |
| 耗時 | 約 26 分鐘 |
| 人工實測 | F1 原字串重現（800／1,600／3,200 空白）＋本檔 regex 逐條 2 萬／8 萬字＋`paragraphs.index` 5,000／2 萬段 |
| DEV 唯讀實查 | 本棒不涉資料庫（轉接器不讀寫表），未查 |

---

## 8. 待首腦裁決

1. **V3「同一個人員 uuid 出現在兩份 SSP」的前提**：Word 匯入與複製兩條路都不成立（5.1）。V3 報告若要保留這條，得指出另一條「使用者能直接給 OSCAL 字典」的入口；目前主專案沒有 OSCAL JSON 匯入入口（盤點檔第 6 節）。**建議 V3 把這條改寫成「套件信任呼叫端給的 uuid，目前所有呼叫端都自產，屬地雷不是洞」。**
2. **W2-1、W2-2 併進 W1 那條中**：同一份上傳、同一種後果，建議總表只登一條「特製 Word 檔讓解析卡死到逾時」，位置列 W1-1～4＋W2-1～2 共六處，同一張修正卡。
   - 修法方向一句話：解析前先截每段長度與總段落數；改寫四條會回溯的 regex；兩處平方級走訪改用索引。
3. **修正卡驗收要點**：W1-1 與 W2-1 是兩支檔裡的兩條 regex，同一份檔會先後跑到。**只修一邊等於沒修**，驗收時要用同一份惡意檔整條跑一次。
