FR-118 掃描盤點——jedi-oscal-v2 套件本體+主專案側 Word 解析器:範圍界定與切棒

FR-118 掃描盤點:jedi-oscal-v2(控制項目錄、SSP、稽核計畫與結果、改善計畫的資料層)

盤點日期 2026-09-24|盤點卡 CM-2123(母卡 CM-2122)|這份文件只盤點、不掃描——切好棒等首腦開卡派工。 行數全部是 wc -l 逐檔實算。基準:BE feature/review commit a7f403856;jedi monorepo feature/review commit eafc7ae5(jedi-oscal-v2/ 工作區乾淨)。每棒段落附兩側各自的驗證指令。

名詞先講清楚(讀者是 PM 與新手工程師):

  • 套件:jedi-oscal-v2,放在另一個 repo(~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/)的一包程式,負責把合規文件存進資料庫、再從資料庫組回來。它沒有網址入口,所有功能都要靠主專案或 jedi-compliance-audit 套件呼叫。
  • OSCAL:美國 NIST 訂的一套合規文件格式。這支套件處理其中五種文件:控制項目錄(catalog)、基準線(profile,從目錄挑出要做的控制項)、系統安全計畫(SSP)、稽核計畫(AP)與稽核結果(AR)、改善計畫(POA&M)。
  • repo(資料存取方法):程式裡專門負責「去資料庫查/改/刪」的那一層。
  • 範圍條件:資料庫查詢時除了「編號是多少」之外,有沒有再加上「而且要屬於某份計畫/某份 SSP/某家客戶」。只有編號、沒有範圍,就是總表第 118/119 項那種「檢查甲、刪乙」的洞。
  • 資料庫隔離(RLS):資料庫這一層自己擋「你只能看到你們公司的資料」。程式層漏檢查時,它是最後一道防線。
  • 判斷邏輯 vs 純宣告:有 if、迴圈、查詢組裝的程式碼叫「判斷邏輯」,可能出錯、要掃;只列欄位名稱、或欄位一對一搬家的程式碼叫「純宣告」,本身不會出錯、不掃。

這批最該先看的四件事

  1. 這支套件本身「不問歸屬」是設計,不是漏洞——但它把整份責任丟給呼叫它的人。套件 54 支 repo 自己寫的查詢方法全部都有帶上一層的編號(例如「某份 SSP 底下的元件」);但每一支 repo 都另外從共用基底類別繼承了「只用編號」的查、改、刪(get_by_id/get_by_uid/update/delete_by_id/delete_by_uid)。套件的服務層有 33 支方法直接拿呼叫者給的編號去讀、改或刪,沒有任何範圍檢查(見第 4 節)。主專案目前的呼叫端都先在正確的父層底下找到那筆、才把編號交進去(形狀對),所以這不是現成的洞,而是「下一個寫呼叫端的人漏一步,就是第 118/119 項」的地雷區。掃描要做的是逐支確認呼叫端都有先找父層。
  2. 整個 oscal 資料區 45 張本套件的表:沒有一張有客戶欄位、沒有一張開了資料庫隔離(出貨 02-schema.sql 與 DEV 實查一致)。第 1 點講的「靠呼叫端先找父層」,底下完全沒有第二道防線。這是總表第 128 項的全貌,本棒補齊清單(第 5 節)。
  3. 「XML 解析攻擊」在這支套件不成立(第 6 節)。全套件正式碼零支 import 任何 XML 解析器(xml/lxml/defusedxml/ElementTree),也沒有 OSCAL XML 格式的匯入匯出——oscal_io_service.py 只做 JSON 匯出、以及「把主專案已經組好的 Python 字典寫進資料庫」的匯入。真正會碰到 XML 的是主專案那側讀 Word(python-docx,底層已關掉外部實體解析)與讀 Excel(openpyxl,主專案已裝 defusedxml,會自動套用防護)。掃描重點從「XML 攻擊」換成「超大/超深的字典」與「Word/PDF 壓縮炸彈」。
  4. Word 解析失敗會把原始錯誤訊息原封不動寫進解析工作、並直接回給前端(app/oscal/service/ssp_docx_import_app_service.py:220、:226 都是 str(e))。這跟總表第 132 項(PDF 解析失敗吐伺服器路徑)同一個形狀,但這支檔在 FR-113 O4 範圍內,本批不重掃;列給 W1 研究員當「解析器丟出來的例外會長什麼樣」的背景。

⚠️ 以上是盤點時開檔看到的「疑點」與「形狀」,不是掃描結論。


1. 範圍界定

1.1 套件側

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2
git ls-files -- 'jedi_oscal_v2/*.py' | xargs wc -l | tail -1     # 363 支、15,852 行

正式程式 363 支/15,852 行,與卡片數字一致(tests 不掃)。另有兩個非 .py 目錄:assets/(兩份 CMMC 官方 PDF+一份對照檔,當範例與種子資料用)、migrations/001-cmmc-2-catalog-seed.sql(CMMC 目錄的種子資料)——都不是程式邏輯,不掃。

363 支的去向:

分類 檔數 行數 為什麼
排進掃描棒 69 7,117 見第 2 節(去重後;含 2 支空的 adapter __init__.py)
domain/entity 純資料結構 99 2,195 全部是 @dataclass 欄位宣告,0 個方法、0 個 if(見附錄 A)
infra/model 資料表宣告 54 2,921 全部是 SQLAlchemy 欄位宣告,0 個方法、0 個驗證器/事件(附錄 A)
infra/mapper 欄位搬家 54 2,671 每支只有 if model is None: return None 一個判斷,其餘一欄一欄搬;沒有任何一支做 json.loads 或遞迴組裝(JSON 欄位由資料庫驅動直接給 Python 物件)(附錄 A)
domain/repository 介面 54 802 抽象方法宣告,沒有實作(附錄 A)
錯誤碼與列舉常數 2 103 common/code/oscal_v2_error_code.py(41)、common/enum/oscal_enums.py(62)
jedi_oscal_v2/__init__.py 1 30 只有 docstring
其餘 __init__.py(0~1 行) 30 13
合計 363 15,852 ※ 不掃的共 294 支/8,735 行,明細見附錄 A

與卡片說法的差異:卡片寫「infra/repository 54」是指 54 張表對應 54 支 repo,但實際 *_repo_impl.py 是 45 支,另外 9 支是 __init__.py。要列查詢條件的就是這 45 支(全部排進 V1/V2/V4a)。

1.2 主專案側

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git ls-files -- domain/oscal/parser domain/oscal/adapter | xargs wc -l | tail -1   # 11 支(含 2 支空檔)、2,986 行

⚠️ 比卡片多 2 支:卡片列 7 支/2,946 行(取自 FR-113 盤點第 196~210 行),但目錄實際還有:

