jedi-oscal-v2 主專案側接線程式 — 切棒盤點

盤點日期:2026-09-21/盤點員:Opus 5/工作目錄:compliance-manager-be 基準 commit:6b011d67(feature/review)


1. 範圍界定結論

實際數字

界定法 檔數 行數 說明
路徑法(oscal 四層 + di_containers/oscal) 159 20,504 api/oscal 61、app/oscal 46、domain/oscal 36、infra/oscal 14、di_containers/oscal 2
引用法(程式裡寫了 jedi_oscal_v2 的檔) 46 — 其中 app/oscal 19、app/module_frame 10、測試檔 9、其餘散落
🔴 建議採用範圍(路徑法 + 5 個外圍接線檔) 167 21,364 見下方「建議範圍」

191 跟 156 的差距怎麼來的

差距不是統計方法不同,而是總表那個 191 是當初排順序時的估算,沒有對得上任何一種數法。實際驗證過的數字:

  • 用 git ls-files 數 oscal 四層加上 di_containers 是 159 支 .py(首腦說的 156 應該是漏了 di_containers 那 2 支跟 api/oscal/__init__.py,或是用了不含 di 的算法=157)
  • 用 find 連未進版控的檔一起數也是 159;連 .pyc 等非 .py 檔全算進去是 327,也不是 191
  • 191 這個數字在總表裡出現過兩次,另一次講的是流程範本表裡 191 筆沒有客戶歸屬的資料(§3.1 第 43 項)。兩處數字一模一樣、但一個是檔案數一個是資料筆數,高度懷疑是當初寫總表時抄串行了

我試過各種加總組合(159+module_frame 的 api 25=184、再加 domain 12=196),沒有任何一種組合剛好等於 191。結論:191 是估的,請以本盤點的 167 為準,並建議首腦回頭把總表 §0.5 第 1 列跟 §5 的「191 個檔案」改成 167。

建議範圍(167 支)

路徑法的 159 支,再加 5 支住在別的目錄、但實際承擔 OSCAL 權限判斷或接線責任的檔(加上 di_containers/dashboard_apis/oscal.py 共 8 支,其中 3 支已含在 159 內):

補進來的檔 行數 為什麼一定要納入
common/authz/ssp.py 225 整個 SSP 的權限判斷只有這一支。app/oscal 裡 9 個檔 import 它、共呼叫 54 次。不納入等於掃了 54 個呼叫點卻看不到被呼叫的那支怎麼判
infra/readmodel/oscal/ssp_control_implementation_query.py 257 控制項實作的查詢實作,直接組 SQL
infra/readmodel/oscal/resource_library_query.py 141 資源庫的查詢實作
app/flow_control/service/oscal_stage_handlers.py 等 3 支 342 稽核流程推進階段時會回頭改 OSCAL 資料,是跨模組的寫入入口
di_containers/dashboard_apis/oscal.py 36 儀表板那側的 OSCAL 接線

module_frame 的建議處置:這次不要納入,但建議另立一棒

module_frame 有 81 支檔(api 25/app 31/domain 12/infra 13/di 2),確實跟 oscal 高度糾纏(10 支 app 檔直接 import jedi_oscal_v2)。但我建議這次不掃,理由是實際 grep 出來的守門狀況兩邊差很多:

  • module_frame 的權限守門做得相當完整:25 支網址入口檔裡有 15 支用了 require_capability(也就是「這個角色能不能用這個功能」那種檢查),是正規做法
  • oscal 這邊 22 支網址入口檔裡只有 1 支碰 common.authz

兩邊的風險型態不一樣,檢查重點寫不到同一張卡裡——oscal 這棒要問的是「這支端點到底有沒有人在守」,module_frame 要問的是「守是守了,但守的那個權限對不對、夠不夠細」。混在一起掃,研究員會拿錯檢查清單(這正是總表 §5 要求 oscal 主專案側跟套件本體必須分開掃的同一個道理)。

