FR-156 · 設計文件 v1 · 2026-10-21(D1~D12 全數定案)

客戶程式碼不進主系統,弱點資料天天自己更新

掃描執行位置從主系統沙箱搬到 Agent;弱點資料三段式供應鏈:我們每日發布 → 主系統自抓 → Agent 跟著同步

顧問在精靈多選一台 Agent,原始碼從瀏覽器直接傳到那台機器、不經主系統;Agent 本機跑 Syft/Trivy/Grype/OSV,回主系統的只有元件清單與弱點結果。弱點資料由 188 每日排程產包、License Center 蓋章、放 Cloudflare R2,主系統每天自抓,Agent 經心跳得知後自取。本文件把 12 項決策展開到「照做就能交」的程度,拆成七個子需求、12 張實作卡;FR-134 尚未進版,伺服器端路徑整條收掉。

D1~D12 全數定案 12 張實作卡、5 批 跨 4 repo:BE/FE/jedi-python-package/evidence-agent 沙箱回到只拆檔,容器數不變 陷阱:Agent 工具 bundle 全 amd64 陷阱:公鑰表只認 189 正式鑰,開發鑰簽的包 Agent 會拒

狀態:設計定案(D1~D12),已開卡待派|日期:2026-10-21|母卡 CM-2989、子卡 CM-2990~CM-3001|前作:FR-134(功能完成未進版)、FR-039/056/058.7/059.3/066 討論稿(PM 意見、選項推演):discussion.html|雛形:prototype/index.html(五幕) 開發 branch:四個 repo 以開工當下 current branch 為準(runner 自己 git branch --show-current,不對就停下問)

🔴 這份是設計決策,不是現況。 內文的行號與座標停在 2026-10-21 定稿當時,之後實作改動的結果不回頭改寫這裡——上線後的現況看 SPEC 頁。

變更紀錄

日期 變更 對應
2026-10-21 初版設計定案(D1~D12),拆 7 子需求 12 張實作卡 母卡 CM-2989,子卡 CM-2990~CM-3001

1. 需求背景與端到端

PM 判定掃客戶程式碼的工具不能預設放在我們的主系統,而 FR-134 做出來的第三方元件掃描正是顧問把客戶原始碼壓縮檔上傳到主系統、由主系統沙箱掃。同時弱點資料更新現在要人手(裝機帶一份、管理員上傳、或按「檢查更新」),PM 判定弱點更新太快不能靠人。

本案兩件事:執行位置搬 Agent、弱點資料全自動。第三件事(工具帳密搬 Agent 端)獨立成 FR-157。

搬完之後

