盤點日期 2026-09-23|盤點卡 CM-2087|這份文件只盤點、不掃描——切好棒等首腦開卡派工。 行數全部是
wc -l逐檔實算(基準:分支feature/review,commitba6cf30b4),每棒段落附驗證指令。
名詞先講清楚(給不寫程式的讀者):
jedi-survey 這個套件裡。套件本身已經掃完。di_containers/):負責「組裝」的檔案,決定每個服務用哪個零件建出來。組錯了不會報錯,只會讓某道檢查靜默失效。app/flow_control/service/job_handlers/survey_handler.py(196 行,W2 棒)——0 個守門關鍵字、而且從來沒有被任何一棒當成掃描目標。 它負責「幫任務掛上問卷、替問卷拍快照」,前端送來的問卷編號直接拿去建關聯。FR-088 H4 的報告已經點名過「問卷識別碼寫入關聯表時只驗證了這筆資料存在,沒看到客戶歸屬檢查」,當時因為不在範圍內沒追下去。infra/flow_control/repository/flow_control_job_repo_impl.py(1,216 行,W4 棒)——整支沒有被任何一棒掃過。 它直接拿問卷、設備套件的資料表做查詢拼接(不經套件的服務層),套件本身的守門在這裡完全不起作用,能擋的只剩資料庫隔離。core/plugins/evidence_classification.py(718 行,W7 棒)——FR-111 E4 讀過它,但那之後這支檔被改了 496 行新增、26 行刪除。 它是 AI 金鑰解析鏈的門面,總表 §7 第 23 項點名的「非平台管理員拿不拿得到原廠金鑰」答案一半在這裡。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)。
方法一:誰 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 支。
只有方法三(檔名)抓到、方法一(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 節 |
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 已掃。研究員會追進去讀,屬正常,不要重報已知的守門實作問題。| 檔 | 行 | 哪一棒掃過 | 掃過後有沒有改 |
|---|---|---|---|
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)改過。這三支當成沒查過。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 行)只有說明文字、沒有邏輯,不派工。
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
EOFcore/plugins/_host.py 在 W1/W3/W6 三棒都列,清單裡只寫一次。W7 那 6 支(證據分類)要不要併進本批由首腦裁(見第 5 節第 1 條)。
切法原則:一棒一條完整接線路徑——從「套件怎麼被掛上產品」(core/plugins/xxx.py)到「套件的服務怎麼被組起來」(DI 容器)到「主專案哪些地方繞過套件直接用它的資料」,放在同一棒。不按層切,因為接線漏洞就活在「套件以為宿主會守、宿主以為套件會守」那條縫上。
共用接縫檔 core/plugins/_host.py(212 行)重複帶入 W1、W3、W6 三棒:它的 host_defaults() 決定「登入」和「能力點」這兩道門各給套件什麼東西——資產、議題整包吃它,日誌取其中幾項;問卷、檢測自己手寫守門,但用它的 lazy()/di() 在請求當下才去 DI 容器取服務(「取錯實例」的坑就在這,見該檔註解)。這三棒各自都要回答「我這支套件拿到的守門和服務是不是跟別人一樣」,所以每棒都要看得到它。
問卷套件怎麼被掛上產品:兩個網址群(設計問卷、填答問卷)、一條即時共編 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)
任務建立、修改時把問卷和檢測工具掛上去;任務回給前端時遮掉敏感參數;任務證據(問卷答案)怎麼讀;批次完成時問卷狀態怎麼查。這些是主專案「拿套件的東西來用」,不是套件自己的路——套件守門管不到這裡。
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,兩邊各看一半會出現責任真空。
檢測套件怎麼被掛上產品:守門給什麼、解壓上限多少、證據寫回哪、通知怎麼拼、規則包被誰引用。
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 行),研究員需要時自己查。
這三支不叫套件的服務、直接 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 層本來就不該有守門),研究員要回答的是「每一條查詢有沒有被資料庫隔離罩住、有沒有哪條查詢把別家的問卷/設備拼進來」。硬塞別的檔只會稀釋。
日誌套件怎麼被掛上產品:操作日誌只給平台管理員看、日誌轉送用能力點分讀寫;每個請求怎麼被寫進操作日誌;錯誤追蹤怎麼遮罩。
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 自己開檔看」即可。
資產(設備、資訊系統清冊)與議題(意見回饋)兩支接線都很薄,而且**都是 <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 讀過(但沒列為範圍)。這棒的價值在「並排」不在「議題本身」。
總表 §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 掃過、掃描後零改動,屬重疊帶入。
每一棒開工前都先做這四件事:
① 算守門落差。對每支有方法的檔跑:
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 這兩個參數有沒有被用到(沒用到就代表只判專案層、不判控制項層)。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(任務層直接查套件資料表):
session.query(Survey)/query(Device) 的查詢,Survey/Device 這兩張表有沒有開資料庫隔離?(FR-098 報告說設備表有開;問卷表要查。)session.query(Survey).filter(Survey.uid.in_(ref_ids)):ref_ids 從哪來?如果是從任務參數讀出來、而任務參數又是前端可寫的,就是「用別家的問卷編號拼進自己的任務」的形狀。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(證據分類金鑰鏈):
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。job_dto.py 與 job_handlers/__init__.py:這兩支是「任務模組使用檢測」的那一面,跟問卷在任務模組的使用是同一群檔案(都在 app/flow_control/),放一起研究員不用換腦。flow_control_job_repo_impl.py 一支就 1,216 行,湊別的會超標。issue.py 195 行+一支 25 行申報),而且是全專案唯二整包吃 host_defaults() 的套件接線(另一支是公告,已掃),並排看正好對照。core/plugins/detection.py,但沒有一棒把它列進範圍。首腦驗收時看到報告寫「此點 FR-108 D1 已查」,要分清楚是「D1 查過這一點」還是「D1 掃過這支檔」。core/plugins/_host.py 重複帶入三棒:三份報告若對它給出不同結論,以最嚴格那份為準,並回頭確認另兩棒是不是漏讀。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 行。
system_config_root_reader.py 的理由見 W7 段。不併的話本批是 6 棒。detection_tools_error_code.py(209 行,W3)與 main.py(302 行,W5)要不要從範圍拿掉? 兩支都只有一小段跟本批有關。拿掉可以省約 500 行,代價是研究員要自己去查。盤點員傾向保留(兩棒都還在門檻內)。workflow_execution_service.py(1,374 行)FR-088 H4 掃過之後改了 +45/-230 行,其中兩筆是 FR-112 的功能改動(合流閘道等兄弟分支、管理人強制開始任務的新端點 525989b7a)。它 import 問卷只是為了一個狀態碼和 TaskSurveyService,本批按「已掃」排除。但 FR-112 新加的「強制開始」端點有沒有人掃過,盤點員沒追到——這不是本批的範圍問題,是 FR-088 的「掃過後新增端點」問題,列出來讓首腦知道。common/code/feedback_code.py(5 行)疑似死碼:全專案只有 docs/api/feedback/generate_docx.py 引用。放進 W6 讓研究員確認,確認後可轉 FR-092 系列清理,不是資安問題。