Guidant AI · FR-058.6 SonarQube 主動掃描

FR-058.6 · SonarQube Active Scan Mode — 需求討論稿

SonarQube 主動掃描:平台自己發動一次掃描

已上線的 SonarQube connector(FR-058.5)是拉取快照型——連客戶的 SonarQube,把「最近一次分析結果」拉回來組報告,平台不觸發掃描。決策者看到實際畫面後指出,他要的是平台真的發動一次掃描。這是新需求,不是 bug——拉取模式做得是對的,只是覆蓋不到這個情境。本案定位為雛形:先把端到端鏈路打通,取捨一律傾向最小可行。經 2026-07-31 多輪裁示後,決策已全數定案

雛形(prototype)· 先打通鏈路 決策 D17–D24 全數定案・無待決策 2026-07-31 初版 已定:兩模式並存 已定:只做 A 類純源碼 已定:公開 Git repo 已定:scanner 打進 image 已定:Guidant-AI- 前綴 已定:只做逾時、tmp 掃完即刪 已定:scan_mode 預設 scan(有寫入副作用) Java / C・C++ / C# 列後續
§0

定位:這是雛形,不是完整產品

這一節放最前面,因為它決定了整份稿子每一處取捨的方向。

先用公開 repo 可以下載的那種,我們先做一個雛形,後面再慢慢加強 — 決策者,2026-07-31

本階段的目標

把端到端鏈路整條打通並驗證
公開 Git repo 拉源碼 → sonar-scanner 掃描 → 推上客戶 server → 輪詢處理佇列 → 拉結果 → 組 HTML 報告 → 進證據池。

只做 A 類純源碼語言,只支援公開 repo,資源保護只做逾時

刻意不做的

檔案上傳、私有 repo 認證、Java、C/C++、C#、Scala、磁碟與記憶體控管、coverage 紅燈處理。

這些全部是裁示排除,不是遺漏——完整清單與各自的理由見 §6 後續強化

給下一棒的閱讀提示

遇到「這裡是不是應該做得更周全」的疑問時,先翻 §6。本案的取捨原則是一律傾向最小可行,所有被砍掉的東西都在那裡列了一句話說明為什麼這階段不做。

§1

需求背景:兩種「檢測」其實是兩件事

這一節先把「為什麼現在做出來的東西不對」講清楚——重點是它不是做錯了,而是做的是另一個情境。

1.1 決策者原話

流程好像不太對,我想要的是 user 有提供一版源碼,可能上傳 或是提供 server 位置,然後透過 sonarqube cli 去掃描並且產出結果報告 — 決策者,2026-07-31,看到 FR-058.5 實際畫面後

拆開來看,這句話包含三個要素: 使用者提供一版源碼(上傳,或給位置)→ 平台透過 sonar-scanner CLI 執行掃描 → 產出結果報告。目前上線的版本只有 ,而且那個「結果」不是平台掃出來的,是客戶自己先掃好放在 server 上的。

1.2 兩種模式的差異

面向拉取快照(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)。

1.3 為什麼這是新需求不是 bug

FR-058.5 的 D13 明確裁示了「任務執行拉取最近一次分析結果組報告,不觸發掃描」,並且當時就把「agent 端自跑 sonar-scanner」列為被排除的選項,理由寫的是「agent 沒有原始碼,也不可能備齊各語言 build 環境」。

那個判斷在當時的前提下是對的——當時沒有「使用者提供源碼」這個輸入。本案要做的,正是補上那個前提:把源碼送進 agent。而「不可能備齊各語言 build 環境」這一條,則是靠收斂語言範圍來解決——本案只做「不需要 build 環境」的那一群(D18),需要編譯產出的語言列後續案。

記錄這一點的用意

設計文件上會留下「D13 排除了 agent 自跑 scanner,D17–D24 又把它做出來」這種看似矛盾的軌跡。寫進稿子是為了讓未來的人知道這不是反覆,是前提變了——輸入從「沒有源碼」變成「使用者會提供源碼」,被排除的理由就不再成立。

§2

現況盤點:平台已經有什麼、缺什麼

這一節是實查結果,全部帶檔案與行號。目的是把「這件事要動多少東西」量化——§5 幾項決策(尤其是源碼來源與 scanner 交付方式)的成本判斷,就是從這裡算出來的。

2.1 可以直接複用的

能力現況本案怎麼用
敏感參數加密
直接可用
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_cancelPopen + 輪詢)+ :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-36self.detection_tool_code 注入,讓一支 connector 依 code 表現不同行為 模式共存形式的候選之一(D17 已定案採 ZAP 式,見 §5.1)
非法值不靜默降級
有先例
zap.py:164-171 _resolve_scan_mode() 對非法 mode 值明確 raise。註解寫得很清楚:選了主動卻因拼錯被降級成被動,會拿到「看起來正常但實際沒掃」的報告 本案 mode 解析照抄這個立場——寧可失敗也不要假裝成功

