FR-108 · 需求索引 · 本頁由 build 掃資料夾生成

FR-108 弱點掃描整合套件(jedi-detection)資安掃描

✅ 完成(2026-09-20);發現已全數處理(2026-10-01 同步)——M03 全部問題隨 1.21.0 出貨已修,無裁定不修項(母卡 CM-1872;D1 已掃完並經首腦驗收(4 條:1 高 3 中,登記跨 arc 總表第 85~88 項);D3 改切五小棒,D3-1 已掃完並驗收(1 條中風險→第 101 項)、D3-2 已掃完並驗收(1 條中風險→第 105 項)、D3-3 已掃完並驗收(範圍內 0 條新發現,第 88 項僅補行號)、D3-4a 已掃完並驗收(1 條中風險:解密帳密明文落 agent_tasks.params 且無人讀、無清理機制)、D3-4b 已掃完並驗收(範圍內 0 條新發現,三條全為舊帳;第 59 項影響面補八支端點)——D3 五小棒全部完成;D2 改切四小棒,其中 D2-1 再切兩半:D2-1b 已驗收(09-19→第 112 項;1 條中風險:zip entry 上限在記憶體配置後才生效;同棒推翻卡片「route 零能力點」前提),D2-1a 已驗(09-20)→第 113/114 項(2 條中風險:七支讀取端點不執法已宣告的 detection-profile.read/重抽無併發上限與去重)、D2-3 已驗(09-20)→第 115/116 項(1 高 1 中:上傳規則包被 cinc-auditor 當 ERB 樣板執行→BE 主機 RCE/網址型來源跳過壓縮檔驗證器;本 arc 第二條高風險)、D2-2a 已驗(09-20)→第 117 項(1 條中風險:網址型基準放行明文 http 且不記指紋)、D2-4a 已驗(09-20)→範圍內 0 條新發現(卡片重點⑥三項人工查證皆乾淨;另記兩條註解與事實不符的體質項,不進總表)、D2-2b 已驗(09-20)→範圍內 0 條新發現(卡片重點④五項人工查證皆乾淨並實連 DEV 核對政策與帳號;另記一條前提事實不進總表:這四個檔零租戶隔離程式碼、安全性全靠 RLS 外包),D2-2 打 17 小時零產出改判看門狗誤殺、⛔ 重切為 D2-2a/D2-2b(均已收口)、D2-4 亦改切 D2-4a/D2-4b(均已收口**)、D2-4b 已驗(09-20)→淨新增 0(工具報的那條與第 105 項為同一條鏈,獨立重現不另計;卡片重點⑥三項人工查證皆乾淨)**、🔴 D2 八小棒全部完成,D1/D2/D3 三棒全數收口;2026-09-16 決策者裁重縮成三棒,CM-1876 D4 作廢)。套件正式碼 160 檔,扣 R2b 的 11 支與 66 支無邏輯檔(空 __init__、mapper、純資料 entity、repository 介面殼、七支離線工具),三棒 83 檔:D1 工具目錄+租戶憑證+守門殼+插件骨架與 migration(36)→ D3 執行編排(17)→ D2 掃描基準全鏈(30)。這支從沒掃過。CM-1595 明文帳密鏈的終點在這支。

狀態:✅ 已完成 文件 13 份 handoff 1 份

這是資安掃描系列的一個 arc。 跨 arc 的結論、修正卡狀態、還沒開卡的待辦與覆蓋率,在跨 arc 總表:security-scan-consolidated/。本頁只講這一支。

🔴 一頁看完

✅ 這支套件的掃描已於 2026-09-20 全部收口。 14 批(D1×1/D3×5/D2×8,D4 作廢併入 D1)共 83 檔,淨新增 2 高 + 11 中(總表 §3.1 第 85~88/101/105/107/112~117 項)+ §3.2 第 28~33 項六條非資安程式錯誤。兩條高風險是第 85 項(測試連線端點無權限檢查、任何登入者可解密外送掃描工具帳密)與第 115 項(客戶上傳的規則包會被外部程式當 Ruby 樣板執行、等於 RCE)——D1 段落與 D2-3 段落各自的敘述才是準的,其餘地方若出現不同編號組合以此為準。

D1 掃完找到 4 條(1 條高風險);D3 五小棒全部掃完並驗收——D3-1/D3-2/D3-4a 各 1 條中風險,D3-3/D3-4b 範圍內 0 條;D2 改切四小棒,D2-1 再切兩半——D2-1b/D2-1a 已驗收(→第 112/113/114 項)、🔴 D2-3 已驗收(→第 115/116 項,第 115 項是整個 arc 至今影響面最大的:上傳的規則包會被外部程式當 Ruby 程式碼執行),D2-4a/D2-2b/D2-4b 已驗收(皆淨新增 0),D2-2 打 17 小時零產出、判定看門狗誤殺,已改切 D2-2a/D2-2b 收口、D2-4 亦改切 D2-4a/D2-4b 收口,🔴 D2 全部完成——D1/D2/D3 三棒全數收口。(原四棒 149 檔,決策者裁重縮:剔掉 66 支無邏輯檔、D4 併入 D1,三棒 83 檔。)

