盤點日期 2026-09-22(2026-09-22 依首腦回饋修訂:B1 由水平切改縱切、補 2 支懸案檔、標 B4/B6 同 runner 接縫)|這份文件只盤點、不掃描——切好棒等首腦開卡派工。
在看範圍界定跟切棒之前,先點名這批裡訊號最強的兩支檔案——兩支都是整支抓不到任何權限關鍵字,而且都涉及「刪除」或「下載/匯出」這兩種master 總表已經證實最容易出事的路徑:
app/module_frame/service/module_frame_reference_document_service.py(B2,257 行):8 個對外方法、0 個權限關鍵字命中,其中含一支刪除路徑。app/module_frame/service/module_frame_template_import_service.py(B5,1,011 行):6 個對外方法、0 個權限關鍵字命中,其中含兩支下載/匯出路徑,且涉及批次寫入。為什麼這兩支要優先看:master 總表第 128 項(下載/匯出,跨公司下載 SSP Excel)跟第 133 項(刪除,刪框架沒查用量)已經證實「刪除」跟「下載/匯出」是這批程式碼裡最容易漏掉歸屬檢查的兩種路徑形狀;而「整支檔案 grep 不到任何權限關鍵字」,訊號強度跟 O2 當初鎖定、後來證實真的有洞的 resource_library_app_service.py 一模一樣——O2 就是靠同樣的訊號抓到真正的高風險漏洞。這兩支已經分別排進 B2、B5 的重點清單(見第 3 節),這裡先整段列出來,讓首腦一眼看到本次盤點裡風險訊號最集中的地方。
module_frame 是「合規範本/資源庫」那塊程式碼——客戶自己建的合規範本、原廠給大家用的公版範本,都放在這裡。FR-113 原本盤點時判斷這裡「入口層的守門看起來比較齊全」(25 支路由檔裡有 15 支已經用了 require_capability 這種角色權限檢查),所以沒有排進原本的十一棒,準備另外處理。
這個判斷後來被推翻了。 O5 那一棒本來是去查 SSP 底下六類附屬資料(人員、設備清單等)的權限有沒有守好,查完結論是「六組全部守齊,乾淨」。但餵資料給這六組的那支「把整份系統安全計畫匯出成 Excel 範本」的功能,剛好就是 module_frame 的程式碼,而它完全沒有檢查『這份計畫是不是你的』——這條被列進總表第 128 項,判定「高風險」:任何一個登入的人,只要知道別人的計畫編號,就能把對方整份系統安全計畫(人名、email、電話、地址、設備 IP、每條控制項的實施狀況)下載走。
這件事說明一個道理:「有守門」不等於「守對了門」。 module_frame 的路由層確實比較常見 require_capability 這種「你的角色能不能用這個功能」的檢查,但這種檢查只問「你是誰」,不問「這筆資料是不是你的」——真正防止跨公司資料外洩的第二道檢查(「這份範本/這份計畫是你們公司的嗎」),module_frame 到目前為止沒有一支檔案被完整讀過、確認過有沒有這道檢查。O2、O3a、O4、O5 四棒陸續在 module_frame 或它緊鄰的檔案裡各撞到一次同一種病(「隔壁端點有守,這支沒守」),這是第三、第四次同一種病出現在同一塊地——值得專門派一棒把 module_frame 自己的程式碼從頭到尾看一遍。
git ls-files 抓檔名git ls-files -- api/module_frame app/module_frame domain/module_frame infra/module_frame di_containers/module_frame | grep '\.py$'共 83 支 .py 檔。扣掉 15 支空的或近乎空的 __init__.py(0~1 行,完全空白或只有 1 行,見下方「不列入切棒的空檔」),剩下 68 支有實質內容的檔案,合計 11,065 行——這裡面還含 2 支「只有 docstring、沒有邏輯」的 __init__.py(app/module_frame/__init__.py 7 行、infra/module_frame/models/__init__.py 4 行),同樣不派工,实際要切棒的是 66 支邏輯檔、11,054 行。
這個數字還要再加上兩支「檔名/路徑抓不到、但判定要納入切棒」的檔案:api/oscal/routes/module_frame_template_ssp_route.py(38 行,掛在 api/oscal/routes/ 底下,原本因為列在 O2 宣告範圍內而排除,首腦回饋後改判定「O2 沒真的查過,要納入」,見下方 1.4)、di_containers/dashboard_apis/module_frame.py(22 行,掛在 di_containers/dashboard_apis/ 底下,原本判斷風險低不排棒,首腦回饋後改判定「DI 漏接會讓守門靜默失效,22 行成本低,要納入」,見下方 1.2)。
最終要切棒的範圍:68 支檔、11,114 行(66 支邏輯檔 11,054 行 + 2 支補入檔 60 行),跟第 2 節「切棒」逐棒行數加總、扣掉跨棒重複的接縫檔之後的數字一致。
按規定不能只信任「檔名有沒有 module_frame 字樣」,另外做了兩件事:
① 反查誰在 import module_frame 的東西:
grep -rl "module_frame\|ModuleFrame" --include="*.py" api app domain infra di_containers common config core抓出 46 支檔案提到 module_frame。逐一檢查後,沒有找到任何一支「檔名不含 module_frame、但骨子裡其實是 module_frame 業務邏輯」的檔——這 46 支全部屬於別的模組(oscal/flow_control/auth/project 等),只是呼叫或參照 module_frame 的東西(例如 import 一個 enum、呼叫一個服務方法、在註解裡提到對齊哪支檔的欄位命名慣例)。這些檔案本身的守門邏輯屬於它們自己所在的模組,不屬於 module_frame,所以不納入這次的範圍,但下面「跟別的模組共用的介面」會列出幾支比較值得注意的。
② 特別檢查 DI 接線目錄:di_containers/dashboard_apis/module_frame.py(22 行)—— 這支檔案掛在 di_containers/dashboard_apis/ 底下、不在 di_containers/module_frame/ 這個路徑裡,靠檔名抓不到。內容是把 ModuleFrameService.get_module_frame_list_for_dashboard 這個唯讀查詢方法註冊給 AI 儀表板呼叫。
判斷(已依首腦回饋修正):盤點員原判斷「只是把既有服務的一個唯讀方法登記給另一個系統用,沒有新的商業邏輯,風險低,不需要排棒」——這個判斷被推翻。理由:FR-113 O9b 那一棒的發現是「依賴注入漏接會讓守門靜默失效」——DI 的 Factory 是 lazy 建構,接線對不對只有在那個端點真的被打到時才會現形,平時看起來一切正常。這支檔案就是一個「換一條路徑接出去」的 DI 接線,跟主線(api/module_frame/routes/module_frame_route.py 那條走 require_capability 守門的路徑)是不是同一套邏輯、同一套租戶隔離,沒有實際驗證過。22 行的成本近乎零,改判定納入 B1(縱切後的主體 CRUD 批次),見第 2 節,審查重點是「它接出去的東西跟主線是不是同一套守門」。
以下檔案會 import 或提到 module_frame,但邏輯屬於別的模組,已經在 FR-113 別棒的範圍內或屬於別的模組,這裡列出來只是讓人知道「查到過、判斷過不用重複看」:
app/oscal/service/ssp_*_app_service.py(六支,components/party/inventory/leveraged/resources/system_characteristic)—— OSCAL 側的 SSP 子物件服務,會 import module_frame 對應服務的欄位命名常數來保持一致(例如 ModuleFramePartyService as _PmMap),已經是 FR-113 O5 的範圍,O5 判定「六組全部守齊」。domain/oscal/service/ssp_context_resolver.py——已經是 O5 範圍內的檔案。app/oscal/service/resource_library_app_service.py——O2 範圍,見下方排除清單。app/project/service/project_start_app_service.py——專案啟動時「複製一份範本」的呼叫端,只是呼叫 module_frame 的既有方法,不在這次範圍,但值得留意:呼叫端要是也繞過了守門,即使 module_frame 自己補好了洞,這裡沒補一樣有事——這點寫進第 4 節的風險提示。core/plugins/evidence_classification.py——證據分類外掛程式,讀取 module_frame_id → module_frames.oscal_framework_version_uid 做唯讀查詢,不在範圍內。app/auth/service/user_import_template_app_service.py、app/oscal/service/excel_parser/sheet_handlers.py——這兩支是 excel_template 這個共用套件(在範圍內,見批次 B6)的外部消費者,屬於 auth/oscal 模組自己的範圍,不用看,但代表 B6 這個套件被別的模組共用,改動時要小心波及。以下檔案明確排除,不列入這次切棒:
| 檔案 | 行數 | 哪一棒掃過 | 備註 |
|---|---|---|---|
app/oscal/service/resource_library_app_service.py |
530 | FR-113 O2 | O2 找到的高風險就在這支:改範本的「適用控制項」清單時沒檢查是不是自己公司的範本 |
api/oscal/routes/module_frame_template_ssp_route.py(38 行)不再排除,已改判定納入 B3(依首腦回饋修正)。這支原本因為「列在 O2 的 11 檔範圍表內」被當成已掃過而排除,但 O2 報告在「這棒沒有回答的事」段落明確寫「掃描沒有針對這支提出發現」——這句話的意思是『工具沒有針對這支報出發現』,不是『查過、確認沒事』。這支路由只有 @jwt_required()、完全沒有角色權限檢查,O2 排除它是因為工具沒報、不是查過沒事,這次要把它當成沒查過的檔來看,見第 2 節 B3、第 3 節審查重點。
除了 resource_library_app_service.py,FR-113 十一棒(O1~O9,另加 O3a/O3b/O9a/O9b 這種細分)的宣告範圍沒有任何一支落在 api/module_frame/、app/module_frame/、domain/module_frame/、infra/module_frame/、di_containers/module_frame/ 這五個目錄底下——O5 掃的六組附屬資料 CRUD 全部是 api/oscal/routes/ssp/* 和 app/oscal/service/ssp_*,物理上不在 module_frame 目錄裡,即使找到的洞(ssp_import_template_app_service.py)剛好是 module_frame 的檔案。這點已經對照 scan-inventory.md 全部十一棒的檔案清單逐項確認過。
這些檔案只有 __init__.py 的模組宣告或一行 docstring,沒有任何邏輯,不派工也不用看:
api/module_frame/routes/__init__.py
api/module_frame/serializers/__init__.py
app/module_frame/code/__init__.py
app/module_frame/dto/__init__.py
app/module_frame/service/__init__.py
di_containers/module_frame/__init__.py
domain/module_frame/__init__.py
domain/module_frame/code/__init__.py
domain/module_frame/entity/__init__.py
domain/module_frame/repository/__init__.py(1 行)
domain/module_frame/service/__init__.py(1 行)
infra/module_frame/__init__.py
infra/module_frame/adapter/__init__.py
infra/module_frame/mapper/__init__.py
infra/module_frame/repository/__init__.py
另外 2 支雖然 wc -l > 1,但整支只有 docstring、沒有任何邏輯,同樣不派工:app/module_frame/__init__.py(7 行)、infra/module_frame/models/__init__.py(4 行)。
68 支檔、11,114 行(83 支 git ls-files 抓到的檔 − 15 支空檔 − 2 支純 docstring 檔 = 66 支邏輯檔、11,054 行;加上 2 支路徑/檔名抓不到但改判定納入的檔案 api/oscal/routes/module_frame_template_ssp_route.py(38 行)與 di_containers/dashboard_apis/module_frame.py(22 行),共 68 支、11,114 行)。
驗證指令(首腦可以直接跑,跟本文件第 2 節的批次總數對得起來;已實跑驗證輸出「68 支檔、11114 行」):
{
git ls-files -- api/module_frame app/module_frame domain/module_frame infra/module_frame di_containers/module_frame | grep '\.py$'
echo api/oscal/routes/module_frame_template_ssp_route.py
echo di_containers/dashboard_apis/module_frame.py
} | while IFS= read -r f; do
n=$(wc -l < "$f")
if [ "$n" -gt 1 ] && [ "$f" != "app/module_frame/__init__.py" ] && [ "$f" != "infra/module_frame/models/__init__.py" ]; then
echo "$n $f"
fi
done | awk '{s+=$1; c+=1} END {print c" 支檔、"s" 行"}'母卡與 8 張子卡已一次建完,Case No 連號。
母卡:CM-2073
| 棒 | Case No | 範圍 | 派工順序 |
|---|---|---|---|
| B5 | CM-2074 | 3 檔/1,376 行 | ① 全批最優先 |
| B2 | CM-2075 | 12 檔/1,642 行 | ② 次優先 |
| B4 | CM-2076 | 2 檔/1,669 行 | ③ 接 B6,同一 runner |
| B6 | CM-2077 | 7 檔/1,656 行 | ③ 承 B4,同一 runner |
| B1 | CM-2078 | 19 檔/1,787 行 | ④ |
| B1-item | CM-2079 | 4 檔/675 行 | ⑤ 接 B1c,建議同一 runner |
| B1c | CM-2080 | 8 檔/925 行 | ⑤ 承 B1-item,建議同一 runner |
| B3 | CM-2081 | 15 檔/1,904 行 | ⑥ |
八棒 scope 檔數已全數實跑 git ls-files 驗證,與本文件各棒寫的數字完全對上(19/4/8/12/15/2/3/7),沒有一棒對不上。
開卡時實查補進 B1-item 卡的一件事(本文件第 3 節原本沒有):app/module_frame/service/module_frame_item_service.py 的守門只出現在一處(:7 import assert_scope_writable、:65 呼叫一次),但整支檔有 5 支公開方法(:29 get/:46 update_xml/:89 add/:159 update/:227 delete)——一支有守、四支沒有出現守門呼叫,是本 arc 反覆抓到的「同一支檔裡這條路守了、那條路沒守」形狀。此事實已寫進 CM-2079。
切法原則:按業務路徑垂直切(同一組功能的 route→app→domain→infra→repo→ORM model 放一棒),不按 DDD 分層水平切——權限漏洞活在層與層的接縫上,只給 route 或只給 service 兩邊都看不出來。行數是逐檔 wc -l 實算,不是估的。
這節在首腦回饋後重切過一次:原本的 B1a(route+app 層)/B1b(domain+infra 層)是按 DDD 分層水平切,違反上面這條原則,即使把 module_frame_service.py 重複列在兩棒當接縫,也只補了一個點、整條路徑還是斷的。改法是實際追過呼叫關係,按「範本主體」跟「範本子項」這兩條獨立業務路徑重新縱切,見下面 B1、B1-item。追完之後確認:主體跟子項並沒有落入「domain/infra 幾乎完全共用、縱切會讓兩棒重複八成」的狀況——子項(module_frame_item_service.py)本身完全沒有直接碰 module_frame 的 domain/infra,唯一一次跨進主體地盤是呼叫 module_frame_service.get_module_frame() 這一行,所以子項這棒是「route+serializer+app service+一支接縫檔」的乾淨小棒,不需要重複整組 domain/infra,兩棒之間沒有大量重複的問題。
派工卡:CM-2078
範本本身的新增/查詢/修改/刪除/複製,完整一條路徑,不按層拆。追過呼叫關係確認:module_frame_service.py 只 import domain.module_frame.entity.*、domain.module_frame.service.module_frame_domain_service,不 import domain.module_frame.dto/action_type_enum(那兩支只有 YAML 匯入路徑在用,歸進 B1c);module_frame_domain_service.py 只 import domain.module_frame.repository.module_frame/entity.*,往下到 module_frame_repo_impl.py/infra/module_frame/models/module_frame*.py/mapper/module_frame*.py 是唯一路徑,沒有分岔。di_containers/dashboard_apis/module_frame.py(22 行)依首腦回饋納入這棒——它註冊的 get_module_frame_list_for_dashboard 是 module_frame_service.py 自己的方法,屬於主體這條路徑的另一個入口。
api/module_frame/__init__.py 252 ← Blueprint 路由總表,看得到全部 URL
api/module_frame/routes/module_frame_route.py 102
api/module_frame/serializers/module_frame.py 153
app/module_frame/service/module_frame_service.py 260 ← 主體 CRUD 邏輯
app/module_frame/dto/module_frame_dto.py 70
app/module_frame/dto/module_frame_menu_dto.py 34
app/module_frame/code/module_frame_error_code.py 32
domain/module_frame/service/module_frame_domain_service.py 76
domain/module_frame/entity/module_frame_entity.py 113
domain/module_frame/entity/module_frame_query_filter.py 37
domain/module_frame/entity/module_frame_menu_entity.py 23
domain/module_frame/repository/module_frame.py 33
infra/module_frame/repository/module_frame_repo_impl.py 136
infra/module_frame/models/module_frame.py 91
infra/module_frame/models/module_frame_trans.py 29
infra/module_frame/mapper/module_frame_mapper.py 41
infra/module_frame/mapper/module_frame_menu_mapper.py 23
di_containers/module_frame/module_frame_containers.py 260 ← 全模組 DI 接線,看得到所有 service 怎麼被組起來
di_containers/dashboard_apis/module_frame.py 22 ← 依首腦回饋納入:檢查它接出去的東西跟主線是不是同一套守門
共 19 檔/1,787 行。驗證:git ls-files -- api/module_frame/__init__.py api/module_frame/routes/module_frame_route.py api/module_frame/serializers/module_frame.py app/module_frame/service/module_frame_service.py app/module_frame/dto/module_frame_dto.py app/module_frame/dto/module_frame_menu_dto.py app/module_frame/code/module_frame_error_code.py domain/module_frame/service/module_frame_domain_service.py domain/module_frame/entity/module_frame_entity.py domain/module_frame/entity/module_frame_query_filter.py domain/module_frame/entity/module_frame_menu_entity.py domain/module_frame/repository/module_frame.py infra/module_frame/repository/module_frame_repo_impl.py infra/module_frame/models/module_frame.py infra/module_frame/models/module_frame_trans.py infra/module_frame/mapper/module_frame_mapper.py infra/module_frame/mapper/module_frame_menu_mapper.py di_containers/module_frame/module_frame_containers.py di_containers/dashboard_apis/module_frame.py | xargs wc -l | tail -1
派工卡:CM-2079
範本底下「子項」(流程樣板 Call Activity 相關)的新增/查詢/修改/刪除。追過呼叫關係確認:這條路徑幾乎沒有自己的 domain/infra——module_frame_item_service.py 全文只在 delete_module_frame_item 一支方法裡呼叫一次 self.module_frame_service.get_module_frame(...) 跨進主體地盤,其餘邏輯全部委派給外部套件 jedi_flow_engine 的 WorkflowTemplateService(管流程範本 XML),不直接碰 domain.module_frame 或 infra.module_frame 任何一支檔案,DI 接線上也印證了這點(module_frame_item_service 的 Factory 只依賴 module_frame_service,沒有另外接 domain/infra)。這不是「共用太多切不開」——是這條路徑本來就沒有自己的 domain/infra 層,所以縱切自然乾淨:這棒只需要它自己的 route/serializer/service,加上 module_frame_service.py 當接縫檔(重複自 B1,看得出子項改動有沒有真的經過主體那層的檢查)。
api/module_frame/routes/module_frame_item_route.py 128
api/module_frame/serializers/module_frame_item.py 38
app/module_frame/service/module_frame_item_service.py 249 ← 唯一跨進 B1 地盤的呼叫在 delete_module_frame_item(),呼叫 module_frame_service.get_module_frame()
app/module_frame/service/module_frame_service.py 260 ← 接縫檔,重複自 B1
共 4 檔/675 行。驗證:git ls-files -- api/module_frame/routes/module_frame_item_route.py api/module_frame/serializers/module_frame_item.py app/module_frame/service/module_frame_item_service.py app/module_frame/service/module_frame_service.py | xargs wc -l | tail -1
派工卡:CM-2080
範本可以整批用 YAML 檔匯入,是獨立的一條路,體積小,附在這裡不用單獨開棒。domain/module_frame/dto/module_frame_dto.py/domain/module_frame/code/module_frame_action_type_enum.py 這兩支確認只有這條路徑在用(module_frame_import_service.py import 這兩支,B1 的 module_frame_service.py/module_frame_domain_service.py 都不 import),歸進這棒。這條路徑也會呼叫 module_frame_item_service.add_module_frame_item/update_module_frame_item(批次建子項),所以邏輯上跟 B1-item 也有一條接縫,但因為 module_frame_item_service.py 完整重複進來會讓這棒過重,改成文字提醒:要追「YAML 匯入建出來的子項有沒有跟手動建立走同一套檢查」,去 B1-item 對照 module_frame_item_service.py。
api/module_frame/routes/module_frame_import_route.py 95
app/module_frame/service/module_frame_import_service.py 415
infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py 65
infra/module_frame/adapter/parser_adapter_factory.py 17
infra/module_frame/ports/parser_port.py 9
domain/module_frame/dto/module_frame_dto.py 49
domain/module_frame/code/module_frame_action_type_enum.py 15
app/module_frame/service/module_frame_service.py 260 ← 接縫檔,重複自 B1
共 8 檔/925 行。驗證:git ls-files -- api/module_frame/routes/module_frame_import_route.py app/module_frame/service/module_frame_import_service.py infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py infra/module_frame/adapter/parser_adapter_factory.py infra/module_frame/ports/parser_port.py domain/module_frame/dto/module_frame_dto.py domain/module_frame/code/module_frame_action_type_enum.py app/module_frame/service/module_frame_service.py | xargs wc -l | tail -1
B1/B1-item/B1c 三棒都在 2,000 行門檻內,可以個別派工,也可以視首腦人力安排合併派(例如 B1-item+B1c 合併約 1,600 行,仍在門檻內)——這點留給首腦視實際派工資源決定,這裡只保證每棒單獨派工時本身是完整路徑、不必再靠其他棒補完。
派工卡:CM-2075
範本底下另外三組獨立的清單型資料:這套範本要求哪些控制項要做(control-defaults)、每條控制項底下要驗哪些稽核目標(objective-defaults)、範本可以掛哪些參考文件(reference-documents)。
api/module_frame/routes/module_frame_control_default_route.py 139
api/module_frame/routes/module_frame_control_objective_default_route.py 147
api/module_frame/routes/module_frame_reference_document_route.py 281
api/module_frame/serializers/module_frame_control_default.py 46
api/module_frame/serializers/module_frame_control_objective_default.py 49
api/module_frame/serializers/module_frame_reference_document.py 61
app/module_frame/service/module_frame_control_default_service.py 240
app/module_frame/service/module_frame_control_objective_default_service.py 269
app/module_frame/service/module_frame_reference_document_service.py 257
app/module_frame/dto/module_frame_control_default_dto.py 49
app/module_frame/dto/module_frame_control_objective_default_dto.py 47
app/module_frame/dto/module_frame_reference_document_dto.py 57
共 12 檔/1,642 行。驗證:git ls-files -- api/module_frame/routes/module_frame_control_default_route.py api/module_frame/routes/module_frame_control_objective_default_route.py api/module_frame/routes/module_frame_reference_document_route.py api/module_frame/serializers/module_frame_control_default.py api/module_frame/serializers/module_frame_control_objective_default.py api/module_frame/serializers/module_frame_reference_document.py app/module_frame/service/module_frame_control_default_service.py app/module_frame/service/module_frame_control_objective_default_service.py app/module_frame/service/module_frame_reference_document_service.py app/module_frame/dto/module_frame_control_default_dto.py app/module_frame/dto/module_frame_control_objective_default_dto.py app/module_frame/dto/module_frame_reference_document_dto.py | xargs wc -l | tail -1
派工卡:CM-2081
O5 查過 OSCAL 那一側(真正專案的 SSP)的六類附屬資料(元件/人員/設備清單/繼承授權/參考資料/系統特性),結論是「全部守齊」。module_frame 這邊有一份幾乎一模一樣的鏡射版本——範本本身也可以編輯這六類資料(作為範本的預設內容),程式邏輯是複製過去改的,O5 沒有看過這一份。這批也包含範本專屬的 SSP 匯出入口(mf_ssp_export_route.py,匯出 docx/pdf/odt,底層呼叫已經是 O2 範圍內的 ssp_export_app_service.py,這裡只看 module_frame 自己擁有的路由檔)。依首腦回饋納入 module_frame_template_ssp_route.py(38 行)——O2 排除它是因為工具沒報、不是查過沒事,這次要把它當成沒查過的檔來看:這支只掛 @jwt_required()、沒有 @require_capability,呼叫 resource_library_app_service.get_template_ssp_ref(mf_uid)(唯讀),要確認這個唯讀查詢本身有沒有做歸屬檢查。
api/module_frame/routes/module_frame_system_characteristic_route.py 56
api/module_frame/routes/module_frame_components_route.py 78
api/module_frame/routes/module_frame_inventory_route.py 78
api/module_frame/routes/module_frame_leveraged_route.py 91
api/module_frame/routes/module_frame_party_route.py 95
api/module_frame/routes/module_frame_ssp_resources_route.py 99
api/module_frame/routes/mf_ssp_export_route.py 71 ← 路由本身已核對過守門齊全(@jwt_required + require_capability("module-frame.read")),底層服務在 O2 範圍,這裡只需確認路由本身沒問題即可,不用往下追
api/oscal/routes/module_frame_template_ssp_route.py 38 ← 依首腦回饋納入:O2 沒報不代表查過沒事,只有 @jwt_required,沒有 require_capability,要查 get_template_ssp_ref() 有沒有歸屬檢查
api/module_frame/serializers/module_frame_party.py 43
app/module_frame/service/module_frame_system_characteristic_service.py 186
app/module_frame/service/module_frame_components_service.py 211
app/module_frame/service/module_frame_inventory_service.py 220
app/module_frame/service/module_frame_leveraged_service.py 214
app/module_frame/service/module_frame_party_service.py 323
app/module_frame/service/module_frame_ssp_resources_service.py 101
共 15 檔/1,904 行。驗證:git ls-files -- api/module_frame/routes/module_frame_system_characteristic_route.py api/module_frame/routes/module_frame_components_route.py api/module_frame/routes/module_frame_inventory_route.py api/module_frame/routes/module_frame_leveraged_route.py api/module_frame/routes/module_frame_party_route.py api/module_frame/routes/module_frame_ssp_resources_route.py api/module_frame/routes/mf_ssp_export_route.py api/oscal/routes/module_frame_template_ssp_route.py api/module_frame/serializers/module_frame_party.py app/module_frame/service/module_frame_system_characteristic_service.py app/module_frame/service/module_frame_components_service.py app/module_frame/service/module_frame_inventory_service.py app/module_frame/service/module_frame_leveraged_service.py app/module_frame/service/module_frame_party_service.py app/module_frame/service/module_frame_ssp_resources_service.py | xargs wc -l | tail -1
派工卡:CM-2076
這棒不是去「找」問題,是去確認總表第 128 項這個已知洞、以及它旁邁有沒有同類的其他洞。
api/module_frame/routes/ssp_import_template_route.py 169 ← 3 個入口:MF-範本版、SSP 專屬版(已知洞在這支的呼叫目標)、框架版本版
app/module_frame/service/ssp_import_template_app_service.py 1500 ← 3 個公開方法:generate(MF範本)、generate_for_ssp(已知洞)、generate_by_framework_version
共 2 檔/1,669 行。刻意做成單檔大棒——1,500 行的檔案不拆成好幾份丟給不同人看,因為漏洞就是「檔案裡有 3 個公開方法,只有其中一個被查過」,拆開反而容易漏掉另外兩個。驗證:git ls-files -- api/module_frame/routes/ssp_import_template_route.py app/module_frame/service/ssp_import_template_app_service.py | xargs wc -l | tail -1
接縫提醒:這支服務 import 了 B6 批次裡的 generator.py(第 77 行)和 sheet_definitions.py(第 81 行)來產生實際的 Excel 檔案內容——如果要追「使用者填的文字有沒有被安全處理」這類問題,要跨到 B6 去看,不是這支檔案自己能回答的。
🔴 本棒與 B4/B6 必須由同一個 runner 連續跑,不可分開派給不同人或隔開時間——兩棒有 import 接縫且無物理重複檔案,分開派會出現責任真空。(B4 匯出邏輯呼叫 B6 產生 Excel,B6 沒有自己的歸屬判斷邏輯,兩邊各自只看得到半條路徑,只有同一人接續看才能確認「使用者填的文字」這條線從產生到寫入儲存格全程有沒有被安全處理。)
派工卡:CM-2074
跟 B4 很像但服務不同資料:B4 匯出「整份 SSP」,這裡匯出/匯入的是「範本的控制項清單與 AO 清單」本身(下載空白範本、上傳填好的 Excel、驗證、存檔)。這支檔案是這次盤點裡少數「整支抓不到任何權限檢查關鍵字」的服務層檔案之一(見下方風險提示),值得跟 B4 對照著看。
api/module_frame/routes/module_frame_template_import_route.py 260 ← 6 個入口:下載範本、下載匯出檔、上傳範本、驗證上傳的檔、驗證資料、存檔
api/module_frame/serializers/module_frame_template_import.py 105
app/module_frame/service/module_frame_template_import_service.py 1011
共 3 檔/1,376 行。驗證:git ls-files -- api/module_frame/routes/module_frame_template_import_route.py api/module_frame/serializers/module_frame_template_import.py app/module_frame/service/module_frame_template_import_service.py | xargs wc -l | tail -1
派工卡:CM-2077
B4 跟 B5 各自匯出/匯入不同的資料,但兩邊產生 Excel 檔案時共用同一套底層機制——這套機制本身也要單獨看一次,因為:(a)B4 直接 import 這裡的兩支主檔;(b)O5 已經在這個套件裡找到一條中風險(存進去的文字會被 Excel 當公式執行,見 generator.py:578),那條在 B4 的範圍外、B6 才是它真正的家,值得順便看這個洞有沒有別的姊妹洞;(c)這個套件還被 auth 模組和 oscal 模組的 Excel 解析器共用,改動或新發現要考慮波及面。
app/module_frame/excel_template/__init__.py 11
app/module_frame/excel_template/generator.py 652 ← O5 已知中風險在第 578 行(Excel 公式注入)
app/module_frame/excel_template/sheet_definitions.py 617
app/module_frame/excel_template/styles.py 38
app/module_frame/excel_template/data_validation_builder.py 54
app/module_frame/excel_template/lookup_builder.py 108
app/module_frame/excel_template/header_i18n.py 176
共 7 檔/1,656 行。驗證:git ls-files -- app/module_frame/excel_template | grep '\.py$' | xargs wc -l | tail -1
🔴 本棒與 B4 必須由同一個 runner 連續跑,不可分開派給不同人或隔開時間——兩棒有 import 接縫且無物理重複檔案,分開派會出現責任真空。(B6 本身不含歸屬判斷邏輯、只負責產生 Excel,B4 呼叫這裡的 generator.py/sheet_definitions.py;已知的公式注入洞在 generator.py:578,同支檔案其他寫入儲存格的地方要不要一起算進同一批修法,只有接續看過 B4 呼叫端的人才判斷得準。)
除了下面各棒特別點名的重點,每一棒開工前都先做這四件事(跟 FR-113 前幾棒查出來的問題形狀直接對應):
① 算「守門次數」對「對外方法數」的落差。對每支 app service 檔案跑:
grep -c "require_\|authz\|permission\|Forbidden\|Permission" <檔案>
grep -c "^ def [a-zA-Z]" <檔案>兩個數字差很多(尤其是守門次數是 0,或遠少於對外方法數)就是要優先盯的檔案。這次盤點先跑過一輪,兩支檔案是 0 命中:app/module_frame/service/module_frame_reference_document_service.py(8 個對外方法,0 命中)、app/module_frame/service/module_frame_template_import_service.py(6 個對外方法,0 命中)——這兩支已知的 0 命中要在 B2、B5 優先確認清楚(見第 5 節)。
② 找「一個方法裡好幾條分支,只有部分分支有守」的形狀。O2 的洞就是這樣長出來的:module_frame_service.py 裡一個 update 方法,改「分享身分」那個分支有守衛(assert_scope_writable),改「適用控制項清單」那個分支完全沒有守衛。任何一個 update/save 類方法,只要裡面有 if 分岔去處理不同欄位,就要每個分支分開檢查。
③ 找「檢查 A、動手改 B」的形狀。也是 O2 的洞——資料庫層的防護設在 module_frames 這張表本身,但實際被改被刪的是另外幾張表(profile_imports、ssp_implemented_requirements 之類),那幾張表根本沒有防護。看到「查一個 uid 存不存在」之後,接下來的寫入/刪除動作有沒有精準對應到剛剛查的那個資源,還是動到了別的表/別的資料範圍。
④ 特別注意刪除與下載/匯出路徑。master 總表第 128 項(下載)、第 133 項(刪除)都是這兩種形狀。每一支 delete 開頭的方法、每一支回傳檔案(Excel/docx/pdf)的方法,都要能回答『呼叫者憑什麼有資格動這筆/看這筆資料』這句話,不能只回答『這筆資料存在嗎』。
B1(主體 CRUD 全路徑):module_frame_route.py 的 PUT/DELETE 有沒有像 O2 那樣「檢查 A、動手改 B」的形狀(改分享身分有守、改別的欄位沒守);module_frame_repo_impl.py 的 update/delete_by_uid/soft_delete_by_uid 有沒有在 SQL 層面加上「只能改自己公司的」條件,還是完全信任呼叫端已經檢查過(如果是後者,代表這層沒有第二道防線,跟 O2 report 裡「資料庫沒擋下來」是同一個風險敘事,要點出來讓首腦知道防線只有一層);di_containers/dashboard_apis/module_frame.py(依首腦回饋納入本棒)要檢查它接出去的東西跟主線是不是同一套守門——這支把 module_frame_service.get_module_frame_list_for_dashboard 註冊給 AI 儀表板消費,要確認這個 DI 接線暴露出去的方法本身有沒有做租戶過濾,不是只看「這是唯讀」就跳過(O9b 已證實 DI 漏接會讓守門靜默失效)。
B1-item(子項 CRUD):子項的新增/修改/刪除大部分邏輯委派給 jedi_flow_engine 套件處理 BPMN XML,本身沒有直接查 module_frame 的 domain/infra,重點反而是這支怎麼確認「這個子項屬於哪個範本」——delete_module_frame_item 靠 module_frame_service.get_module_frame(payload.get("moduleFrameUid")) 取得範本再往下刪,這裡的 moduleFrameUid 是前端送來的欄位,要確認拿到的範本有沒有再核對「呼叫者是不是這個範本所屬公司的人」,還是單純信任 uid 查得到就往下動。
B1c(YAML 匯入):YAML 整批匯入時建立的範本歸屬哪家公司是不是由伺服器端決定、不是信任前端送來的欄位;批次建立子項時呼叫 module_frame_item_service.add_module_frame_item/update_module_frame_item,跟 B1-item 對照確認走的是同一套檢查、沒有繞過。
B2(三組清單型資料):module_frame_reference_document_service.py 是本節①提到的 0 命中檔案之一,優先看它的 delete_from_pool/update_meta/attach_to_control_default 這幾支——這些方法目前看到的模式是「拿 module_frame_uid 去查 verify_module_frame_exists_by_uid」,這支方法只確認範本存在、不確認範本屬於誰,是不是真的漏了歸屬檢查,還是靠路由層的 require_capability 加上別的機制頂著,要逐支方法讀完再下結論,不要看到 0 命中就直接斷定是洞。
B3(六類附屬資料鏡射版):逐一對照 O5 report 裡「六組都是:讀用『你要是這專案的人』,寫用『你要是這專案的負責人』」這個標準,看 module_frame 這邊的鏡射版本是不是做到同樣的事,還是因為範本沒有「專案」這個概念,改用了另一套邏輯——如果用了另一套,那套邏輯本身有沒有把「這份範本是不是你家的」問清楚。module_frame_party_service.py(323 行,六組裡最大支)優先看。api/oscal/routes/module_frame_template_ssp_route.py(38 行,依首腦回饋新納入)要當成沒查過的檔案完整讀:只掛 @jwt_required()、沒有 @require_capability,呼叫 resource_library_app_service.get_template_ssp_ref(mf_uid)——確認這支唯讀查詢底層有沒有做歸屬檢查,不能因為是 GET/唯讀就跳過(跟 master 總表第 128 項一樣,讀取類端點一樣可能外洩別家公司資料)。
B4(已知高風險+兩個未查方法):generate_for_ssp 這個方法已經是確認的洞,不用重查,重點是另外兩個方法 generate(MF 範本版)跟 generate_by_framework_version(框架版本版)有沒有同樣的問題——generate 是對範本本身匯出,理論上路由層 require_capability("module-frame.read") 加上如果 service 層有查範本歸屬就夠了,但要實際讀過code確認;generate_by_framework_version 完全不掛在任何 module-frame uid 底下(api/module_frame/__init__.py 裡它的路由是 /ssp-import-template,沒有 MF context),要搞清楚它到底是「查全域框架目錄的唯讀操作」(風險低)還是也會碰到某份具體的客戶資料(風險高)。
B5(範本 Excel 匯入匯出):這支也是 0 命中檔案,是本棒最重要的問題。先確認一件事:module_frame_template_import_route.py 的 6 個入口是不是每個都掛了 @require_capability(盤點時已經抽查過,6 個入口都有 @jwt_required + @require_capability("module-frame.read"/"module-frame.create"),是角色權限檢查)——但角色權限檢查不能回答『這份範本是不是你家的』,要往下追 module_frame_template_import_service.py 每一個方法有沒有在拿到 module_frame_uid 之後,用類似 assert_scope_writable 或「範本 tenant_id 對比呼叫者 tenant_id」的方式確認歸屬,還是像 B2 提到的 verify_module_frame_exists_by_uid 一樣只確認存在。這支涉及寫入操作(save_import),比 B2 的純讀寫清單風險更高,因為它會批次改動範本底下的控制項實施記錄。
B6(excel_template 共用套件):已知洞在 generator.py:578(公式注入),檢查同支檔案裡其他寫入儲存格的地方有沒有同樣的問題(O5 report 已經點名 generator.py:319 直式工作表、lookup_builder.py 也要一起看);另外確認這個套件本身不含任何權限判斷邏輯是正常的(它只負責把資料寫成 Excel 檔案格式,歸屬判斷應該在呼叫它的 B4/B5 那一層做),如果發現這裡面藏了任何跟資料歸屬有關的邏輯,代表判斷邏輯散落在不該散落的地方,要另外標記。
B1 按業務路徑縱切成 B1(主體)/B1-item(子項)/B1c(YAML 匯入)三棒,不是按 DDD 分層切:這節原本是水平切(route+app 一棒、domain+infra 一棒),首腦回饋指出這違反「垂直切一條完整路徑」的原則,即使把 module_frame_service.py 重複列在兩棒當接縫也只補了一個點。改法是實際追過三條路徑各自的呼叫關係:主體 CRUD(route→app→domain→infra→repo→ORM→DI,1,787 行)是一條完整路徑;子項 CRUD 追完發現它幾乎沒有自己的 domain/infra(子項邏輯委派給外部套件 jedi_flow_engine 處理 BPMN XML,只有 delete_module_frame_item 一行呼叫回主體的 module_frame_service.get_module_frame()),所以子項這棒天生就小(675 行),不是「domain/infra 幾乎完全共用、縱切會讓兩棒重複八成」的escape-valve 情境——子項路徑本來就沒有自己的 domain/infra 層可以重複,縱切下去自然乾淨;YAML 匯入是完全獨立的第三條小路徑(925 行),domain 層的 module_frame_dto.py/action_type_enum.py 追確認只有這條路徑在用。三棒之間的接縫(module_frame_service.py)依規則重複列在需要它的每一棒。
B4 只放 2 支檔案(1,669 行):這是刻意的單檔大棒——ssp_import_template_app_service.py 一支就 1,500 行,硬要跟其他檔案湊一棒只會讓研究員在多支檔案之間來回追,反而稀釋掉這支「已知有洞、還有兩個方法沒查過」檔案該得到的關注。也因為這樣,B4 跟 B6(excel_template 套件)之間留了一條沒有物理重複、只用文字提醒的接縫——B4 的檔案 import 了 B6 的 generator.py/sheet_definitions.py,但因為 B4 加上 B6 全部檔案會衝到 3,325 行(超過門檻兩倍),不適合硬塞,所以改成在 B4 的批次說明裡寫清楚「要往下追就去 B6」,B6 自己也是完整一棒、有自己的守備重點。這個接縫需要首腦驗收時特別注意:如果 B4 跟 B6 派給兩個不同的人/不同時間掃,「使用者填的文字最終有沒有被安全處理」這個問題可能兩邊都覺得是對方的責任,變成沒人真的查完整條路徑。
B2、B3、B5 各自維持「一組完整業務功能」不切開:這三棒各自都在門檻之內(1,642/1,866/1,376 行),沒有切的必要,剛好也符合「一棒一條完整業務路徑」的精神。
B3 把 mf_ssp_export_route.py 放進「六類附屬資料」這棒,而不是跟 B4/B5 放一起:雖然都是「匯出」,但 mf_ssp_export_route.py 匯出的是 docx/pdf/odt(底層服務在 O2 範圍),跟 B4/B5 的 Excel 匯出匯入是完全不同的程式路徑,沒有共用程式碼。放進 B3 是因為它服務的資料範圍(範本的六類附屬資料)跟 B3 其他檔案一致。
app/project/service/project_start_app_service.py 這個呼叫端不在範圍內,但邏輯上跟 module_frame 的守門結果連動。專案啟動時會呼叫 module_frame 的「複製資源庫」功能,如果 module_frame 這邊之後補了歸屬檢查,這個呼叫端傳進去的資訊(呼叫者是誰、要複製哪份範本)要確保跟新加的檢查邏輯吻合,不然可能出現「補了檢查但呼叫端傳的參數本來就繞過了」的狀況。這個檔案本身沒有被掃描過,只是提醒這條連動關係存在。
B4/B6 之間留了一條沒有物理重複、只用文字+🔴 標記提醒的接縫:B4 的 ssp_import_template_app_service.py import 了 B6 的 generator.py/sheet_definitions.py,但兩棒合併會衝到 3,325 行(超過門檻兩倍),不適合硬塞成一棒,所以改成「同一個 runner 連續跑兩棒」(見 B4/B6 各自小節的 🔴 提示),不是完全獨立派工。
8 棒總計 68 支不重複檔案、11,114 行,逐一驗證加總(module_frame_service.py 在 B1/B1-item/B1c 三棒間重複列出 3 次,是刻意的接縫檔,不是計算錯誤):
B1(主體) 19 檔 1,787 行
B1-item(子項) 4 檔 675 行(含 1 支跟 B1 重複的接縫檔 module_frame_service.py)
B1c(YAML) 8 檔 925 行(含 1 支跟 B1 重複的接縫檔 module_frame_service.py)
B2 12 檔 1,642 行
B3 15 檔 1,904 行(含新納入的 module_frame_template_ssp_route.py)
B4 2 檔 1,669 行
B5 3 檔 1,376 行
B6 7 檔 1,656 行
------------------------------
逐棒加總(含重複計數) 70 檔、11,634 行
扣掉 module_frame_service.py 多算的 2 次(260 行 ×2=520 行)
= 68 支不重複檔案、11,114 行
跟第 1.6 節最終範圍「68 支檔、11,114 行」核對一致,已用實際腳本跑過 git ls-files + wc -l 加總驗證無誤。
module_frame_reference_document_service.py(B2)跟 module_frame_template_import_service.py(B5)逐支公開方法都抓不到任何權限或歸屬檢查關鍵字,比 O2 當初鎖定 resource_library_app_service.py(0 命中、確認有洞)的訊號還要明確一致。 盤點員只做了關鍵字比對跟抽讀方法簽章,沒有逐行讀完整支邏輯,不確定這是真的漏洞還是有別的機制頂著(例如某個裝飾器、某個共用 base class 做了檢查但關鍵字沒對上)。待首腦確認:這兩支是不是要在派工卡裡明確標成「高懷疑」,要求研究員優先讀。
ssp_import_template_route.py 裡的 SspImportTemplateByFrameworkVersionResource(框架版本版匯出)到底有沒有掛在任何客戶專屬資料上,盤點員沒有讀到底層邏輯,無法判斷風險等級。 待首腦確認是否要在 B4 派工卡裡特別點名要求查清楚。
已裁決:依首腦回饋納入 B3(見第 2、3 節),當成沒查過的檔案處理,不再是待裁決事項。module_frame_template_ssp_route.py 的懸案
已裁決:依首腦回饋納入 B1(見第 2、3 節),理由是 O9b 已證實 DI 漏接會讓守門靜默失效,不再是待裁決事項。di_containers/dashboard_apis/module_frame.py 要不要納入任何一棒
# 一次列出全部 module_frame 有效檔案(排除空檔/純 docstring 檔),加上依首腦回饋新納入的
# 2 支懸案檔(module_frame_template_ssp_route.py、dashboard_apis/module_frame.py),
# 核對總數應為 68 支、11,114 行(已實跑驗證過)
{
git ls-files -- api/module_frame app/module_frame domain/module_frame infra/module_frame di_containers/module_frame | grep '\.py$'
echo api/oscal/routes/module_frame_template_ssp_route.py
echo di_containers/dashboard_apis/module_frame.py
} | while IFS= read -r f; do
n=$(wc -l < "$f")
if [ "$n" -gt 1 ] && [ "$f" != "app/module_frame/__init__.py" ] && [ "$f" != "infra/module_frame/models/__init__.py" ]; then
echo "$n $f"
fi
done | awk '{s+=$1; c+=1} END {print c" 支檔、"s" 行"}'