FR-058.7 · SonarQube Upload Scan — 需求討論稿 · 2026-08-01(v4)
FR-058.6 剛完成 SonarQube 雙模式(pull 拉取 / scan 主動掃描),但 scan 目前只吃公開 Git repo URL。決策者 2026-08-01 提出:客戶要能直接上傳源碼壓縮包做檢測。宏觀流向已定調——上傳 → tenant 儲存空間 → agent 拉下來 → 掃 → agent 端掃完即刪 → 磁碟不夠就跳錯誤。本稿把這條流向落成具體設計;上傳時機定在任務抽屜發起執行時、server 端壓縮包保留供重掃沿用,並列出 D25–D31 決策項(D27 / D29 已定案、D32 併入 D29,餘五項待過稿)。
v2 · 依決策者 2026-08-01 拍板修正:上傳時機從任務設定移到發起執行時 / D29 定案終態即刪 / D30 改用標準庫 zipfile。
v3 · 依決策者 2026-08-01 再拍板:D27 限額設定化(config + 環境變數,payload 下發 agent)/ D29 改為 server 端 zip 全保留+重掃沿用+歷史版本不刪(推翻 v2 終態即刪)/ D31 收緊(FE 只送 uid、BE 權威取檔案資訊)/ D32 併入 D29。
v4 · 依決策者 2026-08-01 審 v3 修正:§9.2 掃毒條的 v2 生命週期論證改寫(檔案已改全保留,理由誠實化)/ 支援格式擴充為 .zip / .tar / .tar.gz / .tgz(D27、D30 連動改寫)。
FR-058.6 的 scan 模式驗收通過後,決策者提出的下一步需求。
還是要可以上傳檔案做檢測,有辦法是上傳到 tenant 設定的儲存空間後,由 agent 拉下來,做掃描,掃完就刪,如果硬碟不夠就跳錯誤就好。 — 決策者,2026-08-01
拆開來看,這句話已經把宏觀流向定調成五段:① 使用者上傳源碼壓縮包 → ② 落地在 tenant 設定的儲存空間(不是憑空找地方放)→ ③ agent 把檔案拉下來執行掃描 → ④ 掃完就刪(源碼不留存)→ ⑤ agent 磁碟不夠時直接報錯(不做複雜的資源調度)。
其中 ② 是關鍵指定:平台已有一套 tenant 級的儲存設定機制(system_configs 的 STORAGE_CONFIG,支援 local / minio / remote_agent 三種後端),上傳檔案要走這一套,不另闢儲存路徑。④⑤ 則定調了生命週期與防護策略的粗粒度——後續討論釐清後,「掃完就刪」的適用範圍是 agent 端 workspace(tenant 儲存空間上的壓縮包保留供重掃沿用,見 D29 定案),本稿的 D28 / D29 是把它們落成具體規則。
初版設計曾把上傳欄位放在任務設定表單(param_schema 加 file 型欄位),決策者審後指出流程不對:上傳的壓縮包是「這一次要掃的東西」——特定時點的源碼快照,不該混進設定表單。放設定表單會有兩個問題:
修正後的分工:任務設定表單只留 scan_mode 的第三選項 upload;上傳動作發生在任務抽屜發起執行時——scan_mode=upload 的任務,執行對話框出現上傳區,首次執行必須上傳(無檔不給按執行),檔案參照隨「發起執行」請求送出。參照落點是任務綁定層的 sticky 狀態(供之後重掃沿用,見 D29 定案的沿用模型),每筆執行的 scan_params 快照同時各存一份(承接「這筆掃的是哪個檔」的稽核回溯)。附帶兩個好處:孤兒檔視窗大幅縮小(上傳與執行是同一個動作);FE 參數表單元件完全不用動(DetectionConfigField 不需要新型別分支)。
FR-058.6 的 scan 模式是「平台主動發動掃描」的第一版,取源方式只有一種:公開 Git repo URL(agent 直接 git clone)。這在雛形階段是刻意的最小化取捨(見 FR-058.6 討論稿 §6 後續強化清單,「檔案上傳」與「私有 repo 認證」皆列 scope 外)。但實際客戶情境裡,「源碼不在公開 repo」才是常態:
上傳檔案模式讓客戶「把源碼打包丟上來」,一次涵蓋以上三種情境——特別是它部分涵蓋了原列 scope 外的「私有 repo 認證」需求:客戶可以把私有源碼打包上傳,完全不用把 git 憑證交給平台。這是本案除了直接需求之外的附帶收益。
這一節先把本案的邊界釘死,避免設計發散。
FR-058.6 scan 模式的管線是:取源(git clone) → sonar-scanner 掃描 → 推上 SonarQube server → CE 等待 → 報告回收 → 品質門檻渲染。
本案只把第一段的取源方式多加一種:從 tenant 儲存空間下載上傳的壓縮包並安全解壓。後面整條掃描管線(scanner 執行、CE 等待、報告回收、品質門檻渲染、證據池落地)全部複用,一行不動。
不是新模式——不新增第四種檢測型態,就是 scan 模式的 scan_mode 多一個 upload 選項(見 D26)。
語言限制原樣繼承——C# / VB.NET 永遠只能走 pull 模式(SonarScanner for .NET 必須整合 MSBuild 建置流程,架構限制,非本案能解)。Java 需要 .class 檔的限制也一樣繼承。
不做檔案清理機制(孤兒檔與歷史版本統一列 follow-up,見 D29 ④)。
一句話定位
本案=「scan_mode 多一個 upload 選項;選了它,發起執行時上傳壓縮包當這一次的輸入」。所有設計取捨都圍繞這個定位:能複用就複用,新造的東西只有取源通道(D25)、執行時輸入的傳遞(D26)、檔案生命週期(D28/D29)、解壓額度檢查(D30)四塊。
三 repo 皆已實查(附檔案座標)。結論先講:可複用的比要新造的多很多——上傳、儲存、刪除、掃描工作區生命週期、參數表單渲染全是現成的;真正的缺口集中在「agent 怎麼拿到檔案」「磁碟檢查」「解壓防護」三處。
| 能力 | 狀態 | 座標與說明 |
|---|---|---|
| tenant 儲存設定 | 可複用 | public.system_configs(group=STORAGE_CONFIG, key=CONFIG,JSONB,RLS 隔離)。實際支援後端只有 local / minio / remote_agent 三種。 |
| 檔案上傳 canonical service | 可複用 | app/upload_file/service/managed_file_upload_service.py —— upload_files_for_tenant()(:280-295)、_load_config_for_tenant()(:256-267,背景 job 顯式帶 tenant 的正解)、delete_file(uid)(:319-321,依檔案自己的 storage_type 選 adapter 刪,remote_agent 會連帶刪 agent 上的 blob)。 |
| 使用者面下載 | 存在但不適用 | 統一 GET /file/download/<uid>(api/uploadfile/routes/uploadfile_route.py:99-124),@signed_token_or_jwt(scope="file_access"),簽章 token TTL 僅 120 秒(common/authz/signed_token.py:41)。agent 沒有取得簽章 token 的路徑(換 token 的 endpoint 掛 @jwt_required)。 |
| agent→BE 既有通道 | 僅控制面 | 只有四支控制面端點:register / heartbeat / ack / result(api/remote_agent/__init__.py:34-38),純 mTLS 不掛 jwt。BE 對 agent 開的檔案下載端點:不存在——此為本案新造點之一(D25)。 |
| 派工參數流 | 可複用 | job_execution_detection_tools.tool_params(secret 欄位加密 envelope)→ 執行時 app/detection_tools/service/detection_orchestration_service.py:117 注入 _credentials 寫進 compliance.agent_tasks.params → 心跳時 app/remote_agent/service/agent_enrollment_service.py:241-249 _collect_pending_tasks() 組裝下發 payload(當場解密 secret、重查 credentials)。回 FE 時 _resolve_scan_params()(detection_orchestration_service.py:385-400)剝 _ 前綴 key 並 strip secret。 |
| param_schema 機制 | 小改即可 | config.detection_tool_param_schemas,現有 7 種型別(text / textarea / password / number / boolean / select / select_or_text)。condition 是單層 {"field","value"} 等值比對,BE 零驗證(FE 渲染與 agent connector 各自把關)。版本策略:不可原地 UPDATE,INSERT 新 version + 舊版 is_current=FALSE——SonarQube 現為 v2,本案出 v3。上傳時機改到執行時後,v3 只需在 scan_mode options 加 upload,不新增型別、不新增欄位。 |
| 執行端點 | 要改 | POST /detection-tools/jobs/<uid>/execute 目前不收 body(純快照綁定任務設定)。上傳模式需要它可帶當次輸入(source_file),由 start_execution 合併進 agent_tasks.params——此為本案 BE 改造點之一(D26 / §6.3)。 |
| 上傳大小限制 | 待驗證 | BE 全域無 MAX_CONTENT_LENGTH;上版環境 BE 前面的 nginx body size 限制待驗證(列入 D27 待辦)。 |
| 能力 | 狀態 | 座標與說明 |
|---|---|---|
| scan 工作區生命週期 | 可複用 | core/task_executor_connectors/sonarqube.py _scan_workspace()(:604-623)——tempfile.mkdtemp(prefix="sonar-scan-", dir="/tmp")、chmod 0700、finally: shutil.rmtree。test/test_sonarqube_connector.py:1132-1180 五條路徑(成功 / clone 失敗 / scanner 失敗 / CE 失敗 / cancel)皆斷言工作區已刪——掃完即刪的 agent 端已有測試鎖約,零新工作。_SCAN_TMP_ROOT="/tmp" 註解明文絕不可改 /dev/shm(tmpfs 吃 RAM)。 |
| 掃描執行框架 | 可複用 | _clone_repo()(:625-654)、_run_scanner()(:656-711)、可取消子行程框架 _run_with_cancel()(:782-817,clone 與 scanner 共用,stdout 導檔避免 PIPE 死鎖)。sonar-scanner CLI 7.3.0(自帶 JRE)在 /usr/bin/sonar-scanner。取源改成解壓後,_run_scanner() 之後整段原樣沿用。 |
| mTLS 入站(BE→agent) | 語意不合 | 8443 = nginx mTLS sidecar(deploy/nginx/agent.conf,client_max_body_size 0)反代 guidant-ai-agent:8000。/blob 路由本來就雙向:POST /blob 上傳(BE 推檔給 agent 的既有通道)/ GET|DELETE /blob/<uid>。但 POST /blob 走 FileUploadService 永久落地(寫 agent 自己的 upload_files 表),與 /tmp 即用即棄語意不同——這是 D25 排除「推檔給 agent」方案的原因之一。 |
| agent 出站 client(agent→BE) | 可仿造 | core/task_executor.py:148-153 _post()(httpx.Client(cert=(cert,key), verify=ca))——要讓 agent 下載檔案,最自然是照這形狀加 _get_stream()。executor _run_one(task, cfg, state, container) 已拿得到 cfg/state 但目前沒傳進 connector;可比照 params["_task_uid"] 夾帶慣例(:81)由 executor 先下載、把本地路徑塞進 params,或比照 connector.cancel_event(:89)注入。 |
| 磁碟檢查 | 缺口 | 全 repo 零(grep disk_usage / statvfs / ENOSPC 全 0 命中)。/tmp 在容器可寫層(無 volume 掛載),吃爆影響 docker host 分割區;容器無任何資源限制。shutil 已 import,shutil.disk_usage(_SCAN_TMP_ROOT) 放 _scan_workspace() 建目錄前是最小侵入點(D28)。 |
| 壓縮檔處理 | 缺口 | Python 層零(zipfile / tarfile 全 0 命中);image 內雖有 unzip CLI(Dockerfile:26,裝 sonar-scanner 用),但路徑逃逸 / 解壓炸彈防護無既有可參考(標準庫可補位:zipfile 內建路徑消毒、tarfile 3.11.4+ 有 filter="data",agent 基底 python:3.11-slim,Dockerfile:1——見 D30)。輸入把關的既有先例是 _resolve_repo_url()(:354-368,拒收 file://)——精神可循,額度檢查層要新寫。 |
| 能力 | 狀態 | 座標與說明 |
|---|---|---|
| 參數表單渲染 | 不用動 | schema-driven 單一元件 src/components/detection-tools/DetectionConfigField.vue(128 行,7 型別分支,condition 互斥 :23-27,必填紅框 :31-34)。上傳時機改到執行時後,此元件完全不用動——scan_mode 多一個 select 選項由既有 select 分支自動渲染。改造點移到 JobExecutionDrawer 的發起執行流程(見下)。 |
| 上傳互動範例 | 可移植 | src/views/survey-v2/SurveyPreview.vue fileupload 題型(:985-1027 上傳拿 uid 寫回、:1559-1600 UI 已上傳態 / 拖拉 / 預覽刪除)——「先上傳拿 uid、再隨請求送出」的同構範例直接移植,移植目的地是任務抽屜的執行對話框。上傳統一打 POST /file/upload(API.UPLOAD_FILE),FormData file + json Blob 帶 upload_type。 |
| 大小限制與進度條 | 留意 | 現行 pattern 為 10~50MB;BaseService.post 不支援 onUploadProgress——要進度條得繞過 BaseService。D27 若定 100MB,建議 v1 不做進度條(保持 BaseService 路徑),列 follow-up。 |
| 任務抽屜(執行發起+紀錄顯示) | 主要改造點 | src/components/grc/JobExecutionDrawer.vue。執行發起:scan_mode=upload 的任務要在執行對話框加上傳區(首次無檔不給按執行;已有 sticky 檔案顯示「目前檔案」可沿用或替換),uid 隨發起執行請求送出。紀錄顯示:_PRIMARY_PARAM_KEYS = ['profile','project_key','scan_mode','repo_url'](:315)要加新 key;paramValueLabel(:287-293)對非 select 型原樣輸出——快照若只存 uid 會顯示裸 UUID,故快照應存 {uid, file_name}(D31)。 |
從使用者上傳 zip,到掃描完成、兩處檔案(agent workspace + tenant 儲存空間的 zip)都清乾淨的完整生命週期。虛線框內是本案新造的段落,其餘全是 FR-058.6 既有管線。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
autonumber
participant U as 使用者 (FE)
participant BE as BE
participant TS as tenant 儲存空間
(local/minio/remote_agent)
participant AG as agent
participant SQ as SonarQube server (171)
Note over U: 任務設定已存 scan_mode=upload
(設定表單裡沒有檔案欄位)
rect rgb(246, 235, 213)
Note over U,TS: 新造:任務抽屜發起執行時上傳(首次必上傳;重掃可沿用)
U->>U: 抽屜執行對話框出現上傳區
已有檔案時顯示「目前檔案:xxx.zip(上傳於 M/D)」
U->>BE: POST /file/upload(壓縮包,驗副檔名與大小上限)
BE->>TS: upload_files_for_tenant() 存入 tenant 儲存
BE-->>U: 回 file uid
U->>BE: POST /jobs/<uid>/execute
(body 帶 source_file uid;沿用舊檔則不帶)
end
BE->>BE: start_execution:帶新 uid 則覆寫綁定的 sticky 參照
不帶則沿用;從 upload_files 權威取 file_name/size/sha256
寫 job_execution(scan_params 快照)+ agent_tasks
(params 夾 _source_file 參照)
AG->>BE: 心跳(既有 mTLS 通道)
BE-->>AG: 下發任務 payload(含 source_file 參照)
rect rgb(246, 235, 213)
Note over AG,TS: 新造:agent 取源段
AG->>AG: shutil.disk_usage 磁碟檢查
不足 → 直接 fail「agent 磁碟空間不足」
AG->>BE: GET /api/1.0/agents/files/<uid>(mTLS,串流)
BE->>TS: 讀出 zip(依 storage_type)
BE-->>AG: 串流回傳(BE 不落地)
AG->>AG: sha256 驗證
AG->>AG: 標準庫安全解壓進 scan workspace
(zipfile / tarfile filter=data;累計大小/檔數超限即中止清理)
end
Note over AG,SQ: 以下全複用 FR-058.6 scan 管線
AG->>AG: sonar-scanner 掃描(_run_with_cancel)
AG->>SQ: 推分析結果
AG->>SQ: 輪詢 CE 處理佇列
SQ-->>AG: 分析完成
AG->>SQ: 拉 issues / 品質門檻
AG->>AG: 組報告;finally: shutil.rmtree workspace
(既有機制,五路徑測試鎖約)
AG->>BE: 回報 result(既有 mTLS 通道)
Note over BE,TS: D29 定案:tenant 儲存上的壓縮包保留供重掃沿用
(不做終態刪檔;清理機制列 follow-up)
BE-->>U: 執行紀錄顯示結果(檔名而非裸 UUID)
圖 1 · 端到端時序:發起執行時上傳(或沿用舊檔)→ tenant 儲存 → execute → 心跳下發 → mTLS 下載 → 驗證/解壓 → 掃描 → 回收 → agent workspace 清理(server 端壓縮包保留)
時序圖裡的兩個關鍵設計點
① 磁碟檢查在下載之前(步驟 9)——不是解壓失敗才發現,呼應本 arc 教訓「錯誤要在最早能發現的地方被發現」。錯誤分類為 agent 環境問題,不是掃描失敗。
② BE relay 不落地(步驟 10-12)——tenant 儲存為 minio / remote_agent 時,BE 用串流轉發,遵守 FR-039 不變式「雲端任何路徑不得把 binary 寫進雲端磁碟」。
五個參與方與其間通道。實線=既有通道,粗標「新」=本案新造。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
subgraph FEG["前端 FE"]
FE["JobExecutionDrawer.vue
執行對話框加上傳區(改)
SurveyPreview 上傳範例移植"]
end
subgraph BEG["後端 BE"]
UP["POST /file/upload
(既有)"]
MFS["ManagedFileUploadService
(既有,upload 與讀取)"]
EX["POST /jobs/<uid>/execute
收 source_file body(改)"]
HB["心跳下發 _collect_pending_tasks
+夾檔案參照(改)"]
DL["GET /api/1.0/agents/files/<uid>
純 mTLS+任務綁定驗證(新)"]
end
subgraph TSG["tenant 儲存空間"]
LO["local"]
MI["minio"]
RA["remote_agent"]
end
subgraph AGG["agent(evidence-agent)"]
GS["_get_stream mTLS 下載 client(新)"]
DK["磁碟檢查 shutil.disk_usage(新)"]
UZ["標準庫安全解壓(新)
zipfile / tarfile filter=data"]
WS["scan workspace /tmp
掃完即刪(既有+測試鎖約)"]
SC["sonar-scanner CLI 7.3.0(既有)"]
end
SQ["SonarQube server
192.168.50.171"]
FE -->|"發起執行時上傳 zip"| UP
UP --> MFS
MFS --> LO
MFS --> MI
MFS --> RA
FE -->|"execute 帶 source_file"| EX
EX --> HB
HB -->|"心跳 payload(既有 mTLS)"| GS
GS -->|"mTLS 串流下載(新)"| DL
DL --> MFS
GS --> DK
DK --> UZ
UZ --> WS
WS --> SC
SC -->|"推分析+輪詢 CE+拉報告"| SQ
MFS -.->|"zip 保留供重掃沿用(D29)"| LO
圖 2 · 元件架構:FE / BE / tenant 儲存(三後端)/ agent / SonarQube server(171)
FR-039 不變式對照(本設計不破壞任何一條)
① agent 不連雲端 DB——檔案參照由心跳 payload 下發,agent 只打 BE 的 HTTP 端點。② 雲端不落地 binary——BE 下載端點對 minio / remote_agent 後端用串流轉發。③ 只有 agent 對外連出、無對內連入——下載是 agent 主動發起的出站請求,方向與 heartbeat / result 一致。
三塊:param_schema v3 欄位表、params 儲存形狀、新端點契約草案。此為討論基準,細節以 design.md 落版為準。
版本策略照既有規範:INSERT 新 version=3 + v2 is_current=FALSE,不原地 UPDATE。上傳時機定在執行時後,v3 的變更縮小到只有 scan_mode 一欄(options 加 upload + 相關 hint / 警語)——不新增型別、不新增欄位,source_file 不進 param_schema(它是執行時輸入,見 §6.3)。
| key | type | required | condition | 說明 |
|---|---|---|---|---|
profile | select | 是 | — | 沿用 v2 不變 |
scan_mode | select | 是 | — | 唯一變更:選項由 pull / scan 增為 pull / scan / upload(D26),附 hint「上傳模式於發起執行時提供源碼壓縮包」。預設值沿用 v2 裁示 |
server_url、token 等連線欄位 | (同 v2) | (同 v2) | (同 v2) | 沿用 v2 不變(secret 欄位加密 envelope 機制照舊) |
repo_url | text | 是 | {"field":"scan_mode","value":"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() 剝除(既有機制自動涵蓋 _source_file),FE 只看得到 source_file 的 {uid, file_name}——執行紀錄顯示檔名、不顯示裸 UUID(D31)。
| 項目 | 內容 |
|---|---|
| 路徑 | 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 模式)行為完全不變 |
| 項目 | 內容 |
|---|---|
| 路徑 | 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——防任意檔案讀取。任務終態後參照即失效 |
| 回應 | 串流回傳 zip binary(Content-Length + 建議帶 X-Checksum-Sha256 header);BE 對 minio / remote_agent 後端串流轉發不落地 |
| 錯誤 | uid 不存在或不屬於該 agent 任務 → 404(不區分兩種情況,避免探測);檔案已被終態清理刪除 → 404。error code 依規範新增 GRC_404xxx 序號 |
編號接續 FR-058.6 design.md 的 D24。每項附建議與排除理由;決策者已定調的宏觀方向與上傳時機(發起執行時)不在此重議,這裡是把它們落成可實作的具體規則。D27、D29 已於 2026-08-01 定案,D32 併入 D29;待過稿的是 D25、D26、D28、D30、D31 五項(其中 D26 / D30 已按前輪拍板方向改寫,爭點低)。
D25 · 取源通道:agent 如何拿到上傳的源碼包
問題:檔案在 tenant 儲存空間(BE 側),掃描在 agent 側,中間隔著 mTLS 邊界。agent 沒有 jwt、也沒有取得簽章下載 token 的路徑,現有下載機制都到不了 agent。
選項:① BE 新開 agent 專用純 mTLS 下載端點,agent 收到任務後主動拉;② 複用使用者面 GET /file/download/<uid> 簽章 URL,心跳時夾帶簽好的 URL;③ 派工時 BE 主動 POST /blob 把檔案推給 agent。
建議:方案 ①——BE 新開 GET /api/1.0/agents/files/<uid>,比照 tasks ack / result 的「純 mTLS 不掛 jwt」形狀,外加綁定驗證(該 uid 必須屬於該 agent 名下 pending / running 任務的 source_file,防任意檔案讀取)。心跳 payload 夾檔案參照,agent 收到任務即串流下載進 scan workspace 並驗 sha256。
排除 ②(簽章 URL):簽章 token TTL 只有 120 秒(common/authz/signed_token.py:41),是為瀏覽器「點了就下載」設計的;agent 從心跳收到任務到實際開始下載的間隔不可控(前面可能排了別的任務),時效模型對不上。且把使用者面資料通道與 agent 資料面混用,授權語意混濁。
排除 ③(推檔給 agent):推檔時機與任務生命週期耦合(派工瞬間 agent 可能離線);POST /blob 走 FileUploadService 永久落地(寫 agent 自己的 upload_files 表),與 /tmp 即用即棄語意不同,agent 端要另管清理;且 tenant 儲存為 remote_agent 時檔案本來就在 agent 上,會變成「推自己」。
附註:tenant 儲存為 remote_agent 且掃描 agent 恰為同一台時,方案 ① 的 BE relay 會繞一圈(agent→BE→agent)。v1 接受此低效(罕見組態),列 §9 known limitation。
D26 · 參數模型:scan_mode 三選項 + 執行時輸入
問題:上傳掃描在參數模型上長什麼樣?哪些進任務設定、哪些跟著執行走?
建議:scan_mode 增 upload 選項——param_schema 出 v3(INSERT 新版 + v2 is_current=FALSE,版本策略照舊),v3 變更僅此一欄;project_key_suffix 沿用(scan 與 upload 皆出現)。source_file 不是 schema 欄位,而是執行時輸入:任務抽屜發起執行時上傳、隨 execute request body 送出,由 start_execution 合併進 agent_tasks.params(_source_file,形狀見 §6.2 / §6.3-A)。
排除 ①(file 型 schema 欄位,初版方案,決策者裁示排除):把上傳放任務設定表單會導致——每掃新版都要改設定再執行(兩段式繞路,且任務設定僅專案管理者可改);語意錯位(zip 是「要掃的東西」,該在執行當下提供,不該混進「怎麼掃」的設定表單)。改到執行時後還附帶兩個好處:FE 的 DetectionConfigField 完全不用動、孤兒檔視窗大幅縮小。(註:D29 定案保留檔案後,參照落點回到綁定層做 sticky 沿用——但上傳動作仍在執行時,與本方案①「設定表單放上傳欄位」不同,見 D29 ②。)
排除 ②(scan 選項內做 repo_url / source_file 互斥):condition 機制是單層 {"field","value"} 等值比對,表達不了「二欄擇一」;required 驗證也會亂(兩欄都掛 required 就都必填,都不掛就都可空)。硬做要改 condition 語法引擎,成本遠超過多一個選項。
D27 · 檔案格式與限額——限額設定化已定案(2026-08-01 決策者拍板)
格式:收 .zip / .tar / .tar.gz / .tgz(2026-08-01 拍板擴充 tar 系)。裸 .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 下發同構,形狀見 §6.2 的 limits),agent 只做執行時 enforcement——單一真相在 BE,不違反 FR-039「agent 不連雲端 DB」不變式。
被排除方案:system_config——那張表定位是租戶層業務設定(有 FE 設定頁),這幾個限額是平台營運護欄、無 per-tenant 差異化需求;走 system_config 要多做 reader / fallback / 快取 plumbing,不划算。缺點「改動要重啟 BE」可接受(調上限是罕見動作)。未來若需 per-tenant 放寬,payload 下發機制不變、只換讀取來源,升級容易。
待辦:驗證上版環境 BE 前 nginx 的 client_max_body_size 實際限制(BE 本身無 MAX_CONTENT_LENGTH,但 nginx 預設 1MB,若沒調過會先在 nginx 被擋)。此為部署驗證項,列 T-7.4。
D28 · 磁碟防護的具體規則
問題:決策者已定調「硬碟不夠就跳錯誤」,這裡定「怎麼算不夠」與「錯誤怎麼呈現」。agent 現況全 repo 零磁碟檢查,/tmp 在容器可寫層、吃爆影響 docker host 分割區。
建議: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 上限也即時中止並清理(與磁碟檢查是兩道獨立防線:一道防「空間真的不夠」,一道防「惡意壓縮包」)。
D29 · 檔案生命週期:server 端全保留+重掃沿用+歷史不刪——已定案(2026-08-01 決策者拍板,推翻 v2 的終態即刪)
定案:「掃完即刪」只適用 agent 端 workspace(既有 finally: shutil.rmtree 機制+五路徑測試鎖約,零新工作,不變);tenant 儲存空間上的壓縮包保留。v2 曾定案「終態即刪」,其理由「掃完不留平台」不成立、明予推翻——tenant 儲存空間本來就是客戶自己設定的位置(local / minio / 客戶自家 agent),保留不構成「平台持有客戶源碼」問題。
完整模型四點:
① 重掃沿用——scan_mode=upload 的任務,抽屜執行對話框顯示「目前檔案:xxx.zip(上傳於 M/D)」,直接按執行=沿用舊檔;要換版就上傳新檔替換。首次執行仍必須上傳(無檔不給執行)。
② 檔案參照落點回到任務綁定層(sticky 狀態供沿用,形狀見 §6.2)。注意與被否決的初版方案①不同:上傳動作仍在抽屜執行時,流程不變,只是參照回寫進綁定;初版被否決的主因「終態刪檔→參照懸掛」在保留檔案後自然消失。
③ 替換不刪舊檔,歷史版本全保留(決策者明示:留歷史,萬一未來 user 要找舊檔,還有容錯修正空間)。每筆執行紀錄 scan_params 快照的 {uid, file_name} 都能對回實體檔案,稽核可回溯「那次掃的是哪一包」。
④ 清理機制列 follow-up(v1 不做)——孤兒檔(上傳未執行)、歷史版本、殘檔統一由未來的清理機制管(原 D32 併入本項)。給未來清理機制的約束先立在此:必須以「綁定仍參照中的檔案」為保留名單,不能只看檔案年齡——否則會清掉三個月沒重掃、但仍要沿用的 zip。
D30 · 解壓實作:標準庫 zipfile + tarfile + 共用額度檢查層
問題:agent 端 Python 層零壓縮檔處理先例;外部輸入的壓縮包有路徑逃逸(zip-slip / tar-slip)與解壓炸彈(超高壓縮比撐爆磁碟)兩類攻擊面。D27 定案收 .zip 與 tar 系兩個格式家族——要不要引套件?防護要自己寫多少?
選項:① 兩家族都用標準庫(外包一層共用額度檢查);② shell 出去跑 unzip / tar CLI;③ 引第三方解壓套件。
建議:方案 ①(標準庫 zipfile + tarfile)——兩個格式家族的安全提取標準庫都有現成方案: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(沿用既有可取消框架的精神)。
排除 ②(unzip / tar CLI):無法逐檔做計量與取消——整包解完才知道大小,解壓炸彈防護等於沒有;路徑穿越保護依版本行為不一。
排除 ③(第三方套件):標準庫已涵蓋兩個家族的需求,agent 依賴保持精簡,不為二十行的事引套件。
D31 · 顯示與紀錄:完全複用既有上傳機制+兩點收緊
問題:執行紀錄抽屜 paramValueLabel 對非 select 型參數原樣輸出——快照若只存 file uid,畫面會顯示裸 UUID。另外檔案 metadata 該信誰?
建議:每筆執行的 scan_params 快照存 source_file: {uid, file_name} 物件(形狀見 §6.2),FE 顯示 file_name;JobExecutionDrawer.vue 的 _PRIMARY_PARAM_KEYS(:315)加 source_file;paramValueLabel 加物件型分支(取 file_name)。兩點收緊:
① FE 只送 uid,檔名不由 FE 送——BE 收到 execute 請求後從 upload_files 表權威取回 file_name / size / sha256 快照進執行紀錄;顯示歷史不依賴檔案仍存在,也不信任前端送來的檔名。
② 白拿清單(複用既有上傳機制、零新工作的部分):sha256(agent 下載後對帳防竄改)、RLS 租戶隔離、三種 storage backend 全支援。BE 要補的只有:新 upload_type 分類(如 DETECTION_SOURCE)+ execute 時驗 uid 存在 / 副檔名在允許清單(.zip / .tar / .tar.gz / .tgz,D27)/ 大小在 D27 限額內。
D32 · 孤兒檔——已併入 D29 第 ④ 點
上傳後放棄執行的孤兒檔、替換後的歷史版本、其他殘檔,統一由未來的清理機制管理(v1 不做),完整說明與「以綁定參照為保留名單」的約束見 D29 ④。上傳時機移到發起執行時後,孤兒檔視窗已大幅縮小(上傳與執行幾乎是同一個動作),本項不再獨立成案。
| 編號 | 主題 | 建議一句話 | 拍板急迫度 |
|---|---|---|---|
| D25 | 取源通道 | BE 新開 agent 專用純 mTLS 下載端點+任務綁定驗證,心跳夾參照、agent 主動拉 | 待過稿・高 |
| D26 | 參數模型 | scan_mode 增 upload 選項(v3 僅此一欄);source_file 是執行時輸入不進 schema | 待過稿・已按前輪拍板改寫 |
| D27 | 格式與限額 | 收 .zip / .tar / .tar.gz / .tgz(裸 .gz 不收);限額設定化(config+環境變數,payload 下發 agent),預設 100MB / 500MB / 50,000 檔 | 已定案 |
| D28 | 磁碟防護 | 下載前 disk_usage 檢查 free < zip×12+1GB 即 fail(係數納 D27 設定組),錯誤標「agent 磁碟空間不足」 | 待過稿・中 |
| D29 | 檔案生命週期 | agent workspace 掃完即刪;server 端壓縮包全保留+重掃沿用+歷史不刪;清理列 follow-up | 已定案(推翻 v2) |
| D30 | 解壓實作 | 標準庫 zipfile + tarfile filter="data"(路徑逃逸皆內建擋掉)+兩家族共用約二十行額度檢查(解壓炸彈 / cancel) | 待過稿・已按前輪拍板改寫 |
| D31 | 顯示與紀錄 | FE 只送 uid、BE 從 upload_files 權威取檔案資訊;快照存 {uid, file_name};白拿 sha256 / RLS / 三後端 | 待過稿・低 |
| D32 | 孤兒檔 | 併入 D29 ④(統一清理 follow-up,保留名單以綁定參照為準) | 併入 D29 |
四段,接續 FR-058.6 的 T-6.x 編號。T-7.1 與 T-7.2 可並行,但並行前必須先凍結欄位 key 契約——本 arc 既有教訓:並行實作各自取名會對不上,FR-058.6 就是用契約表避開的,本案照辦(契約項:source_file / _source_file 的形狀、下載端點路徑、sha256 header 名)。
| 階段 | Repo | 工作內容 | 依賴 |
|---|---|---|---|
| T-7.1 | BE | param_schema v3 migration(INSERT 新版+v2 退場,僅 scan_mode options 加 upload);限額 config(config class 屬性+環境變數覆寫,D27 定案,比照 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);新 upload_type 分類(如 DETECTION_SOURCE);agent 檔案下載端點 GET /api/1.0/agents/files/<uid>(純 mTLS+任務綁定驗證+串流轉發);心跳下發注入 _source_file 參照(含 size / sha256 / limits) |
D25、D26 過稿;與 T-7.2 並行前凍結 key 契約 |
| T-7.2 | agent | mTLS 下載 client(比照 _post() 加 _get_stream());磁碟檢查(D28 規則);標準庫解壓——zip 走 zipfile、tar 系走 tarfile filter="data"——+兩家族共用約二十行額度檢查(解壓炸彈計量 / cancel,D30);接進既有 scan 管線(取代 _clone_repo() 分支);單元測試(含五路徑 workspace 清理斷言擴充、兩格式家族解壓案例) |
同左;key 契約凍結後可與 T-7.1 並行 |
| T-7.3 | FE | JobExecutionDrawer.vue 執行流程加上傳區(scan_mode=upload 時顯示;accept 限 .zip / .tar / .tar.gz / .tgz;移植 SurveyPreview fileupload pattern:先上傳拿 uid、只送 uid;已有 sticky 檔案時顯示「目前檔案:xxx.zip(上傳於 M/D)」可直接執行沿用、上傳新檔即替換;首次無檔不給按執行);紀錄顯示:_PRIMARY_PARAM_KEYS 加 key+物件型 label(D31)。DetectionConfigField.vue 不用動 |
T-7.1 的 v3 schema 與 execute 端點落 DEV 後開工 |
| T-7.4 | 端到端 | 驗收清單:抽屜發起執行時上傳 zip → 掃描 succeeded → 171 出現專案 → agent workspace 已刪 → tenant 儲存壓縮包保留且重掃沿用可用(D29 定案)→ 不上傳直接重掃=沿用舊檔跑通 → 上傳新檔替換後舊檔仍在(歷史保留)→ 上傳 .tar.gz 跑通(tar 系格式,D27)→ 磁碟不足模擬跳「agent 磁碟空間不足」錯誤 → 超額壓縮包(解壓炸彈計量)被擋 → 環境變數調限額生效(D27 設定化)。另含部署驗證:上版環境 nginx body size(D27 待辦) | T-7.1–7.3 全數完成 |
照 FR-058.6 的慣例,被砍的東西都列在這裡並附一句話理由——遇到「是不是應該做更周全」的疑問先翻這節。
| 限制 | 說明 |
|---|---|
| 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 相關規則。 |
下一步
決策者過稿 D25、D26、D28、D30、D31(D27 / D29 已定案,D32 併入 D29)→ 落 design.md(接續 D24 之後)→ 凍結 T-7.1 / T-7.2 欄位 key 契約 → 開工。