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,同一份檔會先後跑到。只修一邊等於沒修,驗收時要用同一份惡意檔整條跑一次。