D2-2a 找到一條(✅ 已驗收 09-20→總表第 117 項,三票一致通過、零駁回):填網址建基準時系統允許填明文 http://,而且網址型來源不記指紋。 遠端代理程式拿到網址後直接叫外部程式 cinc-auditor exec <網址> 下載並執行裡面的 Ruby 規則,路徑上任何能動手腳的節點(同段區網、上游一跳、被控 DNS)換掉那包檔案,就等於在代理程式容器裡跑自己的程式碼——而那個容器握著它要去掃的每一台主機的 SSH 憑證、就住在客戶內網裡,跑出來的掃描報告也一起假掉。沒有 TLS 要破解,因為根本沒有 TLS;後端自己的下載器 safe_http_fetch 只准 https,這裡卻放行 http,同一系統兩套標準。容易誤判為「已擋住」的地方:後端自己抓不到(只准 https)所以那一版停在 failed,但派工不看抽取狀態(_load_profile() 只檢查 is_active),基準照樣派得出去。修法兩件:_resolve_source 只收 https://;第一次抽取成功時記下 sha256 並放進送給代理程式的封包讓它對帳(上傳檔案那條路本來就這樣做,網址這條只是沒補上)。⚠️ 與第 116 項不是同一條——同一條網址路徑上的兩種病:第 116 項是「檔案太大沒人管」,這條是「檔案是不是原來那一份沒人管」,修法不同不要合併。卡片重點④(公版 vs 租戶 scope 守門)工具一條候選都沒提,逐項開檔查證守門本身沒有破口:八支寫入全掛 _guard_system_writable、判定走統一的 viewer_is_platform_admin() 沒硬寫租戶編號、未接線時拋例外而非放行(預設放行會讓「忘記接線」變成無聲破口而服務照常綠燈)、fork 與 copy-to-tool 的不對稱是刻意且正確的(fork 硬寫成自己租戶、不需公版守門因為寫的是新租戶列;copy-to-tool 沿用來源、需要公版守門因為產物仍是公版)、公版 tenant_id 填 root(1)不填 NULL、版本從檔顯式沿用主檔 scope/tenant(靠自動填會把公版填成操作者租戶而變私有)、改分享設定另過 assert_scope_writable(母租戶可存取路徑涵蓋子樹,光靠 RLS 擋不住改別人的)、列表顯式收斂可見範圍(root 的 /1/ 前綴涵蓋全部子租戶,只靠 RLS 平台管理員會看到全站私有基準)、資料庫四條政策 INSERT/DELETE 直接禁止任何人碰公版。但查出兩個工具與卡片都沒問到的現況落差(不是漏洞、是會長成漏洞的地方,已記進跨 arc 總表 §3.2 第 28/29 項與 §7 第 18b 項待裁):① FR-094 已把 SHARED 加進資料庫值域與查詢政策,這支服務還停在兩種 scope 的世界觀——_normalize_scope() 只收 SYSTEM/TENANT(建立時填 SHARED 被擋),但 update_profile 的 scope 變更沒過 _normalize_scope 直接交給宿主(那支只收 TENANT/SHARED),於是建立時不能指定 SHARED、修改時可以改成 SHARED,一個欄位三個地方各認一套值域;② 套件自帶建表腳本與出貨基線的 CHECK 仍是舊值域(只允許 SYSTEM/TENANT),既有環境跑過 FR-094 沒事、新裝客戶會在寫入當下被資料庫拒絕——屬「出貨基線待重產」,只回報不自行處理。⚠️ 監看判準要更正:卡片寫「看 journal 的 failed 事件」,這個工具不寫 failed 這個字,研究員被換掉的呈現是同一個 key 再出現一筆 started——正確判準是數「failed 筆數+同 label 重複 started 筆數」,任一到第二次就停,不是原先寫的「數到第三筆 started」。切成單檔有效:原 D2-2 五檔打 17 小時 25 位全滅,本棒 88 分鐘(10:32→12:01)完成(死 1 位);但收穫沒變多(1,133 行只出 1 條),與前幾棒一致。詳見報告。

D2-4a 範圍內 0 條新發現(✅ 已驗收 09-20,淨新增 0;0 候選=面板 0 票):分類骨幹全鏈(taxonomy route/serializer/dto/service/domain service/repo impl/model/query entity,8 檔 772 行)研究員零重試、33 分鐘完成。卡片重點⑥三支寫入端點(POST/PUT/DELETE)皆掛 require_platform_admin_route、兩支讀取端點只掛授權檢查(刻意,分類法字典是跨租戶共用)、全套件無第四條寫入路徑、引用計數查詢雖未帶租戶條件但這張表本身受資料庫隔離管、查詢條件全部預設非空——首腦逐項開檔查證皆乾淨。帶出一條體質項:detection_profile_taxonomy_repo_impl.py:92-95 的 count_references() 註解寫「刻意不受隔離、要看到全站」,實查該查詢其實受隔離管,只是能走到這步的人本來就是平台管理員、殊途同歸——已記進跨 arc 總表 §3.2 第 30 項。⚠️ 另外兩個工具查到但卡片沒問到、確認無攻擊路徑、不進主表:一是 mount_routes 若忘記接線預設會變成無驗證放行、35 條路由全裸,但兩處呼叫端都已先過必填接線檢查(同 D1 已查證的結論);二是分類法的 key 欄位沒有長度上限,平台管理員填超長值會看到一段未經包裝的原始資料庫錯誤(只有平台管理員摸得到、不是外洩),列為觀察即可。詳見報告。

D2-2b 範圍內 0 條新發現(✅ 已驗收 09-20,淨新增 0;0 候選=面板 0 票):研究員讀完基準的領域層與存取層四個檔(499 行)一條候選都沒提,且不是沉默的空手而歸——它逐條交代了每個候選為什麼不成立(與 D2-2 那 25 位一句話沒寫就被砍完全不同,「0 條」有兩種、可信度天差地別,判準是看 journal 有沒有 result 事件)。卡片重點④五項由首腦逐項開檔查證皆乾淨:兩支列表都掛 _visible_scope_filter 顯式收斂可見範圍(不能只靠 RLS——root 的 /1/ 前綴涵蓋全部子租戶,平台管理員會看到全站私有基準);那個看似無條件放行的 scope == SHARED(repo_impl:52)實查確認擋得住——首腦連 DEV 核對 detection_profiles_select 政策,SHARED 那一段附帶 app_tenant_is_ancestor_of_session(tenant_id),平輩租戶照不出來,程式碼不重算租戶樹的取捨正確(重算=第二套真相);寫入路徑確實沒有 tenant_id 比對,但 RLS 的 UPDATE/DELETE 擋下平輩與子改母、公版政策明寫 scope <> SYSTEM,母改子會成功是 FR-094 明載的刻意設計(子租戶對分享資源唯讀);空條件回全表(總表第 70 項)在這四個檔不成立——兩支列表第一個條件就是不可選的 _visible_scope_filter、控制項三支方法全部強制帶 version_id、唯一走共用基底的 list_all_versions() 的 profile_id 是必填參數;控制項表刻意不掛 RLS、隔離靠上層版本從檔承擔,四個入口全查過都先經受 RLS 的表(領域層的 get_by_uid 全專案零呼叫端、是死碼不構成入口)。另記一條前提事實(不是漏洞、不進總表,但讀這四個檔必須知道):這 499 行裡沒有任何一行在做租戶隔離,安全性完全外包給資料庫——首腦實查確認目前成立(cm_app 的 rolsuper/rolbypassrls 皆為 f、非表 owner,三張表 RLS 開關與政策內容皆與 FR-094 一致),但這代表政策、連線帳號、上層入口三者任一被改動,這四個檔不會有任何反應——不報錯、測試不變紅,只會安靜地開始回傳別人的資料。⚠️ 連帶查出套件自帶的 002-detection-rls-grants.sql 也停在舊版、缺 SHARED 分支(現行政策是主專案 FR-094 migration 後來蓋上去的),與 D2-2a 報的建表腳本舊值域是同一件事的兩面,同屬「出貨基線待重產」——新裝客戶若只跑套件那份,repo_impl:52 就會變成真正的跨租戶外洩。⚠️ 監看判準再次印證 D2-2a 的更正:本棒 journal 全程 failed 為 0 卻實際換過一次研究員,呈現方式同樣是同一個 label 重複出現 started——「看 failed 第二個就停」在這個工具版本上永遠不會觸發。第八個樣本再次印證「切棒保證跑得完、不保證找得更多」(499 行出 0 條);但這四個檔本來就是薄層、無外部輸入解析/子程序/網路/加解密/路徑處理,攻擊面在上一層。詳見報告。

