FR-177 C6 掃描報告:SBOM 掃描三檔+connector 基底與登記

⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。

§1

一句話結論

這一棒沒有找到要處理的問題。 工具找到 0 條。卡片列的 6 個重點我逐條開檔看過,都不成立,或者只有很小的影響、不值得另外開卡(見下方「重點逐條人工查證表」)。要注意的是:工具的研究員一條候選都沒提出,三位檢查員就沒有東西可以投票,所以「0 條」的意思是「沒有東西可投」,不等於「證明乾淨」。

§2

這一棒在檢查什麼

  • SBOM 弱點掃描(sbom-scan): SBOM 是「軟體物料清單」,也就是一份列出程式用了哪些第三方套件和版本的清單。流程是:使用者上傳源碼壓縮包(或一份現成的 SBOM)→ Agent 解壓 → 用 Syft 產出物料清單 → 用 Trivy、Grype、OSV-Scanner 三套工具,對本機的離線弱點資料庫(vulndb)比對已知漏洞。
  • connector 共用層: connector 是「Agent 呼叫某一套檢測工具的那段程式」。base.py 是所有 connector 的共同基底類別,__init__.py 是「工具代號 → connector」的登記表。

範圍是 5 個檔,約 907 行:

  • sbom_scan.py
  • sbom_tools.py
  • sbom_coverage.py
  • base.py
  • __init__.py

為了查證呼叫端,我翻了範圍外的 3 個檔:guidant_agent/shared/archive_safety.py、common/util/scan_tools.py、guidant_agent/console/api/scan_routes.py。這些只為查證而讀,本身不當作發現。

§3

掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 嚴重度
— 工具沒找到問題 — — — — —
§4

每條發現的詳述

沒有發現,這一節沒有內容。

§5

重點逐條人工查證表

首腦標的重點 人工結果 為什麼
① 源碼包從哪來、解壓、symlink(sbom_scan.py:157、:206) 不成立 檔案從哪來: 先找本機上傳槽(find_local_input,用 agent_local_uid 找);找不到才用注入的 file_downloader 向主系統下載。
識別碼: scan_uid 先過 SAFE_DIR_NAME 檢查,只收單層的安全名稱,才拼進路徑(:206-210)。
下載落點: 外來檔名只取 basename,放在 0700 的暫存目錄裡(:248)。
解壓: 走 archive_safety.extract_archive(:274)。tar 用標準庫的 filter="data",會拒收「絕對路徑或指到外面」的 symlink 和裝置檔;zip 的 zipfile.extract 根本不會建立 symlink。所以壓縮包裡放一個 -> /data/secrets 的 symlink,解壓這一步就被擋,不會被 Syft 讀進去。
殘留:
只剩「指到包內」的 symlink,Syft 跟過去讀到的還是包內的檔案,沒有越界。
② tools、sbom_formats 有沒有白名單(:170、:182);Popen 的 argv 與 env 不成立(附一個觀察) tools: 不在 TOOLS 清單裡就直接 ValueError(:175-177),最後照固定順序重組。
格式:
每一項都要對上 SBOM_FORMATS 的格式加版本白名單(:183-186);傳字串而不是陣列,會被逐字拆開,也一樣被拒。
_BINARIES[tool]: 只用常數去查,不會被注入。
argv: 全部是 list 形式、不經 shell。進命令列的只有白名單格式、工作目錄裡由程式自己產生的路徑,以及 --source-name;--source-name 是用 safe_source_name 清過的檔名,而且是獨立一個 argv 元素,不會被當成選項。
env 觀察:
offline_env 是複製整份 os.environ 再覆蓋離線設定(sbom_tools.py:50)。Agent 自己的環境變數會整份交給這四支隨版附帶的工具。工具本身是信任的程式,所以不算漏洞;若要收緊,可以改成只傳必要的變數。
③ scan_tool_path 怎麼找工具(sbom_tools.py:83-90) 不成立 common/util/scan_tools.py:19-31 的查找順序是:
1. AGENT_SCAN_TOOLS_DIR 環境變數
2. 隨版附帶目錄
3. /opt/guidant-agent/tools/bin
4. 前面都找不到,才退回 shutil.which(也就是 PATH)

要換掉工具,得先能改 Agent 的環境變數或可寫入安裝目錄,這等於已經拿到主機權限,不是新的攻擊面。
④ sbom_coverage.py 吃工具輸出的 JSON **不成立(附一個觀察) 這支其實不讀 JSON**:它只走訪解壓目錄,os.walk 預設不跟隨 symlink;再看 requirements.txt 有沒有沒鎖定版本的套件。
讀檔觀察: readlines() 會把整個 requirements.txt 讀進記憶體。上限是解壓總量(預設 500 MB),屬於有上限的資源消耗。
真正吃 JSON 的地方:
sbom_tools.py:207 的 osv_sbom 讀 Syft 產出的 CycloneDX 清單,大小跟著源碼包裡的套件數量長,同樣被解壓上限和 20 分鐘的掃描逾時框住。
⑤ base.py 共用參數過濾、帳密不進 log、報告遮罩 **不成立(屬設計缺口,已在 #223/#224 登記) DetectionConnector.__init__ 只把 params、credentials 原樣存起來,沒有**共用的參數過濾,也沒有規定帳密不進 log;報告遮罩由各支 connector 自己處理。
sbom-scan 本身不用帳密,format_failure_detail 只截工具 stderr 的最後 500 字。
「在基底層加一道每支工具的參數白名單,一次修好 #223/#224」這個想法可行,但屬於修法建議,不是新的問題,所以不重報。
⑥ __init__.py 登記表與 TARGET_PARAM 對不對得上 **不成立(sbom-scan 部分) 登記表有 9 個代號;scan_routes.py:37 的 TARGET_PARAM 沒有 sonarqube 和 sbom-scan。
sonarqube: repo_url 沒過清冊,就是已登記的 B5 F2,不重報。
sbom-scan: 它沒有網路目標,輸入是上傳的檔案,不連任何主機,所以本來就不需要過清冊。
舊 id 對照:** _LEGACY_TOOL_ID_TO_CODE 只有 4 筆,而且有註明「新工具不准加」,沒有錯配的風險。
§6

可信度兩層

  • 工具層: stamp 的 verification.status 是 verified。研究員派出 1 位、回來 1 位,候選 0 條,面板 0 票。沒有東西可投,不等於乾淨。
  • 人工層: 卡片列的 6 個重點逐條開檔核對,都不成立;另外附兩個不必修的觀察:env 整份交給工具、requirements.txt 整份讀進記憶體。

工具和人工都沒有執行任何程式,也沒有實際打過任何攻擊,結論全部來自讀程式碼。

§7

執行概況

  • workflow run ID: wf_6632477e-e9f
  • 掃的版本: commit 6d1e6ca,branch ws-CM-3507-CM-3530,工作目錄沒有未提交的改動(clean)
  • effort: low(一位研究員加三視角面板;沒有盤點、威脅模型、廣度掃描);共 5 檔,耗時 423 秒
  • stamp 的 verification 欄: status: verified、candidates: 0、panel_votes: 0、researchers_dispatched: 1、researchers_returned: 1、reason: null
  • 歸檔: docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261106-035654/