2.2 缺口

缺口現況實查影響
檔案上傳沒接進檢測鏈路
結構性
「雲端 → agent」通道存在且成熟infra/upload_file/remote_agent_adapter.py:54-102save_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 不檢查 走上傳路線要自己定上限,沒有現成的統一守門

2.3 一個管不到的地方:憑證分流

規劃初期的顧慮 · 後經查證在本案不成立

ZAP 式的 condition 機制只能管任務層參數(param_schema),管不到租戶層憑證config_field_schema)。而本案的兩種模式需要兩組不同的憑證

  • pull 模式:SonarQube base_urluser token(squ_——要能呼叫讀取 API
  • scan 模式:SonarQube base_url + 推分析用的 token,再加上取源碼用的憑證(Git token / SSH key)

config_field_schema工具層的設定,無法依任務參數分流——使用者在「設定工具憑證」那一頁時,系統還不知道他之後每個任務會選哪個模式。

InSpec 雙 transport(SSH / WinRM)踩過同一個坑,當時的解法是:兩組憑證都設為非必填,然後在 probe() 的第一段就檢查憑證完整性,明確指出缺哪一組。這是驗證過可行的一般性解法。
⚠️ 但本案不需要它——後續查證確認兩種模式共用同一把 tokensqu_ User Token 推得上去也讀得回來),且 D19 定案只支援公開 repo(沒有 Git 憑證),因此根本不需要兩組憑證。詳見 §5.1 的更正說明。

2.4 量化:兩條源碼路線的成本差

Git URL 路線

要動的層:0 個新層

• param_schema 加一個 text 欄位(repo URL)+ 一個 passwordsecret: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 位置」),但成本量級差一個數量級——雛形階段選成本低的那條先把鏈路打通。

§3

語言範圍:送錯語言會怎樣

本案只做 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# 情境的翻版。

三件事一起做

  • 事前:UI 與 setup_guide 明列支援語言清單,並寫清楚「清單外的語言請改用拉取模式」
  • 事中:可考慮讓使用者宣告主要語言,非清單內的直接擋下並給明確訊息,不要送進 scanner
  • 事後:報告產出時,若分析到的檔案數 / 程式碼行數異常偏低,在報告上明確標示而不是靜靜輸出。「零發現」與「沒掃到」必須看得出來是兩件事

這不是決策項,是縮範圍後的必要配套——不做就會產出誤導性的稽核證據。具體形式(擋在哪一層、訊息怎麼寫)留到實作計畫階段定。

支援語言的兩個已知品質限制(實作時要知道)

Python:不設 sonar.python.version 時,分析器會自動靜音一部分 issue 以避免誤報——從稽核角度就是漏報。本案不加版本欄位(見 §6)。

JS / TSnode_modules 缺席是降級不報錯tsconfig 若 extends node_modules 內的 base config 而解析不到,官方描述為「unexpected analysis results」——看起來有跑,結果是錯的。兩者都不會中止執行,因此建議把 scanner 警告呈現在報告上(見 §7 R6)。

§4

端到端流程設計

先看兩種模式擺在一起長什麼樣,再看主動掃描完整跑一次會經過哪些步驟。圖上標紅的兩處是本案最容易被忽略的坑——它們各自對應一個決策項。

4.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 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
兩模式對照:拉取是唯讀且秒級;主動掃描要把源碼弄進 agent,且對客戶 SonarQube 是寫入操作

相同的部分

• 都連客戶自己架的 SonarQube
• 憑證都是 base_url + token
• 最後都組成自包含 HTML 進證據池
• 取消 / timeout 的行為標準一致

不同的部分(=本案工作量所在)