🔴 D2-3 找到兩條,其中一條是這個 arc 至今影響面最大的(✅ 已驗收 09-20→總表第 115/116 項,兩條都三票一致通過、零駁回):① 客戶上傳的掃描規則包會被當成程式碼執行,整台後端主機等於交出去(高風險)。 系統把使用者給的壓縮檔原封不動交給外部程式 cinc-auditor 解析,而那支程式讀設定檔 inspec.yml 時會先把檔案內容當 Ruby 樣板跑一遍再解析(inspec-core-7.1.7/lib/inspec/metadata.rb:259,首腦開本機安裝的套件原始碼核對過,一字不差)。上傳驗證器自己在檔頭寫明「只驗結構不驗語意」——檢查副檔名、大小、magic、檔名會不會解到別的目錄、有沒有 symlink、entry 數與壓縮比,沒有一項在看 inspec.yml 裡面寫了什麼。所以一份檔名正常、結構正常、放了 inspec.yml 的惡意包一道防線都不會碰到,建好基準後系統自動排抽取,指令就執行了。拿到的是後端行程的全部權限:.env 三把密鑰(含 JWT_SECRET,拿到就能偽造任何人的登入)、以繞過租戶隔離的 cmmgr 身分讀寫全部客戶的資料、MinIO 與代理程式憑證、內網跳板。一個租戶管理員就能打下整台主機與全平台資料。網址型更省事——把同一份檔案放自己的 https 伺服器填進去即可,連上傳都不用。評 HIGH 不評 CRITICAL 是因為要先是登入的租戶管理員;但門檻只有這一道,過了沒有第二道。修法兩件:在重打包之前先看一眼 inspec.yml,含 ERB 樣板標記(<%)就拒收(落在 _repack_flat() 裡面,一處改完上傳與網址兩條路一起好——落在服務層的上傳分支就會漏掉網址那條),並把 cinc-auditor 關進沙箱跑。② 網址型來源完全跳過壓縮檔驗證器(中風險):防炸彈的三道上限(一萬個檔/500MB/200 倍)全專案只有一個呼叫點、就在「上傳檔案」那個分支裡;「填網址」分支只檢查開頭是不是 https:// 就放行,下載回來直接解開。下載器那道 50MB 上限管的是壓縮後大小,45MB 解成數十 GB 完全做得到,讀 tar 是逐筆累積進 list、無任何累計上限。修法建議改在解析器 _read_entries() 裡面而不是補呼叫服務層驗證器——防護長在解析器裡,兩條來源與未來的第三條天然都涵蓋。⚠️ 這條與 D2-1b 那條是同一支驗證器的兩種病、修法不同不要合併:D2-1b 是「上限存在但太晚生效」,這條是「上限根本沒裝在這條路」。卡片指定的重點②③工具一條候選都沒提,由首腦逐項開檔查證全部沒問題:②外抓呼叫端只有兩處、都在本棒範圍內,全套件搜尋 httpx/requests/urlopen 除下載器自己外零命中(沒有第二條沒防護的外連路徑),探測那支與下載共用同一套防線只差動詞、兩處都用預設上限沒放寬、錯誤訊息不漏內網資訊、回前端的只有兩個 HTTP header 比對後的一個布林;③子程序參數用陣列無 shell=True、路徑與命令皆不可控、逾時 900 秒有接、暫存檔 try/finally 保證刪且權限 0600、背景執行緒的使用者身分有正確捕捉還原、重打包是在記憶體裡建全新 entry 所以 symlink/絕對路徑天然進不去。唯一的問題不在「怎麼啟動這支程式」而在「這支程式拿到檔案之後做了什麼」——卡片重點③問的全是啟動面,真正的洞在被啟動者的行為。這棒跑得最順:零重試、6 票全投、82 分鐘,也沒有越界候選(與「不含 route 的自包含模組跑得動」判讀一致)。詳見報告。

D2-1a 找到兩條(✅ 已驗收 09-20→總表第 113/114 項,兩條都三票一致通過、零駁回):① 產品宣告了「檢視掃描設定檔」這顆權限,但後端七支讀取功能沒有一支真的檢查它。管理員在權限矩陣把這一格取消勾選、畫面上看起來生效了(前端選單確實會藏起來),實際上那個帳號直接打 API 照樣拿得到全租戶的掃描基準庫——基準名稱、來源網址與檔名、sha256、每一版抽出來的完整規則清單。同一個檔案裡十支寫入功能每一支都有檢查,只有讀取這半漏了。跨租戶拿不到(資料庫隔離擋住)、也不能寫入,所以評中不評高。🔴 這條要更正 D2-1b 那棒我自己的說法:當時推翻卡片「15 條 route 零能力點」後我說「不應登記為 FR-098 第 80 項同題」,那句話錯在範圍——寫入那半確實不是同題(有掛),但讀取這半與第 80 項形狀完全相同(宣告了、seed 了、前端選單認、後端不守、套件自己在plugin/contract.py:28-31 寫明「read 兩項前端認、BE 不守」)。正確結論:同形不同案,應登記為獨立一筆並標「與第 80 項同形」;第 80 項本身依裁決不撤。② 手動重抽功能可以把主機打掛:每收一次請求就無條件開一條背景執行緒、把最大 50MB 的檔整包讀進記憶體、再開一支最長 900 秒的外部程序,沒有併發上限、也沒有「這一版已經在跑就別再排」的判斷,連打幾百次就是幾百條執行緒同時存在,同機所有客戶一起變慢。要先是租戶管理員。修法極便宜——服務裡早就有 running 狀態且有在寫入(extraction_service.py:97/:463/:472),retry() 只是沒去看它,加三行判斷即可;另建議補併發閘(防多版本同時被打)。同棒答完首腦改題後的重點④(是刻意的,三處自陳可證;與第 80 項同形)與 D2-1b 留下的重點⑤(18 支端點無任何原始檔下載口,真正的下載在主專案通用端點、屬 R2b)。⚠️ 這棒跑得很不順:四位研究員、前三位被砍,耗時 9 小時 22 分——覆蓋均勻度低於前面幾棒,驗證章只證明「被提出的兩條投票完整」,不證明「三個檔被徹底讀過」。詳見報告。