檔 行數 為什麼補進來
domain/oscal/parser/framework_patterns.py 19 四組「控制項編號長什麼樣」的正規表示式(regex)。Word 解析器每一段文字都會拿它去比對;regex 寫法不好會被特製文字卡死 CPU(ReDoS),要一起看。併入 W1
domain/oscal/adapter/adapter_registry.py 21 「框架代號 → 用哪支轉接器」的對照表,DI 在用。併入 W2

兩支 __init__.py 是空檔。所以主專案側實際掃 9 支/2,986 行。

1.3 排除清單(已被別的掃描棒掃過,本批不重掃)

套件本體一支都沒被掃過,零重疊。以下是「呼叫套件的主專案檔」,全部在 FR-113 範圍內:

主專案檔 呼叫套件的什麼 哪一棒掃過
app/oscal/service/ssp_docx_import_app_service.py import_ssp/clear_ssp_body/build_ssp_snapshot FR-113 O4
app/oscal/service/ssp_excel_import_app_service.py 同上 FR-113 O3a
app/oscal/service/oscal_export_app_service.py export_oscal FR-113 O2
app/oscal/service/framework_app_service.py import_catalog_from_pdf/excel FR-113 O8b
app/oscal/service/framework_parse_job_service.py PDF 解析工作 FR-113 O8a
app/oscal/service/ssp_components/inventory_items/leveraged/party_app_service.py ssp_service 的改/刪 FR-113 O5
app/oscal/service/ssp_control_implementation_service.py update_implemented_requirement/update_statement FR-113 O1
app/module_frame/service/module_frame_*_service.py(party/components/inventory/leveraged/control_default/control_objective_default) 同上 FR-113 B 系列
app/project/service/project_start_app_service.py clone_resource_library FR-095 H1

jedi-compliance-audit 套件是第二個大呼叫端(assessment_result_app_service/poam_app_service/audit_round_app_service 直接用本套件的 repo 與 service),在 FR-116 C2a/C3 範圍內。

⚠️ 卡片提到「O1/O6 報告有引用 ssp_reference_document_repo_impl.py」——查證屬實,那支檔在 jedi-compliance-audit、不是本套件,本套件沒有程序書相關的表或程式。第 118/119/129 項的根因不在本套件。

⚠️ 一支例外沒被任何人掃過:app/flow_control/service/assessment_plan_app_service.py 呼叫本套件 generate_draft 與 clone_metadata(:313、:354),它不在 FR-113 任何一棒、也不在 FR-116 盤點的主專案清單。本批不收(它是主專案業務層、不是 Word 解析器),列在第 7 節請首腦決定歸屬。

1.4 最終要掃的範圍

檔數 行數
套件側 69 7,117
主專案側 9 2,986
合計(去重) 78 10,103

2. 切棒(7 棒,每棒 ≤2,000 行、≤30 檔;按 OSCAL 文件類型切)

切法原則:按文件類型縱切——同一種文件的「服務 → repo 實作」放同一棒,因為這支套件要找的是「子物件有沒有被綁在正確的父文件底下」,拆開就看不出來。oscal_io_service.py 1,660 行一支獨立成棒。主專案 Word 解析器拆兩棒,跟套件 CMMC PDF 解析器(V4b)配對:兩邊都是「吃外部檔案、解析成內部格式」,風險形狀一樣(大小上限、壓縮炸彈、錯誤訊息外洩、regex 卡死)。

接縫檔(刻意在兩棒重複列):catalog_service.py(V4a 管資料、V4b 管解析入口)。

棒 做什麼 檔數 行數 側
V1 🔴 SSP 本體:服務、複製與凍結、SSP 子表與 metadata 的 repo 21 1,503 套件
V2 🔴 稽核計畫、稽核結果、改善計畫:服務與 repo 26 1,880 套件
V3 🔴 OSCAL 匯入匯出整份文件(oscal_io_service.py) 1 1,660 套件
V4a 控制項目錄、基準線、框架:服務、目錄複製、repo 14 1,202 套件
V4b CMMC PDF/Excel 解析器 8 1,110 套件
W1 主專案 Word 內容抽取器 5 1,658 主專案
W2 主專案 CMMC 轉接器與中介格式 4 1,328 主專案
逐棒加總(含接縫重複) 79 10,341

逐棒加總扣掉接縫 catalog_service.py(238 行,重複 1 次)= 78 檔、10,103 行,與 1.4 一致。

驗收時的配對建議

  • V4b 與 W1/W2 同一個 runner 連做:三棒都是「外部檔案解析」,研究重點(大小、頁數、壓縮炸彈、錯誤訊息、regex)完全相同,連做省一次建立脈絡。
  • V1 與 V3 共用 SSP 的子表結構:V3 的匯入寫的就是 V1 那批表。不必同一人做,但 V3 研究員遇到「寫進哪張表、用哪個鍵認定是同一筆」時可越界讀 V1 的 repo。

V1 — 🔴 SSP 本體(服務、複製與凍結、子表 repo)

這一棒要回答:ssp_service.py 那一堆「只收編號就改/刪」的方法,每一個呼叫端是不是都先在正確的 SSP 底下找到那筆;SSP 複製/凍結時,子物件的歸屬欄位有沒有全部換成新 SSP、有沒有漏帶或帶到別份 SSP 的東西。

套件側(cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2)
  350 jedi_oscal_v2/app/service/ssp/ssp_service.py                          ← 🔴 21 支只收編號就讀/改/刪的方法
  245 jedi_oscal_v2/app/service/ssp/ssp_clone_service.py                    ← 深複製 12 張子表
  192 jedi_oscal_v2/app/service/snapshot/oscal_snapshot_service.py          ← 凍結、資源庫三件組複製
  129 jedi_oscal_v2/app/service/snapshot/metadata_clone_service.py          ← metadata(角色、人員、資源)複製
   48 jedi_oscal_v2/infra/repository/ssp/ssp_by_component_repo_impl.py
   36 jedi_oscal_v2/infra/repository/ssp/ssp_component_repo_impl.py
   47 jedi_oscal_v2/infra/repository/ssp/ssp_control_implementation_repo_impl.py
   31 jedi_oscal_v2/infra/repository/ssp/ssp_diagram_repo_impl.py
   50 jedi_oscal_v2/infra/repository/ssp/ssp_implemented_requirement_repo_impl.py
   40 jedi_oscal_v2/infra/repository/ssp/ssp_information_type_repo_impl.py
   38 jedi_oscal_v2/infra/repository/ssp/ssp_inventory_item_repo_impl.py
   46 jedi_oscal_v2/infra/repository/ssp/ssp_leveraged_authorization_repo_impl.py
   17 jedi_oscal_v2/infra/repository/ssp/ssp_repo_impl.py
   38 jedi_oscal_v2/infra/repository/ssp/ssp_statement_repo_impl.py
   47 jedi_oscal_v2/infra/repository/ssp/ssp_system_characteristics_repo_impl.py
   43 jedi_oscal_v2/infra/repository/ssp/ssp_system_implementation_repo_impl.py
   38 jedi_oscal_v2/infra/repository/ssp/ssp_system_user_repo_impl.py
   17 jedi_oscal_v2/infra/repository/base/metadata_repo_impl.py
   17 jedi_oscal_v2/infra/repository/base/party_repo_impl.py
   17 jedi_oscal_v2/infra/repository/base/resource_repo_impl.py
   17 jedi_oscal_v2/infra/repository/base/role_repo_impl.py

