範圍:主專案 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-securityplugin,effort low,scoped 掃描。 基準 commit:8f53ebb062c1(feature/review,工作區有平行 session 的未提交改動,stamp 標 dirty;範圍內 4 檔乾淨)。 驗證章:verified,1 條 3:0 通過。只掃不修。
卡片最在意的 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 那條中,一起修。
框架代號查不到時,不會落到預設轉接器(不成立)。
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 → 資料庫
本棒要回答三件事:
外加跟 W1 同一套的惡意檔案檢查:regex 卡死、迴圈爆掉。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度+為什麼 | 來源 |
|---|---|---|---|---|---|---|
| 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 自行開檔+實測,未經三人面板投票(面板理由裡有提到,但沒單獨投票) |
現況:已修(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 是轉接器裡的,兩支在同一次上傳裡都會跑。兩個都修才算修好,只修一個,另一個照樣卡。
這條決定 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 等)。target_party_uid、matched_party_uuid、matched_user_uid(:73、:182、:212、:287)。它們由伺服器端的對帳程式在比對之後填入,轉接器從不寫。② 確認時組 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.1:轉接器不輸出任何 uuid。元件跟外部服務、設備跟元件的關聯都用標題字串(leveraged_authorization_ref=la.title、implemented_component_refs),到確認那一步才在同一份新字典裡用標題反查。查不到就略過(_common.py:505-525),不會跨到別份 SSP。
adapter_registry.py:20-21 用 dict.get,查不到回 None。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。現況: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 的索引。
ssp_intermediate.py(369 行)、i_ssp_docx_adapter.py(78 行)是純資料結構與介面宣告,沒有判斷邏輯。ssp_docx_import_app_service.py:628-632),不在範圍。_extract_information_types(:743-779)最多收 6 條、標題截 120 字;_collect_description(:844-860)最多收 6 段。這兩處有上限,是好的寫法,但沒有截單段長度。=、+、-、@ 開頭。之後匯出 Excel 就是第 148/150 項的形狀,修在匯出端已涵蓋,不另計。「這幾條存在嗎」:高。
「只有這幾條嗎」:中偏高。
import_adapter/docx_to_oscal_ssp.py、_common.py、decision_merge.py、服務層確認入口、套件複製服務。import_adapter/ 與 import_diff/ 兩個目錄(FR-113 O9 範圍)沒有逐支通讀,只讀了跟 uuid 有關的行;| 項目 | 值 |
|---|---|
| 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 唯讀實查 | 本棒不涉資料庫(轉接器不讀寫表),未查 |