建議:module_frame 另開一支母卡、獨立排隊,等 oscal 這批掃完再排。若首腦要現在就排,至少要另寫一份檢查重點。

已掃過的部分:這塊完全沒被掃過

查了總表 §0.5 三張表與已掃完的 18 支套件清單,oscal 主專案側 167 支沒有任何一支被前面的 arc 掃過。唯一沾到邊的是 §3.1 第 67 項(FR-095 H1 掃專案任務平台時順手發現資源庫範本可以被竄改),那一條指向的 resource_library_app_service.py 正是本次 O2 棒的主角——掃到的是問題,這支檔本身沒被完整掃過。


2. 切棒表(10 棒,全部 ≤2,000 行;O9 除外,派工前再切)

界定範圍 167 檔/21,364 行。扣掉移去套件本體那批的 Word 內容抽取器 7 支(原 O7a/O7b,2,868 行)後,實際進掃描 92 檔/17,224 行、切 10 棒(逐檔 wc -l 實算、十棒無重複檔);其餘 68 檔為資料結構宣告(空的 __init__.py、ORM model、entity 介面、純格式定義),沒有判斷邏輯,不排入掃描。

棒 在掃什麼(白話) 檔數 行數 最大單檔
O1 控制項實作:每條合規控制項「我們怎麼做到的」的讀寫,含唯一那支權限判斷程式 6 1,943 1,037 行/60KB
O2 資源庫、公版範本、SSP 匯出(含叫 LibreOffice 轉檔) 11 1,772 530 行/32KB
O3a Excel 上傳入口與確認流程(權限題) 5 1,460 937 行/48KB
O3b Excel 內容解析(惡意檔案題) 7 1,047 428 行
O4 Word 上傳與解析全鏈(含控制項實作單獨匯入) 9 1,950 787 行/40KB
O5 SSP 六種子物件的增刪改查(元件/人員/設備/繼承授權/參考資料/系統特性) 15 1,808 230 行
O6 程序書文件池 + 匯入比對引擎(新舊版差異怎麼算、使用者選了要怎麼合併) 6 1,917 751 行/36KB
O8a 合規框架 PDF 解析工作 6 1,343 674 行
O8b 合規框架與版本增刪改查 9 1,479 351 行
O9 接線總裝:路由註冊表、依賴注入、稽核階段掛鉤、人員對帳、匯入線共用接縫檔 18 2,505 ⚠️ 620 行

O7a/O7b(Word 內容抽取器 7 支)不在本批——純解析無權限議題,風險型態是惡意檔案,併進 jedi-oscal-v2 套件本體那批一起掃(理由見 §1 與 §4)。本批最終 10 棒。

⚠️ O9 是唯一超過 2,000 行的一棒(2,505 行):它接手了 Excel 與 Word 兩條匯入線共用的接縫檔 _common.py(620 行)——該檔兩棒各自帶都會讓那一棒爆上限,重疊帶入則兩棒都爆。O9 排序在最後幾棒,派工前再切(可能的切法:人員對帳五支或 bundle_restore.py 另放)。runner 以派工當下的卡片 scope 為準。


O1 — 控制項實作與權限判斷核心(6 檔/1,943 行)

白話:每一條合規控制項底下「我們公司怎麼做到這條」的文字記錄,是整個系統最核心的資料。這棒同時把唯一一支負責判斷「你能不能看/能不能改這份文件」的程式一起放進來。

api/oscal/routes/ssp/ssp_control_implementation_route.py        309
api/oscal/serializers/ssp/ssp_control_implementation.py          88
api/oscal/serializers/ssp/ssp_control_impl_objective.py          27
app/oscal/service/ssp_control_implementation_service.py        1037  ← 60KB,單檔最大
common/authz/ssp.py                                             225  ← 全系統唯一 SSP 權限判斷
infra/readmodel/oscal/ssp_control_implementation_query.py       257

最大單檔 1,037 行/60KB — 已達安全線上緣,若研究員回報壓縮,下次把 common/authz/ssp.py 移到 O5。


O2 — 資源庫、公版範本、匯出(11 檔/1,772 行)

