狀態:全案已實作完成,決策者 2026-08-01 驗收通過(T-7.4 端到端驗收含 T-7.5/T-7.6 皆過;D1–D34 已拍板;D10 為 tombstone 空編號、D32 併入 D29,見 §2)|建立日期:2026-07-30|FR-056 檢測工具整合平台 → FR-057 OpenSCAP SSH connector 續作|2026-07-31 追加 FR-058.5 SonarQube 拉取快照(§4.6)與 FR-058.6 SonarQube 主動掃描(§4.7)|2026-08-01 追加 FR-058.7 SonarQube 上傳檔案掃描(§4.8,已實作完成並驗收) 討論稿(含流程圖與官方資源盤點):
discussion.html(四工具母案)、discussion-sonarqube-scan-mode.html(FR-058.6 SonarQube 主動掃描) 吸收 Notion CM-953「FR-057 後續:異質 OS 環境(Windows 等非 Linux 主機)的檢測支援方案」(狀態=討論),於 FR-058.2 落地時一併結案,不另開平行卡。 2026-08-01 追加 FR-058.7 SonarQube 上傳檔案掃描(§4.8,拆分表 §5.12)——scan 模式的取源擴充;決策 D25–D31 定案、D32 併入 D29;討論稿:discussion-fr058.7-upload-scan.html(v4,決策者已全數過稿)。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-07-30 | 初版設計定案;D1–D9 + D11 全數拍板(D10 隨 SonarQube 移出本案,編號保留不重排);拆分為 5 子需求 + 1 橫向前置項 | FR-058 母案 |
| 2026-07-30 | D4 改版:ZAP 證據由 PDF 改 HTML(reports API 僅寫遠端磁碟、無回傳內容端點,issue #7821);雙格式精神不變 | FR-058.1 |
| 2026-07-30 | D2 補充:新增 D12——登入後掃描僅支援傳統表單登入,SPA(json 認證 + token session 管理)列後續另案;§4.2 補實測佐證 | FR-058.1 |
| 2026-07-30 | D12 後果更正:實測揭露對 SPA 登入掃描並非「安靜成功產出空報告」,而是認證失敗累積 100 次觸發 ZAP Insights 門檻、daemon 主動關閉自己,連帶中斷共用該 ZAP 的其他任務(營運層級風險);§4.2 補完整因果鏈與 log 摘錄、標明僅限登入模式;同步更正 setup_guide(改為「請勿對 SPA 使用登入後掃描」);新增「connector 自我保護只記錄不實作」待辦 |
FR-058.1 |
| 2026-07-31 | 補 T-2.2 揭露的架構事實:CINC 裝在 Agent 本機、由它自己主動連出去,與 OpenSCAP(引擎裝在目標主機)相反。這才是 D6「雙 transport 邊際成本很低」的真正原因,現有實證(CM-973,evidence-agent dcedb4f)。更正原「對照 openscap.py 的 SSH 模式」描述——openscap.py 僅多目標處理與 host_failures 可借鏡,連線層完全不同。同步記錄 T-2.2 建立的三個實作約定(憑證走 --config 不進 argv/授權紅線以 exit 172 硬失敗釘住/probe 憑證檢查排在引擎檢查之前)。非補不可的理由:T-3.2 content 外部載入接口直接建立在此架構假設上,若誤以為 content 要送到目標主機,設計會做錯且要到端到端驗收才會發現 |
FR-058.2 / FR-058.3 |
| 2026-07-31 | T-3.2 改為「驗證 + 寫文件」,原規劃的 content 外部載入接口裁示不做——CINC 原生支援三種 content 來源,而 params.profile 直接進 cinc-auditor exec 的 argv,外部載入能力本來就在,另做抽象層只是把上游能力重新命名一次並限縮它。§4.4 補「D7 約束的達成方式」全段:三條約束逐條驗收(不硬編/不打包/換 content 零改動)、實跑佐證(兩份公開 content 對 SSH 與 WinRM 各跑通、error 0)、裸 GitHub 網址走 git fetcher 而非 tarball 下載(agent image 原無 git,首次實跑炸在 profile 解析階段,已於 agent 6d1a4fc 補裝)、tarball 形式對 git 零相依的對照實驗(移走 /usr/bin/git 後裸網址失敗、tarball 照常)。一併記錄兩個已知限制(封閉網路無法把 profile 送進 container、私有 repo 無憑證路徑),兩者併入後台 content 更新機制的評估範圍 |
FR-058.3 |
| 2026-07-31 | SonarQube 重新納入,成為第五個子需求 FR-058.5。原規劃期決策者裁示「本期不做」而移出(D10 因此成為空編號),2026-07-31 決策者裁示重新納入;決策以 D13–D16 定案(D13 pull-snapshot 整合模式 / D14 任務參數 / D15 證據格式 / D16 認證與跨版本相容),D10 編號維持 tombstone 不重用、不重排。新增 §4.6 詳細設計、拆分表加 T-5.1〜T-5.3 三子任務、§6 加對應驗收項。SonarQube 是平台第一個「拉取快照(pull-snapshot)」型工具——server 無任何觸發掃描的 Web API,掃描由客戶端 CI / sonar-scanner 執行,connector 只拉取既有分析結果組報告 | FR-058.5 |
| 2026-07-31 | 追加第六個子需求 FR-058.6:SonarQube 主動掃描模式(雛形)。決策者看到 FR-058.5 實際畫面後指出「流程好像不太對」——他要的是平台真的發動一次掃描(使用者提供源碼 → agent 跑 sonar-scanner → 推客戶 server → 輪詢 → 拉結果組報告),而非拉取客戶 CI 已推上去的既有結果。這是新需求不是 bug:D13 當時排除「agent 自跑 scanner」的理由是「agent 沒有原始碼、不可能備齊各語言 build 環境」,本案正是補上那個前提(源碼由使用者提供)並靠收斂語言範圍(D18 只做免 build 環境的 A 類)解掉後半段。決策以 D17–D24 定案(D17 模式共存形式 / D18 語言範圍 / D19 源碼來源 / D20 scanner 交付 / D21 projectKey 命名 / D22 資源保護 / D23 coverage / D24 預設值),D10 維持 tombstone 不重用、不重排。新增 §4.7 詳細設計、拆分表加 T-6.1〜T-6.3、§6 加對應驗收項。定位為雛形——取捨一律傾向最小可行,被排除項全數列於 §4.7「後續強化」並標明是裁示排除非遺漏 | FR-058.6 |
| 2026-08-01 | 追加第七個子需求 FR-058.7:SonarQube 上傳檔案掃描(設計定案)。FR-058.6 scan 模式驗收通過後,決策者提出客戶要能直接上傳源碼壓縮包做檢測(宏觀流向拍板:上傳 → tenant 儲存空間 → agent 拉下來掃 → agent 端掃完即刪 → 磁碟不夠跳錯誤)。定位為 scan 模式的取源擴充,不是新模式——scan_mode 多一個 upload 選項,取源之後整條掃描管線全複用。決策以 D25–D31 定案(D25 取源通道 / D26 參數模型 / D27 格式與限額 / D28 磁碟防護 / D29 檔案生命週期 / D30 解壓實作 / D31 顯示與紀錄),D32 併入 D29 ④ 不獨立成案。上傳時機定在任務抽屜發起執行時(非任務設定表單);server 端壓縮包保留供重掃沿用+歷史不刪(D29,推翻討論過程中「終態即刪」的中間版本)。新增 §4.8 詳細設計、拆分表加 T-7.1〜T-7.4(§5.12)。討論稿:discussion-fr058.7-upload-scan.html(v4,決策者已全數過稿) |
FR-058.7 |
| 2026-08-01 | 追加 D33:scan_mode=upload 的檢測任務不進「開始執行任務」自動派發清單(FR-058.7 驗收發現)。自動派發(FR-056.5 #5,_auto_dispatch_detection_scans)不帶檔案,對 upload 任務必然觸發 DETECTION_TOOLS_400005 且被批次 try/except 吞掉只留 warning log——PM 看到成功 toast、執行者無感,且無執行紀錄使冪等條件永遠成立、每按一次再失敗一次。定案在挑選查詢(get_detection_jobs_without_execution)排除 tool_params->>'scan_mode'='upload' 的綁定;upload 任務一律由執行者在任務抽屜上傳檔案後手動執行;pull / scan 自動派發不變。FE 補批次發佈 confirm 文案一句提示。§4.8 補「自動派發與 upload 模式」段、拆分表加 T-7.5(§5.12) |
FR-058.7 |
| 2026-08-01 | 追加 D34:檢測掃描通知加厚——九項內容+失敗通知+三管道(FR-058.7 驗收期間決策者提出)。現況兩缺陷:成功通知只有「專案 X 的檢測工具掃描已完成」一句且只發 Email;失敗完全不通知(只寫 detection_executions,使用者要自己去抽屜看)。定案三子項:① 內容擴為九項(專案/控制項群組/控制項/AO/任務名稱/工具名稱/掃描設定/時間/成功或失敗,另把現成未用的 evidence 參數用起來附報告檔名);② 失敗也通知(on_scan_failed 補查 job/binding 後發,含 error_message,收件人與成功相同);③ 管道補齊 Discord/Telegram(HTML+純文字兩版文案、i18n 6 msgid);hosts 決策者裁示可出信。新查詢走 8 層 join 鏈落 detection_job_notify_query.py;⚠️ 兩坑:catalog_controls.control_id 跨 catalog 不唯一必帶 catalog_id 過濾、AO title 對 CMMC 全 NULL 用 title→prose→part_id fallback。無 migration、無 DI 變更。§2 加 D34、§4.8 補「掃描完成通知」段、拆分表加 T-7.6(§5.12) |
FR-058.7 |
| 2026-08-01 | 收尾:全案驗收通過。決策者 2026-08-01 完成 T-7.4 端到端驗收(含 T-7.5 upload 不自動派發、T-7.6 通知加厚),T-7.1〜T-7.6 全數完成,FR-058 全案(.0〜.7 + 橫向前置項)結案。T-7.4 四個未實測項裁示:磁碟不足與解壓炸彈接受單元測試涵蓋、環境變數調限額不另實測、nginx client_max_body_size 列上版必驗項。§5.12 拆分表補各 T-7.x 完成標記 |
FR-058 全案 |
| 2026-07-30 | D9 改案:Nmap 由 CLI 型改 SSH 型 + 補 NPSL 授權法律依據。實作前發現「客戶自備」在容器化部署下無法成立(agent 在容器內看不到主機的 nmap),查證 NPSL v0.95 §3 後確認不能 bundle 且理由比原記載更強(衍生作品定義明文涵蓋「專門執行本軟體並解析其結果」的程式),同條款末段的自留出口正好讓 SSH 型成立。連帶:nmap 改為需 SSH 憑證(requires_credentials=TRUE)、T-4.1 的零憑證平台能力保留但 nmap 不再是其使用者、connection_type='CLI' 至今仍無實際使用者 |
FR-058.4 |
FR-056 建立的檢測工具整合平台是 tool-agnostic 的:後端對任何具體工具零知識(不存在 if tool.code == "openvas" 這類分支),前端純 schema 驅動渲染,加一個工具原則上只要一條 SQL seed。FR-057 又補齊了 SSH 連線型態、setup_guide 說明欄位、多檔證據回收(result_ref.upload_uids)與兩條「靜默假成功」防護通道(host_failures / content_mismatch_hosts)。
本案站在這兩層地基上,一次接入四個工具,並補上平台唯一的結構性缺口——任務層敏感參數。
範圍註記(2026-07-31):本文件各處的「四工具」指原始規劃的四棒(ZAP / InSpec / GCB / Nmap)。SonarQube 原於規劃期裁示「本期不做」移出,2026-07-31 決策者裁示重新納入,成為追加的第五棒 FR-058.5(拉取快照,詳見 §4.6);同日再追加第六棒 FR-058.6(主動掃描,詳見 §4.7)——兩者同一個工具的兩種模式(D17),不是兩個工具。既有段落的「四工具」措辭不逐一改寫。
| 順位 | 工具 | 類型 | 排這個位置的理由 |
|---|---|---|---|
| 1 | ZAP | DAST 網頁動態掃描 | 有可參考的既有實作(前同事的 auto-pentest 專案),API 呼叫序列與踩過的坑都已成文,摸索成本最低 |
| 2 | InSpec / CINC Auditor | 組態檢測(Windows / Linux) | 先做這個,GCB 的引擎就是現成的——讓 GCB 只剩驗證鏈路一件事 |
| 3 | GCB | 政府組態基準(TWGCB) | 引擎複用第 2 棒;本案只做引擎與一個 Windows 最小 Demo profile,content 產製與後台更新機制不納入(D7) |
| 4 | Nmap | 網路埠掃描 | 技術上最單純(呼叫 CLI + XML 解析)。原以「排除平台零憑證阻礙」為主要工作,2026-07-30 改案為 SSH 型後(D9),主要工作變成複用 OpenSCAP 既有的 SSH 連線模式 |
| 5 | SonarQube(拉取快照) | SAST 靜態程式碼分析 | 2026-07-31 追加(原裁示本期不做,後重新納入,見 D13–D16)。平台第一個「拉取快照(pull-snapshot)」型工具——不觸發掃描、只拉取客戶端 CI 已推上 server 的分析結果組報告;平台面零改動,工作集中在 connector 與 seed |
| 6 | SonarQube(主動掃描) | SAST 靜態程式碼分析 | 2026-07-31 追加(見 D17–D24,§4.7)。不是新工具,是第 5 棒的第二種模式——同一張工具卡加 scan_mode 參數分流(D17)。平台真的發動掃描:公開 Git repo 取源碼 → agent 跑 sonar-scanner → 推客戶 server → 輪詢處理佇列 → 拉結果(複用第 5 棒程式碼)組報告。定位為雛形,只做 A 類純源碼 7 語言 |
以「連線型態」分,這四個工具分屬三種完全不同的模式——差別在於 connector 面對的是遠端服務 API、目標主機的遠端 shell,還是 agent 本機的命令列:
| 形態 | 已上線 | 本案新增 |
|---|---|---|
| ① 連遠端服務 API | OpenVAS | ZAP(FR-058.1);SonarQube(FR-058.5,拉取快照型,見 §4.6——歸此形態但語意不同:連的是 API 卻不下掃描指令,只讀取既有結果) |
| ② 遠端登入目標主機執行(引擎裝在目標主機) | OpenSCAP(SSH / Linux) | Nmap(SSH,FR-058.4;2026-07-30 改案,原定 ③) |
| ③ agent 本機 CLI | 尚無(connection_type='CLI' 已定義但未被使用) |
仍無——原定 Nmap 走此型態,2026-07-30 改案後改走 ②(D9),此型態至今仍無實際使用者 |
| ②′ agent 本機引擎、由引擎自己連出去(引擎裝在 agent 本機,目標主機 agentless) | 尚無 | InSpec / CINC(SSH + WinRM,FR-058.2)、GCB(複用 .2 引擎,FR-058.3) |
| ①′ agent 本機執行 CLI **+ 連遠端服務 API(先在本機跑二進位,再把結果推 API、輪詢、讀回) | 尚無 | SonarQube 主動掃描**(FR-058.6,見 §4.7)——connection_type 仍是 API(憑證與連線標的都是 SonarQube server),但執行期多了「agent 本機跑 sonar-scanner」這一段 |
②′ 是 T-2.2 實作後才浮現的第四種形態(原歸在 ②,實際上方向相反)。
connection_type仍宣告為SSH/WinRM——那是目標主機開放的協定,不代表 connector 自己建連線。詳見 §4.3「架構事實」。①′ 是 FR-058.6 帶來的第五種形態。要注意的是
connection_type='CLI'(FR-056 定義至今無使用者)仍然沒有使用者——SonarQube 主動掃描雖然在 agent 本機跑二進位,但它的連線標的與憑證都是遠端 SonarQube server,歸API型而非CLI型;CLI指的是「整個任務只在 agent 本機完成、不連任何遠端服務」。
現有設計把憑證與參數分成兩處放,而只有憑證那一條有加密:
| 層級 | 存放位置 | 加密 | 適合放什麼 |
|---|---|---|---|
| 租戶層 | tenant_detection_tool_configs |
Fernet 加密 | ZAP 服務本身的位址與 API key——全租戶共用、設定一次就不動 |
| 任務層 | job_execution_detection_tools.tool_params |
明文 | 掃描目標、profile 這類非敏感參數 |
| 任務層敏感 | 目前不存在 | — | 「這次掃 A 網站用 A 的帳密,下次掃 B 網站用 B 的帳密」正好落在這一格 |
若直接把被掃網站的登入帳密塞進 tool_params,會發生兩件事:① 帳密明文躺在資料庫;② FE 執行紀錄視窗會顯示任務參數——BE 現有的 secret 剝除機制是針對租戶層設定的 secret 定義寫的,任務層參數沒有這套保護,等於帳密直接顯示在畫面上。
這不是 ZAP 專屬問題(Nmap 若要做認證掃描、GCB 若每台主機帳密不同都會撞上同一格),因此當成平台級能力補上,成為 FR-058.0,並且是 ZAP 的硬前置。
稽核員發佈任務:填 target_url、選「登入後主動」、填測試帳密
→ BE 依 param_schema 對 secret:true 欄位 Fernet 加密後寫入 tool_params(敏感欄位為密文)
→ 按「開始執行任務」:挑 agent、解密租戶工具憑證、建 agent_tasks(pending)、建執行紀錄
→ agent 心跳(mTLS,預設 300 秒)領回 pending_tasks(含當場解密的明文參數,僅傳輸中存在)
→ agent 起 daemon thread:ack → get_connector → ZAP 設定 context/登入認證/logged_in_indicator
→ ajaxSpider 爬取 → ascan 主動掃描(輪詢,支援 cancel_event / timeout)
→ core.htmlreport / core.jsonreport 雙格式(HTML 上傳當證據 / JSON 僅記憶體解析 summary)
→ 報告 bytes 走既有 blob 通道上傳 → 回報 result
→ 雲端取回存 job_evidences(source=DETECTION_TOOL)→ 通知負責人 → 依完成模式收尾
→ FE 執行紀錄顯示參數時,敏感欄位已剝除
敏感參數的生命週期:資料庫為密文、僅在派工傳輸與 agent 執行期間為明文、agent 用完即丟不落地、FE 一律剝除。
D10 原隨 SonarQube 移出本案而成為空編號;2026-07-31 SonarQube 重新納入為 FR-058.5 後,決策以 D13–D16 重新定案,D10 編號維持 tombstone 不重用、不重排(避免與先前溝通過的編號錯位),其餘編號不變。同日追加 FR-058.6 SonarQube 主動掃描,決策以 D17–D24 定案(全數已拍板,無待決策項)。2026-08-01 追加 FR-058.7 SonarQube 上傳檔案掃描,決策以 D25–D31 定案(全數已拍板)、D32 併入 D29 ④ 不獨立成案(編號保留、內容指向 D29)。同日 FR-058.7 驗收期間追加 D33(upload 任務不進自動派發清單)與 D34(檢測掃描通知加厚:九項內容+失敗通知+三管道)並定案。
| # | 決策 | 定案 | 被排除的方案與原因 |
|---|---|---|---|
| D1 | 任務層敏感參數的實作範圍 | 加密存取 + FE 執行紀錄顯示剝除 + 稽核 log 剝除,一起做。 只做加密等於「加密了卻在 UI 洩漏」——資料庫是密文,畫面上照樣看得到帳密,防護實質等於零。 | 「只做加解密、UI 顯示留待後續」——排除:稽核 log 寫入敏感值等於在另一張表留下明文副本,防護有洞就等於沒有。 |
| D2 | ZAP 掃描深度的預設值 | schema 三種模式都支援(被動 / 主動 / 登入後主動),預設給被動;主動類選項在 FE 加明確警語(會產生攻擊流量,請確認已獲目標方授權)。 | 預設主動——排除:預設值選錯的代價不對等。預設被動最多是掃得淺;預設主動則可能在使用者未察覺的情況下對正式環境送出攻擊流量。 |
| D3 | ZAP 服務的部署形態 | 客戶自備已安裝的 ZAP daemon(同 OpenVAS / OpenSCAP 模型),agent 只負責連上去;setup_guide 寫清楚 daemon 啟動指令與 API key 設定方式。使用者公司已安裝 2.17.0,這條路本來就是現況。 |
agent 端自帶 ZAP container(參考碼的 ZAPContainerDomainService 那一套)——排除:agent image 膨脹且要背負拉容器與版本維護責任,與既有兩個工具的模型不一致。 |
| D4 | ZAP 證據格式 | (2026-07-30 改版)雙格式取報告,精神對齊 OpenVAS connector 既有模式——core.htmlreport() 回傳的 HTML bytes 上傳當證據(檔名 ZAP掃描報告_<target>_<date>.html)、core.jsonreport() 回傳的 JSON 只在記憶體解析出 summary 不上傳。證據池維持人可讀,統計數字又有結構化來源,且 ZAP 端零設定、維持 D3 部署模型。 |
只存單一格式——排除:只存人可讀格式則 summary 要靠文字解析(脆弱);只存 JSON 則證據池不是人可讀。兩份都上傳——排除:在證據池塞入兩份等價內容。 2026-07-30 改版:原定案「PDF 上傳當證據( reports.generate)」實作時發現不可行——ZAP 的 reports API(reports.generate)只把檔案寫進 ZAP daemon 主機的磁碟、回傳路徑字串,官方沒有回傳檔案內容的端點(issue #7821 至今未實作);而 D3 定的部署模型是遠端 daemon,agent 取不到那個路徑的檔案。改版時被排除的走法:①「PDF + 要求客戶開檔案傳輸通道」——違反 D3 部署模型,等於從後門把部署負擔帶回來;②「檔案傳輸為主、HTML 自動退回」——兩條路徑的長期維護與測試成本換一個罕用 fallback,違反本案「不做無意義測試」紀律。已知風險與處置:證據池 PDF 預覽鏈不吃 HTML,接受「可下載、不可預覽」為第一批出貨標準;若日後要求可預覽,可在 agent 端用既有 LibreOffice --convert-to pdf 補(ZAP 端仍零設定),但轉檔品質未驗證故不預做。 |
| D5 | InSpec 版本選擇 | CINC Auditor(Apache 2.0 自由重建版,Progress 官方認可的 drop-in 替代品,功能等價)。授權乾淨,採購與部署文件都要寫明。 | 官方 InSpec 6+ 商業 binary——排除:需接受 Chef EULA 並持有 license key,會把 license key 管理問題帶進每一個客戶部署。 |
| D6 | InSpec connector 的 transport 範圍 | SSH + WinRM 雙 transport 一次到位。 同一套 CLI 與 profile 格式,多做一個 transport 的邊際成本很低,而 GCB 的 Linux 線立刻就能複用。這條同時是 D7 引擎層的實作依據。 | 只做 WinRM(Windows)——排除:GCB Linux 那條線之後還要回頭補一次,等於同一份工作做兩趟。 |
| D7 | GCB 的引擎優先與 content 後置 | 引擎層:Windows 與 Linux 雙邊一次到位,由 FR-058.2 的 CINC Auditor connector 承擔,GCB 不另立引擎、不另寫 connector。content 層:本案只做一份 Windows 最小 Demo profile,證明端到端鏈路(WinRM 連線 → 認證 → profile 執行 → 結果解析 → 證據上傳)可運作即可。FR-058.3 的驗收條件是「鏈路可運作」,不是「涵蓋多少規則」。 另有本案必須做到的設計約束:content 不可硬編進 connector 或 agent image,必須可從外部載入。 | ① content 產製(逐條 TWGCB-ID 轉檢查規則、Windows GPO 產生器、Linux 人工撰寫)——排除出本案:那是持續性人力投入,且是最沒有技術風險的部分,一條規則也能證明鏈路通。② 後台 content 更新機制(上傳/版更/套用)——排除出本案,日後另案。③ macOS 15(TWGCB-01-015)——排除:基準 2026/6 才發布、版本尚新,且現有 connector 未曾處理 macOS。④「接口留到日後再補」——排除:事後補要改動 connector 結構,現在留成本極低。 |
| D8 | GCB 的 content 格式 | **InSpec profile。 Ruby DSL 比 XCCDF + OVAL 好寫也好維護,且 Windows GCB 只有這條路走得通。與 D7 引擎層一致:既然兩邊都由同一套 CINC Auditor 執行,content 自然只需維護一種格式。 | XCCDF + OVAL(沿用 OpenSCAP)——排除:同一份 TWGCB 基準要用兩種語法各寫一次(Linux 用 XCCDF、Windows 用 InSpec),TWGCB 改版時要改兩份,長期成本明顯較高。引擎統一的真正目的是 content 格式統一。** |
| D9 | Nmap 的整合型態與零憑證放行 (2026-07-30 改案) |
(改案後)Nmap 走 connection_type='SSH'——nmap 由客戶安裝在自己指定的主機上,agent 透過 SSH 登入該主機執行,與 OpenSCAP 完全同構。法律依據:NPSL v0.95 §3 末段明文不主張控制「執行使用者早已安裝在自己系統上的 nmap」的軟體(原文見右欄),SSH 型正落在此描述內。連帶:nmap 需要 SSH 憑證(requires_credentials=TRUE)。(原定案,平台能力部分保留)放寬 start_execution 的憑證檢查——在 detection_tools 增加「是否需要憑證」的宣告欄位,讓平台知道零憑證是一種合法狀態。此為平台級能力,改案後仍然正確且保留,只是 nmap 不再是它的第一個使用者,日後其他真正零憑證的工具會用到。 |
為 Nmap 建立一筆空的租戶設定當佔位——排除:會留下一筆語意不明的資料,日後看到的人無法判斷那是刻意還是遺漏。 2026-07-30 改案(原定案為 connection_type='CLI'、agent 本機執行、不 bundle 理由僅記「NPSL 授權」):① 觸發點:實作前發現「客戶自備」在容器化部署下有實務障礙——agent 跑在 Docker container 內,看不到主機上安裝的 nmap(容器有獨立檔案系統)。原 CLI 設計隱含假設 nmap 在 agent 可執行範圍內,但「不 bundle」又「不能裝進容器」,兩個條件互相矛盾。 ② 查證 NPSL 後確認不能 bundle,且理由比原記載更強。 NPSL v0.95 §3(出處 https://svn.nmap.org/nmap/LICENSE)明文列舉的「衍生作品」定義包含這一條:Is designed specifically to execute Covered Software and parse the results (as opposed to typical shell or execution-menu apps, which will execute anything you tell them to).我們的 connector 正是「專門呼叫 nmap 並解析其 XML 輸出」,正中此條。因此「只用子行程呼叫、不連結函式庫」這個 GPL 慣用的免責理由在 NPSL 底下不成立——該條款是特意加來堵掉這個論點的(NPSL 基於 GPLv2 但加了額外限制)。GPLv2 的 mere aggregation 條款也救不了,因為它只適用於「not based on the Program」的作品,而 §3 已把我們定義為 based on。官方 Legal Notices( https://nmap.org/book/man-legal.html)另明說免費授權「doesn't allow Nmap to be used and redistributed within commercial software or hardware products」(明列 appliances / virtual machines / traditional applications),並為此販售 Nmap OEM Edition(https://nmap.org/oem/)。③ NPSL §3 最後一段自己留了出口:軟體若解析的是使用者提供的結果、或執行的是「使用者早已安裝在自己系統上的 nmap」,授權方不主張控制( Licensor does not purport to control through this license any software which does not require the rights granted herein)。SSH 型態正好落在這個描述裡——nmap 是客戶裝在自己主機上的,我們只是遠端下指令。④ 評估過並排除的方案: • 掛載主機 binary 進容器( -v /usr/bin/nmap:/usr/bin/nmap:ro 加上共享函式庫目錄)——排除:nmap 非靜態連結,依賴 libssl / libpcap / liblua 等一串共享函式庫,掛主機的 lib 目錄進容器會與容器內 glibc 版本賭運氣,且可能蓋掉容器自身函式庫把 agent 弄壞;架構也綁死 x86_64。把「客戶自備」變成一個需要客戶理解共享函式庫的任務,故障時難以遠端診斷。• 客戶自建一層 image(以我方 image 為 FROM,自行 apt install nmap 後 build)——技術上可行且乾淨(函式庫由套件管理員解決),法律上也成立(nmap 由客戶裝入,非我方散布)。排除理由:客戶每次 agent 升版都要改 Dockerfile 版號並重新 build,是持續性負擔;且「誰把它裝進去」雖在法律上是關鍵,技術結果與我方 bundle 完全相同,此種形式上的區隔在交付溝通上彆扭。• 購買 Nmap OEM 授權——本案不採,未評估成本;若日後產品策略需要零客戶設定的 nmap 整合,這是唯一能 bundle 的合法途徑,列為備查。 ⑤ 拍板走 SSH 型的理由:「最沒爭議」——法律上落在 NPSL 自留出口內、技術上與已上線的 OpenSCAP 同構(connector 可複用 SSH 連線處理、私鑰暫存、 host_failures 逐台回報)、客戶只需在指定主機 apt install nmap,無任何 docker 操作。⑥ 連帶影響:nmap 不再是零憑證工具( requires_credentials 應為 TRUE);T-4.1 的零憑證放行平台能力保留(是平台級能力,只是換掉第一個使用者);connection_type='CLI' 因此仍然沒有實際使用者(FR-056 定義至今未被用過),原本「Nmap 是 CLI 型態首個實際使用者」的價值消失;agent 端 connector 範本從「本機子行程」改為對照 openscap.py 的 SSH 多目標模式。 |
| D10 | — | tombstone 空編號。原隨 SonarQube 移出本案而懸空;2026-07-31 SonarQube 重新納入為 FR-058.5,決策以 D13–D16 重新定案,本編號維持 tombstone 不重用(避免與先前溝通過的編號錯位)。 | — |
| D11 | _TOOL_ID_TO_CODE 硬編映射的處理 |
改為派工 payload 夾帶 tool code,agent factory 改依 code 取 connector,移除硬編 id 映射。趁本案一次加四個工具時一併解決。 | 維持硬編 id 映射——排除:一次加四個工具會讓脆弱性顯著放大。DEV / STG / POC 三環境的 seed id 必須完全一致,錯一位就整條派工錯配到別的 connector(例如 ZAP 任務被送給 OpenSCAP 執行),而且這種錯誤不會拋例外、只會安靜地跑錯工具。 界線說明:這項改動不會讓新增工具完全免於動 agent——connector 實作本身仍必須裝進 agent,因此每加一個全新工具,agent 仍需更新一次。D11 消除的是另一個更脆弱的問題:額外維護一份 id 對照表、且三個環境的 seed id 必須完全一致。 |
| D12 | ZAP 登入後掃描的 SPA 支援範圍 (2026-07-30 追加,實測後補列) |
本案(第一批)只支援傳統表單登入(form-based),param_schema 的 login_url / login_username / login_password / logged_in_indicator 四欄即為此模型。SPA(單頁式應用)的登入後掃描列為後續另案,本案文件與 setup_guide 明白標示此限制,不做半套。技術原因有三,且三者必須同時解決才會通:① 表單登入無效——SPA 的登入是 JS 呼叫 API 取 token,頁面上沒有傳統表單提交;ZAP 的 form-based 認證是對登入頁 POST 帳密欄位,對 SPA 不會發生作用,需要的是 json-based 認證(對登入 API 送 JSON payload)。② token 不會自動帶——SPA 多用 JWT 存 localStorage、靠 Authorization header 帶;ZAP 預設 session 管理是 cookie-based,不會把 token 塞進後續請求,需要 script-based session management(要撰寫並上傳 ZAP script)。③ logged_in_indicator 對 SPA 不可靠——ZAP 比對的是 HTTP 回應的原始內容,而 SPA 初始 HTML 是空殼、登入後文字要 JS 渲染才進 DOM,原始回應裡找不到,ZAP 會誤判未登入並反覆重跑登入。實測後果更正(2026-07-30):原記載「掃描仍會完成、只是涵蓋範圍侷限於登入前頁面」是錯的。第 ③ 項的重跑登入是無限迴圈,認證失敗會持續累積,達到 ZAP 2.17.0 Insights 機制的門檻後 ZAP daemon 主動關閉自己,掃描根本不會完成。log 原文關鍵行: Shutting down ZAP due to High Level Insight: HIGH : EXCEEDED_HIGH : http://192.168.50.151 : insight.auth.failure : 100,隨後 Discard Session → ZAP 2.17.0 terminated. → zap exited with code 2 (restarting)。由於 ZAP 是全租戶共用服務,單一 SPA 登入任務會連帶打斷同一台 ZAP 上其他正在執行的掃描(實測同時發生:另一任務在 ZAP 重啟空檔收到 Connection refused)。這已非報告品質問題,而是營運層級風險。範圍界定:僅限登入後掃描模式——被動與主動模式不建 context、不設認證、不比對特徵字串,不累積認證失敗,碰不到 Insights 門檻。 |
① 本案一併擴充 json-based 認證 + script-based session management——排除:那是「認證方式擴充 + script 管理 + 測試站台」三件獨立工作,塞進第一批會拖住已可出貨的部分。第一批的核心價值(ZAP 接入、HTML 證據進證據池、任務層敏感參數平台能力)不依賴登入後掃描,被動與主動掃描已實測可交付。 ② 只做 json-based 認證、session 管理留待日後——排除:沒有中間地帶。認證送出去了但 token 帶不進後續請求,ZAP 一樣掃不進登入後頁面,結果與完全不做相同,卻已付出一次工程成本並在 UI 上留下「看起來支援了」的誤導。 ③ 改用 logged_in_indicator 以外的判定繞過——排除:那只解第三個原因,前兩個原因未解則掃描本來就進不去,判定準不準沒有意義。 |
| D13 | SonarQube 整合模式(pull-snapshot) (2026-07-31 追加,FR-058.5) |
任務執行拉取最近一次分析結果組報告,不觸發掃描。 本質差異:SonarQube Server 沒有任何觸發掃描的 Web API——掃描由 sonar-scanner 在有原始碼的環境(客戶 CI)執行後把結果推上 server,Web API 只能讀取既有結果(唯一例外是 SonarQube Cloud SaaS 的 Automatic Analysis,與 self-hosted 無關)。因此 connector 是平台第一個「拉取快照(pull-snapshot)」型工具。報告必含分析時間(api/project_analyses/search 最新一筆 date)供稽核員判斷資料新鮮度;setup_guide 與工具描述明寫「本平台不觸發掃描,掃描由客戶端 CI / sonar-scanner 執行」。 |
agent 端自跑 sonar-scanner——排除:agent 沒有原始碼,也不可能備齊各語言 build 環境。 |
| D14 | SonarQube 任務參數 (2026-07-31 追加,FR-058.5) |
project_key 必填;branch 選填——branch 分析是付費版(Developer Edition 起)功能,Community Build 只有單一 branch。不填=不送 branch 參數(Community 相容);填了但 server 不支援時 connector 回明確錯誤訊息。 |
① branch 必填——排除:Community 使用者直接不能用。② 自動偵測 edition——排除:多一次 API 往返,換不掉一個明確錯誤訊息的價值。 |
| D15 | SonarQube 證據格式 (2026-07-31 追加,FR-058.5) |
connector 拉 JSON 自行 render 自包含 HTML 上傳當證據——Community Build 沒有任何報告匯出功能(付費版才有 PDF),自組報告是必做項不是 fallback。報告內容=Quality Gate 狀態+核心度量(bugs / vulnerabilities / code_smells / security_hotspots / coverage / duplicated_lines_density / ncloc)+severity 分佈+issue 清單(severity 由高至低取前 200 條,報告註明「僅列前 200 條/總數 N」——防 api/issues/search 的 10,000 筆硬上限;統計數字一律用 facets 與 measures 取全量,不逐頁撈)+Security Hotspots+分析時間。summary 數字從 measures API 取(bugs / vulnerabilities / code_smells / security_hotspots / quality_gate),完全繞開 MQR 模式的 severity 語意差異。出貨標準同 ZAP:證據可下載、不可預覽。 |
① 依賴 server 端報告匯出——排除:Community Build 根本沒有此能力,PDF 匯出是付費版功能。② issue 清單逐頁撈全量——排除:api/issues/search 有 10,000 筆硬上限(第 21 頁起報錯),大專案必炸;且統計本可由 facets / measures 全量取得,不需逐條。 |
| D16 | SonarQube 認證與跨版本相容 (2026-07-31 追加,FR-058.5) |
憑證=base_url + user token(squ_ 前綴,secret,HTTP 用 Authorization: Bearer header),token 所屬帳號只需目標專案 Browse 權限。issue 解析 impacts[] 優先(MQR 語意,10.2+ 回應就有),缺席時 fallback 舊 type+severity(相容 9.9 LTS 至 2026.x LTA)。probe=api/system/status(免認證,驗連通與服務狀態)+帶 token 呼叫 api/authentication/validate(陷阱:匿名呼叫也回 valid:true,必須帶認證 header 呼叫結果才有意義)。 |
sqp_ / sqa_ analysis token——排除:那兩種只能執行分析、不能呼叫讀取 API,對 pull-snapshot 型 connector 無用。只認 impacts[] 或只認舊 type+severity——排除:單押一邊會在另一個版本區間解析失敗,雙軌 fallback 才能覆蓋 9.9 LTS 至 2026.x LTA 全區間。 |
| D17 | SonarQube 兩模式的共存形式 (2026-07-31 追加,FR-058.6) |
ZAP 式——同一個 sonarqube 工具 + scan_mode 參數分流。 工具清單只有一張卡,租戶層憑證設定一次、兩模式共用,模式差異只出現在「建任務時填什麼參數」。四個理由:① 概念一致——使用者心裡想的是「我要用 SonarQube」,不是「SonarQube-讀取版 / SonarQube-掃描版」,出現兩張同名卡等於把內部實作差異推給使用者;② 設定只做一次——server 位址與 token 填一次兩模式都能用,走兩張卡要填兩次,換 token 時漏改一邊會變成「一個模式能用一個不能用」的怪問題;③ FE 零改動——condition:{field,value} 是現成機制(DetectionConfigField.vue:23-27 的 visible computed、:31-34 對不可見欄位跳過必填驗證),ZAP 已實證;④ 憑證共用(見右欄更正)。 |
GCB 式(另立 tool code、工具清單兩張卡)——排除:它唯一的優勢是「憑證 schema 各自乾淨」,而該優勢在本案不存在,只剩「工具清單多一張同名卡」的代價。 重要更正 · 規劃初期的「憑證分流痛點」在本案不成立:曾以為兩模式需要兩組憑證(pull 要讀取 token、scan 要推送 token + Git 認證),因而擔心 config_field_schema 是工具層、無法依任務參數分流。此顧慮被兩件事推翻——① squ_ User Token 推得上去也讀得回來(只有 sqp_ / sqa_ analysis token 是「只能推不能讀」),兩模式共用同一把 token;② D19 定案只支援公開 repo → 根本沒有 Git 憑證這回事。實查 DEV config.detection_tools,sonarqube 的 config_field_schema 目前就 base_url + token 兩欄,兩模式完全共用、一個字都不用改。InSpec 雙 transport 那個「probe 第一段檢查憑證完整性」的先例仍是有效的一般性技術參考,但不是本案的痛點——記錄於此以免日後誤讀。 |
| D18 | 主動掃描的語言範圍 (2026-07-31 追加,FR-058.6) |
只做 A 類純源碼語言:Python / JS / TS / Go / PHP / Ruby / Kotlin。 這一群的共同特徵是不需要編譯產出,把源碼交給 scanner 就能分析,agent 端零額外環境需求——正好解掉 D13 當初排除 agent 自跑 scanner 的第二個理由(「不可能備齊各語言 build 環境」)。 | Java(B 類,需 .class)/C·C++(AutoConfig)——排除:技術上都評估過可行或部分可行,但雛形階段一律收斂(各自的已知技術結論見 §4.7「後續強化」)。Scala——排除:官方文件完全沒表態是否需要 bytecode,傾向 A 類但必須實測,不可憑推論寫進使用者說明。C# / VB.NET / Objective-C——不是取捨而是技術上真的做不到:SonarScanner CLI 明文 unsupported,begin/build/end 是硬性架構(分析發生在編譯器內部的 Roslyn analyzer),永遠只能走 pull 模式。縮範圍後浮現的必要配套(不是決策項,是配套):使用者不知道範圍而送錯語言時,兩種失敗方式嚴重性差很多——Java 是硬失敗(沒有 .class,scanner 直接報錯中止,難看但安全);C# 是靜默淺掃(CLI 不支援但不會中止,很可能產出一份看起來正常、實際幾乎沒分析到東西的報告,稽核員可能把「沒掃到」誤讀成「很乾淨」)。三件事必須一起做:① 事前——UI 與 setup_guide 明列支援語言並寫清楚「清單外請改用拉取模式」;② 事中——可考慮讓使用者宣告主要語言,非清單內直接擋下不送進 scanner;③ 事後——報告產出時若分析檔案數 / 程式碼行數異常偏低,明確標示而非靜靜輸出(「零發現」與「沒掃到」必須看得出來是兩件事)。具體形式(擋在哪一層、訊息怎麼寫)留到實作計畫階段定。 |
| D19 | 主動掃描的源碼來源 (2026-07-31 追加,FR-058.6;2026-08-01 註記:右欄排除的「檔案上傳」已由 FR-058.7 立案實作,見 D25–D31 / §4.8——當時排除是雛形階段取捨,非永久裁示;本列原文保留為歷史紀錄) |
公開 Git repo——一個 text 欄位填 URL,不需要任何憑證。決策依據是兩條路線的成本量級差:Git URL 路線要動 0 個新層(param_schema 加一個 text 欄位;加密鏈一行不用改;agent 的 git 已裝且 CINC/InSpec 已實證「參數填 Git URL、引擎自己去拉」在本平台跑得通;FE 零改動,text 是既有型別)。 |
檔案上傳——排除:要動三層——① FE 元件(param_schema 目前 7 種型別無 file,權威來源是 DetectionConfigField.vue 的渲染分支,且沒有 else 分支=未知型別靜默不渲染,欄位會直接消失在畫面上且不報錯);② 上傳中繼(源碼包要先進雲端再送 agent,而 RemoteAgentAdapter 只在租戶 storage_type=REMOTE_AGENT 時才被實例化,MINIO 模式的客戶根本建不出這個物件,會出現「A 客戶能用、B 客戶按下去就爆」);③ 派工鏈路(payload 目前只有 JSON params,agent 端收到 task 後沒有任何下載檔案的邏輯,兩端都要補)。額外還要自訂大小上限、暫存空間與解壓炸彈防護。私有 repo 認證——排除:雛形階段先求打通,且平台現況本來就只支援公開 repo(CINC/InSpec 的 Git URL 用法同樣只支援公開 repo,私有認證在本平台完全沒做過)。 決策者原話兩種都提了(「可能上傳 或是提供 server 位置」),但成本量級差一個數量級——雛形階段選成本低的那條先把鏈路打通。 |
| D20 | sonar-scanner 怎麼進 agent (2026-07-31 追加,FR-058.6) |
sonar-scanner CLI 打包進 agent image(同 CINC / InSpec 模式)。平台專屬 zip 自帶 JRE,且 v6.0 起有 JRE auto-provisioning(搭配 SonarQube 10.6+,驗收用的 server 是 26.7.0.124771 遠超門檻)→ 不必額外裝 JDK。Node 同理——JS/TS analyzer 自帶 Node runtime。 | agent 內跑官方 sonar-scanner-cli 容器——排除:實查確認 agent 容器無 docker CLI、未掛 socket、無 privileged / DinD,且平台先前已刻意避開此路線兩次(zap.py:7-8、nmap.py:1-6)。⚠️ 易誤判:poetry.lock 內的 docker 7.2.0 是 testcontainers 經 jedi-common 拉進來的測試相依洩漏,不是既有能力,不可據此以為 agent 能操作 docker。執行時下載 scanner——排除:客戶 agent 常在不能連外的內網。 |
| D21 | 推上客戶 server 的 projectKey 命名 (2026-07-31 追加,FR-058.6) |
**一律加前綴 Guidant-AI-<使用者輸入>——使用者只填後半段,前綴由平台組上。 |
直接用使用者輸入——排除**:① 可能撞到客戶既有專案 key,把一次性稽核掃描結果覆蓋到他們正式的專案歷史上;② 客戶無法辨識哪些專案是本平台建的。前綴同時是 R4 寫入副作用的緩解手段——「撞號」與「無法辨識來源」兩個問題由它解決,但「每掃一次多一筆專案」本身沒有解(SonarQube 無專門開關,想擋只能移除 token 擁有者的 "Create Projects" 全域權限,這點寫進 setup_guide)。 |
| D22 | 資源保護與暫存位置 (2026-07-31 追加,FR-058.6) |
只做掃描逾時上限;暫存用磁碟型 tmp 目錄、掃完即刪。agent 容器內 /data/content 是 :ro,可寫的是容器自身 /tmp(已有 PDF_CACHE_DIR=/tmp/pdf-cache 前例,且刻意不做 bind-mount 以避開跨裝置 rename 的 EXDEV)。 |
磁碟配額 / 記憶體上限控管——排除:雛形階段不做,風險記錄在 §4.7「風險與已知限制」R2。 ⚠️ /dev/shm——排除且必須明確避開:既有暫存慣例是 inspec.py:197-200 的 _SHM_DIR = "/dev/shm"(0600 + contextmanager 必刪),但那是 tmpfs 記憶體型,是為「小的憑證檔」設計的。解壓一個幾百 MB 的源碼包到記憶體會直接吃掉容器的 RAM 配額,不可無腦沿用。 |
| D23 | coverage 紅燈的處理 (2026-07-31 追加,FR-058.6) |
**照實顯示,不濾除、不加特別說明。 決策者原話:「主要掃品質就好」。 | 濾除 coverage 條件後再判定 Quality Gate——排除**:等於我們自己改寫客戶的判定標準。加醒目說明——排除:雛形階段不做。 事實記錄(§4.7 R3):SonarQube 自己不產生 coverage、只匯入第三方報告,而我們不跑測試 → 報告必然無 coverage,客戶 Quality Gate 若含覆蓋率條件會亮紅燈,而那個紅燈不代表程式碼有問題。Go 更糟——無 coverage 資訊時視為「全部未覆蓋」而非「無資料」,會顯示 0%。實作時仍建議在 setup_guide 提一句讓管理員事前知道。 |
| D24 | scan_mode 的預設值(2026-07-31 追加,FR-058.6) |
預設 scan(原始碼掃描)。 決策者原話:「預設不要,預設一定是 scan mode」。理由:主動掃描是本需求的主線意圖,pull 只是先落地的那一個;預設值應指向主要使用情境,讓使用者少一步操作。連帶要求——事前告知從「最好有」升級為「必須有」(三項配套實作必做):① 建立任務時的提示必須顯眼,hint 文字不夠(使用者會略過),建議在 scan 模式的參數區塊做明顯標示或送出前給明確提示(具體形式列實作建議,不在此定死);② setup_guide 必須明講「本工具預設為原始碼掃描模式,執行時會在貴公司的 SonarQube 建立 / 更新專案(key 為 Guidant-AI-<識別>)」,讓管理員在設定階段就知道,而非看到專案清單多東西才發現;③ 收緊模式寫進 setup_guide(預先建好專案 + 移除 token 擁有者的 "Create Projects" 全域權限)。 |
預設 pull——排除:雖然 pull 純唯讀可回復、scan 會寫入客戶系統不可回復,但那是配套要解決的問題,不該讓預設值偏離主線意圖。 ⚠️ 與 ZAP D2 取捨方向相反是刻意的,不是疏漏。D2 預設被動的理由正是「預設值選錯的代價不對等」,本案結論方向相反,差別在代價的量級: • ZAP D2 誤觸=對目標送出真實攻擊流量,影響第三方外部系統——可能觸發對方告警甚至造成服務中斷,不是我們能收拾的。 • 本案 D24 誤觸=在客戶自有的 SonarQube 建立一個專案——一筆專案紀錄、客戶可自行刪除,且有 Guidant-AI- 前綴可辨識來源(D21)。兩者都是「預設值有副作用」,但一個是對外部系統送攻擊流量、一個是在客戶自有系統留一筆可刪除的紀錄,量級不同故結論不同。 |
| D25 | 上傳檔案的取源通道:agent 如何拿到源碼包 (2026-08-01 追加,FR-058.7) |
BE 新開 agent 專用純 mTLS 檔案下載端點 + 任務綁定驗證——GET /api/1.0/agents/files/<uid>,認證比照 tasks ack / result 的「純 mTLS 不掛 jwt」形狀(api/remote_agent/__init__.py:34-38 先例);授權由 client 憑證識別 agent 身分後,驗證該 uid 必須屬於該 agent 名下 pending / running 任務的 source_file(防任意檔案讀取,任務終態後參照即失效)。心跳 payload 夾檔案參照,agent 收到任務後主動串流下載進 scan workspace 並驗 sha256。BE 對 minio / remote_agent 後端串流轉發不落地(遵守 FR-039「雲端任何路徑不得把 binary 寫進雲端磁碟」不變式)。 |
簽章 URL(複用使用者面 GET /file/download/<uid>,心跳夾簽好的 URL)——排除:簽章 token TTL 只有 120 秒(common/authz/signed_token.py:41),是為瀏覽器「點了就下載」設計;agent 從心跳收到任務到實際開始下載的間隔不可控(前面可能排了別的任務),時效模型對不上。且把使用者面資料通道與 agent 資料面混用,授權語意混濁。推檔給 agent(派工時 BE 主動 POST /blob)——排除:推檔時機與任務生命週期耦合(派工瞬間 agent 可能離線);POST /blob 走 FileUploadService 永久落地(寫 agent 自己的 upload_files 表),與 /tmp 即用即棄語意不同,agent 端要另管清理;且 tenant 儲存為 remote_agent 時檔案本來就在 agent 上,會變成「推自己」。附註:tenant 儲存為 remote_agent 且掃描 agent 恰為同一台時,BE relay 會繞一圈(agent→BE→agent)。v1 接受此低效(罕見組態),列 §4.8 known limitation。 |
| D26 | 上傳掃描的參數模型:scan_mode 三選項 + 執行時輸入 (2026-08-01 追加,FR-058.7) |
scan_mode 增 upload 選項——param_schema 出 v3(INSERT 新版 + v2 is_current=FALSE,版本策略照舊),v3 變更僅此一欄(options 由 pull / scan 增為 pull / scan / upload,附 hint「上傳模式於發起執行時提供源碼壓縮包」;預設值沿用 D24 裁示);project_key_suffix 沿用(scan 與 upload 皆出現)。source_file 不是 schema 欄位,而是執行時輸入:任務抽屜發起執行時上傳、隨 execute request body 送出,由 start_execution 合併進 agent_tasks.params(_source_file,形狀見 §4.8)。 |
file 型 schema 欄位(把上傳放任務設定表單;初版方案,決策者裁示排除):會導致——① 每掃新版都要改設定再執行(兩段式繞路,且任務設定僅專案管理者可改,執行者不見得改得動);② 語意錯位(壓縮包是「這一次要掃的東西」——特定時點的源碼快照,該在執行當下提供,不該混進「怎麼掃」的設定表單)。改到執行時後還附帶兩個好處:FE 的 DetectionConfigField 完全不用動(不需新型別分支)、孤兒檔視窗大幅縮小(上傳與執行幾乎是同一個動作)。scan 選項內做 repo_url / source_file 互斥——排除:condition 機制是單層 {"field","value"} 等值比對,表達不了「二欄擇一」;required 驗證也會亂(兩欄都掛 required 就都必填,都不掛就都可空)。硬做要改 condition 語法引擎,成本遠超過多一個選項。 |
| D27 | 上傳檔案格式與限額 (2026-08-01 追加並定案,FR-058.7) |
格式:收 .zip / .tar / .tar.gz / .tgz;裸 .gz(單檔 gzip)不收——解開只有一個檔,對源碼掃描無意義。限額設定化,不寫死:四個限額——上傳上限 MB / 解壓後總量 MB / 解壓檔案數 / 磁碟預留公式(D28 的係數與底值)——做成 BE config class 屬性 + 環境變數可覆寫(現成前例 DRIVE_FILE_SIZE_LIMIT_MB,config/config.py:166 同 pattern)。預設值:上傳 100MB / 解壓後 500MB / 50,000 檔(皆可由環境變數調整)——100MB 壓縮包對純源碼專案已相當大(源碼壓縮比高,約當數十萬行等級),不夠再調。agent 不自己配置:BE 派工時把限額隨任務 payload 下發(與 credentials 下發同構),agent 只做執行時 enforcement——單一真相在 BE,不違反 FR-039「agent 不連雲端 DB」不變式。 待辦:驗證上版環境 BE 前 nginx 的 client_max_body_size 實際限制(BE 本身無 MAX_CONTENT_LENGTH,nginx 預設 1MB,沒調過會先在 nginx 被擋)——部署驗證項,列 T-7.4。 |
system_config——排除:那張表定位是租戶層業務設定(有 FE 設定頁),這幾個限額是平台營運護欄、無 per-tenant 差異化需求;走 system_config 要多做 reader / fallback / 快取 plumbing,不划算。缺點「改動要重啟 BE」可接受(調上限是罕見動作)。未來若需 per-tenant 放寬,payload 下發機制不變、只換讀取來源,升級容易。 |
| D28 | 磁碟防護的具體規則 (2026-08-01 追加,FR-058.7) |
agent 下載前執行 shutil.disk_usage("/tmp"),free < (壓縮包大小 × 12 + 1GB)即 fail——壓縮包 100MB 時需求約 2.2GB;×12 涵蓋壓縮包本體 + 解壓後源碼 + sonar-scanner 工作檔的粗估安全係數。係數與底值納入 D27 定案的同一組 BE 設定(環境變數可調,隨 payload 下發、agent 只 enforcement)。錯誤訊息明確標「agent 磁碟空間不足」並分類為 agent 環境問題、不是掃描失敗——呼應本 arc 教訓「錯誤要在最早能發現的地方被發現」,讓運維一眼知道該去清 agent 主機而不是排查源碼。實作放 _scan_workspace() 建目錄前,最小侵入(shutil 已 import)。另:解壓過程累計超過 D27 上限也即時中止並清理——與磁碟檢查是兩道獨立防線:一道防「空間真的不夠」,一道防「惡意壓縮包」。 |
現況背景:agent 全 repo 零磁碟檢查(grep disk_usage / statvfs / ENOSPC 全 0 命中);/tmp 在容器可寫層(無 volume 掛載),吃爆影響 docker host 分割區;容器無任何資源限制(R2 已記)。「解壓失敗才發現磁碟不足」的被動路線不採——錯誤要在下載前就攔下。 |
| D29 | 檔案生命週期:server 端全保留 + 重掃沿用 + 歷史不刪 (2026-08-01 追加並定案,FR-058.7;推翻討論過程 v2 的「終態即刪」中間版本) |
「掃完即刪」只適用 agent 端 workspace(既有 finally: shutil.rmtree 機制+五路徑測試鎖約,零新工作,不變);tenant 儲存空間上的壓縮包保留。完整模型四點:① 重掃沿用—— scan_mode=upload 的任務,抽屜執行對話框顯示「目前檔案:xxx.zip(上傳於 M/D)」,直接按執行=沿用舊檔;要換版就上傳新檔替換。首次執行仍必須上傳(無檔不給執行)。② 檔案參照落點在任務綁定層(sticky 狀態供沿用,形狀見 §4.8)。注意與被否決的 D26 初版方案不同:上傳動作仍在抽屜發起執行時,流程不變,只是參照回寫進綁定;初版被否決的主因「終態刪檔→參照懸掛」在保留檔案後自然消失。 ③ 替換不刪舊檔,歷史版本全保留(決策者明示:留歷史,萬一未來 user 要找舊檔,還有容錯修正空間)。每筆執行紀錄 scan_params 快照的 {uid, file_name} 都能對回實體檔案,稽核可回溯「那次掃的是哪一包」。④ 清理機制列 follow-up(v1 不做)——孤兒檔(上傳未執行)、歷史版本、殘檔統一由未來的清理機制管(原 D32 併入本項)。給未來清理機制的約束先立在此:必須以「綁定仍參照中的檔案」為保留名單,不能只看檔案年齡——否則會清掉三個月沒重掃、但仍要沿用的 zip。 |
終態即刪(討論過程 v2 曾定案,後推翻):其理由「掃完不留平台」不成立——tenant 儲存空間本來就是客戶自己設定的位置(local / minio / 客戶自家 agent),保留不構成「平台持有客戶源碼」問題;且終態即刪會造成「重掃必重傳」與「執行紀錄參照懸掛(快照指向已刪檔案)」兩個實際代價。特此記錄推翻軌跡,避免日後誤讀為反覆——是理由被檢驗後不成立,不是需求搖擺。 |
| D30 | 解壓實作:標準庫 zipfile + tarfile + 共用額度檢查層 (2026-08-01 追加,FR-058.7) |
兩個格式家族都用標準庫:zip 走 zipfile——extractall() 內建路徑消毒(剝絕對路徑與 ..、不建 symlink),zip-slip 已由標準庫擋掉;**tar 系(.tar / .tar.gz / .tgz)走 tarfile + filter="data"——Python 3.11.4+ 官方 backport 的安全提取機制,擋絕對路徑、.. 逃逸、symlink 逃逸與裝置檔(agent image 基底 python:3.11-slim,Dockerfile:1,版本足夠)。額度檢查層兩家族共用同一層(約二十行):解壓前加總封存目錄宣告的總大小與檔數、超過 D27 上限即拒絕(解壓炸彈防護是標準庫沒有的部分)、逐檔解時看 cancel_event(沿用既有可取消框架的精神)。 |
shell 出去跑 unzip / tar CLI——排除**:無法逐檔做計量與取消——整包解完才知道大小,解壓炸彈防護等於沒有;路徑穿越保護依版本行為不一(image 內雖有 unzip CLI,那是裝 sonar-scanner 用的,Dockerfile:26)。第三方解壓套件——排除:標準庫已涵蓋兩個家族的需求,agent 依賴保持精簡,不為二十行的事引套件。 |
| D31 | 顯示與紀錄:完全複用既有上傳機制 + 兩點收緊 (2026-08-01 追加,FR-058.7) |
完全複用既有上傳機制:FE 上傳統一打 POST /file/upload(API.UPLOAD_FILE),新 upload_type 分類(如 DETECTION_SOURCE);白拿清單(零新工作)——sha256(agent 下載後對帳防竄改)、RLS 租戶隔離、三種 storage backend 全支援。兩點收緊:① FE 只送 uid,檔名不由 FE 送——BE 收到 execute 請求後從 upload_files 表權威取回 file_name / size / sha256 快照進執行紀錄;顯示歷史不依賴檔案仍存在,也不信任前端送來的檔名。BE 要補的驗證:uid 存在且屬本 tenant(RLS)、副檔名在允許清單(D27)、大小在 D27 限額內。② 快照存 {uid, file_name} 物件——執行紀錄抽屜 paramValueLabel 對非 select 型參數原樣輸出,快照若只存 file uid 畫面會顯示裸 UUID;故 scan_params 快照存 source_file: {uid, file_name},FE 顯示 file_name;JobExecutionDrawer.vue 的 _PRIMARY_PARAM_KEYS(:315)加 source_file、paramValueLabel 加物件型分支(取 file_name)。 |
信任 FE 送來的檔案 metadata——排除:檔名 / 大小由前端自報則快照可被竄改,稽核回溯失去意義;權威來源只有 upload_files 表。另立新上傳通道——排除:既有 POST /file/upload + upload_files_for_tenant() 已涵蓋三後端與 RLS,另立通道違反「新增前先找既有能力」鐵則。 |
| D32 | 孤兒檔處理 (2026-08-01,FR-058.7) |
併入 D29 第 ④ 點,不獨立成案。 上傳後放棄執行的孤兒檔、替換後的歷史版本、其他殘檔,統一由未來的清理機制管理(v1 不做),完整說明與「以綁定參照為保留名單」的約束見 D29 ④。上傳時機移到發起執行時後,孤兒檔視窗已大幅縮小(上傳與執行幾乎是同一個動作)。 | — |
| D33 | scan_mode=upload 與「開始執行任務」自動派發的關係(2026-08-01 追加,FR-058.7 驗收發現) |
upload 任務不進自動派發清單——在挑選查詢排除。 背景:專案規劃頁「開始執行任務」會自動派發檢測掃描(FR-056.5 #5,TaskExecutionService.start_task_execution → _auto_dispatch_detection_scans,app/grc/service/task_execution_service.py:90-133),自動路徑不帶檔案,對 scan_mode=upload 的任務必然觸發 DETECTION_TOOLS_400005(首次無檔 400),且該錯誤被批次派發的 try/except 吞掉只留 warning log——PM 看到成功 toast、執行者無感的靜默失敗;又因失敗不產生執行紀錄,「查無 detection_executions」的冪等條件永遠成立,每按一次「開始執行任務」就再失敗一次。定案:挑選查詢 get_detection_jobs_without_execution(infra/grc/repository/task_execution_query.py:134-159)排除 tool_params->>'scan_mode'='upload' 的綁定;upload 任務一律由執行者在任務抽屜上傳檔案後手動執行(抽屜已有完整前置擋門:首次無檔不給按執行,T-7.3)。pull / scan 模式自動派發行為不變。FE 配套:專案規劃頁批次發佈 confirm 文案補一句「上傳型掃描任務需由執行者上傳檔案後手動執行」。 |
① 給 start_execution 加 auto 旗標、在 service 層判斷——排除:多一個參數汙染共用簽名(抽屜手動執行與自動派發共用同一 method),過濾在挑選階段做更乾淨——upload 任務根本不該出現在待派清單裡,而不是進了清單再被擋。② 維持現狀讓它失敗——排除:靜默失敗會讓 PM 反覆觸發且無人察覺,是要修的洞不是可接受行為。 背景註記:自動路徑「悄悄沿用舊檔」的疑慮不會發生——自動派發只挑「從未執行過」的任務(冪等條件),已執行過的 upload 任務(有 sticky 檔案可沿用者)本來就不在清單內,故本修正只涉及首次執行段。 |
| D34 | 檢測掃描通知加厚:九項內容+失敗通知+三管道 (2026-08-01 追加,FR-058.7 驗收期間提出;三子項決策者已逐項拍板) |
① 內容擴為九項:專案(升級走 round 鏈取代現行 ORDER BY id LIMIT 1)、控制項群組、控制項、AO(title→prose→part_id fallback)、任務名稱、工具名稱、掃描設定(複用 _resolve_scan_params() 現成兩層剝除:_ 前綴+secret envelope;profile UUID 轉譯成名稱)、時間(started/finished)、成功或失敗;順手把現成卻沒用的 evidence 參數用起來(報告檔名/份數,零額外查詢)。② 失敗也通知:on_scan_failed 補查 job/binding(抄 on_scan_succeeded :472-475 的 pattern)後發通知,含 error_message;收件人與成功相同(指派人+專案 manager+reviewer)——決策者裁示失敗更需要管理層知道。③ hosts(目標主機 IP)顯示——決策者裁示可出信(客戶自己的信箱,掃描目標是核心資訊)。④ 管道補齊 Discord/Telegram(對齊平台其他通知的三管標準)——文案準備 HTML(email 表格)+純文字(Discord/Telegram)兩版,i18n 6 個 msgid。技術要點(探查已實證):新查詢走 8 層 join 鏈(job→workflow_execution_control_mapping→round→project→extension→ssp→profile_imports→catalog_controls/groups/parts),加在 infra/detection_tools/repository/detection_job_notify_query.py 新增 get_notify_context(job_uid) 一次 SQL;⚠️ 兩坑:catalog_controls.control_id 跨 catalog 不唯一(DEV 實測同 control_id 41 筆)必須帶 catalog_id=profile_imports.source_catalog_id 過濾;AO 的 title 對 CMMC 全 NULL,用 title or prose or part_id fallback(job_export_query.py:145 前例)。無 schema migration、無 DI 變更(_profile_domain/_tool_domain/_notify_query 全已注入)。改動 4 實作檔+1 測試檔。明確不在範圍:站內鈴鐺通知(平台無 notifications 表,另案);收件人 locale 切換(通知在 agent request context 發、Accept-Language 是 agent 的,一律 zh_Hant_TW——既有限制另案)。 |
① 失敗只發指派人(避噪音)——排除:決策者裁示失敗更需要管理層知道,收件人與成功通知相同。 ② hosts 遮蔽——排除:客戶自己的信箱、掃描目標是核心資訊,遮蔽只會降低通知可用性。 ③ 只修 Email 不補管道——排除:文案反正重寫,一次對齊平台三管標準(Email/Discord/Telegram),避免之後再開一案補管道。 |
技術上 Linux GCB 確實可以用 OpenSCAP 執行——它是 Linux 原生工具,FR-057 也已有可用的 SSH connector。不選它的原因不在 Linux 這一側,而在維護成本:Windows GCB 只有 CINC 這條路走得通(OpenSCAP 官方已聲明放棄 Windows native 支援,docs/windows.md 明文標示 no longer usable;引擎層也沒有 WinRM transport 與 Windows OVAL probe)。若 Linux 走 OpenSCAP,同一份 GCB 基準就要維護兩種 content 格式。
OpenSCAP 的定位不變,兩者按「基準來源」分工而非按 OS 分工:
同一台 Linux 主機要查 CIS 就派 OpenSCAP 任務、要查 GCB 就派 CINC 任務,兩個任務可掛在同一個控制項底下、證據進同一個證據池——這正是 CM-953 已定的**「一任務一工具,靠多任務處理異質環境」**模型,本案沿用,不需改架構。
核心設計:雲端不主動推,agent 定期來問。 雲端連不到客戶內網,故不能主動下指令。agent 每隔 heartbeat_interval_sec(預設 300 秒)向雲端報到並領回待辦。這同時滿足防火牆限制——只有 agent 對外連出,無對內連入。
| 階段 | 發生什麼 | 程式位置 |
|---|---|---|
| ① 使用者按「開始執行」 | 雲端做四件事:_pick_agent() 挑一台符合 detection_scan capability 的 agent(取 agents[0],尚無負載平衡)/_resolve_credentials() 解密租戶工具憑證/建一筆 agent_tasks(狀態 pending)/建執行紀錄。此階段完全沒有對外連線,雲端只是在 DB 放一張待辦單。 |
app/detection_tools/service/detection_orchestration_service.py → start_execution() |
| ② agent 心跳 | POST /api/1.0/agents/heartbeat(帶 mTLS client cert)。雲端回應夾帶 pending_tasks,每筆為 {uid, detection_tool_id, params, credentials}。credentials 是雲端當場 Fernet 解密的明文,走 mTLS 下發,agent 用完即丟不落地(FR-056 D9)。 |
app/remote_agent/service/agent_enrollment_service.py → heartbeat() / _collect_pending_tasks() / _resolve_tool_credentials() |
| ③ agent 分派 | 每筆各開一條 daemon thread(一筆失敗不影響其他筆、不阻塞心跳)。每條 thread:ack(pending→dispatched)→ get_connector(detection_tool_id, params, credentials) → connector.run() → 報告走既有 blob 通道上傳 → 回報結果。 |
evidence-agent/core/task_executor.py → dispatch_tasks() |
| ④ 取消(唯一的反向推送) | 取消不能等 5 分鐘,故雲端直接連 agent 的 base_url;agent 端 _RUNNING_TASKS(task_uid → threading.Event)找到執行中任務並 .set() 通知中止(CM-931)。 |
POST /detection/cancel |
對加新工具的意義:派工鏈路本身是 tool-agnostic 的,四個新工具全部沿用,「發指令」機制完全不用改。真正要動的只有兩點——雲端一條 SQL seed(detection_tools 一列 + param_schema,零 Python);agent 加一筆 _TOOL_ID_TO_CODE 映射 + 寫 connector 實作。而 D11 要解的正是後者的映射脆弱性。
| 元件 | 現況 | 本案動作 |
|---|---|---|
config.detection_tools |
4 筆:openvas(available) / nessus / sonarqube(coming_soon) / openscap(available);已有 connection_type(API / CLI / SSH)、setup_guide(FR-057 加) |
seed 四筆新工具(ZAP API / InSpec SSH+WinRM / GCB / Nmap SSH——2026-07-30 改案,原定 CLI);加「是否需要憑證」宣告欄位(D9 平台能力,nmap 改案後其值為 TRUE);2026-07-31 追加:既有 id=3 sonarqube 佔位列 UPDATE 成可用工具(FR-058.5 / T-5.1,不是 INSERT 新列,見 §4.6 seed 特殊點) |
config.detection_tool_param_schemas |
版本化參數 schema(openvas / openscap 各一筆) | seed 四份新 param_schema;schema 語意擴充 secret: true(D1);2026-07-31 追加 sonarqube param_schema v1(FR-058.5 / T-5.1) |
job_execution_detection_tools.tool_params |
明文 JSONB | 依 param_schema 對 secret:true key 加密後才寫入(FR-058.0) |
tenant_detection_tool_configs + Fernet 加密鏈 |
完備 | 直接沿用(ZAP API key 進 credentials 密文);任務層敏感參數複用同一條 Fernet 鏈,不另立加密機制 |
start_execution() 憑證檢查 |
憑證為空即擋,回 DETECTION_TOOL_CONFIG_NOT_FOUND |
放行零憑證工具(D9 平台能力,仍要做)——原動機是 Nmap 會被誤擋,2026-07-30 改案後 Nmap 需 SSH 憑證不再是使用者,此能力保留供日後真正零憑證的工具使用 |
| 派工鏈(心跳夾帶 + 憑證解密下發) | 完備 | 沿用;組裝派工時一併解密任務層敏感參數(FR-058.0);payload 加 detection_tool_code 欄位(D11) |
agent factory _TOOL_ID_TO_CODE |
{1: openvas, 2: nessus, 3: sonarqube, 4: openscap} 硬編 id 映射 |
移除硬編映射,改依派工夾帶的 code 取 connector(D11,橫向前置項) |
agent DetectionConnector base |
run() / probe();注入點 cancel_event(CM-931)/host_failures(CM-952)/content_mismatch_hosts(CM-954) |
介面不動;新增 zap.py / inspec.py / nmap.py 三支實作(GCB 複用 inspec.py);2026-07-31 追加 sonarqube.py(FR-058.5 / T-5.2) |
| agent Dockerfile | LibreOffice + curl + openssh-client + openscap-utils | 加入 CINC Auditor(引擎裝在 agent 本機、由它主動連出去掃目標,與 OpenSCAP 相反——見 §4.3 架構事實);Nmap 不 bundle(NPSL v0.95 §3 衍生作品條款,客戶裝在自己指定的主機上、agent 走 SSH 呼叫,見 D9 改案);ZAP 不 bundle(D3 客戶自備) |
| 多檔證據回收鏈 | FR-057 已擴為 result_ref.upload_uids 陣列(向下相容單數 key) |
直接沿用,不再改 |
| FE 設定頁 / 任務參數渲染 | DetectionConfigField.vue 七種欄位型態(text / textarea / password / number / boolean / select / select_or_text)+ condition 條件顯示 |
不需新元件——ZAP 登入欄位用現成的 password + condition;需加 secret 欄位在執行紀錄的剝除規則(FR-058.0) |
| 項目 | 現況 | 本案的關聯 |
|---|---|---|
| 心跳延遲 5 分鐘 | agent 心跳預設 300 秒,任務發佈到實際開跑最長有 5 分鐘延遲 | 使用者按下開始按鈕後畫面無反應、五分鐘後才動。這是設計上的正常行為,但 UI 需說明否則會被當成故障 |
| FE 硬編死碼工具清單 | WorkflowSetupEditor.vue 有一段 toolsMenu = ["Nessus","Nmap","OWASP ZAP","Wireshark"],與 detection_tools 完全脫鉤 |
本案加入 ZAP 與 Nmap 之後,畫面上兩處都出現 Nmap,其中一處是死的。建議一併清掉 |
| Windows GPO tattoo settings | HKLM\Software\Policies 底下屬正規政策區、套用前會先清空;不在此位置的設定會殘留(業界稱 tattoo settings) |
檢查規則若讀取非 Policies 路徑,可能讀到上次殘留的舊值而誤判為通過,實際上政策根本沒生效。稽核情境屬嚴重問題。本案 Demo profile 必須避開此陷阱 |
tenant_config_id 從未被寫入 |
job_execution_detection_tools.tenant_config_id 永遠是 NULL,實際靠 (tenant_id, detection_tool_id) 的 UNIQUE 約束回退查詢 |
加工具不會惡化,但欄位存在卻不被使用,日後看到的人容易誤判其用途 |
| 證據關聯靠字串前綴 | 證據與執行的關聯靠 job_evidences.description 的字串前綴 [檢測工具] {agent_task.uid} |
四個工具都會走這條路徑,任何改動 description 格式的需求都會全面波及 |
id=3 SonarQube 佔位列 |
FR-056 留下,status='coming_soon'、config_field_schema 為空 |
(2026-07-31 更新)原裁示「本案不處理維持 coming_soon」已被推翻——SonarQube 重新納入為 FR-058.5,此列由 T-5.1 UPDATE 成可用工具(不是 INSERT 新列,見 §4.6 seed 特殊點)。原記載的「硬編 id 表中無對應 connector 的一格」問題在 D11 解耦 + T-5.2 補上 connector 後雙重消失 |
_pick_agent 無負載平衡 |
直接取 agents[0] |
工具變多後同一站點併行任務量上升,單一 agent 被打滿的機率提高 |
| 文件債 | docs/claude/database-schema.md 完全沒有 config schema 與 detection 三表的記載(grep 零命中) |
本案要動這三張表,是補上記載的合適時機 |
實作前先讀這些檔案,四個子需求都會用到:
| 用途 | 檔案 |
|---|---|
| 平台三表 seed 範本 | scripts/sql/2026-07-26-fr056-1-detection-tools-config-schema.sql |
| 加工具的 SQL 範本(最重要) | scripts/sql/2026-07-28-fr057-1-openscap-seed.sql——含 setup_guide 欄位、config_field_schema、param_schema、schema_migrations 收尾。四個新工具的 seed 都照這支寫 |
| connector 介面 | evidence-agent/core/task_executor_connectors/base.py(DetectionConnector ABC、ScanResult、ScanCancelledError、cancel_event / host_failures / content_mismatch_hosts 注入點) |
| connector 實作範本(API / daemon 型) | evidence-agent/core/task_executor_connectors/openvas.py——連線設定解析、probe()、輪詢、雙格式取報告、cancel_event、timeout、summary 解析容錯,ZAP 照這支的形狀寫 |
| connector 實作範本(SSH / 多目標多檔型) | evidence-agent/core/task_executor_connectors/openscap.py——Nmap 照這支寫(SSH 型,D9 改案);InSpec 只借鏡多目標處理與 host_failures 用法,連線層完全不同(CINC 裝在 agent 本機自己連出去,見 §4.3 架構事實) |
| connector 實作範本(本機引擎 / shell out 型) | evidence-agent/core/task_executor_connectors/inspec.py(T-2.2,dcedb4f)——GCB(FR-058.3)直接複用這支;--config 憑證機制、/dev/shm 暫存檔、probe 檢查順序都在裡面 |
| factory(D11 要改的檔) | evidence-agent/core/task_executor_connectors/__init__.py |
| 派工編排 | app/detection_tools/service/detection_orchestration_service.py |
| 心跳派工 | app/remote_agent/service/agent_enrollment_service.py(_collect_pending_tasks / _resolve_tool_credentials) |
| FE schema 驅動欄位 | compliance-manager-fe/src/components/detection-tools/DetectionConfigField.vue(七種欄位型態 + condition) |
| FE 工具管理頁 | compliance-manager-fe/src/views/plugin/ToolPluginManage.vue |
| FE 執行抽屜 | compliance-manager-fe/src/components/grc/JobExecutionDrawer.vue——注意 _PRIMARY_PARAM_KEYS 硬編 ['profile']、summaryLabel() 查 lang.my_tasks.summary_${key},找不到會印原始 key |
SQL migration 撰寫鐵則(本案所有 seed 都要遵守):檔頭寫 -- Date:、每個語句加日期註解、新建 table 必加 GRANT ... TO cm_app + sequence 權限、收尾必 INSERT public.schema_migrations;套用一律 psql --single-transaction -v ON_ERROR_STOP=1 -f <檔> 用 cmmgr 帳號(密碼請查 .env 或部署文件)。三環境(DEV / STG / POC)都要套。
_TOOL_ID_TO_CODE 解耦(D11)現況:evidence-agent/core/task_executor_connectors/__init__.py 用 _TOOL_ID_TO_CODE = {1: "openvas", 2: "nessus", 3: "sonarqube", 4: "openscap"} 硬編 id 到 code 的映射。agent 不連雲端 DB 查工具目錄,只靠這張表認工具。
改動:
_collect_pending_tasks() 組裝 pending_tasks 時,payload 加 detection_tool_code 欄位(值取自 detection_tools.code)。get_connector() 簽章改為優先吃 code;detection_tool_id 保留為 fallback(舊 agent / 舊 payload 過渡期不壞),fallback 路徑加 warning log。_TOOL_ID_TO_CODE 硬編表(或降級為僅 fallback 用的相容表,並在註解標明退役時機)。為何排在最前面:三條開發線都會動到 agent 這個共用 repo。各自新增一支 connector 檔案彼此不衝突,但這個 factory 檔三條線都要改,同時開工會反覆衝突。而解耦本身工作量很小(改一個 factory + 派工 payload 加一個欄位),先做完三條線就不會撞同一個檔;等四個 connector 都寫完再回頭改,要重測的範圍會擴大四倍。
detection_tool_param_schemas 的 schema JSON 每個欄位定義新增 "secret": true 旗標,語意與租戶層 config_field_schema 的 secret 定義一致。
| 時機 | 行為 |
|---|---|
| 寫入任務參數 | 依該工具當前版本的 param_schema 找出所有 secret: true 的 key,對其值 Fernet 加密後再寫入 tool_params;非 secret 欄位維持明文 |
| 組裝派工 | _collect_pending_tasks() 對 tool_params 中的 secret key 解密,與租戶層 credentials 同一時機、同一路徑下發(走 mTLS,agent 用完即丟) |
| 讀取供 FE 顯示 | secret key 一律剝除(不是遮罩成 **** 後仍回傳密文,是整個 key 不出現在 response) |
| 稽核 log | 寫事件內容時同樣剝除 secret key,避免在 system_logs 留下明文副本 |
已知坑:param_schema 是版本化的。加解密要以「該任務當初綁定的 schema 版本」為準來判定哪些 key 是 secret,否則 schema 改版後舊任務會解錯(把明文當密文解、或把密文當明文送)。
secret: true 且 type: password 走現成的 password 型態,不需新元件。JobExecutionDrawer.vue):BE 已剝除,FE 不需再過濾;但要確認 _PRIMARY_PARAM_KEYS 之類的硬編邏輯不會意外把缺席的 key 顯示成空白或原始 key 名。原 OWASP ZAP,現歸 Checkmarx 維護,仍為 Apache 2.0 開源授權。屬 DAST(動態應用程式安全測試)——對執行中的網站主動送出測試請求,找出網頁層弱點。
走 API / daemon 模式(與 OpenVAS 同型),不走 baseline CLI 模式。理由:API 模式能做「登入後掃描」,那是稽核場景真正有價值的能力;CLI baseline 只能被動掃表層,對合規證據的價值有限。
| key | label | type | required | secret | condition |
|---|---|---|---|---|---|
target_url |
掃描目標網址 | text | ✅ | — | — |
scan_mode |
掃描模式 | select(被動 / 主動 / 登入後主動) | ✅ | — | — |
login_url |
登入頁網址 | text | 條件 | — | scan_mode = authenticated |
login_username |
測試帳號 | text | 條件 | — | scan_mode = authenticated |
login_password |
測試密碼 | password | 條件 | ✅ | scan_mode = authenticated |
logged_in_indicator |
登入成功特徵字串 | text | 條件 | — | scan_mode = authenticated |
沒有選登入模式時,後四欄整組不顯示(用 FE 現成的 condition 機制,不需新元件)。這對上「有帳密就深掃、沒有就一般掃描」的需求。scan_mode 預設值=被動(D2),主動類選項在 FE 加警語。
租戶層 config_field_schema:ZAP 服務位址 + API key(secret)。
上表這組參數是傳統表單登入模型——ZAP 對登入頁 POST 帳密欄位、用 cookie 維持 session、用 logged_in_indicator 比對回應內容判定是否仍在登入態。這個模型對 SPA(單頁式應用)天生不成立,三個原因彼此獨立、必須同時解決才會通:
| # | 現象 | 根本原因 | SPA 需要的做法 |
|---|---|---|---|
| ① | 填了帳密但 ZAP 從未登入成功 | SPA 的登入是 JS 呼叫 API(如 POST /api/.../login)取 token,頁面上沒有傳統表單提交可供 ZAP 對付 |
json-based 認證(對登入 API 送 JSON payload) |
| ② | 就算登入成功,後續請求仍是未登入身分 | SPA 多用 JWT 存 localStorage、靠 Authorization header 帶;ZAP 預設 session 管理是 cookie-based,不會把 token 塞進後續請求 |
script-based session management(需撰寫並上傳 ZAP script) |
| ③ | ZAP 誤判未登入、反覆重跑登入 | logged_in_indicator 比對的是 HTTP 回應的原始內容;SPA 初始 HTML 是空殼,登入後才出現的文字是 JS 渲染後才進 DOM 的,原始回應裡找不到 |
改用 API 回應特徵或其他非 DOM 判定(前兩項未解時此項無意義) |
本案定位:第一批只支援傳統表單登入,SPA 登入掃描列後續另案(決策與被排除方案見 D12)。setup_guide 需明白寫出此限制——且措辭必須是禁止使用而非「結果會不完整」,理由見下方實測驗證。
⚠️ 後果更正(2026-07-30 實測):本節原先記載「對 SPA 掃描仍會完成、只是涵蓋範圍侷限於登入前頁面」(安靜成功),該描述是錯的。實際後果嚴重得多:ZAP daemon 會被自身的保護機制強制關閉,掃描不會完成,且連帶中斷同一台 ZAP 上其他任務。
實測驗證(2026-07-30,目標 http://192.168.50.151):
完整因果鏈——
logged_in_indicator 在原始回應中找不到(原因 ③),ZAP 判定「未登入」;insight.auth.failure;daemon log 摘錄:
[ZAP-Insights] INFO org.parosproxy.paros.control.Control - Shutting down ZAP due to High Level Insight:
HIGH : EXCEEDED_HIGH : http://192.168.50.151 : insight.auth.failure : 100
[ZAP-Insights] INFO ... - Discard Session
... (後續大量 Selenium NoSuchSessionException,為 ZAP 死後的餘波,非原因)
ZAP 2.17.0 terminated.
zap exited with code 2 (restarting)
讀 log 時容易誤判的一點:
NoSuchSessionException洗版看起來像 Selenium 出問題,但它發生在Shutting down ZAP之後,是 ZAP 已死造成的下游噪音,不是根因。根因只有insight.auth.failure : 100這一行。
營運層級影響(本次更正的重點):ZAP daemon 是全租戶共用的單一服務,不是每個任務一個實例。因此一個 SPA 登入掃描任務可以把整台 ZAP 打掛,連帶中斷該台 ZAP 上其他正在執行的掃描——實測當下即發生:另一個任務在 ZAP 重啟的空檔收到 Connection refused 而失敗。這使問題性質從「報告涵蓋範圍不足」升級為「單一任務可造成全域服務中斷」。
範圍界定(避免過度解讀):此問題僅限登入後掃描模式。被動掃描與主動掃描不建立 ZAP context、不設定任何認證、不比對登入特徵字串,因此不會產生認證失敗、不會累積計數、碰不到 Insights 門檻。第一批出貨的被動與主動兩種模式完全不受影響。
實測佐證(2026-07-30,對一個 Next.js SPA 站台):
_next/static 下的 JS / CSS chunk、favicon;無任何 API 端點、無任何登入後頁面。這組數字同時也是匿名掃描的價值邊界的具體證據:對 SPA 而言,未解決登入即等同匿名掃描,能查出組態層問題但碰不到應用邏輯——這正是登入後掃描在稽核場景的價值所在,也是它值得列為獨立後續案、而非在第一批做半套的理由。
已知但本案不做:connector 端的自我保護(2026-07-30 裁示:只記錄不實作)
上述營運層級風險有一個成本很低的緩解手段,但本案不做:
| 項目 | 內容 | 影響 |
|---|---|---|
| Python 套件版本 | 用 zaproxy 0.6.0(2026-06 發布)。舊參考碼用的 0.4.0(2025-01)太舊;0.5.0 與 ZAP 2.17.0 同日(2025-12-15)發布以對齊新版 API |
ZAPv2 類別仍是進入點,呼叫風格不必改寫 |
| 警報去重 | 2.17.0 加入警報去重與 Systemic alert 支援(Issue 9067 / 9097) | alert 數量會比舊版少,統計解析要對齊新語意,不能沿用舊版計數假設 |
| 報告時間欄位 | JSON / XML 報告新增 ISO 8601 格式的 created 欄位;舊的 generatedString 官方已預告未來移除 |
解析器不要依賴 generatedString |
| 暫存訊息保留 | headless 模式下,主動掃描產生的暫存 HTTP 訊息預設不再保留 | 對取報告流程無影響 |
沒有破壞性的 API 端點移除,主線呼叫序列可以沿用。
位置 ~/Documents/Projects/Jedicogy/auto-pentest/(DDD 分層 + MinIO 儲存)。它的價值不在架構,而在踩過坑才整理得出的那幾份知識。
| 可直接搬的知識 | 必須丟棄的部分 | 必須修的問題 |
|---|---|---|
API 呼叫主線序列(auto_pentest/domain/zap/service/zap_domain_service.py):ajaxSpider.scan → 輪詢 ajaxSpider.status(回字串 'running')→ ascan.scan → 輪詢 ascan.status()(回百分比數字)→ 取報告(舊碼用 reports.generate;本案依 D4 改版改用 core.htmlreport / core.jsonreport 取 bytes)。⚠️ 兩種輪詢的回傳型態不同,這個細節在 2.17.0 沒有改變 |
整個 DDD 分層:domain service / app service / entity / MinIO storage adapter 全不需要——我們的 connector 是扁平單一類別,上傳走 task_executor 既有 blob 通道,沒有 storage adapter 的位置 | 硬編憑證(紅線):zap_auth.py 與 zap_domain_service.py 內有寫死的帳號密碼與固定 IP。憑證禁入版控是本專案紅線,一律改走 self.credentials(FR-056 D9 解密下發)與 self.params |
12 種報告模板 + sections / themes 相容矩陣(auto_pentest/common/enums/zap_report/report_enum.py 的 TEMPLATE_CONFIGS)。⚠️ D4 改版後主線不再走 reports.generate 模板體系(改用 core.htmlreport / core.jsonreport 直接取 bytes),此矩陣降為背景知識,不再搬去當 FE 下拉選項來源 |
自起 Docker 容器(ZAPContainerDomainService):ZAP 應視為「客戶已安裝好的服務」,connector 只負責連上去(D3) |
沒有取消機制、沒有 timeout 上限:while status == 'running' 是無限等待。必須支援 cancel_event 與 timeout 上限。另外 print 當 log 要換成 logger |
四種登入認證設定方式(auto_pentest/domain/zap/service/zap_auth.py):form-based / json-based / script-based / Selenium-based,加上 set_logged_in_indicator。登入後掃描是整個 ZAP 接入最難的部分,這裡有完整的 API 序列可參考邏輯 |
報告落地檔案系統再上傳:reports.generate(reportdir=...) 會寫檔到磁碟——而且寫的是 ZAP daemon 主機的磁碟,遠端部署模型下 agent 根本取不到(D4 改版的直接動因,issue #7821)。本案改用 core.htmlreport / core.jsonreport 直接取回 bytes,記憶體轉手不落地 |
Oracle Nashorn script engine:JDK 15 之後已移除,ZAP 新版預設改用 Graal.js。舊碼的 upload_script() 已改用 Graal.js,但 java_script() 還停在 Nashorn——是會踩到的雷 |
evidence-agent/core/task_executor_connectors/zap.py)run():依 scan_mode 分支(被動 / 主動 / 登入後主動)。登入後主動要先設定 context + 登入認證 + logged_in_indicator,再跑 ajaxSpider 與 ascan。core.htmlreport() 回 bytes 上傳當證據(檔名 ZAP掃描報告_<target>_<date>.html);core.jsonreport() 回 bytes 只在記憶體解析出 summary,不上傳。不走 reports.generate(只寫 daemon 主機磁碟、無回傳內容端點)。probe():只驗「連得到 ZAP daemon 且 API key 正確」,不建立任何掃描資源(與 base.py 的 probe() 語意一致)。cancel_event 與 timeout:輪詢迴圈每圈檢查 cancel_event,命中就 raise ScanCancelledError;另設 timeout 上限,避免無限等待。setup_guide:寫 daemon 啟動指令與 API key 設定說明(客戶自備,D3)。組態合規檢測引擎,用 Ruby DSL 撰寫檢測規則(profile)。本案接入它是為了解決 Windows 主機組態檢測這個既有缺口——FR-057 的 OpenSCAP connector 是 SSH 加 Linux 寫死的(/usr/bin/oscap、/etc/os-release、sudoers),無法處理 Windows 主機。
吸收 Notion CM-953(「FR-057 後續:異質 OS 環境(Windows 等非 Linux 主機)的檢測支援方案」,狀態=討論,https://app.notion.com/p/3ac346da4cd0819390d2c5b85c754bf8),不另開平行卡。CM-953 提的三個選項中,選項 1(OpenSCAP Windows 走 WinRM)已被證實不可行——OpenSCAP 官方放棄 Windows 支援,且引擎沒有 WinRM transport、沒有 Windows OVAL probe,無論怎麼配置都掃不到 Windows 目標。InSpec / CINC 屬選項 2 的具體答案。CM-953 定的「一任務一工具、靠多任務處理異質環境」模型本案沿用,不需改架構。本子需求落地時一併結案 CM-953。
| 候選 | 判定 |
|---|---|
| OpenSCAP on Windows | 死路。官方 docs/windows.md 明文標示 no longer usable,最後一個勉強可用的版本是 1.3.4 且不再修 bug |
| 「Linux 掃描機遠端掃 Windows」 | 引擎做不到——不是設定問題,是能力缺失(無 WinRM transport、無 Windows OVAL probe) |
| CIS-CAT Pro | 唯一能做「單機遠端 WinRM 掃多台 Windows」的 SCAP 系工具,但需要 CIS SecureSuite 付費會員資格 |
| SCC | 免費但遠端能力弱,偏「目標本機安裝 agent」模式,與我們 agentless 的既有模型不合 |
| CINC Auditor | 採用。 agentless(目標主機不必安裝掃描器,與 OpenSCAP-SSH 同一種遠端模型);SSH 與 WinRM 同一套 CLI 與 profile 格式,--reporter json 統一輸出,connector 不必寫兩套解析;Apache 2.0 免費;一個 connector 供三條線用(Windows 組態檢測 + Linux GCB + Windows GCB) |
授權關鍵:必須用 CINC Auditor(Apache 2.0 自由重建版,Progress 官方認可的 drop-in 替代品),不要用官方 InSpec 6+ 商業 binary(需接受 Chef EULA 並持有 license key)。這一點在採購與部署文件上都要寫明(D5)。
這一點與 OpenSCAP 的方向完全相反,後續實作者務必先看清楚:
| OpenSCAP(FR-057) | CINC Auditor(本案) | |
|---|---|---|
| 掃描引擎裝在哪 | 目標主機(客戶自行安裝 oscap) |
Agent 本機(隨 agent image 安裝) |
| 誰發起連線 | connector 用 paramiko 自組 SSH 指令進目標主機執行 | cinc-auditor 自己連出去;connector 只是 shell out 呼叫本機執行檔 |
| 目標主機的前提 | 需安裝 oscap + content + sudoers |
不需安裝任何掃描程式(agentless),只需開放 SSH / WinRM |
| connector 處理 transport 的方式 | 自己寫 SSH 連線邏輯 | 交給 cinc-auditor(-t ssh://… / -t winrm://…) |
這正是 D6「多做一個 transport 的邊際成本很低」的真正原因——transport 差異被 CINC 自己吸收了,connector 兩邊共用同一套指令組裝與 --reporter json 解析。原設計文件只有推論,T-2.2(CM-973,evidence-agent dcedb4f)實作後已有實證。
因此 openscap.py 只有「多目標逐台處理」與「host_failures 逐台回報」兩個形狀可借鏡,連線那一層完全不同,不可照抄。 本文件先前「對照 openscap.py 的 SSH 模式」之類的描述已依此更正(§4.0 參考檔案座標)。
對 FR-058.3 GCB 的直接影響:T-3.2 的 content 外部載入接口是建立在這個架構上的——profile content 由 agent 本機讀取後交給 cinc-auditor,不需要、也不應該送到目標主機。若誤以為 content 要先傳到被掃描的機器上,接口會設計錯,而那要等到端到端驗收才會發現。
cinc-auditor --password 會讓密碼出現在 Agent 主機的 ps 輸出,而證據 Agent 是長駐服務——任何能看到該主機行程的人都讀得到客戶的稽核帳號密碼。故改走官方 --config <JSON 檔> 的 credentials 區塊,命令列只帶 -t <transport>://<連線名>。config 檔與私鑰一律寫 /dev/shm(tmpfs,不落實體磁碟)、權限 0600、try/finally 用完即刪(含例外與取消路徑)。exit 172。若日後有人把 Dockerfile 換成官方 InSpec,掃描會整批硬失敗,那個失敗是刻意留的路障,不該靠加 --chef-license accept 之類的參數「修好」(那等於預設替客戶接受商業授權條款)。已有測試釘住此約定。probe() 講清楚。順序若對調,憑證不齊的使用者會先看到「Agent 沒裝檢測引擎」——把客戶端設定問題誤報成我方部署問題,會把人導向完全錯誤的排查方向。已有測試釘住此順序。detection_tools.connection_type 需涵蓋這兩種型態(SSH 已存在,WinRM 為新值)。實作落點為任務參數 params.transport(同租戶的 Linux / Windows 目標分屬不同任務,沿用 CM-953「一任務一工具」模型),憑證則兩組並存於租戶層(ssh_* / winrm_* 前綴)。--reporter json:兩個 transport 輸出格式一致,connector 只需一套解析。html2 上傳、json 只算 summary(2026-07-31 實測後定案,對齊 D4 ZAP / D9 Nmap 的雙格式精神):一次執行帶兩個 reporter,json 走 stdout 只在記憶體算 summary 不上傳,html2:<檔案> 寫出的 HTML 上傳當證據。兩份等價內容都進證據池是 D4 已排除過的走法。
html2 而非內建的 html:後者是 RSpec 風格測試輸出,實測 grep 控制編號命中 0 次,無控制標題與嚴重程度,對稽核判讀無用;官方文件亦說明 html 僅為向下相容保留、「unaware of profiles or controls」。html2 三者皆有,且產出的 HTML 不引用任何外部 CDN 資源(自包含、離線可開)——證據要能長期保存後再開啟,這是必要條件而非加分項。host_failures 逐台失敗回報通道直接沿用(避免「靜默假成功」)。連線那一層不沿用——見上方架構事實表。tenant_detection_tool_configs 加密鏈(Linux SSH 帳號/金鑰、Windows WinRM 帳號密碼)。若日後需要每台主機不同帳密,走 FR-058.0 的任務層敏感參數。政府組態基準(TWGCB),我國公務機關資通設備的組態安全基準,屬合規稽核情境的高需求項目。
本案範圍已由 D7 收斂:只做引擎(直接複用 .2 的 CINC Auditor,不另立引擎、不另寫 connector)與一份 Windows 最小 Demo profile,證明端到端鏈路可運作即可。
| 面向 | Linux GCB | Windows GCB |
|---|---|---|
| 引擎 | 統一走 FR-058.2 的 CINC Auditor(Linux 用 SSH transport、Windows 用 WinRM transport,同一套 CLI 與 profile 格式) | 同左 |
| 引擎層工作量 | 已由 .2 承擔,GCB 本身不需要新的 connector | 同左 |
| 本案 content | 不做 | 一份最小 Demo profile,僅用於驗證鏈路 |
| content 載入方式 | 一律從外部載入,不硬編進 connector、不打包進 agent image(本案設計約束) | 同左 |
| 後續 content 產製 | 列為後續,與後台 content 更新機制一併另案評估 | 同左 |
資料來源為 NICS 兩個官方頁面(GCB 說明文件頁標示 2026/6/26 更新、GCB 部署資源頁標示 2026/3/30 更新)。本次查訪最重要的發現:Linux 與 macOS 在部署資源頁完全沒有任何機器可讀檔案。
| 平台 | 可用來源 | 產製方式 | 自動化程度 |
|---|---|---|---|
| Windows(Win11 / Server 2016·2019·2022 + Chrome / Edge) | GPO backup ZIP(含 registry.pol + GptTmpl.inf) |
可寫產生器解析產出骨架,再人工補判定邏輯 | 約七成 |
| Linux(RHEL 8 / RHEL 9 伺服器·工作站 / Ubuntu 22.04) | 只有 DOCX / PDF | 逐條人工閱讀文件、手寫檢查規則 | 幾乎零 |
| macOS 15 | 只有 DOCX / PDF | 同 Linux;且現有 connector 未曾處理 macOS | 幾乎零(且已由 D7 排除出本案) |
說明文件頁有完整的 RHEL 8 / RHEL 9 / Ubuntu 22.04 / macOS 15 基準,但沒有任何對應的機器可讀部署檔——連 shell script 都沒有。這證實並擴大了 NICS FAQ v1.6 §7.1「未提供 RHEL8 等平台自動化檢測工具」的說法。
Windows 有 GPO 檔這個結構化來源可直接參考,寫一個最小 Demo profile 的成本最低;Linux 只有 DOCX,連一條 Demo 規則都要先人工翻譯文件敘述。這是選 Demo 平台的理由,不是要在本案做 Windows content 工程。
常見誤解澄清:「是不是要把官方 PDF 轉成 content」——Windows 不需要(有 GPO 檔可解析);Linux 需要,但那不是「轉檔」,而是逐條人工把文件敘述翻譯成檢查邏輯。content 工程已不在本案範圍,此處僅為背景說明。
content 不可硬編進 connector 或 agent image,必須設計成可從外部載入。理由:content 本質上是會持續變動的資料,不該綁在程式版本裡。日後要接後台更新機制時,若當初把 profile 打包進 agent,等於每次更新 content 都要重新發版並更新所有客戶站點的 agent。這個接口現在留成本極低,事後補要改動 connector 結構。
結論:約束已滿足,且不需要寫任何 code。 T-3.2 原規劃要做一層「content 外部載入接口」,實作前確認後裁示不做這層抽象——CINC 原生就支援三種 content 來源,而 params.profile 是直接進 cinc-auditor exec 的 argv,外部載入能力本來就在。再包一層只是把 CINC 的能力用我們的詞彙重新命名一次,徒增一個要維護、要測試、且會限縮上游能力的中間層。
| content 來源 | 形式 | 取得時機 |
|---|---|---|
| 網址(Git repo 或壓縮檔) | https://github.com/<帳號>/<專案>/archive/refs/heads/master.tar.gz 等 |
執行時由 cinc-auditor 自行下載 |
| Agent 主機上的本機路徑 | 目錄或壓縮檔路徑 | 執行時本機讀取 |
| Supermarket 上的 Profile 名稱 | profile 名稱字串 | 執行時自 Supermarket 取得 |
D7 約束的逐條驗收:
inspec.py 對 profile 的處理只有「取 params.profile、缺就報錯、有就原樣放進 argv」,connector 內不含任何 content 內容或路徑常數。cinc-auditor)與其相依(git、xsltproc),無任何 profile 檔。架構前提再強調一次(§4.3 架構事實):content 由 Agent 本機讀取後交給 cinc-auditor,不送到目標主機。CINC 裝在 agent 端、由它自己連出去掃,目標主機不需要安裝任何東西、也不需要拿到 profile。若誤以為 content 要先傳到被掃描的機器上,接口會設計錯,而那要到端到端驗收才會發現。
0.2.17 / 192.168.50.123)① 兩份公開 content 對兩種 transport 各跑通一次(這同時是 T-2.3 的雙 transport 驗收):
| content | 目標 | transport | 結果 |
|---|---|---|---|
dev-sec/linux-baseline |
192.168.50.151 | SSH | pass 131 / fail 60 / error 0 / n/a 2 |
dev-sec/windows-baseline |
192.168.50.160 | WinRM | pass 300 / fail 472 / error 0 / n/a 122 |
error 0 是這裡的重點——引擎確實把 content 取到、看懂、跑完,不是連上去之後靜默空轉。
② 裸 GitHub 網址走的是 git fetcher,不是 tarball 下載(踩過才知道的事實):CINC 對裸 GitHub 網址會 shell out 呼叫 git 去查 default branch。agent image 原本沒裝 git,首次實跑因此在還沒連上目標主機之前就炸在 profile 解析階段(Errno::ENOENT)。已於 agent commit 6d1a4fc 補裝 git,DEV 現行 0.2.17 容器內為 git 2.47.3。
連帶的錯誤分類修正:當時所有失敗都被歸類成「連線目標失敗」,把 agent 端的 profile 取得問題誤報成客戶的網路/憑證問題。inspec.py 已補上 profile 階段的 marker 比對,並刻意排在連線判定之前(同 T-2.2 第 3 點的精神:分類籠統比分類錯更貴)。
③ tarball 形式對 git 零相依(T-3.1 為決定 profile 下拉預設值而做的對照實驗):把容器內 /usr/bin/git 移走後重跑同兩個網址——
| 形式 | 正常容器 | 移走 git 後 |
|---|---|---|
.../archive/refs/heads/master.tar.gz |
✅ Valid、431 條控制 | ✅ 照常 Valid、431 條控制 |
https://github.com/dev-sec/windows-baseline |
✅ Valid、431 條控制 | ❌ Ruby stack trace 直接失敗 |
耗時無實質差異(84s / 88s)。裸網址今日之所以可用,完全建立在 6d1a4fc 補裝的 git 上;tarball 形式對此無相依。故 T-3.1 的 profile 下拉預設值一律採 tarball 形式(少一個外部相依與故障點),自行輸入的彈性不減。
「不做抽象層」的取捨有代價,兩者都是能力缺口而非 bug,且都不影響本案驗收(Demo 走公開網址)。記在此處是為了讓日後接後台 content 更新機制的人不必重新踩一次:
| # | 限制 | 成因 | 後果 |
|---|---|---|---|
| ① | 封閉網路客戶只能用本機路徑,但目前沒有把 profile 檔送進 agent container 的機制 | agent 跑在 Docker container 內,有獨立檔案系統。deploy/docker-compose.yml 現有的 bind-mount 只有 ./filedata(證據落地)、./certs(mTLS 憑證)與三個 host 識別碼唯讀掛載,沒有任何 profile / content 用途的掛載點。客戶把 profile 放在主機上,容器內看不到 |
agent 不能連外時,三種來源全部不可用(網址與 Supermarket 需連外,本機路徑指不到)。與 D9 Nmap 「容器看不到主機上的 nmap」是同一個結構性問題 |
| ② | 私有版本庫無憑證路徑 | params.profile 是 param_schema 裡 secret: false 的純文字欄位,值原樣進 argv。CINC 的 git fetcher 要認證需要 token 或 SSH key,而任務參數沒有地方放 secret——FR-058.0 的任務層敏感參數雖已具備此能力(標 secret: true 即走 Fernet 鏈),但 profile 是「位置」不是「憑證」,把 token 塞進位置字串(https://<token>@github.com/...)會讓憑證出現在 FE 執行紀錄與稽核 log |
客戶的 content 放私有 repo 時無法取用,只能改放公開位置或本機路徑(而本機路徑受限制 ① 影響) |
兩者的解法都指向同一件事:後台 content 管理機制(上傳/版更/套用),由平台把 content 送到 agent 可讀的位置,順帶消掉私有 repo 的認證需求。該機制 D7 已排除出本案,兩個限制併入其評估範圍。
必須避開 tattoo settings 陷阱:規則若讀取非 HKLM\Software\Policies 路徑,可能讀到上次殘留的舊值而誤判為通過,實際上政策根本沒生效(詳見 §3.3 風險表)。Demo 選題時就要挑正規政策區的項目。
2026-07-30 改案:本節原為
connection_type='CLI'(agent 本機子行程)設計,已改為 SSH 型。改案的完整推導、NPSL 條款原文與三個被排除方案見 §2 的 D9。
網路埠與服務探測工具。在合規情境對應「開放服務盤點」「非必要服務關閉」這類控制項的證據。技術上是四個工具裡最單純的一個。
connection_type='SSH',與 OpenSCAP 完全同構:nmap 由客戶安裝在自己指定的主機上,agent 透過 SSH 登入該主機執行 nmap -oX 並取回 XML 再解析。agent 本身不安裝 nmap、不在容器內執行它。
之所以不走原定的 CLI(agent 本機執行),有法律與技術兩個各自獨立成立的理由(詳見 D9):法律上,NPSL v0.95 §3 的衍生作品定義明文涵蓋「專門執行本軟體並解析其結果」的程式,我們的 connector 正中此條,因此不能 bundle;同條款末段又明說不主張控制「執行使用者早已安裝在自己系統上的 nmap」的軟體,SSH 型正落在這個出口內。技術上,agent 跑在 Docker container 內,看不到主機上安裝的 nmap,原 CLI 設計的「客戶自備」在容器化部署下根本無法成立。
連帶結果:Nmap 需要 SSH 憑證(requires_credentials 為 TRUE),不再是零憑證工具。connection_type='CLI' 因此至今仍無任何實際使用者(FR-056 定義後未被用過)。
授權:不要打包進我們的 image。 Nmap 採 NPSL(非標準開源授權,基於 GPLv2 但加了額外限制),不可 bundle 進我們發布的 agent image,也不可要求客戶把它裝進我們的容器。做法是客戶在自己指定的主機上安裝 nmap(apt install nmap 之類,無任何 docker 操作),agent 走 SSH 遠端呼叫。setup_guide 要寫清楚:在哪台主機安裝、各主流發行版的安裝與驗證指令、SSH 連線與帳號權限前提,以及埠掃描的授權警語(比照 ZAP)。
D9 原列的第二個前提「零憑證會被平台誤擋」在改案後不再適用於 Nmap(它現在需要 SSH 憑證)。但該平台能力(
detection_tools的「是否需要憑證」宣告欄位 +start_execution()放行)仍然正確且保留,供日後真正零憑證的工具使用。
evidence-agent/core/task_executor_connectors/nmap.py)主要對照範本是 openscap.py(SSH 型),不是本機子行程模式。 SSH 連線處理、私鑰暫存與用完清除、多目標主機逐台執行、host_failures 逐台失敗回報這些形狀直接複用。
run():SSH 登入指定主機 → 執行 nmap -oX → 取回 XML → 解析產出 summary(開放埠數、服務清單等)→ XML 或轉出的人可讀報告上傳當證據。多台執行主機時逐台一份證據,某台失敗以 host_failures 明確回報(不靜默假成功)。probe():四段式錯誤分類,各段回各自明確的訊息——① SSH 連得上嗎(網路 / port)→ ② 認證過嗎(帳密 / 私鑰)→ ③ 該主機上有沒有 nmap 且可執行(command -v nmap + nmap --version)→ ④ 執行權限夠不夠(部分掃描類型需要提權)。第 ③ 段的訊息要讓管理員一看就知道「要去那台主機裝 nmap」,而不是丟原始的 exit code 127。cancel_event 與 timeout:取消時要中止遠端執行中的 nmap 並清理 SSH channel,raise ScanCancelledError;照 openscap.py 的遠端子行程取消形狀處理,不要留下對端的孤兒行程。tenant_detection_tool_configs,走既有 Fernet 鏈),不放任務參數。決策脈絡見 D13–D16。原規劃期裁示「本期不做」移出(D10 因此成為 tombstone 空編號),2026-07-31 決策者裁示重新納入為第五個子需求。
SAST(靜態應用程式安全測試)——分析原始碼找出 bug、弱點與程式碼異味,在合規情境對應「安全開發流程」「程式碼安全檢測」這類控制項的證據。
平台第一個「拉取快照(pull-snapshot)」型工具。 連線形態歸 §1.1 的 ①(連遠端服務 API),但語意與 OpenVAS / ZAP 不同——連的是 API 卻不下掃描指令。根本原因:SonarQube Server 沒有任何觸發掃描的 Web API,掃描由 sonar-scanner 在有原始碼的環境(客戶 CI)執行後把結果推上 server,Web API 只能讀取既有結果(唯一例外是 SonarQube Cloud SaaS 的 Automatic Analysis,與 self-hosted 無關)。因此任務執行=連 server 拉指定專案最近一次分析結果、組成 HTML 報告當證據(D13)。
[
{"key": "base_url", "label": "SonarQube 服務位址", "type": "text", "required": true, "secret": false},
{"key": "token", "label": "User Token", "type": "password", "required": true, "secret": true}
]token 必須是 user token(squ_ 前綴),HTTP 用 Authorization: Bearer header 送出;token 所屬帳號只需目標專案的 Browse 權限。不可用 sqp_ / sqa_ analysis token——那兩種只能執行分析、不能呼叫讀取 API(D16),setup_guide 要寫明這個區別。
| key | label | type | required | 備註 |
|---|---|---|---|---|
project_key |
專案 key | text | ✅ | SonarQube 專案識別碼 |
branch |
分支 | text | — | label 註明付費版(Developer Edition 起)才支援多分支;Community Build 只有單一 branch。不填=不送 branch 參數(Community 相容);填了但 server 不支援時 connector 回明確錯誤訊息(D14) |
皆非敏感參數,不涉及 FR-058.0。
| endpoint | 用途 | 備註 |
|---|---|---|
api/project_analyses/search?project=<key> |
取最新分析時間(最新一筆 date,進報告供稽核員判斷資料新鮮度) |
回空=該專案尚無分析結果,raise 明確錯誤「請先於客戶端執行 sonar-scanner」——不可繼續往下組空報告 |
api/qualitygates/project_status?projectKey=<key> |
Quality Gate 狀態(OK / ERROR)+各 condition | — |
api/measures/component?component=<key>&metricKeys=bugs,vulnerabilities,code_smells,security_hotspots,coverage,duplicated_lines_density,ncloc,alert_status |
核心度量 | summary 數字的唯一來源,繞開 MQR severity 語意差異(D15) |
api/issues/search?components=<key>&ps=500 + facets(severities / types) |
分佈統計用 facets、issue 清單取前 200 條(severity 由高至低) | 10,000 筆硬上限(第 21 頁起報錯),故絕不逐頁撈全量;統計一律用 facets 與 measures 取全量 |
api/hotspots/search?project=<key> |
Security Hotspots | — |
全部只需 Browse 權限;branch 參數僅在使用者有填時附加到各呼叫。
兩段式:
api/system/status——免認證,驗連通與服務狀態(server 是否 UP)。Authorization: Bearer 呼叫 api/authentication/validate——驗 token 有效性。陷阱:匿名呼叫此端點也回 valid:true(它驗的是「當前身分是否有效」,匿名也是一種有效身分),必須帶認證 header 呼叫、結果才有意義(D16)。probe 是租戶層動作、當下沒有 project_key,專案層權限問題不在 probe 驗——留到 run() 時以明確訊息回報(例如專案不存在或無 Browse 權限)。
evidence-agent/core/task_executor_connectors/sonarqube.py)與既有 API 型 connector(OpenVAS / ZAP)的關鍵差異:沒有輪詢迴圈——拉取型不需要等掃描完成,run() 是一串順序 API 呼叫。
cancel_event,命中 raise ScanCancelledError。httpx(FR-039 心跳已用),不加新相依。impacts[] 優先(MQR 語意,10.2+ 回應就有),缺席時 fallback 舊 type + severity,相容 9.9 LTS 至 2026.x LTA。SonarQube掃描報告_<project_key>_<date>.html、content_type="text/html"。內容=Quality Gate 狀態+核心度量+severity 分佈+issue 清單(前 200 條,註明「僅列前 200 條/總數 N」)+Security Hotspots+分析時間。這是必做項不是 fallback——Community Build 沒有任何報告匯出功能(付費版才有 PDF)。{"findings": <issues 總數>, "bugs": …, "vulnerabilities": …, "code_smells": …, "security_hotspots": …, "quality_gate": "OK"/"ERROR"}——數字取自 measures API,不從 issue 清單自行加總。_CONNECTOR_BUILDERS 加 "sonarqube": _build_sonarqube(lazy import,3 行 builder 形狀比照 _build_zap)。絕不動 _LEGACY_TOOL_ID_TO_CODE——表內既有的 3: "sonarqube" 是 legacy fallback(D11 解耦後的過渡相容表),保持原樣。
DB 已有 FR-056 留下的 id=3 sonarqube 佔位列(status='coming_soon'、config_field_schema=[]、無 param_schema)。因此——
INSERT ... ON CONFLICT (code) DO NOTHING 寫,會被既有列擋下靜默跳過,看起來成功實際什麼都沒改。config_field_schema(上方兩欄)+ status → 'available' + setup_guide + description 只拿掉句尾「此工具尚在規劃中、目前無法使用,敬請期待。」其餘不動。公司自有 SonarQube(http://192.168.50.171:9000,專案 guidant-ai-backend / guidant-ai-fe)直接當 DEV 端到端驗收目標。token 為 user token——不寫進任何文件,執行者自查本機環境。
純 schema 驅動、零新元件。僅兩處微調(T-5.3):
JobExecutionDrawer.vue 的 _PRIMARY_PARAM_KEYS 加 'project_key'。my-tasks.json(zh-tw + en,比照既有 summary_* key 所在檔)補 summary_bugs / summary_vulnerabilities / summary_code_smells / summary_security_hotspots / summary_quality_gate。| # | 限制 | 說明 |
|---|---|---|
| ① | 不觸發掃描 | 資料新鮮度取決於客戶端掃描頻率;報告內的分析時間是稽核員判斷依據(D13) |
| ② | issue 清單僅前 200 條 | 統計數字為全量(facets / measures),僅明細清單截斷並在報告註明(D15) |
| ③ | 證據可下載、不可預覽 | 同 ZAP 第一批出貨標準(證據池預覽鏈不吃 HTML) |
| ④ | Community 無多 branch | branch 留空即可;填了而 server 不支援時回明確錯誤(D14) |
決策脈絡見 D17–D24;討論稿:
discussion-sonarqube-scan-mode.html。⚠️ 定位:這是雛形,不是完整產品。 本節每一處取捨都朝「最小可行」傾斜。遇到「這裡是不是應該做得更周全」的疑問時,先翻本節末的「後續強化」——所有被砍掉的東西都在那裡列了一句話說明為什麼這階段不做。決策者原話:「先用公開 repo 可以下載的那種,我們先做一個雛形,後面再慢慢加強」。
FR-058.5 做的是拉取快照——連客戶的 SonarQube,把「最近一次分析結果」拉回來組報告,平台不觸發掃描。決策者看到實際畫面後指出:
流程好像不太對,我想要的是 user 有提供一版源碼,可能上傳 或是提供 server 位置,然後透過 sonarqube cli 去掃描並且產出結果報告 — 決策者,2026-07-31
這句話包含三個要素:① 使用者提供一版源碼 → ② 平台透過 sonar-scanner CLI 執行掃描 → ③ 產出結果報告。上線的 FR-058.5 只有 ③,而且那個「結果」不是平台掃出來的,是客戶自己先掃好放在 server 上的。
這是新需求不是 bug。 FR-058.5 的 D13 明確裁示了「拉取最近一次分析結果組報告、不觸發掃描」,當時就把「agent 端自跑 sonar-scanner」列為被排除的選項,理由是「agent 沒有原始碼,也不可能備齊各語言 build 環境」。那個判斷在當時的前提下是對的——當時沒有「使用者提供源碼」這個輸入。本案正是補上那個前提(把源碼送進 agent),而「不可能備齊各語言 build 環境」則靠收斂語言範圍解決(D18 只做不需要 build 環境的 A 類)。
為什麼要把這段軌跡寫進文件:設計文件上會留下「D13 排除了 agent 自跑 scanner,D17–D24 又把它做出來」這種看似矛盾的紀錄。寫明是為了讓未來的人知道這不是反覆,是前提變了——輸入從「沒有源碼」變成「使用者會提供源碼」,被排除的理由就不再成立。
| 面向 | 拉取快照(FR-058.5,已上線 DEV) | 主動掃描(本案 FR-058.6) |
|---|---|---|
| 誰發動掃描 | 客戶自己的 CI 流程 / 開發者機器 | 平台(agent 執行 sonar-scanner) |
| 源碼在哪 | 平台從來沒碰過源碼 | 必須送到 agent(本案=Git clone) |
| 報告新鮮度 | 取決於客戶上次掃描的時間,可能是三個月前 | 就是現在——稽核當下的源碼狀態 |
| 對客戶 SonarQube 的動作 | 唯讀(只呼叫查詢類 API) | 寫入——推分析結果,專案不存在時自動建立 |
| agent 需要什麼 | 只要連得到 SonarQube 的 HTTP | sonar-scanner 執行檔、源碼落地空間、記憶體 |
| 執行時間量級 | 秒級(幾個 API 呼叫) | 分鐘到數十分鐘——掃描 + server 端排隊處理 |
| 適用客戶 | 已有成熟 CI、SonarQube 天天在跑,只要把結果收進證據池 | 沒有 CI 整合、或稽核當下才要看某個特定版本的源碼 |
兩模式並存,拉取模式不移除(D17)。理由:① 對「客戶已有成熟 CI」的情境,拉取模式是正確且更輕量的答案,主動掃描反而多餘(會在他們的 server 上多建一個專案、多跑一次分析);② 成本已付掉(connector / seed / setup_guide 都已完成並上 DEV);③ C# / VB.NET 這類技術上做不了主動掃描的語言,拉取模式是唯一的路。
一個容易誤會的地方:兩種模式都需要客戶有 SonarQube server——因為 sonar-scanner 沒有 dry-run,掃完必須把結果推到 server 才讀得到。差別只在「誰發動掃描」,不在「要不要有 server」。
相同的部分(可直接複用):都連客戶自架的 SonarQube、憑證都是 base_url + token、最後都組成自包含 HTML 進證據池、取消 / timeout 的行為標準一致。
使用者建任務:選 SonarQube、scan_mode=原始碼掃描(預設)、填 Git repo URL + 專案代碼後綴
→ BE 派工(既有鏈路,本案無敏感參數:公開 repo 不需憑證)
→ agent 領回 task → 步驟一:git clone 到磁碟型 tmp 目錄(不可用 /dev/shm,D22)
→ 步驟二:Popen 起 sonar-scanner(輪詢式取消,stdout 走檔案)
→ scanner 分析(分鐘~數十分鐘)→ 推送結果到客戶 SonarQube(寫入!專案不存在會自動建立)
→ scanner 產出 report-task.txt
→ 步驟三:解析 report-task.txt 取 ceTaskId → 輪詢 GET /api/ce/task?id=<id> 直到 status 脫離 IN_PROGRESS
→ 步驟四:拉 quality gate / measures / issues / hotspots → render 自包含 HTML(複用 FR-058.5 既有程式碼)
→ 報告 bytes 走既有 blob 通道上傳 → 進證據池
→ finally:刪除 tmp 目錄(掃完即刪,D22)
步驟一到三是全新的;步驟四可直接複用已上線的拉取模式程式碼。
坑一:沒有 dry-run。 sonar-scanner 沒有「只分析不推送」的模式——每一次掃描都是對客戶 SonarQube 的寫入,且專案不存在時會自動建立(provision),等於污染客戶的專案清單。管理員想擋,只能移除 token 擁有者的 "Create Projects" 全域權限,沒有專門的開關。D21 的 Guidant-AI- 前綴解決「撞號」與「無法辨識來源」,但「每掃一次多一筆專案」本身沒有解。
坑二:掃完不等於能讀。 scanner 印 EXECUTION SUCCESS 只代表上傳完成,server 端是非同步序列佇列。必須解析 scanner 產出的 report-task.txt 取得 ceTaskId,再輪詢 GET /api/ce/task?id=<ceTaskId> 直到 task.status 脫離 IN_PROGRESS 才能拉結果。如果掃完就直接去拉,很可能拉到上一次的舊資料——而且拉得到、不會報錯。
⚠️ 補充陷阱:CE 介面顯示的「處理時間」不含排隊時間,不能拿它當 timeout 的依據。實際等待時間取決於客戶 server 當下的佇列長度,這是我們控制不了的變數。逾時錯誤訊息建議能區分兩段——「掃描超過 N 分鐘未完成」與「掃描已完成但貴公司 SonarQube 佇列超過 N 分鐘未消化」是兩種完全不同的客戶端問題,混在一起客戶不知道要查什麼。
FR-058.5 已 seed 的兩欄(base_url text required / token password secret required)兩模式完全共用。原因見 D17 更正說明:squ_ User Token 推得上去也讀得回來(只有 sqp_ / sqa_ analysis token 是只能推不能讀),且 D19 定案只支援公開 repo → 沒有 Git 憑證這回事。
這代表本案不需要動 config_field_schema,也不需要 FR-058.0 的任務層敏感參數能力(公開 repo 無憑證可加密)。
| key | 型別 | 顯示條件 | 說明 |
|---|---|---|---|
scan_mode |
select・required・**預設 scan |
永遠顯示 | 兩個選項:pull(讀取既有結果)/ scan(原始碼掃描)。預設 scan**(D24),配套見下 |
project_key |
text | condition 綁 pull |
既有欄位,語意不變 |
branch |
text・選填 | condition 綁 pull |
既有欄位。付費版才支援多分支(D14) |
| Git repo URL | text・required | condition 綁 scan |
新增。公開 repo,不需憑證(D19) |
| 專案代碼後綴 | text・required | condition 綁 scan |
新增。實際推上去的是 Guidant-AI-<此欄>(D21) |
形狀直接照 ZAP seed 的既有寫法({"key":"...","condition":{"field":"scan_mode","value":"..."}}),FE 走現成 visible computed,不可見欄位跳過必填驗證——因此 pull 模式不會被 scan 的必填欄位誤擋,反之亦然。
setup_guide必須改寫(D24 配套②③):現行setup_guide開頭寫的是「本平台不會觸發程式碼掃描」——加入 scan 模式後這句話變成錯的,必須改寫為分模式說明,並明講「預設為原始碼掃描模式,執行時會在貴公司的 SonarQube 建立 / 更新專案(key 為Guidant-AI-<識別>)」+ 支援語言清單(D18 配套①)+ 收緊模式做法(預先建好專案 + 移除 "Create Projects" 權限)。
evidence-agent/core/task_executor_connectors/sonarqube.py)模式分流:在 run() 入口依 scan_mode 分兩條路。pull 分支=現況原封不動,scan 分支新增。
⚠️ 模式值非法必須明確 raise,絕不靜默降級。 直接沿用 zap.py:164-171 _resolve_scan_mode() 的既有立場——那段註解的理由是:使用者選了「主動」卻因拼錯被降級成「被動」,會拿到一份看起來正常但實際沒掃的報告,那比直接失敗更糟。本案同理且更嚴重:選了 scan 卻退回 pull,使用者會拿到舊的快照卻以為是剛掃出來的,而稽核報告上會標示分析時間——一份「時間是三個月前但使用者以為是今天」的證據,是會誤導稽核結論的。
子行程執行:直接照抄 inspec.py 的既有骨架,兩個設計要點必須一起帶走:
:685-722 _run_with_cancel(Popen + 輪詢)——不用 subprocess.run(timeout),因為它無法中途取消;:657-683 _terminate(SIGTERM → 寬限 → SIGKILL)處理取消與逾時。暫存與清理(D22):clone 目錄開在容器自身 /tmp(磁碟型),不可用 /dev/shm(tmpfs 記憶體型,既有 inspec.py:197-200 慣例是為小憑證檔設計的)。清理走 finally / contextmanager,掃完即刪——含失敗與取消路徑。
輪詢 CE 佇列:解析 scanner 產出的 report-task.txt 取 ceTaskId → 輪詢 GET /api/ce/task?id=<id> 直到 task.status 脫離 IN_PROGRESS;逾時訊息區分「掃描未完成」與「佇列未消化」兩段(見坑二)。
結果拉取與報告:完全複用 FR-058.5 既有的 _fetch_* 與 _render_report——這是兩模式共用的部分,不另寫一套。
| 檔案 | 改什麼 | 為什麼 |
|---|---|---|
JobExecutionDrawer.vue:308 |
_PRIMARY_PARAM_KEYS = ['profile', 'project_key'] 硬編需一併調整 |
scan 模式下 project_key 是使用者填的後綴、不是最能辨識這筆任務掃了什麼的欄位——Git repo URL 才是。若不處理,執行紀錄的主要區會顯示一個沒什麼資訊量的後綴,真正的識別(掃了哪個 repo)反而被收進摺疊的技術細節區。該檔既有註解已說明分區原則是「執行前需要知道的參數 vs 技術細節」,Git repo URL 明確屬於前者 |
| scan 模式警語 | D24 配套① | 預設值指向不可回復的一邊,事前告知從「最好有」升級為「必須有」。hint 文字不夠(使用者會略過),具體形式列實作建議 |
scan_mode 的 select 與條件欄位本身走現成 schema 驅動渲染,零新元件。
| 項目 | 為什麼這階段不做 | 日後撿起來時的已知結論 |
|---|---|---|
| 檔案上傳取源碼 | 要動三層(FE 元件 / upload 中繼 / 派工鏈路),雛形先求打通。→ 2026-08-01 更新:已由 FR-058.7 立案並設計定案(D25–D31,§4.8),本列保留為歷史紀錄 | 雲端→agent 通道已存在且成熟:infra/upload_file/remote_agent_adapter.py:54-102 的 save_file()(SHA-256 雙邊對帳 + mTLS/JWT),agent 端接收在 evidence-agent/api/blob/routes/blob_route.py:44-66。兩個坑:① param_schema 沒有 file 型別且未知型別靜默不渲染;② RemoteAgentAdapter 只在 storage_type=REMOTE_AGENT 的租戶存在,MINIO 租戶建不出來。FR-058.7 落地時的走法與此欄當年預估不同:不做 file 型 schema 欄位(改為執行時輸入,D26)、不走 BE 推檔給 agent 的 /blob 通道(改為 agent 主動 mTLS 拉取,D25) |
| 私有 repo 認證 | 公開 repo 已足以驗證整條鏈路,且平台現況本來就只支援公開 repo。→ 2026-08-01 註記:FR-058.7 上傳模式已部分涵蓋此情境(客戶把私有源碼打包上傳即可,不用把 git 憑證交給平台);真正的 git 憑證管理仍列後續強化 | 三個選項:token 內嵌 URL(會進 argv、可能落 log)、credential helper 檔案化(建議走這個)——inspec.py:44-49 已有一模一樣的前例(憑證寫進設定檔、命令列只帶引用、密碼不進 argv)、SSH deploy key(安全性佳但要處理 known_hosts) |
| **Java(B 類) | 需要客戶另外交出編譯產出,打包規則要有真實客戶樣本才定得準 | sonar.java.binaries required,值必須是目錄**、war/jar 不能直接餵、不支援萬用字元;libraries 選填支援 **/*.jar。兩種失敗模式:全缺 → 直接失敗;部分缺 → 繼續跑但降級("Class 'XXX' is not accessible through the ClassLoader")。【推論待驗】 war 解開後 WEB-INF/classes→binaries、WEB-INF/lib/*.jar→libraries |
| **C / C++ | 技術上可行,但雛形階段收斂語言範圍 | AutoConfig 是 scanner 預設模式(官方 "enabled by default"),agent 端無分歧邏輯,複雜度只在使用者說明。代價是官方明說的「in rare instances, may result in some issues being overlooked」。Objective-C 不支援 AutoConfig**,不能一起納入 |
| Scala | 官方文件完全沒表態是否需要 bytecode | 傾向 A 類但必須實測——不可憑推論寫進使用者說明 |
| C# / VB.NET | 技術上真的做不到(不是取捨,是不可行) | SonarScanner CLI 明文 "unsupported";begin/build/end 是硬性架構,分析發生在編譯器內部(Roslyn analyzer 隨編譯執行),sonar.sources 對 .NET scanner 也 "not supported"。預先編譯好的 DLL 無法補做分析。永遠只能走 pull 模式 |
| 磁碟 / 記憶體控管 | 決策者裁示只做逾時(D22) | agent 容器目前完全沒有資源限制(兩份 compose 皆無 mem_limit / cpus / ulimits,無 k8s manifest)。已知 OOM 熱點:JS/TS 與 Kotlin analyzer。SONAR_SCANNER_JAVA_OPTS 可調。詳見下方 R2 |
| coverage 紅燈處理 | 決策者裁示不處理(D23,「主要掃品質就好」) | SonarQube 自己不產生 coverage,只匯入第三方報告;我們不跑測試 → 報告必然無 coverage。Go 特例:無 coverage 資訊時視為「全部未覆蓋」而非「無資料」。詳見下方 R3 |
| 語言版本參數 | 雛形階段不加欄位 | Python 不設 sonar.python.version 時,分析器自動靜音部分 issue 以避免誤報——從稽核角度就是漏報。日後若加,建議做成選填並在報告上標註「未指定語言版本,部分檢查已略過」 |
| # | 風險 | 說明與處置 |
|---|---|---|
| R1 | 使用者送錯語言(縮範圍後的主要風險) | Java → 硬失敗(沒有 .class,scanner 直接中止,難看但安全)。C# → 靜默淺掃(CLI 不支援但不會中止,很可能產出看起來正常、實際幾乎沒分析到東西的報告,稽核員可能把「沒掃到」誤讀成「很乾淨」)。配套已列為必做(見 D18 右欄三件事) |
| R2 | 資源控管缺席(已裁示本案不做) | agent 容器完全沒有記憶體 / CPU 限制,而 OOM 風險真實存在——官方已知兩個熱點 JS/TS analyzer(純 JS 無 tsconfig 時全部檔案進單一 program)與 Kotlin analyzer,正好都在 D18 的 A 類清單裡。一旦吃爆,影響的是整台 agent 主機而不只是這個任務。 ⚠️ 資源量級:官方零基準數據。 文件唯一出現過的數字是 SONAR_SCANNER_JAVA_OPTS="-Xmx512m",而那只是語法示例不是建議值。調研中推得的「至少 2GB、JS/TS 配 4GB」是推論不是文件,不可寫進任何對外文件,必須實測。日後若要補,最小一步是在 compose 加 mem_limit——讓 OOM 只殺容器、不拖垮宿主機 |
| R3 | 報告必然沒有 coverage(已裁示本案不處理) | 我們不跑測試 → 無覆蓋率 → 客戶 Quality Gate 若含覆蓋率條件會亮紅燈,而那個紅燈不代表程式碼有問題。Go 更糟:無 coverage 資訊時視為「全部未覆蓋」,顯示 0%。已裁示照實顯示(D23),但實作時仍建議在 setup_guide 提一句 |
| R4 | 對客戶 SonarQube 的寫入副作用 | 沒有 dry-run,每次掃描都是寫入,且專案不存在時自動 provision。D21 前綴解決「撞號」與「無法辨識來源」,但「每掃一次多一筆專案」沒有解。UI 必須事前告知這是寫入行為;收緊模式(預先建好專案 + 移除 "Create Projects" 全域權限)寫進 setup_guide——SonarQube 沒有專門開關,只有這個辦法 |
| R5 | 等待時間有一段我們控制不了 | 結果進客戶 server 的佇列序列處理,必須輪詢 ce/task 直到脫離 IN_PROGRESS。排隊多久取決於客戶 server 當下負載。⚠️ CE 顯示的「處理時間」不含排隊時間,不能當 timeout 依據。 逾時訊息建議區分「掃描未完成」與「佇列未消化」兩段 |
| R6 | 兩個 JS/TS 的靜默降級 | 缺 node_modules → 型別推導精度降低,不報錯。tsconfig extends node_modules 內的 base config 而解析不到 → 官方描述為 "unexpected analysis results"——看起來有跑,結果是錯的。建議把 scanner 的警告輸出呈現在報告上,不要只顯示 issue 清單。與 R1 同一原則:「執行降級」不能假裝成「執行完整」 |
docker save,體積直接影響客戶體驗。192.168.50.171:9000)+ 一個公開 repo。決策脈絡見 D25–D32;討論稿:
discussion-fr058.7-upload-scan.html(v4,決策者已全數過稿)。定位:scan 模式的取源擴充,不是新模式。 FR-058.6 scan 模式的管線是:取源(git clone)→ sonar-scanner 掃描 → 推上 SonarQube server → CE 等待 → 報告回收 → 品質門檻渲染。本案只把第一段的取源方式多加一種:從 tenant 儲存空間下載上傳的壓縮包並安全解壓。後面整條掃描管線全部複用,一行不動。新造的東西只有四塊:取源通道(D25)、執行時輸入的傳遞(D26)、檔案生命週期(D28 / D29)、解壓額度檢查(D30)。
決策者原話(2026-08-01):
還是要可以上傳檔案做檢測,有辦法是上傳到 tenant 設定的儲存空間後,由 agent 拉下來,做掃描,掃完就刪,如果硬碟不夠就跳錯誤就好
宏觀流向由這句話定調成五段:① 使用者上傳源碼壓縮包 → ② 落地在 tenant 設定的儲存空間(system_configs 的 STORAGE_CONFIG,支援 local / minio / remote_agent 三後端——走既有機制,不另闢儲存路徑)→ ③ agent 把檔案拉下來執行掃描 → ④ 掃完就刪 → ⑤ agent 磁碟不夠直接報錯。後續討論釐清後,「掃完就刪」的適用範圍是 agent 端 workspace(tenant 儲存空間上的壓縮包保留供重掃沿用,D29 定案)。
FR-058.6 的 scan 模式取源只有公開 Git repo URL(D19 的刻意最小化取捨),但實際客戶情境裡「源碼不在公開 repo」才是常態:私有 repo(客戶不願交 git 憑證)、源碼根本不在 git(委外交付源碼包、歷史版本快照)、稽核要特定一版(客戶打包當下狀態最直接)。上傳模式一次涵蓋三種情境,並部分涵蓋原列 scope 外的「私有 repo 認證」需求(客戶打包上傳即可,完全不用把 git 憑證交給平台)。
初版設計曾把上傳欄位放在任務設定表單(param_schema 加 file 型欄位),決策者審後指出流程不對:上傳的壓縮包是「這一次要掃的東西」——特定時點的源碼快照,不該混進設定表單。放設定表單的兩個問題:① 每掃新版都要改設定再執行(兩段式繞路,且任務設定僅專案管理者可改,執行者不見得改得動);② 語意錯位(設定表單是「這個任務怎麼掃」,源碼包該在「要執行了」的當下提供)。
修正後的分工:任務設定表單只留 scan_mode 的第三選項 upload;上傳動作發生在任務抽屜發起執行時——scan_mode=upload 的任務,執行對話框出現上傳區,首次執行必須上傳(無檔不給按執行),檔案參照隨「發起執行」請求送出。參照落點是任務綁定層的 sticky 狀態(供之後重掃沿用,D29),每筆執行的 scan_params 快照同時各存一份(承接「這筆掃的是哪個檔」的稽核回溯)。附帶兩個好處:孤兒檔視窗大幅縮小(上傳與執行是同一個動作);FE 參數表單元件完全不用動(DetectionConfigField 不需要新型別分支)。
任務設定已存 scan_mode=upload(設定表單裡沒有檔案欄位)
使用者在任務抽屜發起執行:
執行對話框出現上傳區——已有 sticky 檔案時顯示「目前檔案:xxx.zip(上傳於 M/D)」可直接沿用
→ (換新版時)POST /file/upload 上傳壓縮包(驗副檔名與大小上限)
→ upload_files_for_tenant() 存入 tenant 儲存空間 → 回 file uid
→ POST /detection-tools/jobs/<uid>/execute(body 帶 source_file.uid;沿用舊檔則不帶 body)
→ BE start_execution:帶新 uid 則覆寫綁定的 sticky 參照、不帶則沿用、首次無檔 400
從 upload_files 權威取 file_name / size / sha256(不信 FE,D31)
寫執行紀錄(scan_params 快照含 source_file: {uid, file_name})
+ agent_tasks(params 夾 _source_file 完整參照,含 size / sha256 / limits)
→ agent 心跳(既有 mTLS 通道)領回任務 payload(含 source_file 參照)
→ agent 取源段(本案新造):
步驟一:shutil.disk_usage 磁碟檢查——不足直接 fail「agent 磁碟空間不足」(D28)
步驟二:GET /api/1.0/agents/files/<uid>(mTLS 串流下載;BE 依 storage_type 讀出、
minio / remote_agent 後端串流轉發不落地)
步驟三:sha256 對帳(防竄改)
步驟四:標準庫安全解壓進 scan workspace(zipfile / tarfile filter="data";
累計大小 / 檔數超過 D27 限額即中止清理,D30)
→ 以下全複用 FR-058.6 scan 管線:sonar-scanner 掃描(_run_with_cancel)
→ 推客戶 SonarQube → 輪詢 CE → 拉 issues / 品質門檻 → 組自包含 HTML 報告
→ 報告上傳進證據池 → 回報 result(既有 mTLS 通道)
→ finally:刪 agent workspace(既有機制+五路徑測試鎖約)
→ tenant 儲存上的壓縮包保留供重掃沿用(D29 定案,不做終態刪檔;清理機制列 follow-up)
→ FE 執行紀錄顯示檔名(非裸 UUID,D31)
流程上兩個關鍵設計點:① 磁碟檢查在下載之前——不是解壓失敗才發現,呼應本 arc 教訓「錯誤要在最早能發現的地方被發現」;錯誤分類為 agent 環境問題,不是掃描失敗。② BE relay 不落地——tenant 儲存為 minio / remote_agent 時,BE 用串流轉發,遵守 FR-039 不變式「雲端任何路徑不得把 binary 寫進雲端磁碟」。
FR-039 三條不變式對照(本設計不破壞任何一條):agent 不連雲端 DB(檔案參照由心跳 payload 下發,agent 只打 BE 的 HTTP 端點);雲端不落地 binary(串流轉發);只有 agent 對外連出、無對內連入(下載是 agent 主動發起的出站請求,方向與 heartbeat / result 一致)。
版本策略照既有規範:INSERT 新 version=3 + v2 is_current=FALSE,不原地 UPDATE。v3 的變更縮小到只有 scan_mode 一欄——不新增型別、不新增欄位,source_file 不進 param_schema(它是執行時輸入)。
| key | 型別 | 顯示條件 | 說明 |
|---|---|---|---|
profile |
select | (同 v2) | 沿用 v2 不變 |
scan_mode |
select・required | 永遠顯示 | 唯一變更:選項由 pull / scan 增為 pull / scan / upload(D26),附 hint「上傳模式於發起執行時提供源碼壓縮包」。預設值沿用 D24 裁示 |
server_url、token 等連線欄位 |
(同 v2) | (同 v2) | 沿用 v2 不變(secret 欄位加密 envelope 機制照舊) |
repo_url |
text・required | condition 綁 scan |
沿用 v2;condition 維持只在 scan 出現——upload 模式下不出現、不必填 |
project_key_suffix |
text・選填 | scan 與 upload 皆出現 | 沿用(D26)。既有 condition 是單層等值比對,無 OR——實作上可能要出兩條同 key 的 condition 欄位或擴充 condition 語法,實作時定案,屬 T-7.1 細節 |
source_file 不進 param_schema(設定表單沒有檔案欄位),但參照落點在任務綁定層的 tool_params(sticky 狀態,供重掃沿用,D29):execute body 帶新 uid 時覆寫、不帶時沿用現值。每筆執行的 scan_params 快照另存一份完整資訊(含檔名,稽核回溯用)。
// 任務綁定 tool_params(schema 欄位 + sticky 檔案參照;後者不是 schema 欄位、FE 設定表單看不到)
{
"profile": "...",
"scan_mode": "upload",
"project_key_suffix": "release-1.2",
"_source_file": { "uid": "<file-uid>" } // sticky:execute 帶新 uid 覆寫、不帶沿用
}
// 每筆執行的 scan_params 快照(每筆各存一份;file_name 等由 BE 從 upload_files 權威取,D31)
{
"...任務設定各欄...",
"source_file": { "uid": "<file-uid>", "file_name": "my-project-src.zip" }
}
// compliance.agent_tasks.params(心跳下發給 agent 的 payload,BE 注入內部 key)
{
"...同上...",
"_task_uid": "...", // 既有夾帶慣例
"_credentials": { ... }, // 既有:心跳時當場解密注入
"_source_file": { // 新:agent 取源所需的完整參照
"uid": "<file-uid>",
"file_name": "my-project-src.zip",
"size_bytes": 52428800,
"sha256": "<上傳時算好的摘要>", // agent 下載後對帳防竄改
"limits": { ... } // D27:BE 設定的限額隨 payload 下發(與 credentials 下發同構)
}
}
註:_ 前綴 key 回 FE 時由 _resolve_scan_params()(detection_orchestration_service.py:385-400)剝除——既有機制自動涵蓋 _source_file,FE 只看得到 source_file 的 {uid, file_name}:執行紀錄顯示檔名、不顯示裸 UUID(D31)。
A. 執行端點改造(既有端點加 request body)
| 項目 | 內容 |
|---|---|
| 路徑 | POST /detection-tools/jobs/<uid>/execute(既有)——目前不收 body,純快照綁定任務設定 |
| 改造 | 改為可帶當次輸入:{"source_file": {"uid": "..."}}——FE 只送 uid,檔名不由 FE 送(D31)。scan_mode=upload 時:帶 uid → 驗證後覆寫綁定的 sticky 參照;不帶 → 沿用綁定現值;綁定也沒有(首次)→ 400「請先上傳源碼包」。BE 驗證:uid 存在且屬本 tenant(RLS)、副檔名在允許清單(.zip / .tar / .tar.gz / .tgz,D27)、大小在 D27 限額內;通過後從 upload_files 表權威取回 file_name / size / sha256,寫進 scan_params 快照與 agent_tasks.params(_source_file) |
| 相容性 | 不帶 body 的既有呼叫(pull / scan 模式)行為完全不變 |
B. 新端點:agent 檔案下載
| 項目 | 內容 |
|---|---|
| 路徑 | GET /api/1.0/agents/files/<uid>(命名比照既有 agent 控制面端點) |
| 認證 | 純 mTLS、不掛 jwt——與 tasks ack / result 同形狀(api/remote_agent/__init__.py:34-38 先例) |
| 授權(綁定驗證) | 由 client 憑證識別 agent 身分後,驗證該 uid 必須屬於該 agent 名下 pending / running 任務的 source_file——防任意檔案讀取。任務終態後參照即失效 |
| 回應 | 串流回傳壓縮包 binary(Content-Length + 建議帶 X-Checksum-Sha256 header);BE 對 minio / remote_agent 後端串流轉發不落地 |
| 錯誤 | uid 不存在或不屬於該 agent 任務 → 404(不區分兩種情況,避免探測);檔案已被清理刪除 → 404。error code 依規範新增 GRC_404xxx 序號 |
_scan_workspace() 建目錄前 shutil.disk_usage("/tmp"),free < (壓縮包大小 × 12 + 1GB)即 fail,係數與底值吃 payload 下發的 limits。core/task_executor.py:148-153 _post() 的 httpx mTLS client 形狀加 _get_stream(),串流寫進 workspace,下載後 sha256 對帳。zipfile(extractall 內建路徑消毒)、tar 系走 tarfile filter="data"(Python 3.11.4+,agent 基底 python:3.11-slim 足夠);兩家族共用約二十行額度檢查層(解壓前加總宣告總大小與檔數、超過 D27 限額即拒絕;逐檔解時看 cancel_event)。_clone_repo() 分支),_run_scanner() 起整段原樣沿用;workspace 清理走既有 finally: shutil.rmtree(五路徑測試鎖約,test/test_sonarqube_connector.py:1132-1180)。專案規劃頁「開始執行任務」在發佈時會對 detection_tool 任務自動派第一次掃描(FR-056.5 #5:TaskExecutionService.start_task_execution → _auto_dispatch_detection_scans,app/grc/service/task_execution_service.py:90-133,冪等條件=該任務查無任何 detection_executions 紀錄)。自動路徑不帶檔案,對 scan_mode=upload 的任務必然打中「首次無檔 400」(DETECTION_TOOLS_400005),且被批次派發的 try/except 吞掉只留 warning log——形成 PM 看到成功 toast、執行者無感、且每按一次就再失敗一次的靜默失敗迴圈。D33 定案:upload 任務在挑選階段就排除——get_detection_jobs_without_execution(infra/grc/repository/task_execution_query.py:134-159)的查詢加上排除 tool_params->>'scan_mode'='upload' 綁定的條件,upload 任務一律由執行者在任務抽屜上傳檔案後手動執行(抽屜已有完整前置擋門),pull / scan 自動派發不變。FE 同步在批次發佈 confirm 文案補一句提示。實作為 T-7.5(§5.12)。
現況兩缺陷:① 成功通知內容過簡——只有「專案 X 的檢測工具掃描已完成」一句,且只發 Email(平台其他通知已是 Email/Discord/Telegram 三管標準);② 失敗完全不通知——on_scan_failed 只寫 detection_executions,使用者要自己去任務抽屜看才知道掃描失敗。
九項資料鏈:通知內容擴為九項——專案(升級走 round 鏈取代現行 ORDER BY id LIMIT 1)、控制項群組、控制項、AO、任務名稱、工具名稱、掃描設定、時間(started/finished)、成功或失敗;另把 handler 現成卻沒用的 evidence 參數用起來(報告檔名/份數,零額外查詢)。掃描設定複用 _resolve_scan_params() 現成兩層剝除(_ 前綴內部欄位+secret envelope),profile UUID 轉譯成名稱。新查詢走 8 層 join 鏈(job→workflow_execution_control_mapping→round→project→extension→ssp→profile_imports→catalog_controls/groups/parts),加在 infra/detection_tools/repository/detection_job_notify_query.py(天然擴充點)新增 get_notify_context(job_uid) 一次 SQL 取齊。失敗路徑:on_scan_failed 補查 job/binding(抄 on_scan_succeeded :472-475 pattern)後發通知,含 error_message,收件人與成功相同(指派人+專案 manager+reviewer)。hosts(目標主機 IP)照出(D34 ③)。文案準備 HTML(email 表格)+純文字(Discord/Telegram)兩版,i18n 6 個 msgid(兩語系 messages.po+pybabel compile)。
兩坑(探查已實證,實作必守):① catalog_controls.control_id 跨 catalog 不唯一(DEV 實測同 control_id 41 筆)——join 時必須帶 catalog_id = profile_imports.source_catalog_id 過濾,否則會撈到別的 catalog 的同名控制項;② AO 的 title 對 CMMC 全 NULL——用 title or prose or part_id fallback(job_export_query.py:145 前例)。
範圍外(本段不做,另案):站內鈴鐺通知——平台無 notifications 表,屬平台級新能力;收件人 locale 切換——通知在 agent request context 發、Accept-Language 是 agent 的,一律 zh_Hant_TW(既有限制,非本案引入)。
無 schema migration、無 DI 變更(_profile_domain/_tool_domain/_notify_query 全已注入)。實作為 T-7.6(§5.12)。
| 限制 | 說明 |
|---|---|
| remote_agent 同台繞圈 | tenant 儲存為 remote_agent 且掃描 agent 為同一台時,檔案走 agent→BE→agent 一圈(D25 附註)。罕見組態,正確性不受影響,只是低效 |
| 檔案只增不減(v1 無清理機制) | D29 定案保留沿用+歷史不刪,孤兒檔(原 D32)與替換下來的歷史版本都留在 tenant 儲存空間,v1 統一不清理、列 follow-up。未來清理機制必須以「綁定仍參照中的檔案」為保留名單、不能只看檔案年齡(D29 ④) |
| 上傳無進度條 | BaseService.post 不支援 onUploadProgress;100MB 內先接受轉圈等待,列 follow-up |
| 語言限制同 scan 模式 | C# / VB.NET 永遠只能走 pull(MSBuild 架構限制);Java 需 .class 檔的限制照舊——上傳的壓縮包是純源碼時 Java 專案掃不出 bytecode 相關規則 |
scope 外清單(本案不做,有需求另立案):7z / rar 等其他格式(已收 .zip / .tar / .tar.gz / .tgz,再擴充等實際需求出現);私有 repo 認證(上傳模式已部分涵蓋,真正的 git 憑證管理仍列 §4.7 後續強化);上傳檔案掃毒 / 內容檢查(源碼包長存於 tenant 設定的儲存空間;平台任何環節都不執行壓縮包內容——agent 只解壓後交給 sonar-scanner 做靜態分析;BE relay 串流不落 BE 自身磁碟。防毒屬防毒層議題,有需求另議);檔案清理機制(D29 ④ follow-up);agent 容器資源限制(部署面議題,本案只做應用層磁碟檢查);多檔上傳 / 資料夾上傳(一次任務一個壓縮包,多模組專案由客戶自行打成一包)。
兩條真依賴(不可並行)
FR-058.0 任務層敏感參數 → FR-058.1 ZAP:ZAP 要收「被掃網站的登入帳密」屬任務層敏感資料。.0 未完成,ZAP 登入後掃描只能把帳密明文存進 tool_params,等於白做一次要重改。FR-058.2 InSpec/CINC connector → FR-058.3 GCB:GCB 不另立引擎,跑的就是 .2 的 connector。.2 沒有,.3 沒東西可跑。FR-058.5 SonarQube 拉取 → FR-058.6 SonarQube 主動掃描:同一個工具的兩種模式(D17),.6 的 seed 是在 .5 的 param_schema 上加 scan_mode 與條件欄位、connector 是在 .5 的檔案內加分支,且步驟四(拉結果組報告)直接複用 .5 的程式碼。.5 沒有,.6 無處可加。可同時跑的線(原三條;2026-07-31 追加線 D 後為四條)
橫向 FR-058.X:_TOOL_ID_TO_CODE 解耦(建議最先單獨做完)
↓
線 A:FR-058.0 敏感參數 → FR-058.1 ZAP
線 B:FR-058.2 InSpec/CINC → FR-058.3 GCB
線 C:FR-058.4 Nmap(獨立)
線 D:FR-058.5 SonarQube 拉取(T-5.1 → T-5.2 → T-5.3)
→ FR-058.6 SonarQube 主動掃描(T-6.1 → T-6.2 → T-6.3)
→ FR-058.7 SonarQube 上傳檔案掃描(T-7.1〜T-7.4,2026-08-01 追加;
T-7.1 BE 與 T-7.2 agent 可並行,但並行前必須先凍結欄位 key 契約,見 §5.12)
線 D 的 UI 驗收合併(2026-07-31 裁示):FR-058.5 的三支子任務程式碼皆已完成並 commit,但端到端 UI 驗收尚未做(DEV
detection_executions內 sonarqube 仍 0 筆,只有 probe 通過與容器內手動驗證)。決策者裁示「那版基本不能用了,先不測試,要等這版」——因此 T-5.3 的 UI 驗收併入 T-6.3 一起做,一輪走完 pull 與 scan 兩模式(詳見 §5.9 與 §6 第 11 項)。→ 2026-08-01 已完成:pull 與 scan 兩模式端到端通過(detection_executionssonarqube 2 筆皆succeeded,171 上長出Guidant-AI-soybean-ui專案),本段保留為歷史紀錄。
兩個實務但書(排程建議,不是純技術描述)
_TOOL_ID_TO_CODE 那個 factory 檔三條線都要改,同時開工會反覆衝突。ID 解耦工作量很小(改一個 factory + 派工 payload 加一個欄位),先做完三條線就不會撞同一個檔。.0 是 BE + FE 的加解密與欄位渲染,.2 是 agent 端的 WinRM 連線與 profile 執行,適合分給不同 session 平行跑。_TOOL_ID_TO_CODE 解耦 — 依賴:無(所有工具的共同前置)| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-X.1 | BE:_collect_pending_tasks() 派工 payload 加 detection_tool_code 欄位 |
心跳回應含 code 欄位;既有 openvas / openscap 派工不受影響 | — | BE |
| T-X.2 | agent:get_connector() 改依 code 取 connector,detection_tool_id 降為 fallback(含 warning log),移除硬編 id 表 |
新舊 payload 都能正確取到 connector;openvas / openscap 既有派工迴歸綠 | T-X.1 | evidence-agent |
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-0.1 | BE:param_schema 支援 secret: true 語意;寫入 tool_params 前對 secret key Fernet 加密(複用既有加密鏈,依任務綁定的 schema 版本判定) |
帶 secret 欄位的任務參數落庫為密文;非 secret 欄位維持明文;schema 改版後舊任務仍解得開 | — | BE |
| T-0.2 | BE:派工組裝時解密 secret key(與租戶層 credentials 同一時機下發) | agent 收到的 pending_tasks 內敏感參數為明文;DB 內仍為密文 |
T-0.1 | BE |
| T-0.3 | BE:讀取供 FE 顯示與稽核 log 寫入時,secret key 一律剝除(整個 key 不出現,非遮罩) | 執行紀錄 API response 無敏感 key;system_logs 事件內容無明文敏感值 |
T-0.1 | BE |
| T-0.4 | FE:secret 欄位渲染(走現成 password 型態)+ 執行紀錄顯示驗證(確認 _PRIMARY_PARAM_KEYS 等硬編邏輯不會因 key 缺席出錯) |
任務參數表單可填敏感欄位;執行紀錄畫面不顯示敏感值且不出現異常空白/原始 key | T-0.3 | FE |
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-1.1 | migration:seed ZAP(connection_type='API' + config_field_schema:服務位址 / API key)+ param_schema(§4.2 六欄,含 login_password 的 secret: true、scan_mode 預設被動)+ setup_guide(daemon 啟動與 API key 設定);三環境套用 |
設定頁清單出現 ZAP(available);租戶可完成設定並測試連線;任務可選 ZAP 並依 scan_mode 條件顯示登入四欄 |
T-0.1 | BE |
| T-1.2 | agent:zap.py 骨架 + probe()(只驗連得到 daemon 且 API key 正確,不建立掃描資源)+ zaproxy 0.6.0 相依 |
測試連線在「連不到 / API key 錯 / 正常」三情境回清楚訊息 | T-1.1、T-X.2 | evidence-agent |
| T-1.3 | agent:run() 被動與主動模式 + 雙格式取報告(core.htmlreport HTML 上傳 / core.jsonreport 僅記憶體解析 summary)+ cancel_event + timeout 上限;輪詢兩種回傳型態正確處理(ajaxSpider.status 回字串、ascan.status 回百分比) |
手塞派工掃真實目標 → HTML 進證據池(可下載)、summary 有數字、取消可即時中止、逾時不無限等待 | T-1.2 | evidence-agent |
| T-1.4 | agent:run() 登入後主動模式(context + 登入認證 + logged_in_indicator),帳密取自任務層敏感參數 |
對需登入的測試站台掃描,報告內容顯示已進入登入後頁面;帳密全程不落地、不出現在 log | T-1.3、T-0.2 | evidence-agent |
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-2.1 | migration:seed InSpec/CINC(connection_type 涵蓋 SSH 與 WinRM)+ config_field_schema(雙 transport 憑證,含條件顯示)+ param_schema + setup_guide(含「必須用 CINC Auditor 而非官方 InSpec」的授權說明與目標主機 WinRM 啟用前提);三環境套用 |
設定頁出現 InSpec/CINC(available);雙 transport 憑證可分別設定並落庫為密文 | — | BE |
| T-2.2 | agent:Dockerfile 加入 CINC Auditor + inspec.py 骨架(雙 transport 連線參數解析)+ probe()(連線 / 認證 / CINC 可執行三段檢查與錯誤分類) |
image build 過;測試連線在 SSH 與 WinRM 兩側各自回清楚訊息 | T-2.1、T-X.2 | evidence-agent |
| T-2.3 | agent:run() 執行 profile + --reporter json 統一解析 + 逐台多檔證據 + host_failures 逐台失敗回報 + cancel_event / timeout |
手塞派工掃 Linux(SSH)與 Windows(WinRM)各一台 → 每台一份報告進證據池;其中一台失敗時另一台仍成功且失敗被明確回報 | T-2.2 | evidence-agent |
| T-2.4 | 結案 Notion CM-953(本 FR 吸收,於此子任務完成時標記) | CM-953 狀態更新並註明由 FR-058.2 落地 | T-2.3 | —(Notion) |
驗收條件是「鏈路可運作」,不是「涵蓋多少規則」(D7 明確定案)。這不是 content 工程。
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-3.1 | migration:seed GCB 條目(引擎指向 .2 的 CINC Auditor,Windows 與 Linux 共用)+ param_schema(含 content 來源指定欄位)+ setup_guide;三環境套用。連帶:agent factory 加 code='gcb' → InspecConnector 映射(GCB 不另立 connector,D7);一併把 InSpec 的 param_schema.profile 改為 select_or_text |
設定頁出現 GCB;任務可選 GCB 並指定目標與 content | T-2.1 | BE(+ evidence-agent factory 一行) |
| T-3.2 | (2026-07-31 縮成「驗證 + 寫文件」,不寫 code) 原規劃的 content 外部載入接口裁示不做——CINC 原生支援三種來源、params.profile 直接進 argv,能力本來就在,再包一層只是把上游能力重新命名一次。改為:確認 D7 三條約束皆已滿足並留下實跑佐證,一併記錄兩個已知限制(封閉網路無法送檔進 container、私有 repo 無憑證路徑)。架構前提:content 由 agent 本機讀取後交給 cinc-auditor,不送到目標主機(見 §4.3 架構事實) |
更換 content 不需重建 agent image、不需改 connector 程式碼(已驗證,見 §4.4) | T-2.3 | —(文件) |
| T-3.3 | 一份 Windows 最小 Demo profile(取最單純的一個基準項目,且必須落在 HKLM\Software\Policies 正規政策區以避開 tattoo settings 陷阱) |
端到端鏈路跑通:WinRM 連線 → 認證 → profile 執行 → 結果解析 → 證據上傳,證據進證據池且 summary 有數字。不追求規則覆蓋量 | T-3.2 | evidence-agent |
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-4.1 | BE + migration:detection_tools 加「是否需要憑證」宣告欄位;start_execution() 放行零憑證工具(D9);三環境套用。(2026-07-30 改案後 Nmap 不再是此能力的使用者,但這一棒仍要做——它是平台級能力) |
零憑證工具可正常開始執行,不再誤回 DETECTION_TOOL_CONFIG_NOT_FOUND;需憑證的既有工具檢查行為不變 |
— | BE |
| T-4.2 | migration:seed Nmap(connection_type='SSH'、requires_credentials=TRUE——2026-07-30 改案,原定 CLI + 零憑證)+ config_field_schema(SSH 憑證,對照 OpenSCAP)+ param_schema(掃描目標 / 掃描類型 / 埠範圍)+ setup_guide(客戶在自己指定的主機安裝 nmap、NPSL 授權說明、SSH 連線前提、埠掃描授權警語);三環境套用 |
設定頁出現 Nmap(available) 並可設定 SSH 憑證;任務可選 Nmap 並填參數發佈 | T-4.1 | BE |
| T-4.3 | agent:nmap.py(SSH 登入執行主機 → nmap -oX → XML 解析 summary + 報告上傳,逐台 host_failures 回報)+ probe()(SSH 連線 / 認證 / 該主機 nmap 存在可執行 / 權限四段分類)+ cancel_event / timeout;不 bundle 進 image;主要對照 openscap.py(SSH 型)而非本機子行程模式 |
手塞派工掃真實網段 → 報告進證據池、summary 有開放埠數;執行主機未安裝 nmap 時 probe 回明確訊息 | T-4.2、T-X.2 | evidence-agent |
詳細設計見 §4.6;決策見 D13–D16。pull-snapshot 型,不觸發掃描。
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-5.1 | BE seed:UPDATE 既有 id=3 sonarqube 佔位列(config_field_schema 兩欄 + status → available + description 去尾句「此工具尚在規劃中…」+ setup_guide)+ INSERT param_schema v1(project_key 必填 / branch 選填)——一支 SQL migration。注意不是 INSERT 新工具,照 ZAP 範本的 ON CONFLICT DO NOTHING 會被既有列擋下靜默跳過(§4.6 seed 特殊點)。依環境異動鐵律只套 DEV |
DEV SELECT 確認佔位列已更新且 param_schema 存在;FE 工具卡由 coming_soon 變可設定 | — | BE |
| T-5.2 | agent:sonarqube.py connector——probe()(system/status + 帶 Bearer 的 authentication/validate)+ run()(拉快照 → render 自包含 HTML + summary,逐呼叫檢查 cancel_event、單呼叫 timeout 60s)+ factory 註冊(_CONNECTOR_BUILDERS 加 "sonarqube",不動 _LEGACY_TOOL_ID_TO_CODE) |
單元測試綠(mock HTTP,比照 zap / nmap connector 測試慣例);對 192.168.50.171:9000 實測 probe 與 run |
T-5.1(schema 定形) | evidence-agent |
| T-5.3 | FE 微調+端到端驗收:JobExecutionDrawer.vue _PRIMARY_PARAM_KEYS 加 'project_key'+i18n my-tasks.json(zh-tw + en)補五個 summary_* key;用 171 兩專案(guidant-ai-backend / guidant-ai-fe)完整走一輪 |
測試連線 → 發任務 → 證據進池 → summary 對數全通。驗收需 DEV agent image 重建部署(只動 DEV 123,STG / POC agent 不動)。⚠️ 2026-07-31 裁示:程式碼部分已完成 commit,UI 端到端驗收併入 T-6.3 一起做(見 §5.1 但書)→ 2026-08-01 已完成 | T-5.1、T-5.2 | FE + evidence-agent |
詳細設計見 §4.7;決策見 D17–D24。定位是雛形——驗收標準是「端到端鏈路可運作」,不是「掃描品質有多好」或「語言涵蓋多廣」。
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-6.1 | BE seed(一支 SQL migration,檔名接續既有序號 → 2026-XX-XX-fr058-19-sonarqube-scan-mode.sql):param_schema 升 v2——加 scan_mode(select、required、預設 scan、兩選項 pull / scan)+ scan 模式兩個新欄位(Git repo URL required / 專案代碼後綴 required,皆 condition 綁 scan)+ 既有 project_key、branch 補 condition 綁 pull;setup_guide 改寫(現行開頭「本平台不會觸發程式碼掃描」加入 scan 模式後變成錯的,必須改為分模式說明;明講預設為原始碼掃描、會在客戶 SonarQube 建立 / 更新 Guidant-AI-<識別> 專案;補支援語言清單與收緊模式做法)。版本策略照 2026-07-27-fr056-5 範本:先把舊版 is_current=FALSE 再 INSERT v2,不是覆蓋 v1。依環境異動鐵律只套 DEV |
DEV SELECT 確認 v2 為 is_current、v1 保留;FE 建任務頁選 SonarQube 時預設顯示 scan 模式的兩個欄位,切到 pull 改顯示 project_key / branch;切換模式時隱藏欄位不觸發必填驗證;setup_guide 內無「本平台不會觸發掃描」這類與 scan 模式矛盾的敘述 |
T-5.1 | BE |
| T-6.2 | agent:① Dockerfile 裝 sonar-scanner CLI(平台專屬 zip 自帶 JRE,不裝 JDK;D20);② sonarqube.py 加 scan 模式分支——git clone 到磁碟型 tmp(不可用 /dev/shm,D22)→ Popen 起 scanner(照抄 inspec.py:685-722 _run_with_cancel+:657-683 _terminate,stdout 走檔案不走 PIPE)→ 解析 report-task.txt 取 ceTaskId → 輪詢 GET /api/ce/task?id=<id> 直到脫離 IN_PROGRESS → 複用既有 _fetch_* / _render_report 組報告;③ 逾時上限+finally 刪 tmp(含失敗與取消路徑);④ 模式值非法明確 raise 不靜默降級(照 zap.py:164-171 立場);⑤ 單元測試(mock HTTP + mock 子行程,比照既有 connector 測試慣例) |
單元測試綠,且必含「非法 scan_mode 值 raise」與「tmp 目錄在失敗路徑仍被清掉」兩條;image build 過並記錄體積增量;容器內對一個公開 repo 手動跑通全鏈路(clone → scan → 推 171 → 輪詢 → 拉結果 → HTML);逾時訊息可區分「掃描未完成」與「佇列未消化」 | T-6.1(schema 定形) | evidence-agent |
| T-6.3 | FE 微調+兩模式合併端到端驗收:① JobExecutionDrawer.vue:308 _PRIMARY_PARAM_KEYS 調整——scan 模式下 Git repo URL 才是主要識別(見 §4.7 FE 接入點);② scan 模式警語(D24 配套①,hint 文字不夠);③ DEV 端到端一次驗完 pull 與 scan 兩模式——含 FR-058.5 尚未做的 UI 驗收(§5.1 但書) |
scan:建任務填公開 repo URL + 後綴 → 執行 → 171 上出現 Guidant-AI-<後綴> 專案 → HTML 報告進證據池、summary 有數字、執行紀錄主要區顯示 repo URL。pull(補做 T-5.3 驗收):填 project_key → 報告進池、summary 與 server 畫面對數。共同:取消可即時中止、逾時不無限等待。驗收需 DEV agent image 重建部署(只動 DEV 123,STG / POC agent 不動) |
T-6.1、T-6.2、T-5.2 | FE + evidence-agent |
| # | 子任務 | 驗收 | Repo |
|---|---|---|---|
| T-9.1 | 清掉 WorkflowSetupEditor.vue 的硬編死碼工具清單 toolsMenu(與 detection_tools 完全脫鉤;本案加入 ZAP 與 Nmap 後畫面會出現兩處 Nmap,其中一處是死的) |
畫面不再出現與工具目錄脫鉤的工具名稱 | FE |
| T-9.2 | 補 docs/claude/database-schema.md 的 config schema 與 detection 三表記載(目前 grep 零命中) |
三表結構與 FK 關係有記載 | BE |
2026-07-31 決策者實地比對兩份報告樣本後的結論:本輪照現行 html2 出貨驗收,以下列為後續優化案,最後再做。
同一次掃描(dev-sec/linux-baseline,59 條控制)產出的兩份報告:
現行 html2 |
MITRE SAF hdf2html |
|
|---|---|---|
| 檔案大小 | 167 KB | 1.9 MB(Executive 808 KB / Manager 1.8 MB) |
| 內嵌 JS | 2.6 KB | 702 KB(含 Chart.js) |
| 本質 | CINC 順手產的靜態報表 | 打包成單檔的前端應用(Heimdall 檢視器) |
| 控制涵蓋 / 標題 / 描述 / 原始檢測碼 | ✓ | ✓(相同) |
| 嚴重程度 | Impact: 1.0(數字) |
Critical(文字) |
| 合規統計與圖表 | 無 | 有(Passed 17 / Failed 3 + 百分比) |
| 外部 CDN 依賴 | 0 | 0(皆離線可開) |
關鍵澄清:兩者原始資料完全相同。 SAF 的優勢不是「內容較多」,而是把已有資料加工成結論:① 控制層狀態彙總(原始 JSON 只有逐檢查點的 pass/fail,沒有「這條控制算不算過」)② impact 數字依 InSpec 慣例轉 severity 文字 ③ 圖表。SAF 報告可讀文字量看似多 4 倍,主因是同一條控制重複呈現三次(總表/明細/Result Details),非資料量差異。
亦查明:SAF 的 800-53 Controls & CCIs 欄位對本 profile 是空的——該欄需 profile 自身帶 tags / refs 標記,dev-sec baseline 未帶(實測 59 條控制 tags 與 refs 全空)。若改用 MITRE 維護的 STIG profile 才會有值。
| # | 項目 | 內容與代價 |
|---|---|---|
| ① | SAF CLI 導入評估 | 產出品質確實較佳且離線可用(--network none 實測通過)。代價:agent image 目前 921 MB、base python:3.11-slim、無 Node;SAF 官方 image 自身 711 MB,裝 Node + npm i -g @mitre/saf 估 +300~400 MB。另:本次實測環境為 arm64(@mitre/saf 1.6.0 / node 22),上線需 amd64 重驗。三種裝法(直裝 Node/multi-stage 複製/呼叫獨立容器)各有取捨,其中「呼叫獨立容器」需 docker socket,與 D9「客戶端負擔最小」抵觸。 |
| ② | summary 語意檢討(逐檢查點 vs 逐控制) | 現行 connector summary 為逐檢查點計數(同一份資料:pass=60 fail=6 notapplicable=39);控制層彙總則是 Passed 17 / Failed 3 / Skipped 39。兩個數字都正確、只是語意不同——稽核員的心智模型通常是「3 條控制未通過」而非「6 個檢查點失敗」。不建議單獨改 InSpec:_merge_summaries() 多台加總與 FE i18n key(pass / fail / notapplicable / error)為跨工具共用,OpenSCAP 是逐規則計數,只改一邊會讓同一顆數字在兩個工具間語意不一致,反而更糟。要改就跨工具一起定義。 |
本輪不處理的理由:① 屬純呈現優化,現行 html2 已跨過「人可讀」門檻(控制編號/標題/描述/原始檢測碼齊全),非不可出貨狀態;且全案驗收進行中,變動 image 相依結構會讓「驗的是哪個版本」複雜化。② 非正確性缺陷——證據 HTML 內逐條控制狀態俱在,稽核員點開報告看得到真相;且牽動跨工具一致性,需一併定義。
詳細設計見 §4.8;決策見 D25–D34(D33 為 2026-08-01 驗收發現後追加,對應 T-7.5;D34 為同日驗收期間提出的通知加厚,對應 T-7.6)。T-7.1 與 T-7.2 可並行,但並行前必須先凍結欄位 key 契約——承 FR-058.6 教訓:並行實作各自取名會對不上,FR-058.6 是用契約表避開的,本案照辦。契約項:
source_file/_source_file的形狀(§4.8「執行時輸入與 params 形狀」)、下載端點路徑、sha256 header 名。完成標記(2026-08-01 收尾補記,commit hash 皆經 git log 核實):
- T-7.1 ✅ BE
fc3cbbe5(+404 修正fdfbb091)——param_schema 實為 v4 非設計時寫的 v3:v3 已被同日稍早的fr058-20(scan 選項標籤精簡,e78f1fd9)佔用,故fr058-21為 v3 下架+INSERT v4;execute sticky 參照/agent 下載端點(可插拔 resolver 清單,後被 FR-0591d9e43e4復用)皆落地- T-7.2 ✅ agent
13af0b9(bump 0.2.2464e41d9)+契約對齊修正8a444d3(limits key 名稱+X-Agent-Uid header,bump 0.2.25fd2572f)——mTLS 串流下載+sha256 對帳+磁碟檢查+兩格式家族安全解壓- T-7.3 ✅ FE
a424511——任務抽屜上傳原始碼壓縮包+執行紀錄顯示檔名- T-7.4 ✅ 端到端驗收通過(2026-08-01 決策者親驗)。四個未實測項裁示:磁碟不足模擬與解壓炸彈接受單元測試涵蓋、環境變數調限額不另實測、nginx
client_max_body_size列上版必驗項(DEV 直連 BE 驗不到)- T-7.5 ✅ BE
648d3370(挑選查詢排除 upload 綁定)+FEf6a26d8(批次發佈 confirm 提示)- T-7.6 ✅ BE
85d9ca9f——get_notify_context()8 層 join、九項內容+報告檔名、失敗通知、三管道(Email/Discord/Telegram)、i18n 6 msgid
| # | 子任務 | 驗收 | 依賴 | Repo |
|---|---|---|---|---|
| T-7.1 ✅ | BE:① param_schema 升 v3 migration(INSERT 新版+v2 is_current=FALSE,版本策略照舊;僅 scan_mode options 加 upload,D26);② 限額 config(config class 屬性+環境變數覆寫,D27 定案:上傳 100MB / 解壓後 500MB / 50,000 檔 / 磁碟預留係數,比照 DRIVE_FILE_SIZE_LIMIT_MB pattern);③ execute 端點改造(收 body 帶 source_file.uid——帶則覆寫綁定 sticky 參照、不帶則沿用、首次無檔 400;BE 從 upload_files 權威取回 file_name / size / sha256 寫進 scan_params 快照與 agent_tasks.params,D31);④ 新 upload_type 分類(如 DETECTION_SOURCE);⑤ agent 檔案下載端點 GET /api/1.0/agents/files/<uid>(純 mTLS+任務綁定驗證+串流轉發不落地,D25);⑥ 心跳下發注入 _source_file 參照(含 size / sha256 / limits,D27)。migration 依環境異動鐵律只套 DEV |
DEV SELECT 確認 v3 為 is_current、v2 保留;execute 帶 uid 覆寫 / 不帶沿用 / 首次 400 三路徑正確;下載端點對非本 agent 任務的 uid 回 404(不區分不存在與無權限);不帶 body 的既有呼叫(pull / scan)行為完全不變 |
D25、D26 已定案(本次全定);與 T-7.2 並行前凍結 key 契約 | BE |
| T-7.2 ✅ | agent:① mTLS 下載 client(比照 _post() 加 _get_stream(),串流下載+sha256 對帳);② 磁碟檢查(D28:下載前 shutil.disk_usage,free < 壓縮包×12+1GB 即 fail,係數吃 payload limits,錯誤標「agent 磁碟空間不足」歸類 agent 環境問題);③ 標準庫安全解壓——zip 走 zipfile、tar 系走 tarfile filter="data"——+兩家族共用約二十行額度檢查(解壓炸彈計量 / cancel,D30);④ 接進既有 scan 管線(取代 _clone_repo() 分支,_run_scanner() 起原樣沿用);⑤ 單元測試(含五路徑 workspace 清理斷言擴充、兩格式家族解壓案例、路徑逃逸 / 超額壓縮包被擋案例) |
單元測試綠;image build 過;容器內對一個實際壓縮包手動跑通全鏈路(下載 → 對帳 → 解壓 → scan → 推 171 → 拉結果 → HTML);磁碟不足與超額壓縮包兩條負向路徑皆明確 fail 且 workspace 已清 | 同左;key 契約凍結後可與 T-7.1 並行 | evidence-agent |
| T-7.3 ✅ | FE:JobExecutionDrawer.vue 執行流程加上傳區(scan_mode=upload 時顯示;accept 限 .zip / .tar / .tar.gz / .tgz;移植 SurveyPreview.vue fileupload pattern:先打 POST /file/upload 拿 uid、execute 只送 uid;已有 sticky 檔案時顯示「目前檔案:xxx.zip(上傳於 M/D)」可直接執行沿用、上傳新檔即替換;首次無檔不給按執行);紀錄顯示:_PRIMARY_PARAM_KEYS 加 source_file+paramValueLabel 加物件型分支取 file_name(D31)。DetectionConfigField.vue 不用動 |
首次執行未上傳時執行鈕不可按;上傳後可執行;重掃對話框顯示目前檔案並可沿用 / 替換;執行紀錄顯示檔名而非裸 UUID | T-7.1 的 v3 schema 與 execute 端點落 DEV 後開工 | FE |
| T-7.4 ✅ | 端到端驗收:抽屜發起執行時上傳 zip → 掃描 succeeded → 171 出現專案 → agent workspace 已刪 → tenant 儲存壓縮包保留且重掃沿用可用(D29 定案)→ 不上傳直接重掃=沿用舊檔跑通 → 上傳新檔替換後舊檔仍在(歷史保留)→ 上傳 .tar.gz 跑通(tar 系格式,D27)→ 磁碟不足模擬跳「agent 磁碟空間不足」錯誤 → 超額壓縮包(解壓炸彈計量)被擋 → 環境變數調限額生效(D27 設定化)。另含部署驗證:上版環境 nginx client_max_body_size(D27 待辦) |
驗收清單全項通過;驗收需 DEV agent image 重建部署(只動 DEV,STG / POC agent 不動) | T-7.1–7.3 全數完成 | BE + FE + evidence-agent |
| T-7.5 ✅ | upload 任務不自動派發(D33,驗收發現):① BE——get_detection_jobs_without_execution(infra/grc/repository/task_execution_query.py:134-159)查詢排除 tool_params->>'scan_mode'='upload' 的綁定,upload 任務不進「開始執行任務」自動派發清單;② FE——專案規劃頁批次發佈 confirm 文案補一句「上傳型掃描任務需由執行者上傳檔案後手動執行」(ProjectPlanningView.vue 與 ProjectAuditorOverview.vue 同款 confirm 兩處同步) |
upload 任務按「開始執行任務」後不產生掃描執行紀錄與 warning log(挑選階段即排除,非派了才失敗);pull / scan 任務自動派發行為完全不變;confirm 文案含上傳型任務提示 | T-7.1(upload 綁定已可建立)後開工 | BE + FE |
| T-7.6 ✅ | 檢測掃描通知加厚(D34):① BE——infra/detection_tools/repository/detection_job_notify_query.py 新增 get_notify_context(job_uid)(8 層 join 鏈一次 SQL;⚠️ 必帶 catalog_id=profile_imports.source_catalog_id 過濾、AO 用 title→prose→part_id fallback);② app/detection_tools/service/detection_orchestration_service.py——成功通知內容擴為九項+報告檔名(複用 _resolve_scan_params() 剝除、evidence 參數用起來),on_scan_failed 補查 job/binding 後發失敗通知(含 error_message,收件人同成功)、管道補齊 Discord/Telegram(HTML+純文字兩版文案);③ i18n——兩語系 messages.po 加 6 個 msgid+pybabel compile;④ 測試——test_detection_orchestration.py 補成功/失敗通知案例。無 migration、無 DI 變更 |
成功通知含九項+報告檔名;失敗通知會發且含 error_message;三管道(Email/Discord/Telegram)皆發;secret/內部欄位(_ 前綴)不出現在任何管道;pybabel compile 完成 |
無(可獨立開工;與 T-7.1〜7.5 無依賴) | BE |
system_logs 皆不出現該值。target_url、選「登入後主動」並填測試帳密 → 開始執行 → HTML 報告進證據池(可下載;不可預覽為第一批出貨標準)、summary 有 alert 統計、報告內容顯示已進入登入後頁面。scan_mode 為被動;選主動類時 FE 顯示攻擊流量警語。html2 reporter;JSON 僅在記憶體算 summary 不上傳,見 §4.3「證據格式」);其中一台連不上時另一台仍成功,且失敗主機被明確回報(非靜默假成功)。probe() 回明確訊息(可辨識是「沒裝 nmap」而非「連不上」或「認證失敗」)。DETECTION_TOOL_CONFIG_NOT_FOUND,且需憑證的既有工具(含改案後的 Nmap)檢查行為不變。cancel_event),且皆有 timeout 上限,不會無限等待。base_url 與 user token → 測試連線通過(system/status + 帶認證的 authentication/validate)→ 任務選 SonarQube、填 project_key → 開始執行 → 對公司自有 SonarQube(192.168.50.171:9000,guidant-ai-backend / guidant-ai-fe 兩專案)拉取快照 → 自包含 HTML 報告進證據池(可下載、不可預覽)、summary 數字(bugs / vulnerabilities / code_smells / security_hotspots / quality_gate)與 server 畫面對數;專案尚無分析結果時回明確錯誤「請先於客戶端執行 sonar-scanner」。取消與單呼叫 timeout(60 秒)行為同第 9 項標準。detection_executions 內 sonarqube 仍 0 筆,見 §5.1 但書)。→ 2026-08-01 已完成驗收。scan_mode 預設為原始碼掃描(D24)→ 填公開 Git repo URL + 專案代碼後綴 → 開始執行 → agent clone 源碼、跑 sonar-scanner、推上公司自有 SonarQube(192.168.50.171:9000)→ server 上出現 Guidant-AI-<後綴> 專案(D21 前綴生效)→ 輪詢 ce/task 至處理完成 → 自包含 HTML 報告進證據池、summary 有數字。scan_mode 為讀取既有結果 → 條件欄位改顯示 project_key / branch(隱藏欄位不觸發必填驗證)→ 報告進池、summary 與 server 畫面對數。scan_mode 帶非法值時明確失敗不靜默降級(絕不退回 pull 給出舊快照);掃描逾時與佇列逾時訊息可區分;任務失敗或取消後 agent 容器內 tmp 目錄已清除(docker exec 確認無殘留源碼)。project_key。