共 21 檔/1,503 行。

驗證:

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2 && P=jedi_oscal_v2 && git ls-files -- $P/app/service/ssp/ssp_service.py $P/app/service/ssp/ssp_clone_service.py $P/app/service/snapshot/oscal_snapshot_service.py $P/app/service/snapshot/metadata_clone_service.py "$P/infra/repository/ssp/*_repo_impl.py" "$P/infra/repository/base/*_repo_impl.py" | xargs wc -l | tail -1   # 21 檔、1503

V2 — 🔴 稽核計畫、稽核結果、改善計畫

這一棒要回答:稽核結果與改善計畫的服務收到「風險編號+一串判定編號」「矯正措施編號」這類輸入時,有沒有驗證它們屬於同一份稽核結果;update_poam_item、link_findings 這種只收編號的方法被誰呼叫。

套件側(cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2)
  215 jedi_oscal_v2/app/service/ap/assessment_plan_service.py               ← 用 ap_uid 找 AP 再整批換任務/受評對象
  156 jedi_oscal_v2/domain/service/ap/ap_draft_service.py                   ← 從 SSP 產生 AP 草稿
  184 jedi_oscal_v2/app/service/ar/assessment_result_service.py
  143 jedi_oscal_v2/app/service/ar/assessment_risk_service.py               ← 🔴 link_findings 不驗判定屬於誰
  186 jedi_oscal_v2/domain/service/ar/ar_finding_matrix_service.py
  144 jedi_oscal_v2/app/service/poam/poam_service.py                        ← 🔴 update_poam_item 只收編號
  146 jedi_oscal_v2/app/service/poam/remediation_service.py                 ← upsert 用 risk_id+uuid 定位(有範圍)
   38 jedi_oscal_v2/infra/repository/ap/ap_assessment_activities_repo_impl.py
   51 jedi_oscal_v2/infra/repository/ap/ap_assessment_subjects_repo_impl.py
   36 jedi_oscal_v2/infra/repository/ap/ap_local_objectives_repo_impl.py
   17 jedi_oscal_v2/infra/repository/ap/ap_repo_impl.py
   35 jedi_oscal_v2/infra/repository/ap/ap_reviewed_controls_repo_impl.py
   47 jedi_oscal_v2/infra/repository/ap/ap_task_participants_repo_impl.py
   45 jedi_oscal_v2/infra/repository/ap/ap_task_subjects_repo_impl.py
   52 jedi_oscal_v2/infra/repository/ap/ap_tasks_repo_impl.py
   38 jedi_oscal_v2/infra/repository/ar/ar_assessment_subjects_repo_impl.py
   45 jedi_oscal_v2/infra/repository/ar/ar_finding_repo_impl.py
   68 jedi_oscal_v2/infra/repository/ar/ar_finding_risk_repo_impl.py         ← link/unlink 只用 (finding_id, risk_id)
   34 jedi_oscal_v2/infra/repository/ar/ar_observation_repo_impl.py
   29 jedi_oscal_v2/infra/repository/ar/ar_remediation_repo_impl.py
   17 jedi_oscal_v2/infra/repository/ar/ar_repo_impl.py
   29 jedi_oscal_v2/infra/repository/ar/ar_results_repo_impl.py
   29 jedi_oscal_v2/infra/repository/ar/ar_risk_repo_impl.py
   29 jedi_oscal_v2/infra/repository/poam/poam_item_repo_impl.py
   39 jedi_oscal_v2/infra/repository/poam/poam_milestone_repo_impl.py        ← get_by_assignee 零呼叫者
   28 jedi_oscal_v2/infra/repository/poam/poam_repo_impl.py

共 26 檔/1,880 行。

驗證:

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2 && P=jedi_oscal_v2 && git ls-files -- "$P/app/service/ap/*_service.py" "$P/app/service/ar/*_service.py" "$P/app/service/poam/*_service.py" "$P/domain/service/ap/*_service.py" "$P/domain/service/ar/*_service.py" "$P/infra/repository/ap/*_repo_impl.py" "$P/infra/repository/ar/*_repo_impl.py" "$P/infra/repository/poam/*_repo_impl.py" | xargs wc -l | tail -1   # 26 檔、1880

V3 — 🔴 OSCAL 匯入匯出(單獨一棒)

這一棒要回答:整份文件進來時,內容裡的 uuid、名稱、陣列長度、巢狀深度有沒有上限;「更新模式」用什麼鍵認定「是同一筆」,能不能被拿去覆蓋別份 SSP 的資料;匯出時會不會把不屬於這份文件的東西組進去;整份匯入失敗時會不會寫一半。

套件側(cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2)
 1660 jedi_oscal_v2/app/service/io/oscal_io_service.py

共 1 檔/1,660 行。

驗證:

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2 && git ls-files -- jedi_oscal_v2/app/service/io/oscal_io_service.py | xargs wc -l   # 1 檔、1660

V4a — 控制項目錄、基準線、框架

這一棒要回答:目錄複製(資源庫套用框架時用)有沒有把子物件的父層編號全部換掉;基準線解析時「從哪份目錄挑控制項」這個來源是不是伺服器端決定;框架刪除、版本發佈這幾支「只收 uid」的方法被誰呼叫、有沒有平台管理員檢查。

套件側(cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2)
  238 jedi_oscal_v2/app/service/catalog/catalog_service.py                  ← 接縫(V4b 也帶)
  333 jedi_oscal_v2/app/service/snapshot/oscal_clone_service.py             ← 目錄樹、基準線複製
   72 jedi_oscal_v2/app/service/profile/profile_service.py
  163 jedi_oscal_v2/domain/service/profile/profile_resolution_service.py
  120 jedi_oscal_v2/app/service/framework/framework_service.py              ← delete/publish 只收 uid
   28 jedi_oscal_v2/infra/repository/catalog/catalog_control_param_repo_impl.py
   57 jedi_oscal_v2/infra/repository/catalog/catalog_control_part_repo_impl.py
   41 jedi_oscal_v2/infra/repository/catalog/catalog_control_repo_impl.py
   41 jedi_oscal_v2/infra/repository/catalog/catalog_group_repo_impl.py
   17 jedi_oscal_v2/infra/repository/catalog/catalog_repo_impl.py
   17 jedi_oscal_v2/infra/repository/framework/framework_repo_impl.py
   26 jedi_oscal_v2/infra/repository/framework/framework_version_repo_impl.py
   32 jedi_oscal_v2/infra/repository/profile/profile_import_repo_impl.py
   17 jedi_oscal_v2/infra/repository/profile/profile_repo_impl.py

共 14 檔/1,202 行。