白話:客戶自己建的範本庫、原廠給的公版範本,以及把系統安全計畫匯出成 Word/PDF 的整條路。這棒優先級最高——總表第 67 項已經掃到這裡有問題。

api/oscal/routes/resource_library_route.py                       62
api/oscal/routes/module_frame_template_ssp_route.py              38
api/oscal/routes/ssp/ssp_export_route.py                         72
app/oscal/service/resource_library_app_service.py               530  ← 零權限檢查 + 9 段手寫 SQL
infra/readmodel/oscal/resource_library_query.py                 141
app/oscal/service/export/ssp_export_app_service.py              (見下)
app/oscal/service/export/ssp_docx_generator.py                  258
app/oscal/service/export/ssp_libreoffice_converter.py           124  ← 唯一呼叫外部程式的檔
app/oscal/service/export/ssp_v2_content_loader.py               217
app/oscal/service/export/ssp_export_model.py                    169
app/oscal/service/oscal_export_app_service.py                    47

O3a — Excel 上傳入口與確認流程(5 檔/1,460 行)

白話:使用者把 Excel 傳上來、系統開一張解析工作單,使用者看過預覽後按確認才真的寫進去。這一棒是權限題——誰能傳、誰能看預覽、誰能按確認、送上來的編號有沒有驗歸屬。

api/oscal/routes/ssp/ssp_excel_import_route.py                  112
api/oscal/routes/ssp/ssp_scoped_excel_import_route.py           130
api/oscal/serializers/ssp/ssp_excel_import.py                   137
app/oscal/service/ssp_excel_import_app_service.py               937  ← 48KB
app/oscal/service/import_adapter/excel_to_oscal_ssp.py          144

O3b — Excel 內容解析(7 檔/1,047 行)

白話:把 Excel 檔裡九張工作表的每一格讀出來、轉成系統看得懂的資料。這一棒是惡意檔案題——超大表格、公式注入、外部連結、解析上限有沒有設。

app/oscal/service/excel_parser/__init__.py                        9
app/oscal/service/excel_parser/parser.py                        153
app/oscal/service/excel_parser/sheet_handlers.py                428  ← 本棒最大
app/oscal/service/excel_parser/types.py                         111
app/oscal/service/excel_parser/v2_bundle.py                     217
app/oscal/service/excel_parser/validators.py                     66
app/oscal/service/excel_parser/version_check.py                  63

O3a 與 O3b 的拆點是題目性質(權限 vs 惡意檔案),不是行數——兩種題目的檢查清單不一樣,混在一起研究員會拿錯清單。

O4 — Word 上傳解析鏈(9 檔/1,950 行)

api/oscal/routes/ssp/ssp_docx_import_route.py                   105
api/oscal/routes/ssp/ssp_control_impl_import_route.py           114
api/oscal/serializers/ssp/ssp_docx_import.py                    185
api/oscal/serializers/ssp/ssp_control_impl_import.py             59
app/oscal/service/ssp_docx_import_app_service.py                787  ← 40KB
app/oscal/service/ssp_control_impl_import_service.py            416
app/oscal/service/import_adapter/docx_to_oscal_ssp.py           152
app/oscal/service/import_adapter/party_match_enrich.py           78
app/oscal/service/import_adapter/v2_candidate_loader.py          54

⚠️ app/oscal/service/import_adapter/_common.py(620 行)是 Excel 與 Word 兩條匯入線共用的接縫檔,掛在 O9 掃。它不能放在 O3a 或 O4——哪一棒帶它,哪一棒就爆 2,000 行上限;重疊帶入兩棒則兩棒都爆。O3a 與 O4 的卡片裡已註明「這支共用檔在 O9」。


O5 — SSP 六種子物件增刪改查(15 檔/1,808 行)