D2-1b 找到的那條(✅ 已驗收 09-19→總表第 112 項):客戶上傳掃描規則壓縮檔時,程式設了「最多一萬個檔」的上限想防壓縮檔炸彈,但這道上限對 .zip 形同虛設——Python 的 zip 套件在「打開檔案」那一瞬間就把整份檔案清單展開進記憶體了,程式要等它讀完才開始數、數到第一萬零一個才喊停,記憶體早就吃掉了。runner 實測(CPython 3.11.9):一個 49.7MB、裝 59.5 萬個空檔的 zip,光打開就讓常駐記憶體多吃 360MB;預設 4 個工人,連送幾次就能打掛 API。最難察覺的是日誌看起來防線運作正常(照樣回 400「壓縮檔不安全」),但錢已經付掉了。要先是租戶管理員才打得到,所以不急。修法是打開之前先讀 zip 檔尾的目錄筆數欄位、超量直接擋;且必須一併改掉 :145-148 那段寫反的註解——它明寫「不先 materialize 成 list」,對 tar 成立、對 zip 不成立,那段註解正是這個洞通過歷次 review 的原因。tar 系不受影響(逐筆迭代)。同棒推翻一項卡片錯誤前提:卡片說「15 條基準 route 一顆能力點都沒掛」,實查 18 支方法裡 10 支掛了,且掛的正好是全部寫入類、讀取類不掛是合理設計;git show 兩個歷史版本確認開卡當下就已經掛好,該項不應登記為 FR-098 第 80 項同題。另查證壓縮檔四道防護(最長後綴、magic、zip-slip 四種路徑、symlink 拒收)皆成立、七支離線轉換工具 runtime 零 import 確認不構成攻擊面。詳見報告。

D3-4b 的結果:範圍內沒有新問題(✅ 已驗 09-19,D3 收尾)。工具報的三條全部是前面棒次記過的舊帳(解密帳密明文落工單表=第 107 項、換工具繞過台數上限=第 105 項、改狀態端點守門太寬=第 59 項+第 88 項),淨新增 0 條、總表不加號。唯一的實質產出是把第 59 項的影響面擴大:那道「任何角色都放行」的守門是 2026-06-05 的產品決策(程式註解寫明),決策者 2026-09-13 已裁 viewer 不該能動別人的任務——但原本記的修法範圍只有流程模組的「完成任務/退回任務」兩支,這次查出弱點掃描模組的八支改狀態功能吃的是同一道守門,所以 viewer 同樣能對客戶正式機器發動帶帳密的掃描、反覆取消重派、刪掉失敗紀錄;修法若只改那兩支,這八支仍然敞開,已回寫總表。卡片三個重點工具一條候選都沒提,首腦逐項開檔查證三項皆無問題:逾時排程用的是具名系統身分 detection-timeout@system(走 system_context 顯式宣告繞 RLS,不是借用使用者,屬 FR-094 同題裡做對的那型)、執行歷史與通知信兩條出口共用同一份剝除實作(_ 開頭全剝、任務層密文整個 key 不出現,程式註解自己寫明「兩份實作就是忘了剝的溫床」)、代理程式回報與取消撞在一起的四種競態各有對應防護(領單只在排程中才轉、成功/失敗回呼都先看是否已取消、群組收口用資料庫排隊鎖讓併發只收口一次)。同一個檔案第二次掃,淨新增掉到 0、三條有兩條 D3-4a 也報過——坐實「低強度整檔掃對同一檔的第二次高度重疊、不互補」,切棒只切得動首腦核對那一端。詳見報告。

D3-4a 找到的那條(✅ 已驗收,登記跨 arc 總表第 107 項):按下「開始掃描」時,系統把客戶機器的登入帳密解密之後,一邊交給代理程式、一邊又存了一份明文進工單資料表(agent_tasks.params)——而那份存下來的從頭到尾沒有任何程式讀過,代理程式拿到的其實是心跳組裝時當場重解的另一份。等於產品刻意做的加密保護被這一行繞過:拿到資料庫備份、pg_dump 檔或唯讀帳號的人,一句 SQL 就得到客戶正式機房的可用 SSH 密碼與掃描器管理員 token。而且這張表沒有任何清理或保存期限(九支定期工作逐支查過、程式碼全域 grep 零命中),所以每掃一次就多留一份、無限累積。修法是刪掉那一行再補一支清存量的 migration;首腦跨三個 repo 交叉核對確認刪掉不影響代理程式(代理程式端全域 grep _credentials 零讀取,且同檔既有註解已寫明帳密屬「心跳當場注入」那一類)。另兩條工具有報但都是舊帳:查執行歷史缺參與者檢查=第 88 項、換工具繞過台數上限=第 105 項,不計新發現。卡片重點①「誰能按」工具一條候選都沒提,首腦逐支開檔核對九支寫入端點守門全在、無缺口。詳見報告。

D3-3 的結果:範圍內沒有新問題(✅ 已驗 09-19)。工具唯一報出來的那條落在範圍外(編排服務 detection_orchestration_service.py:1667),而且就是跨 arc 總表第 88 項那條已知舊帳——同一個缺口、同一個修法,只拿到更新的行號(總表記的 :1649 是方法起始行,實際查詢落點是 :1667),不計新發現、總表不加號。卡片重點④指定的三件事工具一條候選都沒提,由首腦逐項開檔查證,三項都沒問題:好幾台同時回報有資料庫排隊鎖擋住重複收口、查詢入口全部都帶條件所以踩不到總表第 70 項那個「空條件回全表」的老坑、兩支錯誤碼共 10 個都是固定短句不帶任何內部資訊(而且「找不到」與「不是你的」刻意回同一個碼,不讓人拿來試編號)。留一條體質改善建議:on_task_claimed 那句 getattr(agent_task, "uid", None) 目前靠上游先驗存在才安全,拿不到 uid 時應該直接返回而不是帶著空值往下查。這棒與另兩條線(E1、D2-1)同時在跑,三線並行首次成立,94 分鐘零重試——「每棒 ≤2,000 行」判準第三次實證有效。詳見報告。