%%{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
    participant U as 顧問(瀏覽器)
    participant API as 主系統 api
    participant AG as Agent 8444(瀏覽器門)
    participant AGC as Agent 心跳
    participant DB as 主系統 DB
    U->>API: POST /source-scan/targets/{uid}/scans(agent_uid、source_type)
    API->>DB: 建 source_scans(status=awaiting_upload)
    API-->>U: 回 scan_uid + upload_ticket(5 分鐘、綁 agent+scan)
    U->>AG: PUT /scan-input/{scan_uid}(Bearer ticket,檔案串流)
    AG->>AG: 落 /data/scan-input/{scan_uid}/,算 sha256
    AG-->>U: 回 {size_bytes, sha256}
    U->>API: POST /source-scan/scans/{uid}/start(sha256)
    API->>DB: source_scans→queued;建 agent_tasks(type=sbom_scan)
    AGC->>API: 心跳 → pending_tasks
    AGC->>AGC: 安全解壓 → syft → trivy/grype/osv(讀本機 vulndb/current)
    AGC->>API: POST /agents/tasks/{uid}/result(SBOM 多格式+三家原始 JSON+vulndb_date)
    API->>API: 正規化(既有 normalize/)→ 六區塊統計
    API->>DB: components/findings/summary(done)
    U->>API: 看結果、匯出、歷次比對(不變)
圖 1 — 掃描時序:源碼不經主系統,回來的只有結果

守住的邊界:主系統從頭到尾沒有碰過原始碼。瀏覽器開發者工具看得到上傳請求目的地是 Agent 的 IP。


2. 分工概述(30 秒版)

整案一句話:顧問多選一台 Agent,程式碼直接傳到那台機器上掃;弱點資料我們每天發、主系統自己抓、Agent 自己來拿,沒有人要按任何按鈕。

子需求 做什麼 產出 完成怎麼判定(決策者檢查法)
A 每日發布鏈 GitLab 排程每日 03:00 在 188 runner 跑:產包→LC 蓋章→上傳 R2→更新 latest.json;保留 30 天 CI job、R2 bucket、vulndb.jedicotech.com 隔天開瀏覽器打 https://vulndb.jedicotech.com/latest.json,bundle_date 是昨天;pipeline 歷史每天一條綠
B 主系統自動抓 更新站預設指 A 的網址、每日自動預設開、切換後刪舊版、連不到站黃字不彈錯 config 預設值、vulndb_update_service 三處改、設定頁文案 連網 DEV 不按任何鍵,隔天設定頁日期自己前進、紀錄多一列「自動下載」;ls /opt/vulndb 只有一個日期目錄
C Agent 同步 心跳回應帶雲端現行弱點資料 {bundle_date, sha256, size_bytes};Agent 比對較舊就走控制面 GET 下載、驗章、原子切換、刪舊;心跳回報 vulndb_date;Agent 清單與設定頁顯示 心跳契約、控制面端點、Agent vulndb_sync 模組、remote_agents.vulndb_date、兩頁 UI 主系統更新後 Agent 清單那台的「弱點資料」欄五分鐘內跟上;拔 Agent 機外網線照樣跟上;磁碟不足那台清單顯示原因
D Agent 掃描執行器 第七支 connector sbom_scan:取檔→安全解壓→syft→三家離線掃→回傳;tools-manifest 加四列;能力宣告 sbom_scan connector、manifest 四列、capability 派一筆掃描,Agent 機 ps 看到 syft/trivy;主系統結果頁數字與 FR-134 伺服器端掃同一包一致
E 源碼直傳 Agent 主系統簽上傳票;Agent 開 8444 單向 TLS 門收 PUT /scan-input;精靈掃描來源頁加「選 Agent」、上傳改目的地;掃描 job 拆派工半/結果半 票簽發、Agent 新 listener、精靈改版、job 拆分 瀏覽器 Network 面板上傳目的地是 Agent IP;主系統 SeaweedFS 找不到該 zip;A 台的票拿去 B 台回 403
F 收掉伺服器端 沙箱 image 拔四支工具與 /v1/scan;server_side_dispatch 退役;工具目錄列 execution_target 改 agent;任務頁出現 Agent 指派欄;FR-058.7 SonarQube 源碼上傳一起改直傳 image 瘦身、分岔刪除、migration、FE 沙箱容器 which syft 找不到;建檢測工具任務選「SBOM 弱點掃描」出現 Agent 指派欄;SonarQube 任務執行時上傳目的地也是 Agent
G 收尾 SPEC 三頁、手冊、安裝文件磁碟需求 文件 手冊「弱點資料」段主路徑寫「不用做任何事」;安裝需求多 7GB

依賴:A → B → C 串行;D 與 A 平行;E 要 D;F 要 E 驗過;G 最後。


3. 決策定案(D1~D12)

# 決策 定案 理由 被排除的方案
D1 SBOM 與弱點結果留雲端? 留 不留就沒有報表、歷次比對、任務證據 全部留 Agent(主系統只存連結)
D2 主系統沙箱掃描保留當選項? 整個拔掉 兩套執行位置兩套弱點資料投遞,維護翻倍;沙箱不再被出貨弱點閘掃三支工具 保留為「沒有 Agent 時的退路」
D3 Agent 弱點資料雲端中繼還是直連? 雲端中繼 守 FR-039「Agent 只連雲端」;封閉網路同一條路 Agent 直連發布站
D4 Agent 程式自動升級/過舊提示? 都不做 決策者裁「先做弱點掃描庫自動更新」;工具版本換等於檢測行為換,釘版隨 Agent 發版 過舊提示;一鍵遠端升級
D5 工具帳密搬 Agent、多台各設一次? 可做,併 FR-157 一個網段通常一台 Agent,或資料台與掃描台分開,各設正常 雲端設一次同步各台(會經雲端,與動機矛盾)
D6 產包/簽章/發布在哪 188 GitLab runner 排程產包、LC 蓋章、Cloudflare R2 發布 CI 跑的地方就是 188(既有 .gitlab-ci.yml 模型);LC vulndb_manifest 型已做好、產包腳本已接;R2 出站零費、與官網同帳號 Cloudflare 自己排程(抓數 GB 跑 docker 做不到);188 自持鑰簽(產品端要多信一把鑰)
D7 每台 Agent 多約 5GB? 先不管 只做切換後刪舊版(零成本) 拿掉 Grype 降到 1.8GB
D8 瀏覽器怎麼直傳到 Agent(8443 強制驗客戶端憑證) 另開 8444 單向 TLS 門,進門帶主系統簽的五分鐘上傳票 源碼真不經主系統;Agent 零新信任關係(註冊時已有雲端 JWT 公鑰);FR-157 登入共用同一張票同一個門 經主系統串流 relay(源碼「經過」我們);8443 放寬(TLS 層做不到按路徑放行)
D9 Agent 自簽憑證瀏覽器警告 第一版接受 畫面提示+手冊教匯入 CA;替代(客戶自備正式憑證)列第二期 第一版就做憑證替換
D10 R2 公開網域與主系統預設 vulndb.jedicotech.com,寫進 config 預設值 現況 VULNDB_UPDATE_URL 預設空字串=不連網,裝完就是舊資料 留空讓客戶自填
D11 Agent 弱點資料拉還是推 拉 Agent 永遠是發起方,與 FR-039/059.3 一致;推要雲端主動連 Agent、處理離線重試 雲端推到 Agent 資料面
D12 FR-058.7 SonarQube 源碼上傳一起改直傳? 一起改 同一條「客戶程式碼不進主系統」理由完全成立;不改則主系統儲存仍躺著源碼包 只改 SBOM 掃描

UI 設計約束(沿用 FR-134)

版面照 Guidant 設計系統自行設計,不仿競品;沒做的功能不在畫面預告(Agent 自動升級不出現任何「有新版」字樣);grep border-left 為零。


4. 現況接入點盤點

元件 現況座標 現況 本案動作 卡
Agent 執行器框架 evidence-agent/core/task_executor.py、task_executor_connectors/__init__.py:68 _CONNECTOR_BUILDERS 六支 connector;sonarqube.py:664 _run_scan 已是「下載源碼包→_extract_archive→跑 CLI→回報」 加第七支 sbom_scan.py,解壓與限額段抽成共用函式與 sonarqube 共用 D2
Agent 工具 bundle evidence-agent/scripts/build/tools-manifest.txt、fetch_bundle_tools.sh 釘版+sha256,啟動驗章;全 amd64 加 syft 1.54.0/trivy 0.75.0/grype 0.120.0/osv-scanner 2.6.0 四列(版本與沙箱 Dockerfile:89-92 同) D1
Agent 能力宣告 evidence-agent/common/util/agent_capabilities.py file_storage、detection_scan 加 sbom_scan;雲端 get_agents_with_capability 多一個值 D1
Agent 資料面 evidence-agent/core/data_plane_server.py(8443 mTLS,AGENT_DATA_PLANE_PORT) 單一 listener,ssl_verify_client 等價 新增第二個 listener 8444(AGENT_BROWSER_PORT),同一張伺服器憑證、不驗客戶端憑證、只掛 /scan-input;compose 多發布一個埠 E1
Agent JWT 驗證 evidence-agent/core/agent_auth.py:102 verify_data_plane_jwt 用註冊時取得的 jwt_public.pem 驗雲端簽的資料面 token 加 verify_upload_ticket(同公鑰,claims 不同) E1
Agent 心跳 observer evidence-agent/core/enroll.py:322-335(_observe_error_tracking、_observe_allowed_targets) 兩個 observer 吃心跳回應 data 加 _observe_vulndb C2
Agent 控制面下載 evidence-agent/core/task_executor.py:183 _get_stream、core/profile_cache.py FR-059.3 profile 投遞:串流下載+sha256 對帳+原子 rename vulndb_sync.py 照抄 pattern C2
Agent 驗章 evidence-agent/common/integrity/verify.py:54 verify_payload(expected_type)、public_keys.py 認 agent_manifest 型 加認 vulndb_manifest 型(常數值與 BE common/integrity/adapters.py:58 同) C2
雲端心跳服務 jedi_remote_agent/app/service/agent_enrollment_service.py:449 _heartbeat 回 pending_tasks、error_tracking、allowed_targets 回應加 vulndb;payload 收 vulndb_date 寫 remote_agents.vulndb_date C1
雲端弱點資料服務 jedi_source_scan/app/service/vulndb/vulndb_update_service.py(apply/status/_switch/_prune) 驗章→日期不倒退→原子切換;_prune 保留舊版 _prune 改刪舊;加 current_bundle_info() 給心跳用;加控制面串流端點 B1、C1
雲端自動更新 api/source_scan/routes/vulndb_route.py:81-99、config/config.py:239 VULNDB_UPDATE_URL 檢查更新、每日開關;URL 預設空 URL 預設 D10 網址;開關預設 true;連不到站紀錄一列 status=skipped B1
雲端掃描 job jedi_source_scan/app/jobs/sbom_scan_job.py(吃 ISandboxScanner) 取檔→送沙箱→存產物→正規化→寫表 拆:派工半建 agent_tasks;結果半收 Agent 回傳後走既有正規化;ISandboxScanner port 與 impl 刪 E2
雲端派工分岔 jedi_detection/app/service/server_side_dispatch.py、server_side_result.py execution_target='server' 不派 Agent 兩檔退役;execution_target 欄保留但值全改 agent F1
工具目錄 scripts/sql/2026-10-10-fr134-h1-sbom-scan-tool.sql sbom-scan 列 execution_target='server'、connection_type NULL migration 改 agent;FE 任務頁去掉「伺服器端不顯示 Agent 欄」分支 F1
源碼上傳(FR-058.7) jedi_detection/common/detection_source_file.py(_source_file 契約)、FE 執行時上傳元件 上傳到主系統 upload_files,agent 走 GET /agents/files/{uid} 取 改:上傳直傳 Agent 8444,_source_file 改帶 {agent_local_uid, sha256, size_bytes, file_name};agent 端 _download 改讀本機 F2
精靈 FE src/views/source-scan/WizardStepSource.vue 兩張來源卡+上傳框 上方加「在哪台 Agent 上掃描」單選(只有一台自動選不顯示);上傳框下加直傳提示;上傳目的地改 Agent E2
Agent 清單 FE src/views/remote-agent/RemoteAgentManage.vue:524- 名稱/位址/狀態/能力/版本 加「弱點資料」「最後同步」兩欄+兩張統計卡 C1
設定頁弱點資料 FE src/views/vulndb/VulndbManage.vue 日期、來源五項、上傳、檢查更新、開關、紀錄 加「Agent 同步狀態」表;文案改「由 Guidant AI 每日發布」 C1
沙箱 image jedi-evidence-classification/docker/Dockerfile:89-101、classifier_service.py /v1/scan 含四支工具與掃描端點 拔掉,回到只拆檔;compose x-anchor 的 vulndb volume 掛載移除 F1
CI BE .gitlab-ci.yml(web/api 觸發,188 runner) 四 stage 出版 pipeline 加獨立 vulndb-publish job,rules: $CI_PIPELINE_SOURCE == "schedule",與出版 pipeline 互斥 A1

5. 詳細設計

5.1 A 每日發布鏈

%%{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 JT["我們(每日 03:00,GitLab schedule → 188 runner)"]
    UP["上游:Trivy DB/Grype DB/OSV/KEV/EPSS"] --> B1["build_vulndb_bundle.sh(既有)"]
    B1 --> LC["License Center 簽 vulndb_manifest(既有)"]
    LC --> R2["R2:guidant-vulndb-YYYYMMDD.tar.zst<br/>latest.json"]
  end
  subgraph CUST["客戶(連網)"]
    R2 -->|"每日 GET latest.json"| API["主系統 worker:驗章→volume→刪舊→紀錄"]
    API -->|"心跳回應 vulndb{date,sha256}"| AG["Agent:較舊就 GET bundle→驗章→切換→刪舊"]
  end
  subgraph CLOSED["客戶(封閉)"]
    ADM["管理員上傳更新包(既有)"] --> API2["主系統"] -->|同一條心跳| AG2["Agent"]
  end
圖 2 — 弱點資料供應鏈

CI job(BE .gitlab-ci.yml 新增,獨立於出版四 stage):

vulndb-publish:
  stage: build
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule" && $VULNDB_PUBLISH == "true"
  script:
    - scripts/build/build_vulndb_bundle.sh --out .build/vulndb      # 既有,含送 LC 簽
    - scripts/build/publish_vulndb_bundle.sh .build/vulndb          # 新:上傳 R2+寫 latest.json+清 30 天外
  • GitLab 排程:每日 03:00 Asia/Taipei,變數 VULNDB_PUBLISH=true;出版 pipeline 的 workflow.rules 加一條 schedule 放行但其他 job 以 $VULNDB_PUBLISH 排除。
  • publish_vulndb_bundle.sh(新):用 rclone 或 aws s3(S3 相容)上傳到 R2 bucket guidant-vulndb;憑證走 CI 變數 R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT(新 env var,先 grep 確認無同義;值不入版控);上傳完才覆寫 latest.json(先包後索引,避免索引指到半個包);刪 30 天前的包。
  • latest.json 契約(凍結,產品端 check_update 消費):
{"bundle_date":"2026-10-20","file":"guidant-vulndb-20261020.tar.zst","sha256":"…","size_bytes":503316480,
 "sources":{"trivy":{"date":"2026-10-20","count":312480},"grype":{…},"osv":{…},"kev":{…},"epss":{…}},
 "tool_versions":{"syft":"1.54.0","trivy":"0.75.0","grype":"0.120.0","osv-scanner":"2.6.0"}}
  • 自訂網域 vulndb.jedicotech.com 綁 bucket(Cloudflare 同帳號);bucket 公開讀。
  • 失敗可見性:pipeline 紅走既有通知;主系統連三天沒抓到新包,設定頁黃字「更新站 3 天無新資料」。
  • 鑰:一律正式鑰(kid f3b562a6d9755527,FR-134 §5.2)。開發鑰簽的包產品與 Agent 都會拒收(FR-134 已踩)。

5.2 B 主系統自動抓

改三處,皆在既有碼內:

  1. config/config.py:239 VULNDB_UPDATE_URL 預設改 https://vulndb.jedicotech.com;.env sample 與 deployment-env.md 同步補一行。
  2. vulndb_update_service 的每日開關預設 true(存在 system_config 的那筆初值;migration 補 seed);check_update 連不到站時寫 vulndb_updates 一列 status=skipped、error=無法連線更新站,不拋錯不寄信;設定頁最近一列是 skipped 時顯示黃字並附「封閉網路請改用上傳更新包」。
  3. _prune(keep) 改為刪除所有非 keep 的日期目錄(現況保留)。進行中的掃描已不在主系統跑(D2),刪舊無競態。

5.3 C Agent 同步

心跳契約擴充(jedi_remote_agent 心跳回應 data):

"vulndb": {"bundle_date": "2026-10-20", "sha256": "<tar.zst 的 sha256>", "size_bytes": 503316480}

無弱點資料時 vulndb: null。心跳 payload(Agent → 雲端)加 vulndb_date(字串或 null)與 vulndb_sync_error(字串或 null,例 磁碟不足:剩 2.1GB,需 6GB)。雲端寫 remote_agents.vulndb_date、remote_agents.vulndb_sync_error(migration 兩欄)。沒帶就不動現值(與 capabilities 相容契約同款)。

控制面端點(jedi_remote_agent 或 jedi_source_scan 掛在 agents blueprint):GET /api/1.0/agents/vulndb/bundle → 通行證驗身 → 串流回 current 對應的 .tar.zst(主系統 volume 要保留原包,_switch 時順便把包留在 <date>/bundle.tar.zst)。回 X-Bundle-Date、X-Bundle-Sha256 標頭。

Agent 端 core/vulndb_sync.py(新,照 profile_cache.py pattern):

步驟 做法
觸發 _observe_vulndb(cfg, data, state):比對 data.vulndb.bundle_date 與本機 /data/vulndb/current/manifest.json 的 bundle_date;較新且沒有同步在跑才起背景 thread(單飛鎖)
磁碟 shutil.disk_usage("/data/vulndb") 要 ≥ size_bytes × 10(解開約 10 倍)+1GB;不足→記 vulndb_sync_error,下次心跳回報,不下載
下載 _get_stream("/agents/vulndb/bundle", "/data/vulndb/.incoming/<date>.tar.zst", expected_sha256=…)(既有,含對帳)
驗章 解到 .staging/<date>/,verify_payload(manifest.sig, expected_type="vulndb_manifest"),逐檔 sha256 對 manifest.json,bundle_date 不得早於現行
切換 os.replace 暫存目錄到 /data/vulndb/<date>/,current symlink 原子換(os.symlink 到暫名再 os.replace),刪其他日期目錄與 .incoming
掃描中 connector 開跑時先 os.path.realpath(current) 鎖定實路徑,切換不影響進行中掃描;刪舊目錄只刪「沒有進行中掃描引用」的(簡單做法:刪前檢查 _running_scans 計數,有就延到下次心跳)
回報 下次心跳 vulndb_date 帶新日期、vulndb_sync_error 清空

Agent 磁碟佈局:/data/vulndb/{<date>/, current -> <date>, .incoming/, .staging/},compose named volume agent-vulndb,原生安裝版 /var/lib/guidant-agent/vulndb。

UI:Agent 清單加「弱點資料」(日期+距今 tag:≤30 綠、31~90 黃、>90 紅、無則「尚未同步」+原因)、「最後同步」;上方統計卡「弱點資料已同步 n/N」「主系統弱點資料 日期」。設定頁弱點資料加「Agent 同步狀態」表(雛形幕 3、4)。

5.4 D Agent 掃描執行器

core/task_executor_connectors/sbom_scan.py(新):

項目 設計
註冊 _CONNECTOR_BUILDERS["sbom-scan"];capability sbom_scan
輸入 `params._source_file = {agent_local_uid, sha256, size_bytes, file_name, source_type(upload
取檔 從 /data/scan-input/<scan_uid>/ 讀(E1 落的),比 sha256;不存在→fail「上傳未完成或已過期」
解壓 抽 sonarqube 的 _extract_archive+限額到 task_executor_connectors/_archive.py 共用(D13 精神:借檢查器);SBOM 檔不解壓直接用
工具 syft scan dir:<src> -o cyclonedx-json=…,spdx-json=…(多格式一次);trivy sbom --skip-db-update --offline-scan --cache-dir <vulndb>/trivy -f json;GRYPE_DB_AUTO_UPDATE=false GRYPE_DB_CACHE_DIR=<vulndb>/grype grype sbom:… -o json;osv-scanner --offline --local-db-path <vulndb>/osv --sbom … --format json。工具路徑走 resource_path() 慣例(FR-066)
進度 四段 ctx.report_progress(接收檔案→產出物料清單→比對弱點→整理報告)映射到既有 agent_tasks 進度回報
輸出 ScanResult 多檔:SBOM 各格式、三家原始 JSON、tool_versions、vulndb_date(讀 manifest)、coverage(缺 lock 清單,FR-134 D3);上傳走既有 _upload_reports(檔案進主系統 upload_files,與現在沙箱產物落點一致)
清理 掃完刪工作區;源碼包保留 AGENT_SCAN_INPUT_RETENTION_DAYS(預設 1)後刪,供「重掃沿用」
資源 單次 20 分鐘 timeout、記憶體不設 cgroup(Agent 是整台機),文件寫建議 4GB 可用
時限內取消 沿用 request_cancel

tools-manifest 四列:id/版本/sha256/URL 照既有格式;fetch_bundle_tools.sh 的「解成目錄樹」對這四支就是放進 tools/bin/。sha256 取 GitHub release 的 checksums 檔實值。

5.5 E 源碼直傳 Agent

上傳票(主系統簽,jedi_remote_agent 新 agent_ticket_service,用既有資料面 JWT 私鑰 RS256):

{"iss":"guidant-cloud","aud":"agent:<agent_uid>","sub":"<user login_name>","tenant_id":102,
 "purpose":"scan_upload","scan_uid":"<uid>","max_bytes":104857600,"jti":"<uuid>","exp":<now+300>}
  • 端點 POST /source-scan/targets/{uid}/scans(既有建掃描端點改)回 {scan_uid, upload: {url: "https://<agent base host>:8444/scan-input/<scan_uid>", ticket}};agent base host 取 remote_agents.base_url 的 host。
  • FR-157 的登入票 purpose:"console_login",同一支服務。

Agent 8444 listener(core/browser_plane_server.py 新,與 data_plane_server.py 同款 Flask thread):

項目 設計
TLS 同一張 Agent 伺服器憑證(CA 是雲端),verify_mode=CERT_NONE,TLS 1.2+
埠 AGENT_BROWSER_PORT 預設 8444;compose 多 ports: 8444:8444;install.sh 防火牆提示加一行
端點 PUT /scan-input/<scan_uid>:Authorization: Bearer <ticket> → verify_upload_ticket(公鑰、aud 是自己、purpose、scan_uid 相符、未過期、jti 未用過——記憶體 set+TTL)→ 串流落 /data/scan-input/<scan_uid>/<file_name>,邊寫邊算 sha256、超 max_bytes 中斷刪檔 → 回 {sha256, size_bytes}
CORS 瀏覽器從主系統網域發 PUT 到 Agent 是跨來源:回 Access-Control-Allow-Origin: <ticket 內 origin claim>、處理 OPTIONS preflight;票加 origin claim(主系統 FE 網址)
不開 其他路徑一律 404;不掛 /blob、不掛 /health

精靈(WizardStepSource.vue):上方加「在哪台 Agent 上掃描」單選(來源 GET /remote-agents?capability=sbom_scan&online=true,過濾 vulndb_date 非空;一台自動選隱藏區塊;零台顯示「本租戶尚無可掃描的 Agent」並擋下一步);上傳框下方直傳提示(雛形幕 1);送出時先建掃描拿票,PUT 到票的 url(進度條顯示目的地,雛形幕 2),成功後 POST /source-scan/scans/{uid}/start 帶 sha256。CORS/憑證錯誤→白話訊息「無法連到 Agent 192.168.x.x,請確認您在客戶內網並已信任 Agent 憑證」。

掃描 job 拆分(jedi_source_scan):

  • source_scans 加 agent_uid、agent_local_uid、狀態多 awaiting_upload(建了還沒傳完;1 小時沒 start 由既有清理排程改 failed)。
  • 派工半:start → 建 agent_tasks(type=sbom-scan, params) → queued。
  • 結果半:Agent POST /agents/tasks/{uid}/result 既有路徑 → sbom_scan_result_handler(新,取代 job 的沙箱段):取回傳檔→既有 normalize→寫表→done;vulndb_date 記 Agent 回報值。
  • ISandboxScanner port、infra impl、/v1/scan client 刪。

5.6 F 收掉伺服器端

項目 動作
沙箱 Dockerfile 刪 :89-101 四支工具下載層;classifier_service.py 刪 /v1/scan;image 重出
compose x-anchor 保留(FR-134 D1 的加固抽象仍有價值),移除 guidant-scan-vulndb 掛載到沙箱;volume 仍由 api/worker 用
server_side_dispatch.py/server_side_result.py 刪;detection_orchestration_service 的 is_server_side 分支刪
migration UPDATE config.detection_tools SET execution_target='agent' WHERE code='sbom-scan';欄位保留(他日真有伺服器端工具再用)
FE 任務頁 刪「execution_target==='server' 不顯示 Agent 欄」分支;抽屜 D10/D18 當時藏掉的 Agent 字樣恢復
FR-058.7 SonarQube(D12) 執行時上傳元件改走同一條票(purpose:"scan_upload",scan_uid 換 execution_uid)直傳被指派的 Agent;_source_file 契約改帶 agent_local_uid;多 Agent 分派(FR-067)每列各自一張票傳到各自 Agent;agent 端 sonarqube _download 改讀本機
守衛測試 沙箱守衛不變;加一條「jedi_source_scan 不得 import ISandboxScanner」防復活

5.7 安全規則對照

規則檔 本案落實
開端點 8444 只掛一條 PUT、票驗 aud/purpose/jti/exp;控制面 bundle 端點通行證驗身
上傳解析 沿用 FR-058.7 限額與 _extract_archive;SBOM 檔 50MB 上限;超 max_bytes 中斷
祕密 票用既有 JWT 私鑰;R2 憑證 CI 變數;log 不記票本體
log 稽核事件:source_scan.upload_ticket_issued、vulndb.agent_synced(Agent 回報時雲端記)、既有 vulndb.update
呼叫外部 主系統只連 VULNDB_UPDATE_URL;Agent 不出網(守衛:Agent 容器 dns 黑洞測試仍過)

6. 拆分(12 張實作卡)

批 卡 白話 改哪些檔(repo) 驗收 依賴
1 A1 每日弱點包發布 pipeline 讓 188 每天凌晨自己產包、簽章、放到 Cloudflare BE .gitlab-ci.yml、scripts/build/publish_vulndb_bundle.sh(新)、.env sample、deployment-env.md 手動 run 一次 schedule pipeline 綠;curl https://vulndb.jedicotech.com/latest.json 日期正確、sha256 與包一致;R2 只留 30 天內 決策者先建 R2 bucket+網域+CI 變數
1 D1 Agent 帶四支掃描工具+能力宣告 Agent 安裝包多裝四支工具,心跳告訴雲端「我會掃元件」 evidence-agent tools-manifest.txt、fetch_bundle_tools.sh、agent_capabilities.py、build_agent_bundle.sh 出包後斷網容器 syft version/trivy --version/grype version/osv-scanner --version 四支都有;雲端 Agent 列 capabilities 含 sbom_scan —
1 D2 Agent sbom_scan 執行器 Agent 收到掃描工單會解壓、跑四支工具、把結果傳回 evidence-agent task_executor_connectors/sbom_scan.py(新)、_archive.py(抽共用)、sonarqube.py(改用共用)、__init__.py 手放一個 zip 到 /data/scan-input/<uid>/、手放弱點資料到 /data/vulndb/current,派工單跑完回傳 SBOM 兩格式+三家 JSON;缺 lock 專案 coverage 有列 D1
2 B1 主系統自動抓預設開+刪舊 裝好就會每天自己更新,不用人按 BE config/config.py、jedi-source-scan vulndb_update_service.py、seed migration、FE VulndbManage.vue 文案 連網 DEV 隔天紀錄多一列自動下載;斷網 DEV 紀錄一列 skipped 且設定頭黃字;volume 只剩一個日期目錄 A1
2 C1 心跳帶弱點資料+控制面下載端點+清單欄位 雲端告訴每台 Agent 現在的資料日期、給它一條路來拿;畫面看得到每台日期 jedi-remote-agent(心跳回應/payload、兩欄 migration)、jedi-source-scan(bundle 端點、current_bundle_info)、FE RemoteAgentManage.vue、VulndbManage.vue 心跳回應 JSON 有 vulndb;curl 帶通行證打 bundle 端點拿到 tar.zst 且 sha256 對;清單出現兩欄、設定頁出現 Agent 同步表 B1
2 C2 Agent 弱點資料同步模組 Agent 發現雲端資料較新就自己下載、驗章、換上、刪舊、回報 evidence-agent core/vulndb_sync.py(新)、enroll.py observer、common/integrity/verify.py 加型別、compose volume、install.sh 目錄 主系統更新後 Agent 五分鐘內 current 指新日期、舊目錄消失、清單日期跟上;磁碟不足機器清單顯示原因不下載;改一位元組的包被拒且 current 不動 C1
3 E1 上傳票+Agent 8444 瀏覽器門 主系統發五分鐘入場券,Agent 多開一道只收檔的門 jedi-remote-agent agent_ticket_service.py(新)、evidence-agent core/browser_plane_server.py(新)、agent_auth.py、compose、install.sh curl -X PUT 帶票傳檔成功回 sha256;過期票 401;A 台票打 B 台 403;同票二用 409;無票 401;超 max_bytes 413 且檔刪 D2
3 E2 精靈選 Agent+直傳+掃描 job 拆分 顧問多選一台 Agent,檔案直接飛到那台;主系統只收結果 FE WizardStepSource.vue、SourceScanWizard.vue、SourceScanService.js;jedi-source-scan sbom_scan_job.py 拆、sbom_scan_result_handler.py(新)、source_scans 三欄 migration、刪 ISandboxScanner 照雛形幕 1、2 走完一筆;Network 面板目的地是 Agent IP;SeaweedFS 無該 zip;結果頁六區塊數字與同一包在 FR-134 沙箱掃的一致;1 小時沒 start 的列變 failed E1
4 F1 收掉伺服器端路徑 沙箱不再帶掃描工具,任務頁「SBOM 弱點掃描」變回要選 Agent jedi-evidence-classification Dockerfile+classifier_service.py、jedi-detection 刪兩檔+分支、migration、FE 任務頁與抽屜、守衛測試 沙箱 which syft 無;建任務選 SBOM 弱點掃描出現 Agent 欄;整條鏈(任務→執行→證據)跑通;守衛測試綠且 import ISandboxScanner 會紅 E2
4 F2 SonarQube 源碼上傳改直傳 Agent SonarQube 掃描的程式碼也不再經主系統 FE 執行時上傳元件、jedi-detection detection_source_file.py 契約、evidence-agent sonarqube.py 取檔 SonarQube 任務執行上傳目的地是 Agent;多 Agent 分派每列各自上傳;舊 GET /agents/files 路徑對源碼包不再使用(profile 仍用) E1
5 G1 SPEC 三頁+手冊+安裝文件 文件改成現況 BE docs/spec-site/current/(第三方元件風險、Agent 管理、系統設定弱點資料)、docs/user-manual/、installer docs 手冊「弱點資料」主路徑寫不用做任何事;安裝需求 Agent 多 7GB、8444 埠;grep "上傳更新包" 只在封閉網路段 全部
5 G2 出貨基線+收尾紀錄 新 migration 進基線、STATE/LOG BE scripts/init/、handoff/ 188 基線庫 schema_migrations 含本案全部;STATE 只描述此刻 G1

決策者功課(開工前):①建 Cloudflare R2 bucket guidant-vulndb、綁 vulndb.jedicotech.com、產一組唯寫 API token 填進 GitLab CI 變數;②確認 188 runner 的 GitLab 排程可用(Settings → CI/CD → Schedules)。


7. 端到端驗收

# 步驤 期望
1 在 DEV 手動觸發 schedule pipeline 綠;latest.json 日期=今天上游資料日期
2 DEV 主系統等到隔天 03:00 後(或按「立即檢查更新」) 設定頁日期前進;紀錄一列自動下載;volume 單目錄
3 兩台 Agent 在線(一台磁碟故意只留 2GB) 五分鐘內一台日期跟上、一台顯示磁碟不足
4 精靈新增掃描,選第一台,上傳決策者桌面 guidant-be-sca-test.zip 幕 1→2→四段進度→結果頁;Network 目的地 Agent IP;SeaweedFS 無 zip
5 同一包用 FR-134 沙箱版(切回舊 commit 的 DEV)掃一次 六區塊統計、元件數、弱點數一致(容許 vulndb 日期不同造成的弱點數差,記錄差異原因)
6 任務頁建「SBOM 弱點掃描」檢測工具任務 出現 Agent 欄;執行→證據→「看完整結果」數字一致
7 SonarQube 任務執行上傳 目的地 Agent;掃描照常完成
8 拔第二台 Agent 外網線(DNS 黑洞),主系統上傳一包新日期 該台仍跟上
9 把 A 台的上傳票拿去 B 台 PUT 403
10 沙箱容器 which syft trivy grype osv-scanner 四個都無

8. 風險與陷阱

風險 怎麼擋
開發鑰簽的弱點包 Agent 拒收(FR-134 已踩) CI 一律正式鑰;DEV 要測 C2 就用 CI 產的真包,不手簽
Agent 機磁碟爆 下載前 disk_usage 檢查、不足不下載並回報;安裝文件寫需求
瀏覽器 CORS 擋 PUT 票帶 origin,8444 回對應 Allow-Origin、處理 preflight;E1 驗收含瀏覧器實測不只 curl
瀏覽器連不到 Agent(顧問在外網) 錯誤訊息白話;手冊寫明只能在客戶內網操作;這是設計結果不是缺陷
排程 pipeline 撞人手 build 既有 pgrep 守門;排 03:00
進行中掃描時切換弱點資料 connector 開跑鎖實路徑;刪舊延到無進行中掃描
jti 防重放只在記憶體,Agent 重啟後失效 票只 5 分鐘,重啟窗口可接受;寫進設計不做持久化
FR-067 多 Agent 分派下 SonarQube 每列各自上傳 F2 驗收明列;FE 元件按分派列各拿一張票
Syft SBOM 帶相對檔案路徑 不含內容;SPEC 寫明回雲端的 SBOM 含「套件名、版本、來源檔相對路徑」

9. 這份文件的定位

你想知道 看
PM 說什麼、選項怎麼推 討論稿
畫面長什麼樣 雛形
每個決策為什麼 本文件 §3
開工要改哪些檔 本文件 §4、§6
帳密搬 Agent 端 FR-157
做到哪了 README 總進度、handoff/(開工後)