api/oscal/routes/ssp/ssp_components_route.py                     85
api/oscal/routes/ssp/ssp_party_route.py                          84
api/oscal/routes/ssp/ssp_inventory_items_route.py                84
api/oscal/routes/ssp/ssp_leveraged_route.py                      78
api/oscal/routes/ssp/ssp_resources_route.py                      88
api/oscal/routes/ssp/ssp_system_characteristic_route.py          51
app/oscal/service/ssp_components_app_service.py                 (各約 150-200)
app/oscal/service/ssp_party_app_service.py                      169
app/oscal/service/ssp_inventory_items_app_service.py            194
app/oscal/service/ssp_leveraged_app_service.py
app/oscal/service/ssp_resources_app_service.py
app/oscal/service/ssp_system_characteristic_app_service.py
app/oscal/service/ssp_resources_context_service.py              230
domain/oscal/service/ssp_project_resolver.py                    184  ← 「這份 SSP 屬於哪個專案」的解析
domain/oscal/service/ssp_context_resolver.py                    158

O6 — 文件池與匯入比對引擎(6 檔/1,917 行)

api/oscal/routes/ssp/ssp_document_pool_route.py                 183
api/oscal/serializers/ssp/ssp_document_pool.py                   37
app/oscal/service/ssp_document_pool_service.py                  313
app/oscal/service/import_diff/ssp_diff_service.py               751  ← 36KB
app/oscal/service/import_diff/decision_merge.py                 318
app/oscal/service/import_diff/snapshot_views.py                 315

O7a — Word 內容抽取器(4 檔/1,639 行)

domain/oscal/parser/docx_section_extractors.py                  892  ← 單檔最大
domain/oscal/parser/docx_parser_core.py                         555
domain/oscal/parser/docx_intermediate.py                         72
domain/oscal/parser/control_id_matcher.py                       120

O7b — CMMC 轉接器與中介格式(3 檔/1,307 行)

domain/oscal/adapter/cmmc_ssp_adapter.py                        860
domain/oscal/parser/ssp_intermediate.py                         369
domain/oscal/adapter/i_ssp_docx_adapter.py                       78

O8a — 合規框架 PDF 解析工作(6 檔/1,343 行)

api/oscal/routes/framework/framework_parse_job_route.py         178
app/oscal/service/framework_parse_job_service.py                674
api/oscal/serializers/framework_parse_job/framework_parse_job_schemas.py  201
domain/oscal/service/framework_parse_job_domain_service.py      123
infra/oscal/repository/framework_parse_job_repo_impl.py          91
domain/oscal/entity/framework_parse_job_entity.py                76

O8b — 框架與版本增刪改查(9 檔/1,479 行)

api/oscal/routes/framework/oscal_framework_route.py             104
api/oscal/routes/framework/oscal_framework_version_route.py     157  ← 唯一有掛 authz 的 route 檔
api/oscal/routes/framework/framework_version_edit_route.py      132
app/oscal/service/framework_app_service.py                      247
app/oscal/service/framework_version_app_service.py              229
app/oscal/service/framework_version_edit_service.py             351
api/oscal/serializers/framework/oscal_framework.py              165
api/oscal/serializers/framework/oscal_framework_version.py       34
api/oscal/serializers/framework_version_edit/schemas.py          60

O9 — 接線總裝、稽核階段掛鉤、人員對帳(18 檔/2,505 行 ⚠️ 派工前再切)

白話:把所有東西接起來的那一層——哪個網址對到哪支程式、哪個物件由誰建立;以及稽核流程推進/退回階段時會回頭改 OSCAL 資料的那三支。

api/oscal/__init__.py                                           201  ← 路由註冊總表,能看出哪些入口是關的
di_containers/oscal/oscal_containers.py                         509
di_containers/oscal/__init__.py                                   0
di_containers/dashboard_apis/oscal.py                            36
app/flow_control/service/oscal_stage_handlers.py                142
app/flow_control/service/oscal_stage_preconditions.py           129
app/flow_control/service/oscal_stage_rollback_handlers.py        71
domain/oscal/import_pipeline/bundle_restore.py                  208
domain/oscal/service/reconciliation/base.py                     107
domain/oscal/service/reconciliation/person_reconciler.py        101
domain/oscal/service/reconciliation/organization_reconciler.py  101
domain/oscal/service/reconciliation/_normalizers.py              51
domain/oscal/service/reconciliation/match_method.py              10
domain/oscal/service/party_reconciliation_service.py             30
infra/oscal/repository/ssp_catalog_title_query.py                93
infra/oscal/model/framework_parse_job.py                         36
infra/oscal/model/ssp_docx_parse_job.py                          30
infra/oscal/model/ssp_excel_parse_job.py                         30
app/oscal/service/import_adapter/_common.py                     620  ← Excel/Word 共用接縫檔