驗證:

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2 && P=jedi_oscal_v2 && git ls-files -- $P/app/service/catalog/catalog_service.py $P/app/service/snapshot/oscal_clone_service.py "$P/app/service/profile/*_service.py" "$P/domain/service/profile/*_service.py" "$P/app/service/framework/*_service.py" "$P/infra/repository/catalog/*_repo_impl.py" "$P/infra/repository/profile/*_repo_impl.py" "$P/infra/repository/framework/*_repo_impl.py" | xargs wc -l | tail -1   # 14 檔、1202

V4b — CMMC PDF/Excel 解析器

這一棒要回答:上傳的 PDF 有沒有頁數與大小上限;解析出錯時錯誤訊息往上丟什麼(總表第 132 項同形);Excel 版本走 pandas+openpyxl,有沒有公式或壓縮炸彈的防護。

套件側(cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2)
  238 jedi_oscal_v2/app/service/catalog/catalog_service.py                  ← 接縫:import_catalog_from_pdf/excel 入口
  117 jedi_oscal_v2/infra/adapter/base_parser_adapter.py                    ← 頁碼範圍計算
   34 jedi_oscal_v2/infra/adapter/oscal_parser_factory.py
  639 jedi_oscal_v2/infra/adapter/cmmc/cmmc2_lv2_parser_adapter.py         ← pdfplumber+pandas+openpyxl(中文 docstring 最多的檔)
   55 jedi_oscal_v2/domain/ports.py                                         ← 解析器介面
   27 jedi_oscal_v2/common/code/parser_adapter_type.py                      ← 宣告了 YAML/JSON 匯入類型、但沒有實作(見第 6 節)
    0 jedi_oscal_v2/infra/adapter/__init__.py
    0 jedi_oscal_v2/infra/adapter/cmmc/__init__.py

共 8 檔/1,110 行(含 2 支空檔)。

驗證:

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2 && P=jedi_oscal_v2 && git ls-files -- $P/app/service/catalog/catalog_service.py "$P/infra/adapter/*.py" "$P/infra/adapter/cmmc/*.py" $P/domain/ports.py $P/common/code/parser_adapter_type.py | xargs wc -l | tail -1   # 8 檔、1110

W1 — 主專案 Word 內容抽取器(原 FR-113 O7a + 1 支)

這一棒要回答:Word 檔打開前有沒有看大小、解壓後的大小與段落數有沒有上限;逐段比對控制項編號的 regex 會不會被特製文字卡死;模糊比對(rapidfuzz)對超長段落的耗時。

主專案側(cd ~/Projects/Billows/Audit-Manager/compliance-manager-be)
  892 domain/oscal/parser/docx_section_extractors.py
  555 domain/oscal/parser/docx_parser_core.py                    ← python-docx Document() 開檔點(:170、:192)
  120 domain/oscal/parser/control_id_matcher.py                  ← rapidfuzz 模糊比對
   72 domain/oscal/parser/docx_intermediate.py
   19 domain/oscal/parser/framework_patterns.py                  ← 補入:四組控制項編號 regex

共 5 檔/1,658 行。

驗證:

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git ls-files -- domain/oscal/parser/docx_section_extractors.py domain/oscal/parser/docx_parser_core.py domain/oscal/parser/control_id_matcher.py domain/oscal/parser/docx_intermediate.py domain/oscal/parser/framework_patterns.py | xargs wc -l | tail -1   # 5 檔、1658

W2 — 主專案 CMMC 轉接器與中介格式(原 FR-113 O7b + 1 支)

這一棒要回答:Word 解析出來的中介資料轉成 OSCAL 字典時,uuid 與名稱是不是由伺服器產生(還是直接沿用檔案裡寫的);這份字典之後會餵給 V3 的 import_ssp——檔案裡的 uuid 能不能對上別份 SSP 已存在的資料。

主專案側(cd ~/Projects/Billows/Audit-Manager/compliance-manager-be)
  860 domain/oscal/adapter/cmmc_ssp_adapter.py
  369 domain/oscal/parser/ssp_intermediate.py
   78 domain/oscal/adapter/i_ssp_docx_adapter.py
   21 domain/oscal/adapter/adapter_registry.py                   ← 補入:框架代號 → 轉接器對照

共 4 檔/1,328 行。

驗證:

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git ls-files -- 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 | xargs wc -l | tail -1   # 4 檔、1328

3. 每棒重點看什麼

每一棒開工前都先做的兩件事

① 找「只收編號就改/刪」的方法,逐一追呼叫端:

# 套件服務層:哪些方法把呼叫者給的編號直接交給 repo 改/刪
grep -nE "\.(get_by_id|get_by_uid|update|delete_by_id|delete_by_uid)\(" <檔>
# 主專案與 compliance-audit:誰呼叫它
grep -rn "\.<方法名>(" --include='*.py' ~/Projects/Billows/Audit-Manager/compliance-manager-be/{api,app} ~/Projects/Jedicogy/module/jedi-python-package/jedi-compliance-audit/jedi_compliance_audit

套件本身沒有守門是預期的(它沒有網址入口、不知道使用者是誰)。要答的是「呼叫端在把編號交進來之前,有沒有先在正確的父文件底下找到它」。主專案目前已知的正確寫法:ssp_components_app_service.py:138-142 的 _resolve(ssp_id, item_uid)——先列出這份 SSP 的所有元件、比對 uid、找不到就 404,再把找到的 id 交給套件。

② 找「呼叫端傳什麼、套件就寫什麼」:套件多支方法收 <strong>fields,然後逐欄 setattr 寫進物件(例如 remediation_service.py:77-78、poam_service.py:141-142)。如果呼叫端把使用者的整包輸入直接傳進去,使用者就能改到 risk_id、ar_result_id 這種歸屬欄位**,把資料搬到別份文件底下。要確認每個呼叫端傳進 **fields 的鍵是白名單。

各棒個別重點

V1(SSP 本體)🔴

  • 🔴 ssp_service.py:125-344:21 支「收編號就讀/改/刪」的方法(update_ssp,以及元件、資產清單、沿用授權、控制項實作、實作敘述、by-component、人員各一組 get/update/delete)。盤點時追到主專案 25 處呼叫(第 4.2 節),分屬 10 支方法;update_ssp、delete_implemented_requirement、delete_statement、update_by_component、delete_by_component、六支 get_* 在兩個呼叫端 repo 零呼叫者。掃描要確認:①25 處都真的先在同一份 SSP 底下找到那筆(盤點只抽了 ssp_components_app_service.py:138-142,形狀對,其餘待掃);②update_* 傳進來的物件是從資料庫讀出來再改欄位,不是用使用者輸入重新組一個(重新組的話 ssp_id/system_implementation_id 就能被改掉)。
  • ssp_service.py:121-123 list_ssps(query):查詢條件全部選填(query or SspQueryEntity()),不帶條件就回全部客戶的全部 SSP(表沒有隔離)。盤點時找到 6 處呼叫,全部都帶 id=,預期不是洞,確認即可。
  • ssp_clone_service.py:103-245 deep_clone_ssp:複製 12 張子表,每張都用 replace(src, id=None, uid=None, ssp_id=new_ssp_id, ...)。要確認:①所有「指向別張表」的欄位(例如 by-component 的 component_id)都有換成新複製出來的那筆,沒有留著指向舊 SSP 的元件;②來源 SSP 編號(src_ssp_id)是誰給的——主專案呼叫端 project_start_app_service.py(FR-095 H1 已掃)與 jedi-compliance-audit 的開輪流程。
  • oscal_snapshot_service.py:52-96 snapshot_ssp:凍結後把 status 改成 frozen。套件內沒有任何地方檢查「凍結的 SSP 不能改」——ssp_service.update_* 都不看 status。確認主專案有沒有擋(第 7 節)。
  • metadata_clone_service.py:58-129:複製角色/人員/資源時,人員的 email_addresses、telephone_numbers 等個資會一起複製到新的 SSP。確認這是預期行為。