• 源碼要進 agent(全新
• 要執行 scanner 二進位(全新
• 要輪詢 server 端處理佇列(全新
• 對客戶環境有寫入副作用全新

4.2 主動掃描完整時序

%%{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 當下的佇列長度,這是我們控制不了的變數。

§5

決策定案表 D17–D24

編號延續 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-8nmap.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)

5.1 D17 展開:使用者實際會看到什麼

這一項決定了使用者的操作動線,值得單獨畫出來。結論是「一張卡、進去選模式」

%%{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
D17 定案動線:憑證只設定一次、兩模式共用;模式差異只出現在「建任務時填什麼參數」

重要更正 · 原以為的「憑證分流痛點」在本案不成立

規劃初期曾認為兩種模式需要兩組不同憑證(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(text, required)與 token(password, secret, required)。兩種模式完全共用,這份 schema 一個字都不用改。

因此 GCB 式(兩張卡)唯一的優勢消失了。InSpec 雙 transport 那個「probe 第一段檢查憑證完整性」的先例仍是有效的一般性技術參考,但不是本案的痛點——記錄於此以免日後誤讀。

D17 定案的四個理由

  1. 概念一致:使用者心裡想的是「我要用 SonarQube」,不是「SonarQube-讀取版 / SonarQube-掃描版」。工具清單出現兩張同名卡,等於把內部實作差異推給使用者
  2. 設定只做一次:server 位址與 token 填一次、兩種模式都能用。走兩張卡要填兩次,換 token 時還得記得兩邊都改(漏改一邊會變成「一個模式能用一個不能用」的怪問題)
  3. FE 零改動condition:{field,value} 是現成機制(DetectionConfigField.vue:23-27visible computed、:31-34 對不可見欄位跳過必填驗證),ZAP 已實證
  4. 憑證共用:見上方更正說明

參數設計

欄位型別顯示條件說明
scan_modeselect・required永遠顯示兩個選項:讀取既有結果(pull)/ 原始碼掃描(scan)
project_keytextconditionpull既有欄位,語意不變
branchtext・選填conditionpull既有欄位。付費版才支援多分支
Git repo URLtext・requiredconditionscan新增。公開 repo,不需憑證
專案代碼後綴text・requiredconditionscan新增。實際推上去的是 Guidant-AI-<此欄>(D21)

D24 已定裁示 · scan_mode 預設 = scan(原始碼掃描)

決策者原話:「預設不要,預設一定是 scan mode」。

定案理由主動掃描是本需求的主線意圖,pull 只是先落地的那一個。預設值應該指向主要使用情境,讓使用者少一步操作。

但這個選擇有代價,而且代價落在不可回復的那一邊——見下方風險與必要配套。

D24 的代價與必要配套 事前告知從「最好有」升級為「必須有」

兩種模式選錯的後果不對等

  • 誤選 pull:拿到一份舊快照。純唯讀、對客戶系統零副作用,發現後重跑即可 → 可回復
  • 誤選 scan對客戶的 SonarQube 寫入,專案不存在時自動建立,在客戶的正式系統上留下痕跡 → 不可回復(要人工去刪)

既然預設值指向不可回復的那一邊,事前告知的責任就從 UI 的「最好有」升級為「必須有」。關鍵風險是:使用者可能完全沒動 scan_mode 就送出,等於在未意識的情況下對客戶的 SonarQube 寫入並建立專案。

三項配套(實作必做)

  1. 建立任務時的提示必須顯眼——hint 文字不夠(使用者會略過)。建議在 scan 模式的參數區塊做明顯標示,或在送出前給明確提示。具體形式列為實作建議,不在本稿定死
  2. setup_guide 必須明講:「本工具預設為原始碼掃描模式,執行時會在貴公司的 SonarQube 建立 / 更新專案(key 為 Guidant-AI-<識別>)」。讓管理員在設定階段就知道,而不是看到專案清單多東西才發現
  3. 收緊模式寫進 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 明確屬於前者。

§6

後續強化(本階段刻意不做)

這一節是給下一棒看的。下列全部是裁示排除,不是遺漏——每項一句話說明為什麼這階段不做,以及日後要撿起來時已知的技術結論。

項目為什麼這階段不做日後撿起來時的已知結論
檔案上傳取源碼 要動三層(FE 元件 / upload 中繼 / 派工鏈路),雛形階段先求打通 雲端→agent 通道已存在且成熟infra/upload_file/remote_agent_adapter.py:54-102save_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 analyzerSONAR_SCANNER_JAVA_OPTS 可調。詳見 §7
coverage 紅燈處理 決策者裁示不處理(「主要掃品質就好」) SonarQube 自己不產生 coverage,只匯入第三方報告;我們不跑測試 → 報告必然無 coverage。客戶 Quality Gate 若含覆蓋率條件會亮紅燈Go 特例:無 coverage 資訊時視為「全部未覆蓋」而非「無資料」。詳見 §7
語言版本參數 雛形階段不加欄位 Python 不設 sonar.python.version 時,分析器自動靜音部分 issue 以避免誤報——從稽核角度就是漏報。日後若加,建議做成選填並在報告上標註「未指定語言版本,部分檢查已略過」
§7

風險與已知限制

下列是本案上線後仍然存在的限制。部分已裁示不處理,記錄在此以免下一棒誤以為是遺漏。

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 是同一個原則:「執行降級」不能假裝成「執行完整」

7.1 落地前必須實測的項目