D3-2 找到的那條(✅ 已驗收,登記跨 arc 總表第 105 項):在一張稽核任務上換掃描工具時,「要掃哪些機器」這欄不會重新檢查台數上限——先綁一支豁免上限的工具(OpenVAS)把範圍填成一整個超大網段,再只換工具不帶參數,舊範圍就原封不動留著跟到新工具底下。按執行時派工端會把它逐台展開(一個 /8 是 1,677 萬台),把後端工人的記憶體吃爆、服務倒掉。要先是專案管理者、分兩次送修改再按執行,所以不急。修法是換工具時拿「實際會生效的值」重驗,並在展開那支加一道硬上限(存量資料的保險)。首腦驗收時另外查到同一個洞的第二個發生點:換工具連派工清單都不送,同一道台數檢查一樣被繞過,修法要一併涵蓋兩個入口。另有 1 條被駁回(綁定寫入沒濾掉內部欄位,但操作者本來就是專案管理者,沒有額外好處),留作體質改善建議。同棒查證加密時機那一半確認沒問題(綁定與 BPMN 兩份同源不留明文副本、留空沿用逐列對位、三處剝除都在)。詳見報告。

D3-1 找到的那條:裝在客戶機房的代理程式把掃描報告傳回來時,說檔名叫什麼系統就存什麼——代理程式若被入侵,可以把一段程式碼取名 報告.html 存進證據庫,稽核人員點開預覽時就會被當網頁執行,等於冒用他的身分在系統裡動作。修法是只允許 html/xml/json/csv 幾種副檔名,或乾脆自己命名。同棒另查證三件事確認沒問題(內容格式謊報無效、檔案編號拿不到別台的、對外下載的五道 SSRF 防護是真的有做)。詳見報告。

D1 最嚴重的一條:檢測工具設定頁的「測試連線」端點忘了檢查權限——客戶公司裡任何一個登得進系統的人,都能叫系統把存好的掃描工具帳密解密、送到他自己指定的一台主機,等於一次拿走全公司維運帳密。修法是補一行權限宣告。另三條中風險:該端點的目標主機照單全收(內網跳板)、資料庫客戶隔離規則方向寫反(子單位反而看得到母單位,同寫法在出貨基線共 9 條)、掃描歷史少了專案參與者檢查(修法落在 D3 的檔)。已修(CM-2042/CM-2057/CM-2058/CM-2040/FR-114.1-2 等,1.21.0 出貨;隔離規則 CM-2271),詳見報告。

客戶買了「弱點掃描」功能後,系統要記住客戶的掃描工具帳密(SonarQube、ZAP、OpenSCAP 等)、讓客戶上傳或指定掃描基準(一包規則檔)、把掃描任務派給裝在客戶機房的代理程式、再把報告收回來存成證據。這條鏈同時碰到四件高風險的事:

高風險動作 套件自己的防護模組 哪一棒驗
客戶第三方系統的明文帳密兩層加密、解密後隨派工下發 detection_secret_params.py/_resolve_credentials D1 存與守、D3 出
客戶上傳的壓縮檔要解開解析 detection_profile_archive.py(自陳 bomb 檢查靠 header 自報值) D2
伺服器代替客戶去抓外部網址 safe_http_fetch.py(決策者裁不做網域白名單) D2
跑外部程式 cinc-auditor 解析規則檔 profile_extractor/inspec.py(subprocess,有 timeout) D2

每一支防護模組都自陳做了什麼、也自陳了界線——要驗的是自陳成不成立、界線外有沒有真的路。

§1

首腦開卡時已開檔核出的(未經三位檢查員投票,寫進卡當錨點)

  • 守門形狀:35 條 route 全部經 _guarded_factory 套宿主的登入檢查,每條再掛「客戶有沒有買這個功能」。🔴 原記「基準 route 一顆能力點都沒掛」是錯的,D2-1b 已推翻:detection_profile_route.py 18 支方法裡 10 支掛了 detection-profile.create/update/delete,且掛的正好是全部 10 支寫入類,8 支不掛的全是讀取類(GET 與查詢用 POST)——合理設計,不是缺口,且 git show 確認開卡當下的 a6475b4 就已經掛好、不是後來補的。不應登記為 FR-098 第 80 項「宣告了不守」同題。「有沒有買」不等於「是不是你的專案」——執行、取消、刪除掃描的專案歸屬檢查在哪一層,D3 已答。
  • 未接線 fail loudly:api/guards.py 的 _guard() 缺 adapter 直接拒絕服務,common/guard.py 同款——正面案例,與 FR-098 A1 ⑤ 同形。mount_routes(bp, auth_required=None) 預設 None 等於 35 條全裸這點,D1 已查證:assembly.py:66 在掛載前跑必填接線檢查,auth_required 在必填名單裡,缺了直接拒絕掛載——不成立。
  • DEV 唯讀實查(2026-09-16 21:50):16 張相關表——7 張開 RLS(5 張只有 1 條 FOR ALL policy)、9 張零隔離(四張疑為碼表或公版內容、四張屬 flow-control 不在本套件、detection_profile_controls 疑有主人)。8 顆能力點全客戶層。
  • 背景排程:converge_timed_out_executions 無 HTTP、無 user context 進來寫 DB——用什麼身分,與 FR-094 排程具名身分那批同題。
  • 檔數更正:跨 arc 總表原寫 251 檔,是把測試碼算進去了;正式碼 160 支(開卡當下 git ls-files 實數)。

總進度表