⚠️ 本棒 2,505 行超過上限,派工前再切。_common.py 放這裡是因為它是兩條匯入線共用的,放進 O3a 或 O4 都會讓那一棒爆上限。可能的切法:人員對帳五支(約 370 行)或 bundle_restore.py(208 行)另放一棒。


不排入掃描(86 檔/約 1,400 行)

__init__.py 空檔、infra/oscal/model|mapper(純資料表定義與欄位對應,205 行)、domain/oscal/entity|repository 介面(329 行)、api/oscal/serializers/oscal_common/(21 支純格式定義,337 行)。這些是資料結構宣告,沒有判斷邏輯,照 CLAUDE.md「純資料表不計」的精神排除。若首腦要求全覆蓋,可合併成一棒約 900 行。


3. 每棒重點看什麼(讀碼後的實數與發現)

🔴 先更正一個關鍵數字:「122 支入口完全沒有權限檢查」不成立

總表 §0.5 寫「這部分共 122 支功能入口只驗證有沒有登入,完全沒有功能權限檢查(實際測試確認)」。我實際 grep 驗證,實數與結論都要修正:

實測項目 實數
api/oscal/routes 底下的功能入口總數 89 支(不是 122;122 應該是把 module_frame 的 62 支也算進去了,89+62=151 也對不上,來源不明)
入口檔(route)掛了 common.authz 的 22 支檔裡只有 1 支(oscal_framework_version_route.py)
jwt_required(只驗有沒有登入)出現次數 110 次
但是:權限檢查下移到下一層(app service)的呼叫次數 54 次(require_manager 33 次、require_participant 21 次),分布在 12 支 app service 檔
app/oscal 46 支檔中有掛權限判斷的 14 支

真實情況是這樣:SSP 那一支線(61 支入口)權限是有守的,只是守在下一層——入口只驗登入,真正判斷「你是不是這個專案的成員/負責人」寫在 app service 裡,呼叫 SspPermissionChecker.require_participant()(讀)或 require_manager()(寫)。這是 CLAUDE.md 明文允許的做法(「資源域守門維持 app service 層」)。框架那一支線(25 支入口)也有守,用的是 require_platform_admin(),共 18 處。

所以這批掃描要問的真正問題不是「有沒有守」,而是「守的那 54 個點,是不是每一支公開方法都守到了」——這是逐支對照題,不是全面缺口。

真正沒守的確認缺口(實查):

  1. resource_library_app_service.py 530 行,require_/authz/Forbidden 一個字都沒有(grep 全檔零命中),而且裡面有 9 段手寫 SQL(sa.text(...))。對應的兩支入口 /oscal/resource-libraries/list 與 /oscal/resource-libraries 也沒守。這正是總表第 67 項那條「租戶管理員可以改掉原廠公版範本」的現場——第 67 項只描述了其中一條路徑(改控制項清單),這支檔裡還有另外 8 段 SQL 沒人看過。
  2. module_frame_template_ssp_route.py(1 支入口)同樣零守門。

各棒重點(建議寫進卡片)