V2(稽核計畫、結果、改善計畫)🔴

  • 🔴 assessment_risk_service.py:109-125 link_findings(risk_id, finding_ids):逐一把判定編號接到風險上,完全不驗判定屬於哪份稽核結果。底下 ar_finding_risk_repo_impl.py:42-55 link() 也只查 (finding_id, risk_id)。唯一呼叫端是 jedi-compliance-audit 的 assessment_result_app_service.py:792,它傳入的 desired 是從 uid 轉出來的——轉換時有沒有限定在本輪的稽核結果底下,是 FR-116 C3 的範圍;本棒只要確認套件這支「完全信任呼叫端」的形狀,並在報告寫明「兩側要一起看」。
  • assessment_risk_service.py:132-143 get_risk_findings:用 get_by_id(link.finding_id) 撈判定,不驗判定跟風險在同一份結果。如果 link_findings 被塞進別份結果的判定,這支就會把它讀出來。
  • 🔴 poam_service.py:126-144 update_poam_item(item_id, <strong>fields):只收編號、</strong>fields 全部照寫。主專案與 compliance-audit 零呼叫者,目前打不到;確認後列「零呼叫者但對外公開」。
  • remediation_service.py:49-94 upsert_remediation:用 risk_id 限定範圍再比 uuid,形狀對。但 **fields 逐欄照寫——確認呼叫端(compliance-audit poam_app_service.py:317、assessment_result_app_service.py:661)傳的是白名單。
  • assessment_plan_service.py:113-215 三支 set_*:用 ap_uid 找計畫、再整批刪掉舊的子物件重建。刪除時用 delete_by_id(existing.id),existing 來自 get_by_ap(ap.id),有範圍。確認 ap_uid 由呼叫端驗過歸屬。
  • poam_milestone_repo_impl.py:31-39 get_by_assignee(user_id):只用使用者編號、沒有任何文件範圍,會回「這個人在所有客戶、所有改善計畫裡被指派的里程碑」。盤點時找不到任何呼叫者,列「零呼叫者」。

V3(匯入匯出)🔴

  • 大小與形狀的防線在哪一層:import_ssp(:321-430)收的是已經是 Python 字典的 oscal_ssp,本身沒有 json.loads。字典的來源只有兩條:主專案 Word 匯入(ssp_docx_import_app_service.py,上傳限 20MB,:50)與 Excel 匯入(ssp_excel_import_app_service.py,限 10MB,:47),全站請求上限 50MB(config/config.py:105)。所以「從外面丟一份超深的 OSCAL JSON 進來」這條路目前不存在;要看的是:Word/Excel 解析出來的字典裡,陣列長度(例如 5 萬個元件)有沒有上限、import_ssp 逐筆 add() 會不會把資料庫卡住。
  • 🔴 更新模式用「自然鍵」認定同一筆(docstring :356-365):人員用 (metadata_id, type, name)、元件用 (system_implementation_id, title, type)。因為範圍都帶了 metadata_id/system_implementation_id,跨 SSP 覆蓋應該不可能;但人員的 uuid 是照檔案裡寫的直接存(:984 uuid=p.get("uuid") or self._gen_uuid()),oscal 區的 parties.uuid 只有索引、沒有唯一限制。掃描要確認:同一個 uuid 出現在兩份 SSP 時,主專案有沒有任何地方「用 party uuid 找人」而會找到別份 SSP 的那一筆(主專案 ResponsiblePartyEntity 就是用 party_uuid 串起來的,見 CLAUDE.md「OSCAL Parties」段)。
  • clear_ssp_body(:281-319):刪掉整份 SSP 的內容。參數是 SSP 數字編號,由呼叫端決定。確認兩個呼叫端(ssp_excel_import_app_service.py:478、ssp_docx_import_app_service.py:384)的編號都來自已驗權的範本。
  • export_oscal(:229-251)與四個 _export_*:用 uid 撈根文件,然後所有子物件都用根文件的數字編號往下撈,範圍是對的。入口 oscal_export_app_service.py 只給了目錄匯出(api/oscal/routes/framework/oscal_framework_version_route.py,短效憑證綁版本 uid,FR-113 O8b 已看過);SSP/稽核結果/改善計畫的 JSON 匯出在主專案目前沒有網址入口,列「有能力、沒入口」。
  • 交易(transaction):套件自己不開交易、由呼叫端的 @transaction 包住,所以整份匯入是一個交易,中途出錯會整份撤回。確認兩個呼叫端都沒有在 import_ssp 中間自己 commit。
  • 錯誤處理:套件一律 raise ValueError(f"... id={...!r}"),訊息帶內部數字編號。確認主專案有沒有把 str(e) 回給前端(ssp_excel_import_app_service.py:621 msg = str(e) 要看去向)。

V4a(目錄、基準線、框架)

  • oscal_clone_service.py:90-315 目錄樹與基準線複製:群組、控制項、部分、參數四層,每層複製後用 get_by_id(new_id) 讀回、改 parent_id 再 update。確認「舊編號 → 新編號」的對照表有涵蓋所有層,沒有子物件的 parent_id 還指向來源目錄的群組。
  • framework_service.py:61-120:delete_framework(uid)、delete_version(uid)、publish_version(uid) 只收 uid。框架是全平台共用資源(主專案 framework_app_service.py:202 有 require_platform_admin()),確認所有呼叫端都有這道檢查(FR-113 O8b 掃過呼叫端,本棒確認套件側沒有第二個入口)。
  • profile_resolution_service.py:基準線「從哪份目錄挑控制項」的來源欄位 source_catalog_id。確認這個欄位只由伺服器端的複製流程寫入,沒有從使用者輸入寫入的路徑。
  • catalog_service.py:161-163 list_catalogs()、framework_service.py:50-54 list_frameworks():不帶條件=回全部。目錄與框架本來就是全平台共用,預期不是洞。