棒 卡 範圍 檔數 狀態 首腦驗收
D1 CM-1873 工具目錄+租戶憑證 CRUD 與測試連線+任務層 secret 信封+api/ 守門與 route 表+**plugin/ 五檔+五支 SQL(原 D4 併入) 36 ✅ 掃完(報告)|4 條:1 高 3 中** ✅ 已驗 09-17(淨新增 4 條→跨 arc 總表第 85~88 項;首腦推翻 runner 一項:解密函式在主專案有呼叫者、不是功能缺口)
D3 CM-1875 編排服務(2,498 行)+任務綁定+結果回收+執行群組狀態機+逾時排程 17→改切五小棒(每棒 ≤2,000 行) ✅ 五小棒全部掃完並驗收(09-18/09-19×4),D3-1/D3-2/D3-4a 各 1 條中風險、D3-3/D3-4b 範圍內 0 條 ✅ 五棒全驗(→總表第 101/105/107 項;D3-3/D3-4b 淨新增 0,分別補第 88 項行號與第 59 項影響面)
↳ D3-1 結果回收 同上 detection_result_handler+agent_auth+safe_http_fetch+detection_source_file 4 檔 859 行 ✅ 掃完(報告)|1 條中風險 ✅ 已驗 09-18(淨新增 1 條→總表第 101 項;駁回 2 條首腦同意;「每棒 ≤2,000 行」判準首次實證有效、研究員零重試)
↳ D3-2 任務綁定 同上 job_binding_handler+secret_params+scan_target_spec+assignment_params+job_execution_detection_tool model 5 檔 1,328 行 ✅ 掃完(報告)|1 條中風險 ✅ 已驗 09-19(淨新增 1 條→總表第 105 項;駁回 1 條首腦同意;首腦補查第二落點 :536;「每棒 ≤2,000 行」判準第二次實證有效、與 FR-111 E1 並行零重試)
↳ D3-3 狀態機與存取層 同上 domain service ×3、query entity ×2、repo impl ×2、model ×2、error code ×2 11 檔 707 行 ✅ 掃完(報告)|範圍內 0 條新發現 ✅ 已驗 09-19(工具唯一那條越界到 orchestration_service.py:1667、且重複總表第 88 項,只更新行號不計數;卡片重點④三項人工查證皆無問題;判準第三次實證、與 E1/D2-1 三線並行零重試)
↳ D3-4a 編排:派工/取消/刪除 同上 detection_orchestration_service.py 第 1~1,151 行 1 檔 2,498 行(工具整檔讀,本棒歸屬前 1,151 行) ✅ 掃完(報告)|1 條中風險(淨新增) ✅ 已驗 09-19(淨新增 1 條→第 107 項;另兩條為重複第 88/105 項不計數;首腦三 repo 交叉核對確認修法成立、且查出 agent_tasks 無任何清理機制;卡片重點①九支端點守門人工查證皆無缺口)
↳ D3-4b 編排:回收/狀態/排程 同上 detection_orchestration_service.py 第 1,152 行~尾 1 檔 2,498 行(工具整檔讀,本棒歸屬約 1,350 行) ✅ 掃完(報告)|範圍內 0 條新發現 ✅ 已驗 09-19(三條全為舊帳:第 107/59+88/105 項,淨新增 0;第 59 項影響面回寫總表——檢測模組八支改狀態端點同吃那道「任何角色都放行」的守門;卡片三重點人工查證皆無問題:逾時排程走具名系統身分、列表與通知共用同一份剝除、四種回呼競態各有防護;同檔第二次掃高度重疊、坐實工具端切不動行號範圍)
D2 CM-1874 基準 CRUD/版本/fork/公版 scope+壓縮檔驗證+SSRF 下載+cinc-auditor 子程序+分類骨幹 30→改切四小棒+D2-2 再拆兩半 ✅ 全部完成:D2-1b/D2-1a 已驗收→第 112/113/114 項、D2-3 已驗收(1 高 1 中)→第 115/116 項、D2-2a 已驗收→第 117 項、D2-4a/D2-2b/D2-4b 已驗收(皆淨新增 0),D2-2 打 17 小時零產出改判看門狗誤殺、D2-4 亦改切兩半,✅ 八小棒全部完成 ✅ 全部收口 09-20
↳ D2-1a 上傳入口守門 同上 profile route+serializer+dto 3 檔 1,058 行 ✅ 掃完(報告)|2 條中風險(零駁回);⚠️ 四位研究員、前三位被砍、9 小時 22 分 ✅ 已驗 09-20(→總表第 113/114 項)
↳ D2-1b 壓縮檔封存驗證 同上 detection_profile_archive+detection_profile_ref 2 檔 422 行 ✅ 掃完(報告)|1 條中風險 ✅ 已驗 09-19(→總表第 112 項;同棒推翻卡片「route 零能力點」前提)
↳ D2-2 基準服務主幹 同上 detection_profile_service+profile/control domain service+兩支 repo impl 5 檔 1,632 行 ⛔ 打 17 小時、25 次派研究員零產出,判定看門狗誤殺自動壓縮,改切回下面兩小棒 —
↳ D2-2a 基準服務本體 同上 detection_profile_service.py 1 檔 1,133 行(必須單獨跑) ✅ 掃完(報告)|1 條中風險(零駁回);研究員 2 位死 1 位、1 小時 42 分 ✅ 已驗 09-20(→總表第 117 項)
↳ D2-2b 基準領域與存取層 同上 profile/control domain service+兩支 repo impl 4 檔 499 行 ✅ 掃完(報告)|範圍內 0 條新發現(0 候選=面板 0 票);研究員 2 位死 1 位、1 小時 45 分 ✅ 已驗 09-20(淨新增 0;卡片重點④五項人工查證皆乾淨,並實連 DEV 核對政策與帳號;另記一條前提事實不進總表——這四個檔零租戶隔離程式碼、安全性全靠 RLS 外包,且套件自帶 RLS 腳本缺 SHARED 分支同屬「出貨基線待重產」)
↳ D2-3 解析與外抓 同上 extraction_service+profile_extractor/inspec+base+safe_http_fetch 4 檔 1,677 行 ✅ 掃完(報告)|2 條:1 高 1 中(零駁回);研究員零重試、82 分 ✅ 已驗 09-20(→總表第 115/116 項;第 115 項為本 arc 第二條高風險)
↳ D2-4 分類與版本存取層 同上 taxonomy 全鏈+version/taxonomy repo impl+四支 model+四支 query entity+scan_target_spec 16 檔 1,609 行 ⛔ 改切下面兩小棒(原 16 檔屬「多檔跨層」型,與 D2-1 同形狀;token 才是判準) —
↳ D2-4a 分類骨幹全鏈 同上 taxonomy route/serializer/dto/service/domain service/repo impl/model/query entity 8 檔 772 行(36KB) ✅ 掃完(報告)|範圍內 0 條新發現(0 候選=面板 0 票);研究員零重試、33 分 ✅ 已驗 09-20(淨新增 0;卡片重點⑥三項人工查證皆乾淨:三支寫入守門全掛、零隔離字典表照不出別人租戶、空條件回全表對字典表正當;另記兩條體質項不進總表——count_references() 註解寫「不受 RLS 收斂」與事實相反、檔頭說 GET 有 @jwt_required() 實為外掛)
↳ D2-4b 版本存取層與規則清單 同上 version repo impl+profile/control/version 三支 model+三支 query entity+scan_target_spec 8 檔 837 行(42KB) ✅ 掃完(報告)|淨新增 0(1 條成立但與第 105 項為同一條鏈,標重複);研究員派 3 死 2、4 小時 53 分 ✅ 已驗 09-20(淨新增 0;工具那條與 D3-2 第 105 項逐環對得上、獨立重現不另計;卡片重點⑥三項人工查證皆乾淨:detection_profile_controls 零隔離逐條路徑追完確認三個呼叫端都先過受 RLS 的從檔、list_controls 歸屬檢查由 RLS 做、三支 query entity 無幽靈 WHERE;另記兩條體質項不進總表——零隔離那道保護全靠約定無機制強制(第四個呼叫端寫錯會靜默回別人租戶資料)、主檔 query entity 跨欄位字串條件被 OR 起來使篩選放寬而非收緊;規則清單端點缺能力點檢查=第 113 項同病,重複不另計)
D4 CM-1876 ⛔ 作廢:六項人工查證與 plugin/、migrations/ 併入 D1;15 支空 __init__ 剔除 — 不派 —