O1(控制項實作)

  1. ssp_control_implementation_service.py 1,037 行裡有 12 次 require_* 呼叫,但這支檔的公開方法(@transaction 標的)有幾支?逐支對照「每支公開方法是不是都有守門」,找出漏掉的那幾支
  2. require_manager 與 require_participant 有沒有用錯邊——寫入的操作卻只要求 participant(等於任何專案成員都能改)
  3. common/authz/ssp.py 的 require_manager 內含 _require_ap_editable(評估計畫還能不能編輯),require_participant 沒有這層。有沒有寫入操作走了 participant 這條,結果繞過了「已定案不能再改」的限制
  4. SspPermissionChecker 的依賴全靠依賴注入(__init__ 五個參數預設都是 None),若注入漏掉,self._resolver 是 None 會直接炸還是靜默放行——讀 _require_user_is_participant 的 None 處理
  5. infra/readmodel/.../ssp_control_implementation_query.py 257 行的查詢,有沒有靠資料庫隔離擋租戶、還是完全靠上層守門

O2(資源庫與匯出)— 優先級最高

  1. resource_library_app_service.py 全檔零守門,逐支公開方法列出「誰能呼叫、能改到誰的資料」
  2. 9 段手寫 SQL 逐段看:參數是不是綁定的(有沒有字串拼接)、有沒有 WHERE tenant_id、資料庫隔離對這幾張表是否開啟(總表第 67 項已確認 oscal.profile_imports 與 ssp_implemented_requirements 連客戶歸屬欄位都沒有)
  3. ssp_libreoffice_converter.py 是整批唯一呼叫外部程式的檔——檔名/路徑有沒有可能被使用者控制、暫存檔放哪、轉完有沒有刪、同時多人匯出會不會互相踩到
  4. 匯出端點 /ssp/<ssp_uid>/export 只有 1 支入口,ssp_export_app_service.py 只有 1 次 require_*——匯出等同讀取整份文件,這 1 次守門守的是哪個動作
  5. 公版範本(原廠給的)與客戶自建範本的讀寫判斷是不是同一套——公版應該只有平台管理員能改

O3a/O4(Excel 與 Word 上傳的權限線)

  1. ssp_excel_import_app_service.py 三支公開方法(upload_and_parse/get_parse_result/confirm_import),只有 3 次 require_* 呼叫——逐支確認是不是每支都守到。已讀到 get_parse_result 是「先查租戶再查專案成員」兩段式,upload_and_parse 的守門藏在 _verify_source_exists 裡面,這種「守門藏在私有方法裡」的寫法最容易漏
  2. source_uid 由使用者從表單送上來(總表首腦讀碼時的疑點),有沒有檢查這個編號指到的東西是不是使用者自己的
  3. 上傳檔案大小上限(_EXCEL_MAX_SIZE 有設)、Word 那邊有沒有對應的上限、擋在哪一層(讀完整份檔案之後才擋等於沒擋)
  4. 檔名處理:有沒有用 secure_filename、上傳的檔存到哪、解析失敗的殘檔會不會留著
  5. 解析工作(parse job)有存活時間機制,過期後的資料還讀不讀得到;parse_uid 是不是可以猜的(流水號還是隨機碼)
  6. _common.py(620 行,兩條匯入線共用)在 O9 掃,本棒只看「呼叫它之前有沒有驗過權限」

O3b(Excel 內容解析)— 惡意檔案題,與上面完全不同的清單

  1. parser.py:42 用 load_workbook(file_path, data_only=True, read_only=False):read_only=False 代表整份檔載進記憶體(試算表炸彈),data_only=True 代表讀的是快取計算結果
  2. 公式注入:讀進來以 =/+/-/@ 開頭的字串有沒有中和,這條要跟 O2 的匯出線合看(進來沒擋+出去沒擋=完整一條路)
  3. 外部連結/外部參照:載入時有沒有關掉,會不會讓伺服器主動去連使用者指定的網址(SSRF)
  4. sheet_handlers.py 428 行:工作表數/列數/欄數有沒有上限;讀到預期外型態時是回報錯誤還是默默吞掉
  5. validators.py 只有 66 行,對九張工作表的樣板少得可疑——驗出錯誤是擋住整份還是只記一筆繼續跑
  6. version_check.py 拿使用者檔案裡的字串當版本號:直接信任嗎、版本不符是擋下還是照舊解析
  7. v2_bundle.py 組裝時有沒有拿檔案內容裡的編號直接當系統內編號用