V4b(CMMC PDF/Excel 解析器)

  • cmmc2_lv2_parser_adapter.py:232-268 pdfplumber.open(pdf_stream) 後逐頁讀文字與字型資訊。頁碼範圍由 base_parser_adapter.py:37-58 _resolve_pages 算,有夾在總頁數之內,但沒有「總頁數上限」。確認主專案 framework_parse_job_route.py:81-83 只量了檔案大小、沒有擋大小(只記錄),唯一的上限是全站 50MB。一份 50MB、上萬頁的 PDF 會解析多久要實測或估算。
  • 錯誤訊息:套件解析器本身沒有 try/except,例外直接往上丟;主專案 framework_parse_job_service.py:226-228 寫成 f"{error_message}: {e}"——這就是總表第 132 項,已知、不重報,本棒只確認套件丟出來的例外內容(pdfplumber/pandas 的例外會不會帶暫存檔路徑)。
  • excel_parser(:450-472)走 pandas.read_excel:openpyxl 在主專案環境會自動用 defusedxml 防 XML 炸彈(第 6 節);但套件自己的 pyproject.toml 沒有宣告 defusedxml,換個主機環境防護就會消失。
  • parser_adapter_type.py:23-27 宣告了 YAML、JSON 兩種匯入類型,全套件沒有任何一處實作;pyproject.toml 也宣告了 pyyaml 依賴卻零 import。確認不是「預留入口、某天被接上 yaml.load」的地雷(第 7 節)。

W1(Word 內容抽取器)

  • docx_parser_core.py:170、:192 Document(file_path):python-docx 打開 Word 檔(Word 檔本質是 zip 壓縮檔裝一堆 XML)。python-docx 的 XML 解析器已設 resolve_entities=False(不解析外部實體),XML 外部實體攻擊不成立;但解壓縮沒有大小上限——一個 20MB 的 Word 檔解壓後可以是好幾 GB(壓縮炸彈)。確認主專案有沒有在打開前看 zip 內各檔的解壓大小。
  • framework_patterns.py:四組 regex 都是固定長度的數字與字母組合,盤點時目測沒有「巢狀重複」這種會卡死的寫法(例如 (a+)+),但 iso27001 那組 A\.\d+(\.\d+)* 有一層重複,請研究員用超長 A.1.1.1.1… 字串實測。
  • control_id_matcher.py:50-120 fuzzy_match_control:每一段文字都拿去跟所有候選控制項做模糊比對,耗時是「段落數 × 控制項數 × 文字長度」。CMMC L2 有 110 項,一份幾千段的 Word 可能要跑很久。確認有沒有段落長度或段落數上限。
  • docx_section_extractors.py 892 行:逐表格、逐儲存格讀文字。確認合併儲存格、巢狀表格這種特殊結構不會讓迴圈爆掉。

W2(CMMC 轉接器與中介格式)

  • cmmc_ssp_adapter.py 把中介格式轉成 import_ssp 吃的 OSCAL 字典。🔴 要確認字典裡的 uuid(人員、元件、實作項目)是轉接器自己產生,還是沿用 Word 內容——如果 Word 裡能寫 uuid 而轉接器照抄,就接上 V3 講的「同一個 party uuid 出現在兩份 SSP」。
  • 轉接器輸出的 component-uuid 決定 by-component 掛在哪個元件底下(V3 :1342-1397 用 comp_map 查,查不到就跳過,範圍對)。確認轉接器不會輸出「別份 SSP 的元件 uuid」。
  • adapter_registry.py:框架代號 → 轉接器。代號來自使用者選的框架,確認查不到時是回錯誤而不是落到某個預設轉接器。

4. repo 方法範圍條件對照表

「範圍條件」=除了編號之外,查詢有沒有再加上「而且要屬於某份文件」。

4.1 45 支 repo 實作自己寫的查詢方法:全部有範圍

盤點時逐支讀過 45 支 *_repo_impl.py。自己寫的方法共 41 個,每一個的查詢條件都有上一層的編號:

文件類型 方法(查詢條件)
SSP(13 支 repo) get_by_ssp(ssp_id) ×3、get_by_system_implementation(system_implementation_id) ×4、get_by_system_characteristics(system_characteristics_id) ×2、get_by_control_implementation(control_implementation_id)、get_by_implemented_requirement(implemented_requirement_id) ×2、get_by_statement(statement_id)
AP(8 支) get_by_ap(assessment_plan_id) ×5、get_included(assessment_plan_id)、get_roots(assessment_plan_id)、get_children(parent_id)、get_by_task(task_id) ×2、delete_by_task(task_id) ×2
AR(8 支) get_by_ar_result(ar_result_id) ×4、get_by_target(ar_result_id, …)、get_by_assessment_result(assessment_result_id)、get_by_risk(risk_id) ×2、get_by_finding(finding_id)、link/unlink(finding_id, risk_id)
POA&M(3 支) get_by_poam(poam_id)、get_by_remediation(remediation_id)、🟡 get_by_assignee(user_id)、get_by_uid(uid)
目錄、基準線(5 支) get_by_catalog(catalog_id)、get_roots(catalog_id)、get_children(parent_id)、get_enhancements(parent_id)、get_by_control(catalog_control_id)、list_aos(catalog_control_id)、get_by_profile(profile_id)
框架、metadata(6 支) 無自訂方法

例外兩支:

  • 🟡 PoamMilestoneRepoImpl.get_by_assignee(user_id)(poam_milestone_repo_impl.py:31-39):只用使用者編號、跨所有文件。零呼叫者。
  • 🟡 PoamRepoImpl.get_by_uid(uid)(poam_repo_impl.py:21-28):只用 uid,是根文件的查法,唯一呼叫者是 V3 的 _export_poam,而那條匯出目前沒有網址入口。

4.2 🔴 從基底類別繼承、只有編號的方法(45 支 repo 全部都有)

每一支 repo 都繼承 jedi_common 的 BaseRepositoryImpl,自動多出這些方法(jedi-common/jedi_common/session/database/repository/base_repository_impl.py):

方法 位置 查詢條件
get_by_id/get_by_uid :356-383 只有編號
update(entity) :406-427 只有 entity.uid 或 entity.id
delete_by_id/delete_by_uid :429-443 只有編號
delete_by_ids/delete_by_uids :445-453 只有編號清單
get_all_by_fields(query) :286 查詢條件的非空欄位;全空=整張表

這是全專案共用的基底、不是本套件的錯,本身不是發現。重點是本套件的服務層把這些方法原封不動開放出去:

套件服務方法 做什麼 主專案/compliance-audit 呼叫端 呼叫端有沒有先找父層
SspService.update_component/delete_component 改/刪元件 ssp_components_app_service.py:123,135、ssp_resources_context_service.py:106,111、module_frame_components_service.py:115,125 ✅ 抽查第一支有 _resolve(ssp_id, uid);其餘 V1 確認
update_inventory_item/delete_inventory_item 改/刪資產清單 ssp_inventory_items_app_service.py:121,130、module_frame_inventory_service.py:116,125 V1 確認
update_leveraged_authorization/delete_… 改/刪沿用授權 ssp_leveraged_app_service.py:125,134、module_frame_leveraged_service.py:127,134 V1 確認
update_party/delete_party 改/刪人員 ssp_party_app_service.py:108,118、module_frame_party_service.py:132,141 V1 確認
update_implemented_requirement 改控制項實作 ssp_control_implementation_service.py:90、module_frame_control_default_service.py:209,223 V1 確認
update_statement 改實作敘述 ssp_control_implementation_service.py:860,878、module_frame_control_objective_default_service.py:223,240 V1 確認
update_ssp、delete_implemented_requirement、delete_statement、update_by_component、delete_by_component、get_component/get_inventory_item/get_leveraged_authorization/get_implemented_requirement/get_statement/get_party 零呼叫者 —
AssessmentRiskService.update_risk/delete_risk 改/刪風險 compliance-audit assessment_result_app_service.py:747,765 FR-116 C3 範圍
🔴 AssessmentRiskService.link_findings 判定接到風險 compliance-audit assessment_result_app_service.py:792 FR-116 C3 範圍;套件側完全不驗
AssessmentRiskService.get_risk_findings 讀風險的判定 零呼叫者 跟著 link_findings
PoamService.get_poam(poam_id) 讀改善計畫 compliance-audit(FR-116 C3)
🔴 PoamService.update_poam_item(item_id, <strong>fields) 改改善計畫條目 零呼叫者** —
FrameworkService.delete_framework/delete_version/publish_version/update_* 框架增刪改 framework_app_service.py 等(FR-113 O8b 已掃) 平台管理員檢查
OscalIoService.clear_ssp_body(ssp_id) 清空整份 SSP 內容 ssp_excel_import_app_service.py:478、ssp_docx_import_app_service.py:384 V3 確認
各服務 get_ssp/get_ap/get_ar/get_catalog/get_profile/get_framework(by uid) 讀根文件 多處 根文件本來就只能用 uid 找,由呼叫端決定能不能看

標紅統計:

  • 套件 repo 自己寫的方法:0 支只有編號沒範圍(41 支都有範圍;2 支例外都零呼叫者或無網址入口)。
  • 套件服務層把「只有編號」的讀/改/刪開放出去:33 支(ssp_service 21、assessment_risk_service 4、poam_service 2、framework_service 5、oscal_io_service.clear_ssp_body 1)。其中有活呼叫端的 20 支要逐一確認呼叫端先找父層;零呼叫者 13 支列第 7 節清理建議。另外 remediation_service.upsert_remediation 有範圍,但 **fields 全開(第 3 節②)。
  • 最該先看:🔴 link_findings(套件完全不驗、要跟 FR-116 C3 兩側對看)、🔴 update_poam_item(零呼叫者但 **fields 全開)。

5. 資料庫隔離對照表

對照來源:①出貨版 scripts/init/02-schema.sql(靜態快照);②DEV 實查(唯讀 SELECT,2026-09-24 12:49,cm_app 帳號)。

5.1 本套件的 45 張表

群組 表 客戶欄位 出貨 02-schema DEV
共用 metadata、parties、roles、resources 無 關 關
框架 frameworks、framework_versions 無 關 關
目錄 catalogs、catalog_groups、catalog_controls、catalog_control_parts、catalog_control_params 無 關 關
基準線 profiles、profile_imports 無 關 關
🔴 SSP system_security_plans、ssp_system_characteristics、ssp_information_types、ssp_diagrams、ssp_system_implementations、ssp_system_users、ssp_components、ssp_inventory_items、ssp_leveraged_authorizations、ssp_control_implementations、ssp_implemented_requirements、ssp_statements、ssp_by_components 無 關 關
🔴 稽核計畫 assessment_plans、ap_reviewed_controls、ap_assessment_subjects、ap_assessment_activities、ap_local_objectives、ap_tasks、ap_task_subjects、ap_task_participants 無 關 關
🔴 稽核結果 assessment_results、ar_results、ar_assessment_subjects、assessment_observations、assessment_findings、assessment_risks、assessment_finding_risks、assessment_remediations 無 關 關
🔴 改善計畫 poams、poam_items、poam_milestones 無 關 關

45 張:客戶欄位 0 張、隔離開啟 0 張、規則 0 條。

5.2 oscal 區其他 7 張表(不是本套件建的,列出來對照)

DEV 的 oscal 區共 52 張表,開了隔離的只有 5 張,全部是主專案的解析工作紀錄表,而且只有這 5 張有客戶欄位:

表 客戶欄位 隔離
framework_parse_jobs、ap_docx_parse_jobs、ar_xlsx_parse_jobs、ssp_docx_parse_jobs、ssp_excel_parse_jobs 有 開
ssp_reference_documents、ssp_reference_document_mappings(compliance-audit 的程序書,FR-116 已列) 無 關

5.3 白話結論

  • 這支套件管的所有合規文件內容,在資料庫層完全沒有客戶分隔。A 公司的 SSP、稽核判定、改善計畫與 B 公司的放在同一張表、沒有欄位標示屬於誰。能不能跨客戶讀寫,百分之百取決於程式層有沒有先從「有隔離的表」(例如專案表)往下找。
  • 這就是總表第 128 項,本批不重報;列在這裡是讓 V1~V3 的研究員知道:只要發現任何一條「使用者能直接指定 SSP/稽核結果編號」的路,沒有第二道防線。
  • 共用區(目錄、框架、基準線)沒有隔離是設計:這些是全平台公版。但注意「資源庫套用框架」會把公版目錄複製一份給客戶(V4a clone_catalog_tree),複製出來的那份跟公版在同一張表、同樣沒有客戶欄位——無法從資料庫區分哪份是公版、哪份是某客戶的副本。

6. XML 支援確認

結論:OSCAL XML 格式未支援,套件層 XML 攻擊面不存在。交接文件寫的「XML 解析攻擊」在本套件不成立。

查證(2026-09-24,jedi monorepo eafc7ae5):

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2
grep -rnE "import (xml|lxml|defusedxml|yaml)|from (xml|lxml|defusedxml|yaml)|etree|ElementTree|minidom|sax" jedi_oscal_v2 --include='*.py'
# → 0 筆
grep -rniE "xml|yaml" jedi_oscal_v2 | grep -v Binary
# → 只有 common/code/parser_adapter_type.py:26  YAML = "YAML"(一個沒有實作的常數)
項目 結果
套件正式碼 import XML 解析器 0 支(xml、lxml、defusedxml、ElementTree、minidom、sax 全無)
套件正式碼 import YAML 0 支。但 pyproject.toml 宣告了 pyyaml 依賴(沒用到的依賴)
oscal_io_service.py 支援的格式 匯出只有 JSON(:239-240,其他格式直接 raise ValueError);匯入收的是 Python 字典,不解析任何文字格式
parser_adapter_type.py 的 ImportType 宣告了 PDF/EXCEL/YAML/JSON,只有 PDF、EXCEL 有實作(catalog_service.py:118-155)