一次只派一棒,不並行(D3 前四次失敗每次都撞上另一個掃描在跑)。掃描版本基準:jedi 套件 repo a6475b4(branch feature/review)——D1 實際掃的是 6119da5(開卡後套件有新 commit),掃描結束時 HEAD 已為 82d78bc;D3-1 掃的是 3ddd721,D3-2/D3-3/D3-4a/D3-4b/D2-1b/D2-1a/D2-3 掃的都是 955e409,D2-2a 掃的是 ac5a0d6,D2-4a/D2-2b/D2-4b 掃的都是 dfe260a5(期間套件進了兩個 FR-112 的 commit,分類鏈八個檔、D2-2b 四個檔與 D2-4b 八個檔皆一行未動,跨棒比較仍成立——D2-4b 與 D3-2(955e409)撞同一條即為此跨版本比較的直接證據)。

🔴 D2 為什麼也切成四小棒、D2-1 還要再切一半。 重縮後的 30 檔合計 6,398 行,是「每棒 ≤2,000 行」的三倍,依 import 接縫切成四小棒(1,480/1,632/1,677/1,609 行)。但 D2-1 那 1,480 行仍然跑不動——連派六位研究員全部被砍、耗掉 2 小時 53 分零產出,再切成 1,058/422 兩半後,422 行那半一位研究員零重試跑完。所以 2,000 行是上限不是目標,實際安全線比它低;後續三小棒若也卡住,照樣再對半切。

🔴 D2-1a 推翻了「行數決定成敗」的假設——檔案性質才是主變數。 五個樣本:422 行(純函式模組)✅零重試、707 行(domain service/repo/model)✅零重試、1,058 行(route+serializer+dto)⚠️ 三位陣亡第四位才完成、1,328 行(service/handler/spec)✅零重試、1,480 行(含 route)❌六位全滅。1,058 比 1,328 小卻慘得多,而兩次出事的都含 route 檔。 判讀:route 檔會把研究員引向每支端點各自的下游(service/guard/DI 容器/宿主接線)——要證明「這支端點誰打得到」就必須追到宿主那一層,這些讀取是必要的、不是走神,但 context 就是這樣膨脹的;自包含的 service 或純函式模組讀完就能下判斷,不必往外追。給後續:D2-2/D2-3/D2-4 都不含 route 檔,性質接近跑得動的那幾棒,可照原切法派;日後若還有含 route 的範圍,應把 route 單獨切一棒、不要跟其他檔混。

✅ D2-3 為這個判讀再添一個樣本,而且是最大的那個:1,677 行、4 檔、不含 route → 零重試、82 分鐘、零越界候選。它比出事的 D2-1a(1,058 行)大六成,卻跑得比誰都順——「檔案性質是主變數、行數是次要」至此有六個樣本支持。

🔴 D2-4a 把「含 route 就危險」這個判讀再修正一次:主變數是 token 量,route 只是放大器。 772 行 8 檔、含 route(155 行)→ 零重試、33 分鐘、零越界。它與出事的 D2-1a 一樣含 route,卻是至今跑得最順的一棒——差別在 route 檔本身的大小(155 行 vs 1,058 行)。判讀收斂為:route 檔會把研究員引向下游接線,所以它的行數要乘上一個倍數算 token 預算;小 route 檔不是毒。 安全線仍為約 1,000 行或單檔 60KB。

⚠️ 切小不是免費的:D2-1b 的 422 行只跑出 1 條發現,報告裡多數「沒問題」的結論是人工查的。這與 D3-4b 的觀察(同檔第二次掃淨新增掉到 0)指向同一件事——切棒保證的是工具跑得完,不是它找得更多,首腦人工查證那一端的比重會愈來愈高。

⚠️ 死因診斷更正(D2-1 六次失敗換來的):runner 第一次回報把死因歸給「工具判定思考停滯」,不準確。實查 transcript:六支裡有五支的最長思考卡在同一個數字 192 秒(3 分 12 秒)——同一秒數重複五次代表那是一道固定的上游切斷線,不是模型自己想太久。

🔴 D3 為什麼切成五小棒:每棒總行數 ≤ 2,000(含接縫檔),不只數檔數。 前四次嘗試(34/17/16 檔)都在 48~96 分鐘被工具的停滯偵測砍掉,工作流日誌坐實死因是「180 秒沒有任何工具呼叫即視為停滯」——研究員 agent 固定跑 xhigh 強度,上下文累積到 25~30 萬 token 時單次思考就破 3 分鐘。縮檔數沒用(被剔掉的都是空殼檔,工作量一行沒少),要縮的是行數。D3-1 以 859 行跑完全程零重試,判準成立;D3-2 以 1,328 行再次零重試,且是刻意與另一支掃描平行跑的(決策者要驗並行是不是死因)——平行沒有害死它,與「死因是單次思考破 180 秒」的診斷一致。樣本只有一次,「一次只跑一棒」的紀律不因此解除。D3-3 以 707 行單獨跑、同樣零重試,判準第三次實證。