O5(子物件增刪改查)

  1. 六組各 4 支入口共 22 支,對應的 app service 各有 2~4 次 require_*——數字對不對得上(例如元件有 4 支入口但只有 4 次守門,那刪除那支守到了嗎)
  2. ssp_project_resolver.py「這份 SSP 屬於哪個專案」是整條權限鏈的起點,解析不到的時候回什麼(拋錯還是回 None 然後上層當作通過)
  3. 子物件的編號(item_uid)在改/刪的時候,有沒有確認它確實屬於網址上那份 SSP(跨 SSP 改別人的元件)

O6(文件池與比對引擎)

  1. 文件池 9 支入口對 9 次守門,看起來齊——逐支核對是不是一對一
  2. 比對引擎 ssp_diff_service.py 751 行:使用者選擇「這筆要/那筆不要」的決定,合併的時候有沒有再驗一次權限,還是只在進入比對畫面時驗過一次
  3. decision_merge.py 的合併決定是使用者送上來的資料結構,有沒有可能塞進不該改的欄位

O7a/O7b(Word 解析器)

  1. 這兩棒是純解析、沒有權限問題,重點在惡意檔案:巢狀深度、超大表格、外部參照
  2. docx_section_extractors.py 892 行的抽取邏輯,有沒有對段落數/表格數設上限

O8a/O8b(合規框架)

  1. 框架這條線用 require_platform_admin() 守,共 18 處,只守寫入不守讀取(註解明寫「寫入僅 root 租戶」)——讀取是不是真的可以全開
  2. 框架 PDF 解析工作(framework_parse_job_service.py 674 行)只有 3 次 require_platform_admin,但有 5 支入口

O9(接線總裝)

  1. api/oscal/__init__.py 是路由註冊總表,能直接看出哪些入口是關的——已讀到 SspScopedExcelImportRoute 與 Confirm 兩支是註解掉的狀態(FR-038 2A 遺留)。確認被註解掉的入口,其 app service 是不是還被別的地方呼叫得到
  2. 依賴注入設定 509 行:SspPermissionChecker 的五個依賴有沒有全部接上(接不上就是權限判斷失效)
  3. 稽核階段掛鉤三支:推進/退回階段時回頭改 OSCAL 資料,這條路徑繞不繞得過 SSP 的權限判斷

4. 建議派工順序

順位 棒 理由
1 O2(資源庫與匯出) 已知有問題的地方,先掃。總表第 67 項已經證實這裡能改到原廠公版;530 行零守門+9 段手寫 SQL,是全批最可能有斬獲的一棒。而且這棒同時涵蓋唯一呼叫外部程式的檔
2 O1(控制項實作+權限核心) 把 common/authz/ssp.py 先掃過,後面五棒的判斷基準就有了。若這支本身有洞,後面棒次的結論全要重評估
3 O3a/O3b/O4(Excel 入口、Excel 解析、Word 上傳,可換序) 檔案上傳是外部輸入進入系統的正門;三棒互不相干。⚠️ 全機一次只跑一支掃描,「可換序」不是「同時跑」
4 O9(接線總裝) 掃完前四棒後再掃,能回頭驗證「依賴注入有沒有接對」對前面結論的影響;也能確認被註解掉的入口是不是真的關了
5 O5/O6(子物件、文件池比對) 守門看起來齊,屬於逐支核對題,斬獲期待值中等
6 O8a/O8b(合規框架) 用 require_platform_admin 守得最嚴,期待值最低
7 O7a/O7b(Word 解析器) 純解析無權限議題,檢查重點跟前面完全不同(惡意檔案),建議跟套件本體那批(第 8 順位)排在一起掃,共用同一張檢查清單

