FR-115 掃描盤點——五支套件的主專案接線:範圍界定與切棒

FR-115 掃描盤點:五支套件的主專案接線(問卷/檢測/日誌/資產/議題)

盤點日期 2026-09-23|盤點卡 CM-2087|這份文件只盤點、不掃描——切好棒等首腦開卡派工。 行數全部是 wc -l 逐檔實算(基準:分支 feature/review,commit ba6cf30b4),每棒段落附驗證指令。

名詞先講清楚(給不寫程式的讀者):

  • 套件:公司自己的共用模組,例如「問卷」整套功能放在 jedi-survey 這個套件裡。套件本身已經掃完。
  • 接線程式:主專案裡「把套件接上產品」的那層程式——告訴套件「登入怎麼驗、權限怎麼查、資料從哪個服務拿」。套件自己不決定誰能進門,它把這件事交給接線程式,接錯了就等於門沒鎖。
  • DI 容器(di_containers/):負責「組裝」的檔案,決定每個服務用哪個零件建出來。組錯了不會報錯,只會讓某道檢查靜默失效。
  • 守門:檢查「你有沒有登入」「你有沒有這個功能的權限」「這筆資料是不是你的」的程式碼。

這批最該先看的幾支

  1. app/flow_control/service/job_handlers/survey_handler.py(196 行,W2 棒)——0 個守門關鍵字、而且從來沒有被任何一棒當成掃描目標。 它負責「幫任務掛上問卷、替問卷拍快照」,前端送來的問卷編號直接拿去建關聯。FR-088 H4 的報告已經點名過「問卷識別碼寫入關聯表時只驗證了這筆資料存在,沒看到客戶歸屬檢查」,當時因為不在範圍內沒追下去。
  2. infra/flow_control/repository/flow_control_job_repo_impl.py(1,216 行,W4 棒)——整支沒有被任何一棒掃過。 它直接拿問卷、設備套件的資料表做查詢拼接(不經套件的服務層),套件本身的守門在這裡完全不起作用,能擋的只剩資料庫隔離。
  3. core/plugins/evidence_classification.py(718 行,W7 棒)——FR-111 E4 讀過它,但那之後這支檔被改了 496 行新增、26 行刪除。 它是 AI 金鑰解析鏈的門面,總表 §7 第 23 項點名的「非平台管理員拿不拿得到原廠金鑰」答案一半在這裡。
  4. core/plugins/detection.py(362 行,W3 棒)——三道解壓上限的設定在這裡(第 338~340 行),而總表第 115 項(規則包被當成程式執行)、第 116 項(網址來源繞過封裝檢查)的上游防線都靠這幾個數字。FR-108 十四棒只把它當「順手讀的參考」,沒有一棒把它列為掃描目標。

為什麼這幾支要優先看:這個 arc 反覆證明,套件本身的守門寫得再好,接線那一層繞過去或接錯,一樣全開(FR-088 的結論就是「套件 49 檔零新問題,洞全在主專案側」)。上面四支是「整支沒被掃過」或「掃過後大改」,而且都直接經手客戶資料或金鑰。


為什麼要另開這一批

五支套件(問卷、弱點檢測、日誌、資產、議題追蹤)的套件本體都已經掃完(FR-109/108/097/098/081+101),但每一支掃描報告最後都寫了同一句話:「主專案接線另棒」。那一棒一直沒開。

交接文件原本寫「三支小接線合一棒」,首腦 2026-09-23 按 import 實數後發現不成立——光問卷、檢測、資產三支就有三十幾個檔、一萬兩千行。這份盤點就是要回答:接線程式到底是哪些檔、多少行、怎麼切。

盤點結論:真正要掃的是 48 支檔、9,054 行,切成 7 棒(其中第 7 棒是證據分類的接線,6 檔/1,622 行,要不要併進本批由首腦裁;不併的話是 42 支、7,432 行、6 棒)。比首腦按 import 估的 12,400 行少,主因是 import 名單裡有 6 支檔(合計 4,404 行,其中 3 支各破千行)其實已經被 FR-088/FR-113/FR-095/FR-077 當成掃描目標掃過,只是剛好 import 了這幾支套件的東西(見 1.4)。


1. 範圍界定

1.1 起手:三種方法各跑一次

方法一:誰 import 了這五支套件

grep -rlE "(from|import) jedi_(survey|detection|asset|issue|api_log)\b" --include="*.py" \
  api app core di_containers infra domain config common

抓到 35 支。

🔴 日誌套件的 import 名是 jedi_api_log,不是 jedi_log——套件在 pyproject.toml 叫 jedi-log,但 Python 程式裡的名字是 jedi_api_log(core/plugins/api_log.py:87-90 就是這樣 import 的)。所以首腦按 jedi_log 搜零命中。改用 jedi_api_log 抓到 4 支:core/plugins/api_log.py、di_containers/log/apilog_containers.py、common/middleware/app_mw.py、infra/support/diag_masking.py;另外 main.py:240 也有一行 from jedi_api_log.plugin import restart_after_fork(main.py 在根目錄、不在上面的搜尋路徑裡,另外補進來)。

方法二:從接線檔與 DI 容器反查

從五支接線檔 core/plugins/{survey,detection,api_log,asset,issue}.py 出發,追它們叫用了哪些容器(lazy("survey_container", …)、container.asset_container 這類寫法),再從 di_containers/containers.py 找到每個容器定義在哪支檔。追到的:

接線檔 叫用的容器/零件 容器定義檔
core/plugins/survey.py survey_container、task_survey_container、question_answer_container、identity_container;零件 infra/survey/adapters.py di_containers/survey/survey_containers.py、di_containers/task_survey/*.py
core/plugins/detection.py detection_tools_container、detection_orchestration_container、job_evidence_container、remote_agent_container、workflow_execution_container、identity_container di_containers/detection_tools/*.py、di_containers/detection_execution/*.py
core/plugins/api_log.py api_log_container;零件 infra/identity/user_name_resolver.py di_containers/log/apilog_containers.py
core/plugins/asset.py asset_container、identity_container di_containers/asset/asset_containers.py
core/plugins/issue.py auth_container、identity_container(不接任何本套件的容器——服務全由套件自己組) 無

另外還追到兩條「不經 import、用字串指名」的接法:config/socketio_namespaces.py:48 用字串 "jedi_survey.app.handler.fill_survey_socketio_handler" 註冊問卷即時共編;di_containers/dashboard_apis/feedback.py:23 用字串 "jedi_issue.plugin" 把意見回饋申報給 AI 儀表板。這兩支檔 grep import 抓不到。

方法三:按目錄名/檔名撈

git ls-files '*.py' | grep -E "^(api|app|core|di_containers|infra|domain|config|common)/" \
  | grep -iE "survey|detect|api_?log|log_forward|/log/|asset|device|issue|feedback|information_system"

抓到 37 支。

1.2 交叉比對:只被一種方法抓到的檔

只有方法三(檔名)抓到、方法一(import)抓不到的 18 支:

檔 行 為什麼 import 抓不到 處置
infra/survey/adapters.py 163 它是被套件叫用的零件(專案角色守門、Redis 線上名單、任務編號換算),自己不 import 套件 🔴 納入 W1——問卷的專案角色守門就在這支(ProjectRoleGuardAdapter)
di_containers/dashboard_apis/survey.py 44 用容器名字串指到問卷服務,不 import 套件 納入 W1(AI 儀表板這條路不經網頁守門,見 3 節)
di_containers/dashboard_apis/device.py 21 同上,指到資產容器 納入 W6
di_containers/dashboard_apis/feedback.py 25 同上,用字串 "jedi_issue.plugin" 納入 W6
app/detection_tools/task_type_declaration.py 109 檢測任務型別的申報書,只宣告常數 納入 W3
common/code/detection_tools_error_code.py 209 錯誤碼常數表 納入 W3(研究員判斷「錯誤訊息漏資訊」時要對照)
common/code/feedback_code.py 5 三個字串常數,全專案只有文件產生腳本引用它,執行期沒人用 納入 W6(5 行成本零;也順便確認它真的是死碼)
infra/readmodel/detection/detection_job_notify_query.py 161 被 DI 容器 import,本身只 import 主專案的東西 納入 W3
infra/remote_agent/adapter/detection_lifecycle_listener.py 37 同上 納入 W3(FR-077 R3 已掃過,列為接縫、見 1.4)
app/detection_tools/__init__.py 等 8 支 0 空的 __init__.py 空檔,見 1.5
infra/readmodel/detection/__init__.py 14 只有說明文字 不派工,見 1.5

(common/util/system_asset_snapshot.py、infra/remote_agent/adapter/detection_task_payload_provider.py 這類檔名本身帶關鍵字、又 import 套件的,兩種方法都抓得到,不在上下兩張表裡。)

只有方法一(import)抓到、方法三(檔名)抓不到的 16 支——這些都是別的模組的檔案,順便 import 了這五支套件的東西:

檔 行 import 了什麼 處置
app/flow_control/service/job_service.py 455 檢測/問卷的 domain service 納入 W2(任務建立時掛問卷、掛檢測工具的入口)
app/flow_control/dto/job_dto.py 360 檢測的「敏感參數遮罩」函式 納入 W2(任務回給前端前要不要遮掉密碼,在這支決定)
app/flow_control/service/job_handlers/__init__.py 36 檢測的綁定處理器 納入 W2
app/flow_engine/service/job_evidence_service.py 191 問卷 domain service 納入 W2(FR-088 H2 掃過、但之後改了 13 行,見 1.4)
infra/readmodel/tasks/job_batch_complete_query.py 284 問卷 model 納入 W2
infra/flow_control/repository/flow_control_job_repo_impl.py 1,216 問卷、設備的資料表 model 納入 W4
infra/flow_control/repository/job_import_lookup_query.py 149 設備資料表 model 納入 W4
infra/readmodel/tasks/job_export_query.py 264 設備資料表 model 納入 W4
common/middleware/app_mw.py 297 日誌套件的服務 納入 W5(每個請求都經過它,寫操作日誌)
infra/support/diag_masking.py 198 日誌套件的遮罩函式 納入 W5
common/util/profile_extractor/__init__.py 12 轉手檔(把套件的東西掛回舊路徑) 納入 W3
app/flow_engine/service/workflow_execution_service.py 1,374 問卷 排除,見 1.4
app/module_frame/service/ssp_import_template_app_service.py 1,500 資產 排除,見 1.4
app/oscal/service/ssp_control_implementation_service.py 1,037 檢測 排除,見 1.4
app/oscal/service/ssp_resources_context_service.py 230 資產 排除,見 1.4
core/plugins/task.py 138 問卷的任務型別申報 排除,見 1.4

只有方法二(DI 反查)抓到、另兩種都抓不到的 3 支:

檔 行 為什麼只有 DI 反查抓得到 處置
config/socketio_namespaces.py 52 用字串註冊問卷即時共編(/socket/fill-survey),不 import 納入 W1
infra/identity/user_name_resolver.py 85 日誌接線把它傳給套件當「帳號換暱稱」零件 納入 W5
core/plugins/_host.py 212 五支接線共用的守門預設值(host_defaults() 決定「登入」和「能力點」各給什麼) 納入 W1/W3/W6 當接縫檔,見 2 節

1.3 共用但不屬於本批的檔(查到過、判斷過,列出來避免重查)

  • di_containers/containers.py(397 行):總組裝表,每支容器在這裡登記一行。只是登記、沒有邏輯。研究員要追「某容器接到哪」可以讀,但不排進任何一棒。
  • core/plugins/__init__.py(83 行)、config/app_modules.py(146 行):插件清單與模組註冊,只有名字沒有邏輯。
  • app/flow_control/service/job_import_service.py(470 行)、app/flow_control/service/job_batch_complete_service.py(193 行):W4/W2 的讀取模型真正的呼叫者。它們屬於任務平台(FR-095)的地盤,FR-095 決策者裁定停在三棒、這兩支沒被掃。這批不是任務平台的補掃,所以不納入;但 FR-095 已經點名 job_batch_complete_service.py:50-56 預設把呼叫者當管理員(總表第 59 項),W2 研究員讀到時知道有這條就好,不用重報。
  • api/flow_control/routes/job_route.py(259 行):任務 CRUD 的網址入口。只掛「登入+授權模組」、沒掛能力點,權限在 job_service._require_manager 那層。同樣屬任務平台地盤,W2 可讀不列。
  • app/system_config/service/guarded_system_config_service.py(665 行):會 import AI 金鑰加密函式,但它屬系統設定模組,FR-096 H1 已掃。
  • core/plugins/ai_bot.py、di_containers/ai_dashboard/ai_dashboard_containers.py:也叫用 AI 金鑰解析鏈,但分屬 FR-082/FR-083 已掃的套件。
  • common/authz/:全專案共用守門本體,FR-085 C1 已掃。研究員會追進去讀,屬正常,不要重報已知的守門實作問題。

1.4 排除清單(已經被別棒當成掃描目標掃過,不重複掃)

檔 行 哪一棒掃過 掃過後有沒有改
app/flow_engine/service/workflow_execution_service.py 1,374 FR-088 H4(21 檔範圍的主檔) 改過(+45/-230,主要是刪死碼與 FR-112 兩處功能),⚠️ 見第 5 節第 3 條
app/module_frame/service/ssp_import_template_app_service.py 1,500 FR-113 B4(2 檔範圍的主檔) 2026-09-23 剛掃,無
app/oscal/service/ssp_control_implementation_service.py 1,037 FR-113 O1 無明顯改動
app/oscal/service/ssp_resources_context_service.py 230 FR-113 O5 無明顯改動
core/plugins/task.py 138 FR-095 H1(35 檔範圍內) 掃描版本 739a0a61 至今零改動
infra/remote_agent/adapter/detection_task_payload_provider.py 125 FR-077 R3(16 檔範圍內) 零改動
infra/remote_agent/adapter/detection_lifecycle_listener.py 37 FR-077 R3(同上) 零改動——但仍列進 W3 當接縫檔(37 行,順著檢測編排讀下去會碰到它)

合計排除 6 支、4,404 行(detection_lifecycle_listener.py 不算在內,它重疊帶入 W3)。

「被提到」不等於「被掃過」:下面這幾支在舊報告裡出現過檔名,但只是研究員順手讀的參考、不在該棒的掃描範圍內,所以不排除、照樣排進本批:

  • core/plugins/detection.py:FR-108 D1/D2-1a/D2-4a/D3-1、FR-113 O3b 都引用過,全是「查證某一點時打開看了一行」。
  • core/plugins/api_log.py:FR-097 L1 只查了第 185-186 行「轉送端點有掛權限」這一點。
  • core/plugins/asset.py、core/plugins/issue.py:FR-098 A2、FR-101 J1 只引用「mount_api=True 在第 N 行」。
  • common/middleware/app_mw.py:FR-085 C2 在它身上找到 C2-1(高風險,請求原文寫進日誌),但 C2 的範圍是 jedi-common 套件、這支是越界;FR-097 L2 也寫明「不在本棒掃描範圍內」。沒有一棒完整掃過它。
  • core/plugins/evidence_classification.py、app/system_config/service/ai_provider_key_resolver.py:FR-111 E4 明寫「不在本棒範圍、但結論依賴它們」;FR-095 H1 雖然列了 evidence_classification.py,但那是 739a0a61 版本,之後這支改了 +496/-26 行,形同新檔。
  • di_containers/detection_tools/detection_orchestration_containers.py:FR-077 R3 只查了第 119 行「是 Factory 不是 Singleton」。
  • di_containers/dashboard_apis/{survey,device,feedback}.py:FR-083 D2 範圍含整個 dashboard_apis/,但報告自陳工具只查了 auth/project 兩支,survey/device/feedback 標「⬜ 待補」;而且 feedback.py 在 FR-083 之後(09-16)改過。這三支當成沒查過。

1.5 不列入切棒的空檔(11 支,0 行)

app/detection_tools/__init__.py
di_containers/asset/__init__.py
di_containers/detection_execution/__init__.py
di_containers/detection_tools/__init__.py
di_containers/log/__init__.py
di_containers/survey/__init__.py
di_containers/task_survey/__init__.py
infra/survey/__init__.py
di_containers/identity/__init__.py
infra/remote_agent/__init__.py
infra/remote_agent/adapter/__init__.py

(前 8 支是三種方法名單裡的空檔;最後三支 di_containers/identity/__init__.py、infra/remote_agent/__init__.py、infra/remote_agent/adapter/__init__.py 不在原始名單裡,是追接縫時碰到的,一併列出。)另外 infra/readmodel/detection/__init__.py(14 行)只有說明文字、沒有邏輯,不派工。

1.6 最終要掃的範圍

48 支不重複檔案、9,054 行(含 W7;不含 W7 是 42 支、7,432 行)。

來源拆解(每一步都實跑過):

步驟 檔數
方法一(import)35 支 ∪ 方法三(檔名)37 支 53
扣掉 8 支空 __init__.py + 1 支純說明檔 infra/readmodel/detection/__init__.py 44
扣掉 1.4 的 6 支已掃檔(4,404 行) 38
加回方法二(DI 反查)與 1.1 另外追到的 4 支:config/socketio_namespaces.py、core/plugins/_host.py、main.py、infra/identity/user_name_resolver.py 42(W1~W6,7,432 行)
加上 W7 證據分類接線 6 支(FR-111 列的 5 支+infra/system_config/system_config_root_reader.py) 48(9,054 行)

infra/remote_agent/adapter/detection_lifecycle_listener.py(37 行)是 FR-077 R3 掃過的檔,但沒有列進 1.4 的 6 支排除——它當接縫檔帶入 W3,算在 48 支裡。

驗證指令(已實跑,輸出 48 支檔、9054 行;去掉最後 6 行就是 W1~W6 的 42 支檔、7432 行):

cat <<'EOF' | sort -u | while IFS= read -r f; do echo "$(wc -l < "$f") $f"; done | awk '{s+=$1; c+=1} END {print c" 支檔、"s" 行"}'
core/plugins/survey.py
infra/survey/adapters.py
di_containers/survey/survey_containers.py
di_containers/task_survey/task_survey_containers.py
di_containers/task_survey/question_answer_containers.py
di_containers/dashboard_apis/survey.py
config/socketio_namespaces.py
core/plugins/_host.py
app/flow_control/service/job_handlers/survey_handler.py
app/flow_control/service/job_handlers/__init__.py
app/flow_control/service/job_service.py
app/flow_control/dto/job_dto.py
app/flow_engine/service/job_evidence_service.py
infra/readmodel/tasks/job_batch_complete_query.py
core/plugins/detection.py
di_containers/detection_tools/detection_tools_containers.py
di_containers/detection_tools/detection_orchestration_containers.py
di_containers/detection_execution/detection_execution_containers.py
infra/readmodel/detection/detection_profile_usage_query.py
infra/readmodel/detection/detection_job_notify_query.py
infra/remote_agent/adapter/detection_lifecycle_listener.py
app/detection_tools/task_type_declaration.py
app/detection_tools/dto/__init__.py
app/detection_tools/service/__init__.py
common/util/profile_extractor/__init__.py
common/code/detection_tools_error_code.py
infra/flow_control/repository/flow_control_job_repo_impl.py
infra/flow_control/repository/job_import_lookup_query.py
infra/readmodel/tasks/job_export_query.py
core/plugins/api_log.py
di_containers/log/apilog_containers.py
common/middleware/app_mw.py
infra/support/diag_masking.py
main.py
infra/identity/user_name_resolver.py
core/plugins/asset.py
di_containers/asset/asset_containers.py
di_containers/dashboard_apis/device.py
common/util/system_asset_snapshot.py
core/plugins/issue.py
di_containers/dashboard_apis/feedback.py
common/code/feedback_code.py
core/plugins/evidence_classification.py
di_containers/evidence_classification/evidence_classification_containers.py
app/system_config/service/ai_provider_key_resolver.py
infra/flow_engine/models/job_evidence.py
common/middleware/request_context_mw.py
infra/system_config/system_config_root_reader.py
EOF

core/plugins/_host.py 在 W1/W3/W6 三棒都列,清單裡只寫一次。W7 那 6 支(證據分類)要不要併進本批由首腦裁(見第 5 節第 1 條)。


2. 切棒(7 棒,每棒 ≤2,000 行、≤30 檔)

切法原則:一棒一條完整接線路徑——從「套件怎麼被掛上產品」(core/plugins/xxx.py)到「套件的服務怎麼被組起來」(DI 容器)到「主專案哪些地方繞過套件直接用它的資料」,放在同一棒。不按層切,因為接線漏洞就活在「套件以為宿主會守、宿主以為套件會守」那條縫上。

共用接縫檔 core/plugins/_host.py(212 行)重複帶入 W1、W3、W6 三棒:它的 host_defaults() 決定「登入」和「能力點」這兩道門各給套件什麼東西——資產、議題整包吃它,日誌取其中幾項;問卷、檢測自己手寫守門,但用它的 lazy()/di() 在請求當下才去 DI 容器取服務(「取錯實例」的坑就在這,見該檔註解)。這三棒各自都要回答「我這支套件拿到的守門和服務是不是跟別人一樣」,所以每棒都要看得到它。

W1 — 問卷的接線本體(掛載、零件、組裝、即時共編、AI 儀表板)

問卷套件怎麼被掛上產品:兩個網址群(設計問卷、填答問卷)、一條即時共編 socket、AI 儀表板兩支查詢。

core/plugins/survey.py                                        186   ← 接線主檔:守門怎麼給、兩條掛載路徑
infra/survey/adapters.py                                      163   ← 🔴 專案角色守門(ProjectRoleGuardAdapter)、任務編號換算
di_containers/survey/survey_containers.py                     124
di_containers/task_survey/task_survey_containers.py            56
di_containers/task_survey/question_answer_containers.py        97
di_containers/dashboard_apis/survey.py                         44   ← AI 儀表板這條路不經網頁守門
config/socketio_namespaces.py                                  52   ← 即時共編 /socket/fill-survey 用字串註冊
core/plugins/_host.py                                         212   ← 接縫檔,W3/W6 重複帶入

共 8 檔/934 行。驗證:git ls-files -- core/plugins/survey.py infra/survey/adapters.py di_containers/survey/survey_containers.py di_containers/task_survey/task_survey_containers.py di_containers/task_survey/question_answer_containers.py di_containers/dashboard_apis/survey.py config/socketio_namespaces.py core/plugins/_host.py | xargs wc -l | tail -1(預期 8 檔、934 total)

W2 — 問卷與檢測被「任務」使用的那一面

任務建立、修改時把問卷和檢測工具掛上去;任務回給前端時遮掉敏感參數;任務證據(問卷答案)怎麼讀;批次完成時問卷狀態怎麼查。這些是主專案「拿套件的東西來用」,不是套件自己的路——套件守門管不到這裡。

app/flow_control/service/job_handlers/survey_handler.py       196   ← 🔴 0 守門、從沒掃過:問卷掛到任務、拍快照
app/flow_control/service/job_handlers/__init__.py              36   ← 看得到檢測綁定處理器從套件 import 回來
app/flow_control/service/job_service.py                       455   ← 任務 CRUD 入口,_require_manager 守門在這
app/flow_control/dto/job_dto.py                               360   ← 🔴 任務回前端前的密碼遮罩
app/flow_engine/service/job_evidence_service.py               191   ← FR-088 H2 掃過,之後改 +13/-3
infra/readmodel/tasks/job_batch_complete_query.py             284   ← 批次完成時查問卷狀態

共 6 檔/1,522 行。驗證:git ls-files -- app/flow_control/service/job_handlers/survey_handler.py app/flow_control/service/job_handlers/__init__.py app/flow_control/service/job_service.py app/flow_control/dto/job_dto.py app/flow_engine/service/job_evidence_service.py infra/readmodel/tasks/job_batch_complete_query.py | xargs wc -l | tail -1(預期 6 檔、1522 total)

W2 必須在 W1 之後、由同一個 runner 接著跑:survey_handler.py 裡寫進關聯表的問卷,後續是透過 W1 的 infra/survey/adapters.py(TaskUidResolverAdapter)換算回任務編號給套件用。「問卷掛上任務時有沒有檢查歸屬」在 W2,「掛上去之後套件怎麼憑任務編號決定誰能看」在 W1,兩邊各看一半會出現責任真空。

W3 — 檢測的接線本體

檢測套件怎麼被掛上產品:守門給什麼、解壓上限多少、證據寫回哪、通知怎麼拼、規則包被誰引用。

core/plugins/detection.py                                     362   ← 接線主檔:七個 adapter、三道解壓上限(:338-340)
di_containers/detection_tools/detection_tools_containers.py   204
di_containers/detection_tools/detection_orchestration_containers.py 125
di_containers/detection_execution/detection_execution_containers.py  33
infra/readmodel/detection/detection_profile_usage_query.py    150   ← 規則包「還有沒有人在用」的判定
infra/readmodel/detection/detection_job_notify_query.py       161   ← 通知收件人名單怎麼拼
infra/remote_agent/adapter/detection_lifecycle_listener.py     37   ← 接縫檔(FR-077 R3 掃過、零改動)
app/detection_tools/task_type_declaration.py                  109
app/detection_tools/dto/__init__.py                            26   ← 轉手檔
app/detection_tools/service/__init__.py                        26   ← 轉手檔
common/util/profile_extractor/__init__.py                      12   ← 轉手檔
common/code/detection_tools_error_code.py                     209
core/plugins/_host.py                                         212   ← 接縫檔,重複自 W1

共 13 檔/1,666 行。驗證:git ls-files -- core/plugins/detection.py di_containers/detection_tools/detection_tools_containers.py di_containers/detection_tools/detection_orchestration_containers.py di_containers/detection_execution/detection_execution_containers.py infra/readmodel/detection/detection_profile_usage_query.py infra/readmodel/detection/detection_job_notify_query.py infra/remote_agent/adapter/detection_lifecycle_listener.py app/detection_tools/task_type_declaration.py app/detection_tools/dto/__init__.py app/detection_tools/service/__init__.py common/util/profile_extractor/__init__.py common/code/detection_tools_error_code.py core/plugins/_host.py | xargs wc -l | tail -1(預期 13 檔、1666 total)

⚠️ 行數雖在門檻內,但 detection_tools_error_code.py(209 行)幾乎全是中文訊息字串,實際 token 比行數看起來重。若首腦要壓,可把它拿掉(W3 變 12 檔/1,457 行),研究員需要時自己查。

W4 — 任務層直接拿問卷/設備資料表查詢(繞過套件服務層)

這三支不叫套件的服務、直接 import 套件的資料表 model 下 SQL。套件的守門在這條路上完全不起作用,能擋的只有資料庫隔離。

infra/flow_control/repository/flow_control_job_repo_impl.py  1216   ← 🔴 整支沒掃過;直接查 Survey/Device/TaskSurvey/QuestionAnswer
infra/flow_control/repository/job_import_lookup_query.py      149   ← Excel 匯入任務時查設備
infra/readmodel/tasks/job_export_query.py                     264   ← Excel 匯出任務時拼設備名稱

共 3 檔/1,629 行。驗證:git ls-files -- infra/flow_control/repository/flow_control_job_repo_impl.py infra/flow_control/repository/job_import_lookup_query.py infra/readmodel/tasks/job_export_query.py | xargs wc -l | tail -1(預期 3 檔、1629 total)

刻意做成單檔大棒:flow_control_job_repo_impl.py 一支就 1,216 行(62KB),是本批最大一支,也是全專案超標檔之一。它的 19 個方法全部 0 守門關鍵字(repo 層本來就不該有守門),研究員要回答的是「每一條查詢有沒有被資料庫隔離罩住、有沒有哪條查詢把別家的問卷/設備拼進來」。硬塞別的檔只會稀釋。

W5 — 日誌的接線本體(兩半:操作日誌+日誌轉送)

日誌套件怎麼被掛上產品:操作日誌只給平台管理員看、日誌轉送用能力點分讀寫;每個請求怎麼被寫進操作日誌;錯誤追蹤怎麼遮罩。

core/plugins/api_log.py                                       352   ← 接線主檔:兩半守門刻意不共用
di_containers/log/apilog_containers.py                         21
common/middleware/app_mw.py                                   297   ← 🔴 每個請求都經過,寫操作日誌(FR-085 C2-1 越界撿到的高風險就在這)
infra/support/diag_masking.py                                 198   ← 錯誤追蹤的遮罩
main.py                                                       302   ← :240 程序分叉後重啟日誌轉送(多工人模式下的狀態)
infra/identity/user_name_resolver.py                           85   ← 帳號換暱稱零件(日誌、上傳兩支共用)

共 6 檔/1,255 行。驗證:git ls-files -- core/plugins/api_log.py di_containers/log/apilog_containers.py common/middleware/app_mw.py infra/support/diag_masking.py main.py infra/identity/user_name_resolver.py | xargs wc -l | tail -1(預期 6 檔、1255 total)

main.py 只有第 240 行附近跟日誌有關,其餘是啟動流程。若首腦要壓成本可拿掉(W5 變 5 檔/953 行),在卡片上寫「main.py:230-250 自己開檔看」即可。

W6 — 資產與議題的接線本體(兩支都是「套件自帶守門、宿主只給零件」的形狀)

資產(設備、資訊系統清冊)與議題(意見回饋)兩支接線都很薄,而且**都是 <strong>host_defaults() 整包吃預設守門(全專案只有資產、公告、議題三支這樣吃),並排看最容易看出「有沒有哪一支少了什麼」。

core/plugins/asset.py                                         174   ← 接線主檔;引用計數零件(刪除前檢查有沒有人在用)
di_containers/asset/asset_containers.py                        89
di_containers/dashboard_apis/device.py                         21   ← AI 儀表板查設備
common/util/system_asset_snapshot.py                          129   ← SSP 設備清單快照(主專案直接用設備 domain service)
core/plugins/issue.py                                         195   ← 接線主檔;GitHub/GitLab 整合設定從總部那列讀
di_containers/dashboard_apis/feedback.py                       25   ← AI 儀表板查意見回饋(09-16 改過)
common/code/feedback_code.py                                    5   ← 疑似死碼
core/plugins/_host.py                                         212   ← 接縫檔,重複自 W1

共 8 檔/850 行。驗證:git ls-files -- core/plugins/asset.py di_containers/asset/asset_containers.py di_containers/dashboard_apis/device.py common/util/system_asset_snapshot.py core/plugins/issue.py di_containers/dashboard_apis/feedback.py common/code/feedback_code.py core/plugins/_host.py | xargs wc -l | tail -1(預期 8 檔、850 total)

議題那半幾乎是空的:FR-101 三棒重掃時 J1/J2 已經從套件側追進 core/plugins/issue.py 與 _host.py 讀過(但沒列為範圍)。這棒的價值在「並排」不在「議題本身」。

W7 — 證據自動分類的接線(金鑰解析鏈)*要不要併進本批由首腦裁

總表 §7 第 23 項:金鑰解析鏈「租戶自己的鑰 → 原廠鑰 → 環境變數」的本體在主專案,套件五棒只答得了「套件沒把鑰匙露出去」,答不了「解析鏈守不守」。

core/plugins/evidence_classification.py                      718   ← 八個 adapter;:149-203 是 AI 金鑰 adapter
app/system_config/service/ai_provider_key_resolver.py        199   ← 🔴 金鑰解析鏈本體,0 守門關鍵字、9 支函式
di_containers/evidence_classification/evidence_classification_containers.py 198   ← 雲端硬碟 token 綁在這
infra/system_config/system_config_root_reader.py             321   ← 解析鏈讀設定走這支(會關掉資料庫隔離)
infra/flow_engine/models/job_evidence.py                     117   ← 分類結果寫回的表
common/middleware/request_context_mw.py                       69   ← 「現在是誰」的身分脈絡

共 6 檔/1,622 行。驗證:git ls-files -- core/plugins/evidence_classification.py app/system_config/service/ai_provider_key_resolver.py di_containers/evidence_classification/evidence_classification_containers.py infra/system_config/system_config_root_reader.py infra/flow_engine/models/job_evidence.py common/middleware/request_context_mw.py | xargs wc -l | tail -1(預期 6 檔、1622 total)

FR-111 原本寫「5 檔約 1,300 行」,本盤點多加了 system_config_root_reader.py(321 行):解析鏈的第一、二層都呼叫它的 read_tenant_config_value(ai_provider_key_resolver.py:124),而它會用 SET LOCAL app.is_super_admin = 't' 關掉資料庫隔離去讀(FR-096 H1 查到的寫法)。「會不會讀到別家租戶的鑰」這題只有把兩支放一起看才答得了。這支 FR-096 H1 掃過、掃描後零改動,屬重疊帶入。


3. 每棒重點看什麼

每一棒開工前都先做這四件事:

① 算守門落差。對每支有方法的檔跑:

grep -c "require_\|authz\|permission\|Forbidden\|Permission" <檔>
grep -c "^    def [a-zA-Z]" <檔>

盤點時已先跑一輪(下表)。守門 0 次的檔不一定有洞——接線檔的守門多半是「把 decorator 交給套件」而不是自己呼叫,repo/查詢層本來就不該有守門——但要能說清楚「這支為什麼不需要守門、守門在哪一層」。

檔 守門次數 對外方法數 棒
app/flow_control/service/job_handlers/survey_handler.py 0 0(mixin 私有方法 1 支,約 150 行) W2 🔴
app/flow_engine/service/job_evidence_service.py 0 5 W2 🔴
app/flow_control/dto/job_dto.py 0 11 W2
app/flow_control/service/job_service.py 5 6 W2
app/system_config/service/ai_provider_key_resolver.py 0 9 支模組函式 W7 🔴
core/plugins/evidence_classification.py 8 22+3 W7
core/plugins/asset.py 0 3+2 W6
core/plugins/issue.py 0 2+3 W6
core/plugins/survey.py 6 1+3 W1
core/plugins/detection.py 10 15+5 W3
core/plugins/api_log.py 5 5 支模組函式 W5
infra/survey/adapters.py 2 8 W1
infra/flow_control/repository/flow_control_job_repo_impl.py 0 19 W4(repo 層,預期 0)

資產、議題兩支接線檔 0 次是因為它們不自己寫守門、整包吃 host_defaults()——這正是 W6 要驗的:整包吃進去的東西是不是套件真正需要的全部。

② 找「同一套接線複製五份,某一份漏了」。五支接線檔都是同一個三段式契約(① port adapter ② 填表 ③ 掛載),並排看:

接線檔 登入守門 能力點守門 授權模組守門 專案角色守門 平台管理員守門 給法
survey jwt_required() require_capability require_license ProjectRoleGuardAdapter(在 infra/survey/adapters.py) — 逐項手寫
detection (套件內組) CapabilityGuardAdapter LicenseGuardAdapter — IdentityGuardAdapter+platform_admin_check+scope_writable_check 逐項手寫
api_log jwt_required() 轉送那半有,操作日誌那半刻意沒有 — — require_platform_admin_route 取 host_defaults() 部分 key
asset host_defaults() host_defaults() — — — 整包 **host_defaults()
issue host_defaults() host_defaults() — — — 整包 **host_defaults()

問卷和檢測有授權模組守門(客戶有沒有買這個功能),資產和議題沒有——W6 要確認這是刻意的(例如資產清冊是基本功能不分方案)還是漏接。

③ 找「套件以為宿主守、宿主以為套件守」。接線檔裡常見的註解是「守門由套件做」或「套件會自己補名」——每一處都要追到套件那一側確認真的有做。反過來,套件報告裡寫「守門由宿主注入」的地方,要在這批確認宿主真的注入了、注入的是對的東西。

④ 特別注意 AI 儀表板那條路。di_containers/dashboard_apis/ 申報的查詢不經過網頁那層的任何守門(FR-083 D2 已證實 auth 那四支完全裸奔)。本批三支(survey 2 支查詢、device 1 支、feedback 1 支)FR-083 報告自陳「沒查、待補」,每支都要回答:這支查詢的服務方法自己有沒有做權限或歸屬檢查?

各棒個別重點

W1(問卷接線本體):

  • infra/survey/adapters.py:43-63 的 ProjectRoleGuardAdapter.assert_manager——它是問卷所有「只有專案管理者能做」的判斷依據。確認它是直接委派 common.authz.project.assert_project_manager 還是自己另寫一套;group_id/control_id 這兩個參數有沒有被用到(沒用到就代表只判專案層、不判控制項層)。
  • 套件報告的已知問題(第 89~100 項)是「套件側讀取入口 0/8 守門」。本棒要確認的是反面:宿主給套件的 capability_required、license_guard 有沒有給對——套件拿到守門零件卻沒套用是套件的錯(已登記),宿主根本沒給是本棒的錯。
  • 即時共編(config/socketio_namespaces.py:48):socketio 模式「一條 REST 都不掛、只寫 app.extensions」(core/plugins/survey.py 開頭註解)。socket 連線本身有沒有驗登入?套件報告第 F7 條說「房間想進哪間就進哪間」,那是套件側;宿主側要看的是 socket 握手時有沒有帶身分。
  • di_containers/dashboard_apis/survey.py 兩支:SurveyService.get_surveys、SurveyFolderService.get_folders——空條件時回什麼?會不會回整個客戶的問卷?

W2(任務使用問卷與檢測):

  • 🔴 survey_handler.py 的 _reconcile_task_surveys(第 39 行起):前端送來的問卷 uid 清單直接拿去建任務與問卷的關聯。有沒有檢查這些問卷是同一家客戶的?FR-088 H4 已經點名「只驗證了這筆資料存在,沒看到客戶歸屬檢查」、當時沒追。
  • 🔴 job_dto.py 的敏感參數遮罩:檢測工具的帳密存在任務參數裡,任務回前端前用 strip_secret_params 遮掉。每一條把任務序列化回前端的路是不是都經過這支?有沒有哪個 DTO 繞過去直接回原始參數?
  • job_evidence_service.py:FR-088 H2 找到的高風險(任務證明清單不檢查是不是專案成員)在這支第 48 行。掃描後這支改了 +13/-3 行——確認那條洞還在不在、有沒有被改動影響,不要重報成新發現。
  • job_service.py 三個寫入入口都呼叫 _require_manager(FR-108 D3-2 已確認)。本棒要看**讀取入口 get_job(第 140 行)、list_jobs(第 244 行)**有沒有同等檢查——這是本 arc「有人守了一半」最常見的長相(讀的那半沒守)。

W3(檢測接線本體):

  • core/plugins/detection.py:338-340 三道解壓上限:getattr(Config, "...", 預設值)——Config 沒設時用預設值,預設值合不合理?(總表第 115/116 項的上游防線)。另外 FR-113 O3b 建議 Excel 那條路直接重用這三個數字,確認它們是設在 detection 專用還是全域。
  • scope_writable_check(第 181 行)與 platform_admin_check(第 170 行)走「模組級設定」而不是每次請求取——有沒有可能在程序啟動時就被固定成某個人的身分?
  • CryptoAdapter(第 129 行):檢測工具帳密的加解密零件。總表第 85 項(測試連線零守門、可把帳密解密外送)的解密就是走這支——確認它沒有額外的「誰能呼叫解密」限制是設計如此。
  • detection_profile_usage_query.py:規則包「還有沒有人在用」的判定。總表第 133 項(刪框架不檢查還有沒有人在用)是同款形狀——如果這支查詢因為資料庫隔離只看得到自己家的引用,平台管理員刪公版規則包時會以為沒人在用。
  • detection_job_notify_query.py:通知收件人名單拼了檢測、派工、專案、成員四張表——有沒有可能把別家的人拼進收件人。

W4(任務層直接查套件資料表):

  • 19 個方法逐支看:每條直接 session.query(Survey)/query(Device) 的查詢,Survey/Device 這兩張表有沒有開資料庫隔離?(FR-098 報告說設備表有開;問卷表要查。)
  • 第 190 行 session.query(Survey).filter(Survey.uid.in_(ref_ids)):ref_ids 從哪來?如果是從任務參數讀出來、而任務參數又是前端可寫的,就是「用別家的問卷編號拼進自己的任務」的形狀。
  • 第 912、1090 行在方法內 import TaskSurvey、QuestionAnswer——這兩支是在讀填答結果,確認讀的時候有沒有綁定「這個任務」。

W5(日誌接線本體):

  • core/plugins/api_log.py 自陳「兩半的守門刻意不共用」:操作日誌只給平台管理員、日誌轉送用能力點分讀寫。總表已登記「一個客戶的管理員可以把全公司的日誌改送到他自己的機器」——那是套件側能力點設計的問題還是宿主接錯了?本棒要給這個問題一個宿主側的答案。
  • common/middleware/app_mw.py:FR-085 C2-1(高風險,請求標頭與內容原文寫進 log)、FR-097 L2(匯出公式注入的源頭)都在這支,全是越界撿到、沒有一棒完整掃過。本棒把它當正式範圍:before_request 在權限檢查之前就寫日誌,寫進去的欄位哪些有遮罩、哪些沒有。已登記的不重報,找有沒有第三條。
  • main.py:240 程序分叉後重啟日誌轉送:gunicorn 多工人模式下,每個工人各自一份轉送設定,改了轉送目的地之後其他工人會不會還在往舊的地方送?
  • 總表已登記「兩張日誌表的保存期限沒生效」——確認宿主側有沒有負責啟動清理排程的那一段。

W6(資產與議題並排):

  • 授權模組守門:問卷、檢測都有 license_guard,資產、議題沒有。確認 AssetAdapters/IssueAdapters 根本沒有這個欄位(套件契約就沒要)還是有欄位但宿主沒給。
  • AssetReferenceAdapter.count_references(core/plugins/asset.py:73):刪設備前檢查「還有沒有人在用」。總表第 81 項(只有修改權限就能達成刪除效果)的形狀——這支計數查詢受資料庫隔離嗎?隔離下只數得到自己家的引用,會不會低估?
  • common/util/system_asset_snapshot.py:SSP 設備清單快照直接用設備 domain service,不經套件的網頁守門。FR-098 A2 查過 ssp_resources_context_service.py 同款呼叫「走同一套 RLS、沒有繞過」,本棒對這支做同樣確認。
  • core/plugins/issue.py:70 RootIntegrateConfigProvider:GitHub/GitLab 權杖從總部那一列讀(read_root_config_value)。確認任何客戶送意見回饋時,都是用公司的權杖開問題單——這是設計(總表已登記「死碼待裁」),不要重報;但要確認權杖不會出現在回應或日誌裡。
  • common/code/feedback_code.py:確認執行期沒人用,是死碼的話寫進報告給 FR-092 系列清理。

W7(證據分類金鑰鏈):

  • 🔴 非平台管理員拿不拿得到原廠金鑰(FR-111 卡片重點⑤的另一半):ai_provider_key_resolver.py 的解析順序「租戶自己的 → ROOT 原廠 → 環境變數」,租戶沒設時會退到原廠鑰——這是「用」原廠鑰,不是「看到」原廠鑰(CM-1867 裁 D13:鎖改不鎖用)。本棒要確認的是「只有用、沒有看到」:原廠鑰有沒有可能經由回應、錯誤訊息、容器日誌、container_env() 流到租戶手上。
  • ai_provider_key_resolver.py:179 取 get_user_context() 決定是哪個租戶——無登入脈絡時(背景排程、批次任務)回 None,會退到哪一層?FR-085 C1 已證實「沒有登入身分時資料庫隔離整個關閉」,兩件事疊在一起要看清楚。
  • system_config_root_reader.py 的 read_tenant_config_value 關掉資料庫隔離去讀——傳進去的 tenant_id 是誰決定的?有沒有哪條呼叫路徑是前端可控的。
  • evidence_classification_containers.py 的雲端硬碟 token_provider:FR-111 E3 的高風險(舊線預覽端點讀客戶整個硬碟)用的權杖從這裡取,確認新線與舊線是不是同一個 provider。

4. 切棒推導與風險提示

為什麼這樣切

  • 問卷拆成 W1(接線本體)和 W2(被任務使用)兩棒:問卷接線合計約 2,450 行,超過門檻。拆法是按「誰是主角」——W1 是「套件怎麼被掛上來」(主角是套件),W2 是「主專案拿套件的東西去做任務」(主角是任務模組)。兩棒的接縫是「問卷掛上任務」與「套件憑任務編號判權限」這條線,所以要求同一個 runner 連續跑(見 W2)。
  • W2 同時收檢測的 job_dto.py 與 job_handlers/__init__.py:這兩支是「任務模組使用檢測」的那一面,跟問卷在任務模組的使用是同一群檔案(都在 app/flow_control/),放一起研究員不用換腦。
  • W4 單獨一棒:三支檔都是「直接拿套件的資料表下 SQL」,同一種風險形狀;而 flow_control_job_repo_impl.py 一支就 1,216 行,湊別的會超標。
  • 資產與議題合一棒(W6):兩支接線都很薄(議題實質只有 issue.py 195 行+一支 25 行申報),而且是全專案唯二整包吃 host_defaults() 的套件接線(另一支是公告,已掃),並排看正好對照。
  • W7 單獨一棒而非拆進其他棒:它屬於不同的套件(證據分類),只是總表 §7 第 23 項建議「順便一起盤」。拆進別棒會讓那一棒失焦。

給首腦的風險提示

  • 本批大多數檔都被別的 arc「路過」過,這是最容易誤判成已掃的地方。1.4 後半列了 9 支「被引用過但沒被掃過」的檔——FR-108 十四棒有 5 棒引用過 core/plugins/detection.py,但沒有一棒把它列進範圍。首腦驗收時看到報告寫「此點 FR-108 D1 已查」,要分清楚是「D1 查過這一點」還是「D1 掃過這支檔」。
  • W2 與 W1 必須同一個 runner 連續跑(理由見 W2 段)。
  • W4 的結論高度依賴資料庫隔離的現況:這三支 repo 本來就不做程式層守門,研究員只能回答「每條查詢撞到的表有沒有開隔離」。資料庫隔離的現況要以驗收當下 DEV 實查為準(平行 RLS 線可能在掃描與驗收之間改變事實)。
  • core/plugins/_host.py 重複帶入三棒:三份報告若對它給出不同結論,以最嚴格那份為準,並回頭確認另兩棒是不是漏讀。
  • 任務平台(FR-095)剩下的 108 檔不因本批而算「掃過」。本批納入的 app/flow_control/ 那幾支,是因為它們 import 了問卷/檢測,不是因為任務平台要補掃。job_import_service.py、job_batch_complete_service.py、job_route.py 仍然沒人掃。

逐棒加總核對(已實跑)

W1(問卷接線本體)          8 檔    934 行(含 _host.py 212 行)
W2(任務使用問卷與檢測)     6 檔  1,522 行
W3(檢測接線本體)         13 檔  1,666 行(含 _host.py 212 行)
W4(任務層直接查資料表)     3 檔  1,629 行
W5(日誌接線本體)          6 檔  1,255 行
W6(資產與議題並排)         8 檔    850 行(含 _host.py 212 行)
W7(證據分類金鑰鏈)         6 檔  1,622 行
------------------------------------------
逐棒加總(含重複計數)       50 檔  9,478 行
扣掉 _host.py 多算的 2 次(212 × 2 = 424 行)
= 48 支不重複檔案、9,054 行

跟 1.6 的「48 支、9,054 行」一致。不含 W7:逐棒 44 檔/7,856 行,扣 424 行= 42 支、7,432 行。


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

  1. W7(證據分類金鑰鏈,6 檔/1,622 行)要不要併進本批? 卡片寫「由首腦裁」。盤點員建議併:它跟 W5(日誌)一樣是「主專案接線、套件已掃完」的形狀,而且總表 §7 第 23 項已經掛了很久;多加 system_config_root_reader.py 的理由見 W7 段。不併的話本批是 6 棒。
  2. detection_tools_error_code.py(209 行,W3)與 main.py(302 行,W5)要不要從範圍拿掉? 兩支都只有一小段跟本批有關。拿掉可以省約 500 行,代價是研究員要自己去查。盤點員傾向保留(兩棒都還在門檻內)。
  3. workflow_execution_service.py(1,374 行)FR-088 H4 掃過之後改了 +45/-230 行,其中兩筆是 FR-112 的功能改動(合流閘道等兄弟分支、管理人強制開始任務的新端點 525989b7a)。它 import 問卷只是為了一個狀態碼和 TaskSurveyService,本批按「已掃」排除。但 FR-112 新加的「強制開始」端點有沒有人掃過,盤點員沒追到——這不是本批的範圍問題,是 FR-088 的「掃過後新增端點」問題,列出來讓首腦知道。
  4. common/code/feedback_code.py(5 行)疑似死碼:全專案只有 docs/api/feedback/generate_docx.py 引用。放進 W6 讓研究員確認,確認後可轉 FR-092 系列清理,不是資安問題。