# FR-177 C6 掃描報告：SBOM 掃描三檔＋connector 基底與登記

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

## 一句話結論

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

## 這一棒在檢查什麼

- **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`。這些只為查證而讀，本身不當作發現。

## 掃到什麼：總覽表

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

## 每條發現的詳述

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

## 重點逐條人工查證表

| 首腦標的重點 | 人工結果 | 為什麼 |
|---|---|---|
| ① 源碼包從哪來、解壓、symlink（`sbom_scan.py:157`、`:206`） | **不成立** | **檔案從哪來：** 先找本機上傳槽（`find_local_input`，用 `agent_local_uid` 找）；找不到才用注入的 `file_downloader` 向主系統下載。<br>**識別碼：** `scan_uid` 先過 `SAFE_DIR_NAME` 檢查，只收單層的安全名稱，才拼進路徑（`:206-210`）。<br>**下載落點：** 外來檔名只取 basename，放在 0700 的暫存目錄裡（`:248`）。<br>**解壓：** 走 `archive_safety.extract_archive`（`:274`）。tar 用標準庫的 `filter="data"`，會拒收「絕對路徑或指到外面」的 symlink 和裝置檔；zip 的 `zipfile.extract` 根本不會建立 symlink。所以壓縮包裡放一個 `-> /data/secrets` 的 symlink，解壓這一步就被擋，不會被 Syft 讀進去。<br>**殘留：** 只剩「指到包內」的 symlink，Syft 跟過去讀到的還是包內的檔案，沒有越界。 |
| ② `tools`、`sbom_formats` 有沒有白名單（`:170`、`:182`）；`Popen` 的 argv 與 env | **不成立（附一個觀察）** | **tools：** 不在 `TOOLS` 清單裡就直接 `ValueError`（`:175-177`），最後照固定順序重組。<br>**格式：** 每一項都要對上 `SBOM_FORMATS` 的格式加版本白名單（`:183-186`）；傳字串而不是陣列，會被逐字拆開，也一樣被拒。<br>**`_BINARIES[tool]`：** 只用常數去查，不會被注入。<br>**argv：** 全部是 list 形式、不經 shell。進命令列的只有白名單格式、工作目錄裡由程式自己產生的路徑，以及 `--source-name`；`--source-name` 是用 `safe_source_name` 清過的檔名，而且是獨立一個 argv 元素，不會被當成選項。<br>**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` 的查找順序是：<br>1. `AGENT_SCAN_TOOLS_DIR` 環境變數<br>2. 隨版附帶目錄<br>3. `/opt/guidant-agent/tools/bin`<br>4. 前面都找不到，才退回 `shutil.which`（也就是 PATH）<br><br>要換掉工具，得先能改 Agent 的環境變數或可寫入安裝目錄，這等於已經拿到主機權限，不是新的攻擊面。 |
| ④ `sbom_coverage.py` 吃工具輸出的 JSON | **不成立（附一個觀察）** | 這支其實**不讀 JSON**：它只走訪解壓目錄，`os.walk` 預設不跟隨 symlink；再看 `requirements.txt` 有沒有沒鎖定版本的套件。<br>**讀檔觀察：** `readlines()` 會把整個 `requirements.txt` 讀進記憶體。上限是解壓總量（預設 500 MB），屬於有上限的資源消耗。<br>**真正吃 JSON 的地方：** `sbom_tools.py:207` 的 `osv_sbom` 讀 Syft 產出的 CycloneDX 清單，大小跟著源碼包裡的套件數量長，同樣被解壓上限和 20 分鐘的掃描逾時框住。 |
| ⑤ `base.py` 共用參數過濾、帳密不進 log、報告遮罩 | **不成立（屬設計缺口，已在 #223／#224 登記）** | `DetectionConnector.__init__` 只把 `params`、`credentials` 原樣存起來，**沒有**共用的參數過濾，也沒有規定帳密不進 log；報告遮罩由各支 connector 自己處理。<br>sbom-scan 本身不用帳密，`format_failure_detail` 只截工具 stderr 的最後 500 字。<br>「在基底層加一道每支工具的參數白名單，一次修好 #223／#224」這個想法可行，但屬於修法建議，不是新的問題，所以不重報。 |
| ⑥ `__init__.py` 登記表與 `TARGET_PARAM` 對不對得上 | **不成立（sbom-scan 部分）** | 登記表有 9 個代號；`scan_routes.py:37` 的 `TARGET_PARAM` 沒有 `sonarqube` 和 `sbom-scan`。<br>**sonarqube：** `repo_url` 沒過清冊，就是已登記的 B5 F2，不重報。<br>**sbom-scan：** 它沒有網路目標，輸入是上傳的檔案，不連任何主機，所以本來就不需要過清冊。<br>**舊 id 對照：** `_LEGACY_TOOL_ID_TO_CODE` 只有 4 筆，而且有註明「新工具不准加」，沒有錯配的風險。 |

## 可信度兩層

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

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

## 執行概況

- **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/`