強烈建議:O7a/O7b 不要跟前六棒混排。它們的風險型態與 jedi-oscal-v2 套件本體(第 8 順位,364 檔)一樣是「檔案解析安全」,跟權限檢查是兩回事——這正是總表 §5 已經寫明的分離原則,只是當初沒注意到 domain 層的解析器住在主專案這一側。


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

  1. 🔴 總表的「191 個檔案」與「122 支功能入口」兩個數字都跟實際對不上。實數是 167 檔(建議範圍)/89 支入口。191 疑似跟 §3.1 第 43 項的「191 筆資料」抄串行。要不要順手把總表 §0.5 與 §5 的數字改掉?(我沒改,照指示不動檔)

  2. 🔴 「完全沒有功能權限檢查」的描述會誤導研究員。實際上 SSP 線有 54 個守門呼叫、框架線有 18 個,只是守在 app service 層而不是入口層——這是 CLAUDE.md 明文允許的正規做法。若卡片照抄總表的說法,研究員會拿著「找沒守門的端點」去掃,掃到的全是誤報。建議卡片改寫成「逐支公開方法核對守門是否齊全,並重點查 resource_library_app_service.py 這支確認零守門的檔」。

  3. module_frame 要不要現在排。我的建議是不要(理由見 §1),但這 81 檔跟 oscal 有 12 處跨模組 import,首腦若認為「接線」應該包含被接的那一側,可以另立 M1/M2 兩棒(api 25+app 31 約 10,044 行,要切 5~6 棒)。

  4. _common.py 與 bundle_restore.py 的歸屬 — 已裁定:_common.py(620 行)掛 O9(重疊帶入兩棒會讓兩棒都爆上限),O3 另拆成 O3a/O3b。bundle_restore.py(208 行)留在 O9。O9 因此 2,505 行超標,排序在後、派工前再切。

  5. 被註解掉的路由(api/oscal/__init__.py:110-111,兩支 scoped Excel 匯入入口)。FR-038 2A 的遺留狀態。這是不是已經永久廢棄? 若是,對應的 app service 方法是死碼可以不掃;若只是暫時關閉,那它們是「隨時會被打開、但沒人掃過」的入口,風險更高。我不確定,建議首腦查一下 FR-038 的 2B 進度再決定要不要特別標注。

  6. domain/oscal/parser 與 adapter 該不該算「主專案側接線」。這 7 支(O7a/O7b,2,868 行)做的是純檔案解析,性質上更像套件本體那一批。若首腦認同,可以把 O7a/O7b 從本批移出、併進第 8 順位那批(套件本體)一起掃,本批就縮成 10 棒(O3 拆 O3a/O3b 後)、界定範圍 160 檔、實際進掃描 92 檔/17,224 行。

  7. 不排入掃描的 86 檔(約 1,400 行)。我照「純資料表不計」的精神排除,但其中 infra/oscal/model/ 那 4 支資料表定義可能藏著「這張表有沒有客戶歸屬欄位」的答案(總表第 67 項就是栽在這上面)。建議至少讓 O9 那棒的研究員順手看一眼 model 目錄,不必單獨開棒。


附:驗證用指令(首腦復核時可直接跑)

cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be

# 範圍 167 檔/21,364 行
git ls-files 'api/oscal/*.py' 'app/oscal/*.py' 'domain/oscal/*.py' 'infra/oscal/*.py' \
  'di_containers/oscal/*.py' 'infra/readmodel/oscal/*.py' 'common/authz/ssp.py' \
  'app/flow_control/service/oscal_*.py' 'di_containers/dashboard_apis/oscal.py' | xargs wc -l | tail -1

# 89 支功能入口
git ls-files 'api/oscal/routes/*.py' | xargs grep -c '^\s*def \(get\|post\|put\|patch\|delete\)' \
  | awk -F: '{s+=$2} END {print s}'

# 54 次 app service 層守門呼叫
git ls-files 'app/oscal/*.py' | xargs grep -ho \
  'require_participant\|require_manager\|require_read_access\|require_write_access' | sort | uniq -c

# resource_library 零守門(應該零輸出)
grep -n 'require_\|authz\|permission\|raise Forbidden' app/oscal/service/resource_library_app_service.py