⚠️ 剔除的 66 支:空 __init__ 26、mapper 10、純資料 entity 16、repository 介面殼 10、七支 profiles/tools/ 離線轉換工具(首腦 grep 確認 runtime 零 import)——工具讀了不會有產出,只燒面板額度;query entity 保留(它決定空條件行為)。

⚠️ R2b 那 11 支(agent_file_access_service、common/agent_auth、兩個 connector、job_execution_detection_tool_agent 五檔)不在本案,屬 FR-077 CM-1601(2026-09-16 已派)。主專案接線 14 檔(core/plugins/detection.py 362 行、三顆 DI 容器、兩支 readmodel、兩支 adapter)另棒。

需求討論紀錄

§2

為什麼四棒按業務鏈切、不按目錄

domain/detection_tools/ 一個目錄裡混了三條鏈(工具設定、基準、任務綁定)。按目錄切會把「憑證怎麼存」與「憑證怎麼守」切在不同棒;按鏈切則 D1 從 route 到 repo 完整看到憑證的存與守、D3 完整看到出。首腦手冊第一節「不要按 DDD 分層水平切」的直接應用。

§3

為什麼 D1 → D3 → D2(D4 作廢)

CM-1595 那條明文帳密鏈是全計畫最嚴重的事,D1(存)與 D3(出)先看;D2 是風險型態最不同的一棒(解析與外部連線),獨立看。原 D4 骨架棒在 FR-101 J2 已證明工具零產出,且首腦開卡時已核了一半(守門殼 fail loudly、能力點全客戶層、DEV 隔離狀態),剩餘六項併進 D1 讓 runner 順手驗。

§4

為什麼 D2 原本 50 檔、重縮後 30

基準鏈的 route(15 條)、service(1,133+692 行)、四道防護模組切開會讓「上傳→驗證→解析→存」這條線沒人看全。重縮剔掉 mapper、純資料 entity、七支離線轉換工具後 30 檔,實質邏輯一支沒少。

§5

共同設定

項目 值
主 session 模型 Opus 5 (1M context),不要用 Sonnet
effort low
scanRoot ~/Projects/Jedicogy/module/jedi-python-package/jedi-detection
派工節奏 一次只派一棒,驗收完才派下一棒
§6

🔴 前幾個 arc 換來的紀律(本系列必照做)

掃完要逐項回頭核工單的「重點看什麼」——快篩模式下工具常只咬住一個最顯眼的攻擊模式。本套件 detection_orchestration_service.py 2,498 行是全套件最顯眼的檔,不在你範圍時研究員仍會讀進去並把它的問題當你的發現報(FR-101 J2 整棒零產出、四條全越界就是這型)。越界一律標,人工六~七項才是主體。報告格式抄 FR-098 A2。

§7

相關座標

§8

文件

以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。

證據與盤點

文件 類型 標題 最後更新
scan-D1-tools-credentials / md 盤點證據 D1 檢查結果:檢測工具目錄、租戶憑證與守門殼(jedi-detection) 2026-09-17
scan-D2-1a-upload-route / md 盤點證據 D2-1a 檢查結果:上傳入口守門(jedi-detection) 2026-09-20
scan-D2-1b-archive-validation / md 盤點證據 D2-1b 檢查結果:壓縮檔封存驗證(jedi-detection) 2026-09-19
scan-D2-2a-profile-service / md 盤點證據 D2-2a 檢查結果:基準服務本體(jedi-detection) 2026-09-20
scan-D2-2b-profile-domain-repo / md 盤點證據 D2-2b 檢查結果:基準領域層與存取層(jedi-detection) 2026-09-20
scan-D2-3-extraction-fetch / md 盤點證據 D2-3 檢查結果:解析與外抓(jedi-detection) 2026-09-20
scan-D2-4a-taxonomy-chain / md 盤點證據 D2-4a 檢查結果:分類骨幹全鏈(jedi-detection) 2026-09-20
scan-D2-4b-version-repo / md 盤點證據 D2-4b 版本存取層與規則清單 — 掃描報告 2026-09-20
scan-D3-1-result-collection / md 盤點證據 D3-1 檢查結果:掃描報告回收(jedi-detection) 2026-09-18
scan-D3-2-job-binding / md 盤點證據 D3-2 檢查結果:任務與工具綁定(jedi-detection) 2026-09-19
scan-D3-3-state-machine / md 盤點證據 D3-3 檢查結果:執行狀態機與存取層(jedi-detection) 2026-09-19
scan-D3-4a-orchestration-dispatch / md 盤點證據 D3-4a 檢查結果:編排服務前半 — 派工/取消/刪除(jedi-detection) 2026-09-19
scan-D3-4b-orchestration-collect / md 盤點證據 D3-4b 檢查結果:編排服務後半 — 回收/狀態/排程(jedi-detection) 2026-09-19

交接與收口時間軸(handoff/,1 份)

由新到舊。每份是某一棒次交接當下的完整現況快照,看某個時間點「當時知道什麼」請從這裡進。

日期 文件 標題
2026-09-20 2026-09-20-FR-108-SUMMARY / md FR-108 弱點掃描整合套件(jedi-detection)資安掃描 — arc 收口 SUMMARY
§9

Notion 卡

卡片內容(決策紀錄、驗收條件)以 Notion 為準,本頁只記座標。

關係 卡號 標題 狀態
母案 CM-1872 FR-108 jedi-detection 弱點掃描整合套件資安掃描(四棒 149 檔,只掃不修) —
子卡 CM-1873 D1 工具目錄+租戶憑證+守門殼(36 檔) 修正待驗證
子卡 CM-1874 D2 掃描基準全鏈:上傳/解析/公版 scope/SSRF(50 檔) ✅ 已驗收
子卡 CM-1875 D3 執行編排:派工/憑證下發/結果回收/狀態機(34 檔) ✅ 已驗收
子卡 CM-1876 D4 插件骨架(⛔ 作廢,併入 D1) ⛔ 作廢(併入 D1)