真正會碰到 XML 的地方(都不在套件、而且都已有防護):

在哪 用什麼讀 XML 防護 還剩什麼風險
主專案 Word 匯入(W1 docx_parser_core.py:170,192) python-docx → lxml python-docx 建解析器時設 resolve_entities=False(.venv/.../docx/oxml/parser.py:19),外部實體不會被解析 壓縮炸彈(Word 是 zip,解壓沒上限)、段落數與模糊比對耗時
套件 CMMC Excel 匯入(V4b excel_parser) pandas → openpyxl openpyxl 偵測到 defusedxml 就自動使用(openpyxl/xml/__init__.py:29-42);主專案 pyproject.toml:93 有宣告 defusedxml>=0.7.1 套件自己沒宣告 defusedxml,防護依賴主機環境;壓縮炸彈
套件 CMMC PDF 匯入(V4b) pdfplumber(pdfminer) 不是 XML 頁數沒上限、惡意 PDF 物件結構

掃描重點因此換成:①V3——Word/Excel 解析出的字典,陣列長度、字串長度有沒有上限;②W1/V4b——Word/Excel 的 zip 壓縮炸彈、PDF 頁數;③沒有任何地方把 JSON 當輸入的「深度炸彈」,因為目前沒有 OSCAL JSON 匯入入口(主專案只有三處 json.loads,都是解析表單裡的小欄位:ssp_excel_import_route.py:48、ssp_scoped_excel_import_route.py:55、framework_parse_job_route.py:74,受全站 50MB 上限約束)。


7. 盤點時發現、需要首腦裁決的事

  1. app/flow_control/service/assessment_plan_app_service.py 沒被任何一棒掃過:它呼叫本套件的 generate_draft(:313)與 clone_metadata(:354,覆核子輪複製稽核計畫用)。FR-113 十棒、FR-116 盤點的主專案清單都沒有它。它不是 Word 解析器,本批不收。要歸 FR-116(稽核計畫相關)還是另開,由首腦決定。
  2. 零呼叫者但對外公開的套件方法 13 支(第 4.2 節):update_poam_item(**fields 全開)、get_risk_findings、ssp_service 的 update_ssp/delete_implemented_requirement/delete_statement/update_by_component/delete_by_component 與六支 get_*;另加 repo 層的 get_by_assignee(跨全部客戶)。現在打不到,哪天有人接上網址就是現成的「只認編號」。建議併入 FR-092(死碼清理)後續,或在套件裡改成要求父層編號的版本——這是清理建議,不是資安發現。
  3. 套件宣告了 pyyaml 依賴與 ImportType.YAML/JSON 常數,但零實作。如果未來要做 YAML 匯入,必須用 yaml.safe_load(yaml.load 可以執行任意程式)。建議拿掉 pyyaml 依賴,或在常數旁寫明這個陷阱。
  4. 套件自己沒宣告 defusedxml:Excel 解析的 XML 防護目前靠主專案環境剛好有裝。要不要在套件 pyproject.toml 補宣告,由首腦決定(改套件要走發版流程)。
  5. 凍結的 SSP 在套件層沒有「不可修改」的保護(V1):snapshot_ssp 把 status 設成 frozen,但 ssp_service.update_* 都不看 status。主專案有沒有擋要等 V1 確認;如果沒有,這是稽核證據可被事後竄改的問題,嚴重度會比一般的越權高。
  6. 派工順序建議:V2(link_findings 要跟 FR-116 C3 對看,C3 已派)→ V1 → V3 → W2 → W1 → V4b → V4a。V4b、W1、W2 建議同一個 runner 連做(第 2 節)。

附錄 A:不掃的宣告檔(294 支/8,735 行)

驗證指令(套件側):

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/jedi_oscal_v2
# entity:0 個方法、0 個 if/for
grep -rnE "    def |^\s+(if|for)\b" domain/entity | wc -l          # → 0
# model:0 個方法、驗證器、事件監聽
grep -rnE "def |@validates|event\.listen|hybrid|column_property" infra/model | wc -l   # → 0
# mapper:除了「model 是 None 就回 None」之外沒有任何 if/for/try
grep -rnE "^\s+(if|for|while|try)\b" infra/mapper | grep -v "is None" | wc -l        # → 0
grep -rn "json\|loads" infra/mapper | wc -l                         # → 0
# repository 介面:抽象宣告,沒有實作
grep -rnE "^\s+(if|for)\b" domain/repository | wc -l                # → 0
目錄 檔數 行數 為什麼不掃
domain/entity/** 99 2,195 @dataclass 純欄位宣告,沒有方法、沒有判斷
infra/model/** 54 2,921 SQLAlchemy 欄位與索引宣告;沒有 @validates、沒有事件監聽、沒有計算欄位
infra/mapper/<strong> 54 2,671 to_entity/to_model 一欄一欄搬;唯一判斷是「傳入 None 回 None」;JSON 欄位由 SQLAlchemy JSONB 型別直接轉成 Python 物件,mapper 沒有自己做 json.loads 或遞迴組裝**,所以不算判斷邏輯
domain/repository/** 54 802 抽象介面(含 9 支空 __init__.py)
common/** 常數 2 103 錯誤碼與列舉常數
jedi_oscal_v2/__init__.py 1 30 只有 docstring
其他 __init__.py 30 13
合計 294 8,735

⚠️ 例外提醒:infra/model/base/oscal_party.py 的 uuid 欄位只有索引、沒有唯一限制(:17、:30)。這不是判斷邏輯、本身不掃,但它是 V3「同一個 party uuid 出現在兩份 SSP」那條疑點的根據,V3 研究員要知道。


附:與交接說明的差異

  • 主專案側:卡片「7 檔/2,946 行」→ 實數 9 支有內容的檔/2,986 行(補 framework_patterns.py 19 行、adapter_registry.py 21 行)。
  • repo 實作支數:卡片「infra/repository 54」→ 實際 *_repo_impl.py 45 支,另 9 支是 __init__.py。
  • XML 攻擊:交接寫「XML 解析攻擊」→ 確認不成立(第 6 節),套件零 XML import、沒有 OSCAL XML 格式。
  • io service 的 json.loads:卡片寫 io service「import json」並要求查「json.loads 之前有沒有看大小」→ 查證 io service 只用 json.dumps(匯出),匯入收的是字典、沒有 json.loads。大小防線在主專案上傳層(Word 20MB/Excel 10MB/全站 50MB)。
  • 「54 支 repo 方法範圍條件」:預期會找到一批「只有 uid」的 repo 方法 → 實際套件 repo 自己寫的 41 個方法全部有範圍;問題形狀在服務層把基底類別的只認編號方法原封不動開放出去(第 4.2 節)。
  • FR-113 O1/O6 引用的 ssp_reference_document_repo_impl.py:確認屬 jedi-compliance-audit,不在本套件。