FR-058.6 · SonarQube Active Scan Mode — 需求討論稿
已上線的 SonarQube connector(FR-058.5)是拉取快照型——連客戶的 SonarQube,把「最近一次分析結果」拉回來組報告,平台不觸發掃描。決策者看到實際畫面後指出,他要的是平台真的發動一次掃描。這是新需求,不是 bug——拉取模式做得是對的,只是覆蓋不到這個情境。本案定位為雛形:先把端到端鏈路打通,取捨一律傾向最小可行。經 2026-07-31 多輪裁示後,決策已全數定案。
這一節放最前面,因為它決定了整份稿子每一處取捨的方向。
先用公開 repo 可以下載的那種,我們先做一個雛形,後面再慢慢加強 — 決策者,2026-07-31
把端到端鏈路整條打通並驗證:
公開 Git repo 拉源碼 → sonar-scanner 掃描 → 推上客戶 server → 輪詢處理佇列 → 拉結果 → 組 HTML 報告 → 進證據池。
只做 A 類純源碼語言,只支援公開 repo,資源保護只做逾時。
檔案上傳、私有 repo 認證、Java、C/C++、C#、Scala、磁碟與記憶體控管、coverage 紅燈處理。
這些全部是裁示排除,不是遺漏——完整清單與各自的理由見 §6 後續強化。
給下一棒的閱讀提示
遇到「這裡是不是應該做得更周全」的疑問時,先翻 §6。本案的取捨原則是一律傾向最小可行,所有被砍掉的東西都在那裡列了一句話說明為什麼這階段不做。
這一節先把「為什麼現在做出來的東西不對」講清楚——重點是它不是做錯了,而是做的是另一個情境。
流程好像不太對,我想要的是 user 有提供一版源碼,可能上傳 或是提供 server 位置,然後透過 sonarqube cli 去掃描並且產出結果報告 — 決策者,2026-07-31,看到 FR-058.5 實際畫面後
拆開來看,這句話包含三個要素:① 使用者提供一版源碼(上傳,或給位置)→ ② 平台透過 sonar-scanner CLI 執行掃描 → ③ 產出結果報告。目前上線的版本只有 ③,而且那個「結果」不是平台掃出來的,是客戶自己先掃好放在 server 上的。
| 面向 | 拉取快照(FR-058.5,已上線 DEV) | 主動掃描(本案 FR-058.6) |
|---|---|---|
| 誰發動掃描 | 客戶自己的 CI 流程 / 開發者機器 | 平台(agent 執行 sonar-scanner) |
| 源碼在哪 | 平台從來沒碰過源碼 | 必須送到 agent(Git clone 或上傳) |
| 報告新鮮度 | 取決於客戶上次掃描的時間,可能是三個月前 | 就是現在——稽核當下的源碼狀態 |
| 對客戶 SonarQube 的動作 | 唯讀(只呼叫查詢類 API) | 寫入——推分析結果,可能建立新專案 副作用 |
| agent 需要什麼 | 只要連得到 SonarQube 的 HTTP | sonar-scanner 執行檔、源碼落地空間、記憶體、(Java 專案還要 .class) |
| 執行時間量級 | 秒級(幾個 API 呼叫) | 分鐘到數十分鐘——掃描 + server 端排隊處理 |
| 適用客戶 | 已有成熟 CI、SonarQube 天天在跑,只要把結果收進證據池 | 沒有 CI 整合、或稽核當下才要看某個特定版本的源碼 |
已定裁示 · 拉取模式保留,兩模式並存
兩種模式並存,拉取模式不移除。理由有三:① 對「客戶已有成熟 CI、只要把結果收進稽核證據池」的情境,拉取模式是正確且更輕量的答案——它不需要源碼、不需要編譯環境、秒級完成;對這類客戶,主動掃描反而是多餘的(會在他們的 server 上多建一個專案、多跑一次分析);② 成本已經付掉了(connector、seed、setup_guide 都已完成並上 DEV),刪掉是純損失;③ C# / VB.NET 這類技術上做不了主動掃描的語言,拉取模式是唯一的路。
一個容易誤會的地方要講清楚:兩種模式都需要客戶有 SonarQube server——因為 sonar-scanner 沒有 dry-run,掃完必須把結果推到 server 才讀得到。差別只在「誰發動掃描」,不在「要不要有 server」。
本案要回答的不是「換掉哪一個」,而是「兩個怎麼並存」。注意這則裁示只確認兩者都留,沒有指定用哪種技術形式共存——那是 D17 定案的內容(見 §5)。
FR-058.5 的 D13 明確裁示了「任務執行拉取最近一次分析結果組報告,不觸發掃描」,並且當時就把「agent 端自跑 sonar-scanner」列為被排除的選項,理由寫的是「agent 沒有原始碼,也不可能備齊各語言 build 環境」。
那個判斷在當時的前提下是對的——當時沒有「使用者提供源碼」這個輸入。本案要做的,正是補上那個前提:把源碼送進 agent。而「不可能備齊各語言 build 環境」這一條,則是靠收斂語言範圍來解決——本案只做「不需要 build 環境」的那一群(D18),需要編譯產出的語言列後續案。
記錄這一點的用意
設計文件上會留下「D13 排除了 agent 自跑 scanner,D17–D24 又把它做出來」這種看似矛盾的軌跡。寫進稿子是為了讓未來的人知道這不是反覆,是前提變了——輸入從「沒有源碼」變成「使用者會提供源碼」,被排除的理由就不再成立。
這一節是實查結果,全部帶檔案與行號。目的是把「這件事要動多少東西」量化——§5 幾項決策(尤其是源碼來源與 scanner 交付方式)的成本判斷,就是從這裡算出來的。
| 能力 | 現況 | 本案怎麼用 |
|---|---|---|
| 敏感參數加密 直接可用 |
common/util/detection_secret_params.py——envelope 格式 {"__enc__":"fernet","value":"<密文>"},自我描述不依賴 schema 查表 |
Git repo 的存取 token、SonarQube 的分析 token 直接標 secret:true 就進加密鏈。三處必須一起做(存 / 派工解密 / FE 顯示與稽核 log 剝除),這是 D1 定案 |
| agent 已裝 git 直接可用 |
agent Dockerfile 已裝 git;CINC/InSpec connector 已實證「參數填 Git URL、引擎自己去拉」在本平台跑得通 | 「Git URL 取源碼」這條路幾乎零基礎建設——一個 text 欄位就能開工 |
| subprocess 執行範本 直接可用 |
inspec.py:685-722 _run_with_cancel(Popen + 輪詢)+ :657-683 _terminate(SIGTERM → 寬限 → SIGKILL) |
跑 sonar-scanner 照抄。兩個設計要點必須一起帶走:不用 subprocess.run(timeout)(無法中途取消)、stdout 走檔案不走 PIPE(大輸出會在 OS 管線緩衝區填滿時死鎖,scanner 輸出正是大輸出) |
| 同工具內模式分流 有先例 |
ZAP 的 scan_mode select + 其他欄位用 condition:{field,value} 綁定;FE DetectionConfigField.vue:23-27 有現成 visible computed、:31-34 對不可見欄位跳過必填驗證 |
pull / scan 兩模式的分流可以照這套做——但有個管不到的地方,見 §2.3 |
| 一 connector 服務多工具 有先例 |
GCB 式:base.py:29-36 的 self.detection_tool_code 注入,讓一支 connector 依 code 表現不同行為 |
模式共存形式的候選之一(D17 已定案採 ZAP 式,見 §5.1) |
| 非法值不靜默降級 有先例 |
zap.py:164-171 _resolve_scan_mode() 對非法 mode 值明確 raise。註解寫得很清楚:選了主動卻因拼錯被降級成被動,會拿到「看起來正常但實際沒掃」的報告 |
本案 mode 解析照抄這個立場——寧可失敗也不要假裝成功 |
| 缺口 | 現況實查 | 影響 |
|---|---|---|
| 檔案上傳沒接進檢測鏈路 結構性 |
「雲端 → agent」通道存在且成熟:infra/upload_file/remote_agent_adapter.py:54-102 的 save_file() POST 到 agent /blob,含 SHA-256 雙邊對帳 + mTLS/JWT;agent 端接收在 evidence-agent/api/blob/routes/blob_route.py:44-66。但檢測任務鏈路從未用過它——派工 payload 只有 JSON params(app/detection_tools/service/detection_orchestration_service.py:117),agent 端 core/task_executor.py:79-86 收到 task 後沒有任何下載檔案的邏輯 |
要走上傳路線,就得在派工鏈路兩端各補一段——不是無中生有,但也不是接個線就好 |
RemoteAgentAdapter 不一定存在陷阱 |
該 adapter 只在租戶設定 storage_type=REMOTE_AGENT 時才被實例化 |
MINIO 模式的客戶根本建不出這個物件。上傳路線不能直接依賴它,否則會出現「A 客戶能用、B 客戶按下去就爆」 |
| param_schema 沒有 file 型別 結構性 |
權威來源是 FE 渲染分支 DetectionConfigField.vue,共 7 種:text / textarea / password / number / boolean / select / select_or_text。沒有 file,而且沒有 else 分支 |
未知型別會靜默不渲染——欄位就這樣消失在畫面上,不報錯。要做上傳必須改 FE 元件 |
| 私有 repo 認證無先例 未知數 |
CINC/InSpec 的 Git URL 用法只支援公開 repo,私有 repo 認證在本平台完全沒有做過 | 走 Git 路線時,「怎麼帶認證」是要現設計的(token in URL / SSH key / credential helper 各有取捨) |
| agent image 沒有 Java、沒有 Node 要處理 |
現況 python:3.11-slim + LibreOffice 全家 + fonts-noto-cjk + 整套 Ruby/CINC Auditor 7 + openscap-utils |
scanner 需要 JRE——但官方 zip 內含 JRE,且 v6.0 起有 JRE auto-provisioning(搭配 SonarQube 10.6+,我們 server 是 26.7.0.124771 遠超門檻)→ 不必額外裝 JDK。Node 同理:JS/TS analyzer 自帶 Node runtime |
| 暫存慣例不適用大檔 陷阱 |
現有慣例是 inspec.py:197-200 的 _SHM_DIR = "/dev/shm"(0600 + contextmanager 必刪) |
/dev/shm 是記憶體(tmpfs)——這個慣例是為「小的憑證檔」設計的。解壓一個幾百 MB 的源碼包到記憶體,會直接吃掉容器的 RAM 配額。不可無腦沿用(D22 已定案改用磁碟型 tmp) |
| 大小限制各功能硬編 要補 |
SSP docx 20MB / xlsx 10MB 等各自寫死;通用 upload route 不檢查 | 走上傳路線要自己定上限,沒有現成的統一守門 |
規劃初期的顧慮 · 後經查證在本案不成立
ZAP 式的 condition 機制只能管任務層參數(param_schema),管不到租戶層憑證(config_field_schema)。而本案的兩種模式需要兩組不同的憑證:
base_url + user token(squ_)——要能呼叫讀取 APIbase_url + 推分析用的 token,再加上取源碼用的憑證(Git token / SSH key)config_field_schema 是工具層的設定,無法依任務參數分流——使用者在「設定工具憑證」那一頁時,系統還不知道他之後每個任務會選哪個模式。
InSpec 雙 transport(SSH / WinRM)踩過同一個坑,當時的解法是:兩組憑證都設為非必填,然後在 probe() 的第一段就檢查憑證完整性,明確指出缺哪一組。這是驗證過可行的一般性解法。
⚠️ 但本案不需要它——後續查證確認兩種模式共用同一把 token(squ_ User Token 推得上去也讀得回來),且 D19 定案只支援公開 repo(沒有 Git 憑證),因此根本不需要兩組憑證。詳見 §5.1 的更正說明。
要動的層:0 個新層
• param_schema 加一個 text 欄位(repo URL)+ 一個 password+secret:true 欄位(token)
• 加密鏈:現成的,一行不用改
• agent:git 已裝,InSpec 已實證此模式可行
• FE:零改動(text / password 都是既有型別)
本案只支援公開 repo(D19),連 token 欄位都不需要——私有 repo 認證列後續案(§6)
要動的層:3 個
• FE 元件:新增 file 型別渲染分支(目前 7 型別無 file,未知型別靜默消失)
• 上傳中繼:源碼包要先進雲端、再送 agent;RemoteAgentAdapter 在 MINIO 租戶下不存在,不能直接依賴
• 派工鏈路:payload 目前只有 JSON params,agent 端無下載邏輯,兩端都要補
額外(屬 Phase 2 的成本,非本案工作項):上傳大小上限、解壓空間與解壓炸彈防護都要另外定義。本案走 Git 路線不觸及這些
這個成本差是 D19 定案採 Git 路線的主因。決策者原話兩種都提了(「可能上傳 或是提供 server 位置」),但成本量級差一個數量級——雛形階段選成本低的那條先把鏈路打通。
本案只做 A 類純源碼語言(Python / JS / TS / Go / PHP / Ruby / Kotlin,定案見 D18;後續案的語言與各自的技術結論見 §6)。收斂之後浮現一個原本不存在的風險,本節只講這一件事。
風險是:使用者不知道範圍,把不支援的語言送進來。而兩種不支援的語言,失敗方式完全不同——其中一種比失敗更糟。
| 送進來的是 | 會發生什麼 | 嚴重性 |
|---|---|---|
| Java 專案 | 硬失敗 | 沒有 .class → scanner 直接報錯中止。難看但安全——使用者立刻知道不行,不會拿到錯的結論 |
| C# 專案 | 靜默淺掃 | SonarScanner CLI 明文不支援 C#,但它不會因此中止——很可能產出一份看起來正常、實際上幾乎什麼都沒分析到的報告。這是最危險的情況:稽核員拿到一份「零發現」的報告,可能誤以為程式碼很乾淨 |
必要配套 · 沿用平台既有立場
這個風險的處理原則,平台已有明確前例:zap.py:164-171 的 _resolve_scan_mode() 對非法值明確 raise 不靜默降級,註解理由寫得很直白——選了主動掃描卻因拼錯被降級成被動,會拿到看起來正常但實際沒掃的報告。那正是本案 C# 情境的翻版。
三件事一起做:
setup_guide 明列支援語言清單,並寫清楚「清單外的語言請改用拉取模式」這不是決策項,是縮範圍後的必要配套——不做就會產出誤導性的稽核證據。具體形式(擋在哪一層、訊息怎麼寫)留到實作計畫階段定。
支援語言的兩個已知品質限制(實作時要知道)
Python:不設 sonar.python.version 時,分析器會自動靜音一部分 issue 以避免誤報——從稽核角度就是漏報。本案不加版本欄位(見 §6)。
JS / TS:node_modules 缺席是降級不報錯;tsconfig 若 extends node_modules 內的 base config 而解析不到,官方描述為「unexpected analysis results」——看起來有跑,結果是錯的。兩者都不會中止執行,因此建議把 scanner 警告呈現在報告上(見 §7 R6)。
先看兩種模式擺在一起長什麼樣,再看主動掃描完整跑一次會經過哪些步驟。圖上標紅的兩處是本案最容易被忽略的坑——它們各自對應一個決策項。
%%{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 PULL["拉取快照模式(FR-058.5・已上線)"]
direction TB
P1["客戶自己的 CI
跑 sonar-scanner"] --> P2[("客戶
SonarQube")]
P3["Guidant Agent"] -. "唯讀 API
秒級" .-> P2
P3 --> P4["組 HTML 報告"]
end
subgraph SCAN["主動掃描模式(本案 FR-058.6)"]
direction TB
S1["使用者提供源碼
Git URL / 上傳"] --> S2["Guidant Agent"]
S2 --> S3["跑 sonar-scanner
分鐘~數十分鐘"]
S3 == "寫入!可能自動建專案" ==> S4[("客戶
SonarQube")]
S4 -. "排隊處理
必須輪詢" .-> S2
S2 --> S5["組 HTML 報告"]
end
P4 --> E["證據池"]
S5 --> E
style S3 fill:#F6EBD5,stroke:#9C6B12
style S4 fill:#F6E2DE,stroke:#B93A2B
style E fill:#E4F1EA,stroke:#2E7D5B
• 都連客戶自己架的 SonarQube
• 憑證都是 base_url + token
• 最後都組成自包含 HTML 進證據池
• 取消 / timeout 的行為標準一致
• 源碼要進 agent(全新)
• 要執行 scanner 二進位(全新)
• 要輪詢 server 端處理佇列(全新)
• 對客戶環境有寫入副作用(全新)
%%{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 BE as Guidant 雲端
participant AG as Agent
participant SRC as 源碼來源
participant SC as sonar-scanner
participant SQ as 客戶 SonarQube
U->>BE: 選 SonarQube、模式=主動掃描
填源碼位置 + 專案代碼 + 語言類別
BE->>BE: 敏感參數加密(既有加密鏈)
BE->>AG: 派工(params + 解密後憑證)
Note over AG,SRC: 步驟一:取源碼
AG->>SRC: git clone(或下載上傳包)
SRC-->>AG: 源碼落地到暫存目錄
Note right of AG: 暫存用磁碟型 tmp(不可用 /dev/shm,那是記憶體型)
掃完即刪 → D22
Note over AG,SC: 步驟二:執行掃描
AG->>SC: 啟動 scanner(Popen + 輪詢取消)
SC->>SC: 分析(分鐘~數十分鐘)
SC->>SQ: 推送分析結果
Note over SQ: 專案不存在會自動建立
D21:一律加 Guidant-AI- 前綴
SC-->>AG: 產出 report-task.txt
Note over AG,SQ: 步驟三:等 server 消化(掃完≠能讀)
AG->>AG: 解析 report-task.txt 取 ceTaskId
loop 直到 status 脫離 IN_PROGRESS
AG->>SQ: GET /api/ce/task?id=<ceTaskId>
SQ-->>AG: task.status
end
Note right of AG: CE 顯示的處理時間不含排隊
不能拿來當 timeout 依據
Note over AG,SQ: 步驟四:拉結果組報告(複用 pull 模式既有程式碼)
AG->>SQ: quality gate / measures / issues / hotspots
SQ-->>AG: JSON
AG->>AG: render 自包含 HTML
AG-->>BE: 回傳報告 + summary
BE-->>U: 進證據池(可下載)
流程上兩個最容易被忽略的坑
坑一:沒有 dry-run。sonar-scanner 沒有「只分析不推送」的模式——每一次掃描都是對客戶 SonarQube 的寫入。而且專案不存在時會自動建立(provision),等於污染客戶的專案清單。管理員想擋,只能移除 token 擁有者的 "Create Projects" 全域權限——沒有專門的開關。
坑二:掃完不等於能讀。結果推上去之後進佇列序列處理。必須解析 scanner 產出的 report-task.txt 取得 ceTaskId,再輪詢 GET /api/ce/task?id=<id> 直到 task.status 脫離 IN_PROGRESS。如果掃完就直接去拉結果,很可能拉到上一次的舊資料——而且拉得到、不會報錯。
補充陷阱:CE 介面顯示的「處理時間」不含排隊時間,不能拿它當 timeout 的依據。實際等待時間取決於客戶 server 當下的佇列長度,這是我們控制不了的變數。
編號延續 FR-058 既有的 D1–D16(D10 是 tombstone 空編號,不重用)。全部八項已於 2026-07-31 多輪裁示定案,本案無待決策項。下表除定案內容外一併記錄「被排除的方案與理由」,供落地與日後回溯。
| # | 議題 | 定案 | 被排除的方案 |
|---|---|---|---|
| D17 | 模式共存形式 | ZAP 式——同一個 SonarQube 工具 + scan_mode 參數分流。工具清單只有一張卡,建任務時選模式。詳見 §5.1 |
GCB 式(另立 tool code、兩張卡)——排除:它唯一的優勢是「憑證 schema 各自乾淨」,而本案兩種模式的憑證完全共用(見 §5.1),該優勢不存在,只剩「工具清單多一張同名卡」的代價 |
| D18 | 語言範圍 | 只做 A 類純源碼:Python / JS / TS / Go / PHP / Ruby / Kotlin | Java(需 .class)、C/C++(AutoConfig)——技術上都評估過可行或部分可行,但雛形階段一律收斂。C# / VB.NET / Objective-C 則是技術上真的做不到(見 §6) |
| D19 | 源碼來源 | 公開 Git repo(一個 text 欄位填 URL,不需要任何憑證) |
私有 repo 認證——排除:雛形階段先求打通。檔案上傳——排除:要動 FE 元件 + upload 中繼 + 派工鏈路三層,且 RemoteAgentAdapter 在 MINIO 租戶下不存在 |
| D20 | scanner 怎麼進 agent | sonar-scanner CLI 打包進 agent image(同 CINC/InSpec 模式)。平台專屬 zip 自帶 JRE,不需另裝 JDK | 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 常在不能連外的內網 |
| D21 | projectKey 命名 | 一律加前綴 Guidant-AI-<使用者輸入>——使用者只填後半段,前綴由平台組上 |
直接用使用者輸入——排除:可能撞到客戶既有專案 key,把一次性稽核掃描結果覆蓋到他們正式的專案歷史上,且客戶無法辨識哪些專案是本平台建的 |
| D22 | 資源保護與暫存 | 只做掃描逾時上限;暫存用磁碟型 tmp 目錄、掃完即刪。 agent 容器內 /data/content 是 :ro,可寫的是容器自身 /tmp(已有 PDF_CACHE_DIR=/tmp/pdf-cache 前例,且刻意不做 bind-mount 以避開跨裝置 rename 的 EXDEV) |
磁碟配額 / 記憶體上限控管——排除:雛形階段不做(風險記錄在 §7)。/dev/shm——排除:那是 tmpfs 記憶體型,既有慣例是為小憑證檔設計的,放源碼會直接吃容器 RAM |
| D23 | coverage 紅燈 | 照實顯示,不濾除、不加特別說明。決策者原話:「主要掃品質就好」 | 濾除 coverage 條件後再判定 Quality Gate——排除:等於我們自己改寫客戶的判定標準。加醒目說明——排除:雛形階段不做(事實記錄在 §7) |
| D24 | scan_mode 預設值 |
預設 scan(原始碼掃描)。決策者原話:「預設不要,預設一定是 scan mode」。理由:主動掃描是本需求的主線意圖,pull 只是先落地的那個;預設值指向主要使用情境,使用者少一步操作。連帶要求見 §5.1——預設值指向不可回復的一邊,事前告知升級為必做 |
預設 pull——排除:雖然 pull 純唯讀可回復、scan 會寫入客戶系統不可回復,但那是配套要解決的問題,不該讓預設值偏離主線意圖。與 ZAP D2 方向相反是刻意的(代價量級不同,見 §5.1) |
這一項決定了使用者的操作動線,值得單獨畫出來。結論是「一張卡、進去選模式」。
%%{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 TB
T["工具清單
1 張『SonarQube』卡"] --> C["設定工具憑證(只做一次)
base_url + token
✅ 兩種模式共用,一個字都不用改"]
C --> J["建立檢測任務"]
J --> M{"選擇 scan_mode"}
M -- "讀取既有結果(預設)" --> P["填 project_key
(branch 選填)"]
M -- "原始碼掃描" --> S["填 Git repo URL
+ 專案代碼後綴"]
P --> R1["唯讀拉取・秒級"]
S --> R2["clone → 掃描 → 推送
→ 輪詢 → 拉結果"]
R1 --> E["HTML 報告進證據池"]
R2 --> E
style C fill:#E4F1EA,stroke:#2E7D5B
style M fill:#F6EBD5,stroke:#9C6B12
style E fill:#E4F1EA,stroke:#2E7D5B
重要更正 · 原以為的「憑證分流痛點」在本案不成立
規劃初期曾認為兩種模式需要兩組不同憑證(pull 要讀取 token、scan 要推送 token + Git 認證),因而擔心 config_field_schema 是工具層、無法依任務參數分流。這個顧慮被兩件事推翻:
squ_ User Token 推得上去也讀得回來——只有 sqp_ / sqa_ analysis token 是「只能推不能讀」。兩種模式共用同一把 token,不需要分實查 DEV 的 config.detection_tools,sonarqube 的 config_field_schema 目前就兩個欄位:base_url(text, required)與 token(password, secret, required)。兩種模式完全共用,這份 schema 一個字都不用改。
因此 GCB 式(兩張卡)唯一的優勢消失了。InSpec 雙 transport 那個「probe 第一段檢查憑證完整性」的先例仍是有效的一般性技術參考,但不是本案的痛點——記錄於此以免日後誤讀。
condition:{field,value} 是現成機制(DetectionConfigField.vue:23-27 的 visible computed、:31-34 對不可見欄位跳過必填驗證),ZAP 已實證| 欄位 | 型別 | 顯示條件 | 說明 |
|---|---|---|---|
scan_mode | select・required | 永遠顯示 | 兩個選項:讀取既有結果(pull)/ 原始碼掃描(scan) |
project_key | text | condition 綁 pull | 既有欄位,語意不變 |
branch | text・選填 | condition 綁 pull | 既有欄位。付費版才支援多分支 |
| Git repo URL | text・required | condition 綁 scan | 新增。公開 repo,不需憑證 |
| 專案代碼後綴 | text・required | condition 綁 scan | 新增。實際推上去的是 Guidant-AI-<此欄>(D21) |
D24 已定裁示 · scan_mode 預設 = scan(原始碼掃描)
決策者原話:「預設不要,預設一定是 scan mode」。
定案理由:主動掃描是本需求的主線意圖,pull 只是先落地的那一個。預設值應該指向主要使用情境,讓使用者少一步操作。
但這個選擇有代價,而且代價落在不可回復的那一邊——見下方風險與必要配套。
D24 的代價與必要配套 事前告知從「最好有」升級為「必須有」
兩種模式選錯的後果不對等:
既然預設值指向不可回復的那一邊,事前告知的責任就從 UI 的「最好有」升級為「必須有」。關鍵風險是:使用者可能完全沒動 scan_mode 就送出,等於在未意識的情況下對客戶的 SonarQube 寫入並建立專案。
三項配套(實作必做):
hint 文字不夠(使用者會略過)。建議在 scan 模式的參數區塊做明顯標示,或在送出前給明確提示。具體形式列為實作建議,不在本稿定死setup_guide 必須明講:「本工具預設為原始碼掃描模式,執行時會在貴公司的 SonarQube 建立 / 更新專案(key 為 Guidant-AI-<識別>)」。讓管理員在設定階段就知道,而不是看到專案清單多東西才發現setup_guide:不希望平台自動建專案的客戶,做法是預先建好專案 + 移除 token 擁有者的 "Create Projects" 全域權限(見 D21)⚠️ 與 ZAP D2 先例的取捨方向相反 · 這是刻意的,不是疏漏
ZAP 的 D2 預設 passive(被動),理由正是「預設值選錯的代價不對等」——預設主動會對客戶的目標系統送出真實攻擊流量。本案的結論方向相反(預設指向有副作用的 scan),寫在這裡是為了讓下一棒不要誤以為兩處慣例不一致是疏漏。
差別在代價的量級:
| 案例 | 誤觸的代價 | 影響範圍 |
|---|---|---|
| ZAP D2 | 對目標送出真實攻擊流量 | 第三方外部系統——可能觸發對方的告警、甚至造成服務中斷。不是我們能收拾的 |
| 本案 D24 | 在客戶自有的 SonarQube 建立一個專案 | 客戶自己的系統——一筆專案紀錄,客戶可自行刪除,且有 Guidant-AI- 前綴可辨識來源(D21) |
兩者都是「預設值有副作用」,但一個是對外部系統送攻擊流量、一個是在客戶自有系統留一筆可刪除的紀錄——量級不同,所以取捨結論不同。
實作紀律 · 模式值非法必須明確 raise
絕不靜默降級。直接沿用 zap.py:164-171 _resolve_scan_mode() 的既有立場——那段註解的理由是:使用者選了「主動」卻因拼錯被降級成「被動」,會拿到一份看起來正常但實際沒掃的報告,那比直接失敗更糟。
本案同理且更嚴重:選了 scan 卻退回 pull,使用者會拿到舊的快照卻以為是剛掃出來的——而稽核報告上會標示分析時間,一份「時間是三個月前但使用者以為是今天」的證據,是會誤導稽核結論的。
FE 硬編陷阱 · 執行紀錄的「主要參數」區
JobExecutionDrawer.vue:308 有一行硬編:_PRIMARY_PARAM_KEYS = ['profile', 'project_key'],決定哪些參數顯示在執行紀錄的主要區(其餘收進摺疊的技術細節區)。
問題:scan 模式下 project_key 是使用者填的後綴、不是最能辨識這筆任務掃了什麼的欄位——Git repo URL 才是。若不處理,執行紀錄的主要區會顯示一個沒什麼資訊量的後綴,而真正的識別(掃了哪個 repo)被收進摺疊區。
實作時需一併改這行 FE。該檔既有註解已說明分區原則是「執行前需要知道的參數 vs 技術細節」——Git repo URL 明確屬於前者。
這一節是給下一棒看的。下列全部是裁示排除,不是遺漏——每項一句話說明為什麼這階段不做,以及日後要撿起來時已知的技術結論。
| 項目 | 為什麼這階段不做 | 日後撿起來時的已知結論 |
|---|---|---|
| 檔案上傳取源碼 | 要動三層(FE 元件 / upload 中繼 / 派工鏈路),雛形階段先求打通 | 雲端→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 租戶建不出來 |
| 私有 repo 認證 | 公開 repo 已足以驗證整條鏈路,且平台現況本來就只支援公開 repo | 三個選項: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 模式 |
| 磁碟 / 記憶體控管 | 決策者裁示只做逾時 | agent 容器目前完全沒有資源限制(兩份 compose 皆無 mem_limit / cpus / ulimits,無 k8s manifest)。已知 OOM 熱點:JS/TS 與 Kotlin analyzer。SONAR_SCANNER_JAVA_OPTS 可調。詳見 §7 |
| coverage 紅燈處理 | 決策者裁示不處理(「主要掃品質就好」) | SonarQube 自己不產生 coverage,只匯入第三方報告;我們不跑測試 → 報告必然無 coverage。客戶 Quality Gate 若含覆蓋率條件會亮紅燈。Go 特例:無 coverage 資訊時視為「全部未覆蓋」而非「無資料」。詳見 §7 |
| 語言版本參數 | 雛形階段不加欄位 | Python 不設 sonar.python.version 時,分析器自動靜音部分 issue 以避免誤報——從稽核角度就是漏報。日後若加,建議做成選填並在報告上標註「未指定語言版本,部分檢查已略過」 |
下列是本案上線後仍然存在的限制。部分已裁示不處理,記錄在此以免下一棒誤以為是遺漏。
R1 · 使用者送錯語言(縮範圍後的主要風險)
Java 專案送進來 → 沒有 .class,硬失敗(難看但安全)。
C# 專案送進來 → CLI 不支援但不會中止,很可能產出看起來正常、實際幾乎沒分析到東西的報告——這是最危險的,稽核員可能把「沒掃到」誤讀成「很乾淨」。
配套已列為必做(見 §3):UI 與 setup_guide 明列支援語言;考慮讓使用者宣告主要語言並擋下清單外的;報告上對「分析檔案數異常偏低」明確標示。
R2 · 資源控管缺席 已裁示本案不做
agent 容器完全沒有記憶體 / CPU 限制。而 OOM 風險真實存在——官方已知的兩個熱點 JS/TS analyzer(純 JS 無 tsconfig 時全部檔案進單一 program)與 Kotlin analyzer,正好都在本案的 A 類清單裡。一旦吃爆,影響的是整台 agent 主機而不只是這個任務。
⚠️ 資源量級:官方零基準數據。文件唯一出現過的數字是 SONAR_SCANNER_JAVA_OPTS="-Xmx512m",而那只是語法示例不是建議值。調研過程中推得的「至少 2GB、JS/TS 配 4GB」是推論不是文件,不可寫進任何對外文件,必須實測。
已裁示本案只做逾時。日後若要補,最小的一步是在 compose 加 mem_limit——讓 OOM 只殺容器、不拖垮宿主機。
R3 · 報告必然沒有 coverage 已裁示本案不處理
我們不跑測試 → 報告無覆蓋率 → 客戶 Quality Gate 若含覆蓋率條件會亮紅燈,而那個紅燈不代表程式碼有問題。Go 更糟:無 coverage 資訊時視為「全部未覆蓋」,會顯示 0%。
已裁示照實顯示。但實作時仍建議在 setup_guide 提一句,讓管理員事前知道,而不是看到紅燈才來問。
R4 · 對客戶 SonarQube 的寫入副作用
sonar-scanner 沒有 dry-run——每次掃描都是對客戶系統的寫入,且專案不存在時自動建立(provision)。D21 的 Guidant-AI- 前綴解決了「撞號」與「無法辨識來源」,但「每掃一次多一筆專案」這件事本身沒有解。
UI 必須事前告知這是寫入行為。收緊模式(客戶預先建好專案 + 移除 token 的 "Create Projects" 全域權限)建議寫進 setup_guide——SonarQube 沒有專門開關,只有這個辦法。
R5 · 等待時間有一段我們控制不了
掃描完成後,結果進客戶 server 的佇列序列處理,必須輪詢 GET /api/ce/task?id=<ceTaskId>(ceTaskId 從 scanner 產出的 report-task.txt 取得)直到脫離 IN_PROGRESS。排隊多久取決於客戶 server 當下的負載。
⚠️ CE 介面顯示的「處理時間」不含排隊時間,不能拿它當 timeout 依據。
建議逾時錯誤訊息能區分兩段:「掃描超過 N 分鐘未完成」與「掃描已完成但貴公司 SonarQube 佇列超過 N 分鐘未消化」是兩種完全不同的客戶端問題,混在一起客戶不知道要查什麼。
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