---
title: U10 檢查結果：系統診斷包的下載入口與組裝
---

# U10 檢查結果：系統診斷包的下載入口與組裝

> 檢查日期 2026-09-25｜對應卡片 CM-2162（母卡 CM-2152）｜檢查範圍 8 個檔案 1,295 行｜工具 run ID `wf_c03e5bb6-44b`

## 🔴 一句話結論

**下載入口只有兩條，都守得住。但診斷包的「遮罩」（把密碼換成 `***` 的那道手續）認得的密碼寫法太少，而且三種收集物用的遮罩強度不一樣。** 實測有十幾種常見寫法的密碼會原樣進包。

掃描工具提出 1 個候選，三人面板 0:3 否決，正式清單**零條**。我照卡片逐支開檔，也真的在本機組了一包診斷包打開檢查，另外找到四件事：

1. **（中）遮罩漏網。** 設定檔、應用日誌、資料庫紀錄、錯誤事件各走不同的遮罩函式。其中資料庫紀錄和「最近錯誤」頁面用的那支最弱，連 `Authorization: Bearer …` 這種最典型的登入權杖寫法都不遮。四條路徑都不認得 `password=值`、網址裡的帳密、`export 名稱=值`。
2. **（低）稽核只記「有人按了匯出」，沒記「成功還是失敗」。** 程式開頭的說明寫著「成敗另記一筆，見 `_audit_export`」，但這支函式根本不存在。
3. **（低）只帶起點、不帶終點時會回 500。** 起點帶時區、終點沒帶時，兩個時間比大小會當掉。只有平台管理員打得到。
4. **（資訊）資源用量。** 畫面版有 30 天上限，打包期間佔住一個工作程序。日誌切片宣稱上限 40MB，實測記憶體峰值是檔案大小的 1.3 倍。

⚠️ **給首腦：派工提到「總表第 173 項也是診斷包遮罩」，但總表第 173 項實際是「SSP Word 檔卡死解析」。** 診斷包遮罩的既有項目是**第 162 項**（AI 金鑰以 `KEY=值` 形狀進日誌，診斷遮罩不認得這種寫法）。

## 這一棒在檢查什麼

「系統診斷包」是客戶系統出問題時，平台管理員把整台機器的現場（設定、日誌、資料庫紀錄、錯誤事件）打成一個 `tar.gz` 壓縮檔，寄給原廠分析用的。這個包**不加密**，會經過客戶的電腦、郵件或隨身碟、原廠的電腦，最後被貼進 AI 對話。每一站都可能外洩，所以**包裡的密碼遮得乾不乾淨是這個功能唯一的防線**。

要回答的問題是：誰能產這個包？產出來的包裡，密碼真的都被遮掉了嗎？

| 檔案 | 行數 | 角色 |
|------|-----:|------|
| `app/support/diag_cli.py` | 366 | 指令版入口（`sudo guidant diag`），含應用起不來時的降級產包 |
| `app/support/service/diag_bundle_app_service.py` | 334 | 打包核心：指令版與畫面版共用，逐項收集後交給打包器 |
| `domain/support/service/diag_slice_domain_service.py` | 270 | 決定從資料庫撈哪些紀錄；另供「最近錯誤」頁面用 |
| `app/support/service/diag_bundle_export_service.py` | 133 | 畫面版匯出：守門、檢查時間範圍、寫稽核事件 |
| `api/support/routes/diagnostic_bundle_route.py` | 67 | 畫面版下載網址 `POST /support/diagnostic-bundle` |
| `app/support/service/recent_error_app_service.py` | 52 | 支援頁「最近的系統錯誤」 |
| `api/support/routes/recent_error_route.py` | 43 | `GET /support/recent-errors` |
| `api/support/__init__.py` | 30 | 登記上面兩支網址 |

實際做遮罩、讀檔的程式在 `infra/support/`，屬 U11 範圍。本棒為了回答卡片的問題有讀、也有實測，**但該處的問題以本棒視角（組裝時沒有把關）報**，U11 不必重報。

## 掃到什麼（總覽）

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度＋為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U10-1 | 遮罩只認少數幾種密碼寫法，四條收集路徑用的遮罩強度還不一樣 | 系統別處只要用常見寫法把密碼寫進日誌或資料庫紀錄（例如 `password=xxx`、`Bearer xxx`、`postgresql://帳號:密碼@主機`），就會原樣進診斷包，跟著包寄到原廠、貼進 AI 對話 | 客戶產過一次診斷包，而且系統某處剛好用這些寫法記錄了機密。**不需要攻擊**，正常維運就可能發生。已知一個真實來源：第 162 項（AI 金鑰） | 組裝入口 `app/support/service/diag_bundle_app_service.py:115-120`（打包前沒有統一的最後一道遮罩）；遮罩規則本身在 `infra/support/diag_masking.py:48`、`:57-61`、`:108-117`；資料庫紀錄走弱版的位置 `infra/support/diag_db_reader.py:237`；最近錯誤 `domain/support/service/diag_slice_domain_service.py:245` | 🟠 中：不必攻擊、正常維運就會發生；前提是別處把機密寫進日誌，已有真實案例。出貨設定檔本身的 11 個機密欄位全部遮得到，所以不是「設定檔直接外洩」那種高 | runner 自行開檔＋實測（未經三人面板） |
| U10-2 | 稽核事件只記「有人按匯出」，沒記成功或失敗；說明文字指向一支不存在的函式 | 查帳時看得到誰試圖匯出，但不知道他有沒有真的拿到包 | 平台管理員匯出過診斷包 | `app/support/service/diag_bundle_export_service.py:90-99`（產包成功、失敗後各補一筆稽核）；說明文字 `:30` | 🟢 低：意圖有記，只是缺結果；不影響守門 | runner 自行開檔（未經三人面板） |
| U10-3 | 起點帶時區、終點沒帶時，比對時間會當掉，回 500 | 平台管理員看到「系統錯誤」而不是「時間範圍不對」；日誌多一筆 ERROR | 平台管理員身分，而且只送 `since`、不送 `until` | `app/support/service/diag_bundle_export_service.py:120-122`（比大小前先把兩個時間統一成同一種時區格式） | 🟢 低：只有平台管理員打得到，後果只是錯誤訊息難看 | runner 實測（未經三人面板） |
| U10-4 | 畫面版產包同步佔住一個工作程序；日誌切片的記憶體用量是檔案大小的 1.3 倍 | 最壞情況：選 30 天、日誌很大時，產包那段時間少一個工作程序可用，記憶體吃掉數百 MB | 平台管理員身分 | `app/support/service/diag_bundle_export_service.py:91`（改背景工作）；`infra/support/diag_log_reader.py:128-150`（邊讀邊丟最舊的行，不要全部讀完才截） | ⚪ 資訊：門檻是平台管理員，而且已有 30 天上限 | runner 實測（未經三人面板） |

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - U10-1（遮罩只認少數寫法）＝總表第 162 項剩餘（#173），✅ 已修（CM-2212，commit `c8534bfcb`／`8f5755f1d`，1.21.0 出貨）。
> - U10-3（時區比對當掉回 500）＝總表第 206 項，✅ 已修（CM-2214，commit `d39181f3b`／`046553756`，1.21.0 出貨）。
> - U10-2（稽核事件只記意圖）、U10-4（畫面版同步佔工作程序）：SUMMARY 無對應條目，查不到現況。

**工具報的：零條**（1 個候選，面板 0:3 否決，見下節）。

## 工具報的逐條

工具派了兩位研究員：一位讀全部範圍，一位專找寫死在程式裡的密碼金鑰。後者交回空清單。前者提出一個候選：

- **候選 C1：「請求內容原樣進包，會對原廠的 AI 分析造成間接提示注入」**（研究員評低）。說法是：未登入的人送請求時，在瀏覽器識別字串（User-Agent）或網址裡寫一段假指令，這段文字會進存取紀錄，再原樣進診斷包的 `db/api_logs.csv`。原廠把包貼進 AI 對話時，AI 可能把這段文字當成指令。
- **面板 0:3 否決**，三票理由一致：這個程式庫裡沒有任何程式把診斷包送進 AI，貼進 AI 是原廠人員手動做的，發生在系統之外。把使用者送來的資料記進日誌、再匯出日誌，是這個功能本來就要做的事。
- **我的看法：同意否決，但原廠要知道這個風險。** 程式碼確實沒有危險的動作。不過「診斷包會被貼進 AI」是寫在程式說明裡的**預期用法**（`infra/support/diag_masking.py:3-4`，資料庫欄位說明也是專為 AI 寫的）。原廠分析診斷包用的 AI 若有執行指令的能力，應該把包內容當不可信資料看待。這是原廠內部作業規範的事，不是這個 repo 要修的。

## 卡片點名的疑點逐條回答

### ① 環境變數與日誌有沒有遮罩密碼金鑰？——⚠️ 有遮，但認得的寫法不夠 → 成立（U10-1）

**每一類收集物打包前走哪支遮罩**（逐支開檔確認）：

| 收集物 | 包內檔名 | 走哪支遮罩 | 強度 |
|---|---|---|---|
| 設定檔（指令版讀檔／畫面版讀當下環境變數） | `env/guidant.env.masked` | `mask_env_text` | 只認「名稱像密碼」的 `名稱=值` |
| 應用日誌 | `logs/app.log.slice` | `mask_log_lines` | JSON 形狀＋`名稱: 值` 形狀 |
| 存取紀錄、稽核與錯誤紀錄（資料庫） | `db/api_logs.csv`、`db/system_logs.csv` | `mask_json_like` | **只認 JSON 形狀** |
| 錯誤收集站的事件 | `events/glitchtip-events.json` | `mask_structure` | JSON 形狀＋欄位名稱像密碼 |
| 支援頁「最近的系統錯誤」（瀏覽器顯示） | — | `mask_json_like` | **只認 JSON 形狀** |
| 各容器的紀錄 | `logs/docker-*.log` | **完全沒過遮罩** | — （＝U2-2，U11 範圍，本棒不重報） |
| 版號、授權開關、磁碟、容器狀態、資料表筆數、改版水位 | `env/*`、`db/row-counts.csv` | 不需要（本來就不含機密） | — |

**實測**：我寫了一支小程式，把 30 多種寫法的假密碼 `SeCrEt123` 分別丟進四支遮罩函式，看輸出還有沒有 `SeCrEt123`。結果（✅＝遮掉，❌＝原樣留下）：

| 密碼的寫法 | 設定檔遮罩 | 日誌遮罩 | 資料庫紀錄／最近錯誤遮罩 |
|---|:-:|:-:|:-:|
| `DB_PASSWORD=值` | ✅ | ❌ | ❌ |
| `export DB_PASSWORD=值`（shell 常見寫法） | ❌ | ❌ | ❌ |
| `DATABASE_URL=postgresql://帳號:密碼@主機/庫` | ❌ | ❌ | ❌ |
| `DB_PASS=值`、`SMTP_PWD=值`（縮寫名稱） | ❌ | — | — |
| 多行的值（例如 JSON 或憑證跨好幾行，只有第一行被遮） | ❌ | — | — |
| `Authorization: Bearer eyJ…`（標準登入權杖標頭） | — | ✅ | ❌ |
| 單獨出現的 `Bearer eyJ…`（例如錯誤訊息「token invalid Bearer eyJ…」） | — | ❌ | ❌ |
| `cookie: session=值`、`X-Api-Key: 值` | — | ✅ | ❌ |
| `{"password": "值"}`（JSON） | — | ✅ | ✅ |
| `{'password': '值'}`（Python 印字典的寫法，單引號） | — | ❌ | ❌ |
| 網址參數 `?token=值` | — | ❌ | ❌ |
| `["docker","run","-e","ANTHROPIC_API_KEY=值"]`（＝第 162 項的真實形狀） | — | ❌ | ❌ |

錯誤收集站的事件也實測了：欄位名稱叫 `password` 的會遮，但字串裡寫著 `Bearer 值` 或 `password=值` 的不會。

**白話說明**：遮罩的做法是「看到長得像密碼的東西就換成 `***`」，問題出在「長得像」的定義太窄：

- **資料庫紀錄和「最近錯誤」用的是最弱的那支。** 遮罩模組自己的說明寫著「四條路徑共用同一份判準，不分岔——分岔的症狀是『設定檔遮了、存取紀錄沒遮』」（`infra/support/diag_masking.py:26-28`），但實際上資料庫紀錄只過了 JSON 形狀那一道，沒過日誌那一道。`db/system_logs.csv` 的 `message` 欄裡是 ERROR 等級的日誌原文，跟 `logs/app.log.slice` 是同一種內容，遮罩卻比較弱。
- **沒有任何一支認得 `名稱=值` 出現在一行文字中間。** 第 162 項已經記錄了一個真實來源：AI 金鑰以 `ANTHROPIC_API_KEY=明文` 的形狀寫進日誌。本棒實測確認，這個形狀在日誌、資料庫紀錄兩條路都原樣留下。
- **設定檔遮罩不認得網址裡的帳密、`export` 開頭、縮寫名稱、多行的值。** 畫面版讀的是「當下進程的環境變數」，會用 `名稱=值` 一行一個串起來（`infra/support/collector/container_collector.py:94`）。值本身跨好幾行時（例如 JSON 格式的資料庫密鑰），只有第一行被遮。

**出貨設定檔本身沒問題**：我把安裝程式寫出的 `guidant.env` 範本（`scripts/installer/install.sh:1379-1456`、`:1672-1690`）逐欄丟進設定檔遮罩，11 個放機密（或機密檔路徑）的欄位全部遮掉。沒遮的都是主機名稱、埠號、開關、路徑這類本來就不是機密的值。出貨版 compose 也沒有注入帳密連線字串（`docker/production/docker-compose.yml`）。**所以「今天出貨的設定檔直接外洩」不成立**，風險在「日誌與資料庫紀錄裡混進機密」。

**DEV 實際資料查證**（2026-09-25 16:05 +08，全程唯讀連線，只下 `SELECT`）：最近 30 天的 `system_logs` 共 1,491 筆，其中 10 筆含 `password=` 形狀，**逐筆看過，全部是例外堆疊裡印出的程式原始碼那一行**（例如 `server.login(user=self.config.username, password=self.config.password)`），值是變數名稱不是真密碼，所以 DEV 目前**沒有**真密碼進包。`api_logs` 7,723 筆裡沒有網址帶權杖參數的紀錄。**打折處**：DEV 資料量與使用樣態跟客戶現場不同，這只證明「DEV 今天沒有」，不證明「客戶那邊不會有」。

**真的組一包**：我在本機用指令版產了一包（`python -m app.support.diag_cli --since 2h`），設定檔換成一份含 8 個假密碼的測試檔，解開檢查：

- `DB_PASSWORD`、`GUIDANT_DB_ADMIN_PASSWORD`、`JWT_SECRET_KEY`、`AGENT_CA_KEY` 遮掉了，也列在 `manifest.json` 的遮罩清單裡。
- `export REDIS_PASSWORD=`、`DATABASE_URL=postgresql://cmmgr:密碼@…`、`DB_PASS=` **三個假密碼原樣出現在包裡**，而且 `manifest.json` 的遮罩清單沒有列出它們。讀包的人看了清單，會以為這三行是無害設定。
- **打折處**：本機應用起不來（跟本棒無關的平行線錯誤：`FlowControlJobRepoImpl` 缺一支方法），所以這包走的是「降級產包」路徑，沒有資料庫紀錄那半。資料庫紀錄那半的結論來自直接呼叫遮罩函式實測，不是從真包裡看到的。

**建議修法**（遮罩規則在 U11 範圍的 `diag_masking.py`，這裡只給方向）：

1. **資料庫紀錄與「最近錯誤」改走日誌那支**（`mask_log_lines` 的單行版），讓四條路徑真的共用同一套判準。這一步改動最小、效果最大。
2. **日誌遮罩補認 `名稱=值`**（第 162 項修法欄已寫「診斷遮罩補認 KEY=值」，同一件事）、單獨出現的 `Bearer 值`、網址參數、`帳號:密碼@` 形狀的連線字串、單引號字典。
3. **設定檔遮罩**補認 `export ` 開頭、縮寫名稱（`PASS`、`PWD`）、值裡含 `://帳號:密碼@` 的一律遮。畫面版不要用 `名稱=值` 串環境變數，改成逐個判斷。
4. **組裝入口加一道最後檢查**（`diag_bundle_app_service.py:115-120` 收齊之後、交給打包器之前）：拿已知的機密值（程式啟動時讀到的資料庫密碼、權杖簽章金鑰等）逐檔搜一遍，搜到就遮，並在 `manifest.json` 記一筆。這是「不管寫法、只管值」的保底，能擋住所有目前沒想到的寫法。

### ② `since`／`until` 有沒有上限？——✅ 畫面版有，不成立；指令版刻意不設

- **畫面版**：最長 30 天（`diag_bundle_export_service.py:53`），超過就回 400 擋下，不會偷偷縮短範圍（`:122-123`）。起點比終點晚也擋。
- **指令版**：`--since` 沒有上限，而且有 `--no-limit` 可以關掉 100MB 包大小上限（`diag_cli.py:88-91`）。這是給到場工程師用的，要先有主機 root 權限，**不算漏洞**。
- 各層還有第二道上限：日誌切片 40MB（`infra/support/diag_log_reader.py:57`）、資料庫每張表 20 萬筆（`infra/support/diag_db_reader.py:45`）、容器紀錄每支 5,000 行、整包 100MB。
- **實測發現一個副作用 → U10-3**：只送 `since`（帶時區）、不送 `until` 時，`until` 由伺服器補成「現在」（沒帶時區），兩個時間比大小時 Python 直接拋錯（`TypeError: can't compare offset-naive and offset-aware datetimes`），而且這一行在 try 外面，會冒成 500。兩個都送或都不送就正常。打包核心那層有處理混合時區（`diag_bundle_app_service.py:53-66`），但匯出服務在呼叫核心**之前**就先比了，沒走那道處理。
- **資源用量 → U10-4**：我造了一個 132MB 的日誌檔讓切片去讀，輸出正確截在 39MB，但**記憶體峰值 169MB**。原因是程式先把範圍內所有行讀進記憶體，最後才截尾端。畫面版產包在網頁請求裡同步執行，產完才回應，期間佔住一個工作程序（落地版預設只有 4 個）。門檻是平台管理員，不算漏洞，列出供知悉。

### ③ 「最近的系統錯誤」會不會帶出堆疊與內部路徑？——⚠️ 會，屬設計；但遮罩太弱（併入 U10-1）

- 回傳的 `message` 是日誌原文，**會**含完整的例外堆疊和主機上的檔案路徑。DEV 查證：最近 200 筆 ERROR 裡有 10 筆含堆疊與路徑。`source` 欄另外給「檔名.函式:行號」。
- 這是**刻意的設計**：這個頁面就是給平台管理員「使用者剛說壞了，我看一眼是什麼錯」用的（`recent_error_app_service.py:1-11`），需要看到堆疊。守門是平台管理員，不算洩漏。
- **但遮罩只過了最弱的 `mask_json_like`**（`diag_slice_domain_service.py:245`）。程式說明自己寫了「這支端點的內容會顯示在瀏覽器上、也會被貼進工單，與診斷包同一個外流路徑」（`:229-230`），卻只給了比診斷包日誌更弱的遮罩，所以錯誤訊息裡的 `Bearer 值`、`名稱=值` 會原樣顯示在畫面上。歸在 U10-1 一起修。
- 筆數上限 200（`domain/support/entity/diag_bundle.py:95`），超過自動降到上限，不會一次撈爆。

### ④ 下載入口除了平台管理員，還有沒有別的路？——✅ 只有兩條，都守得住，不成立

逐一核對產包核心 `build_bundle()` 的所有呼叫者（全 repo 搜尋）：

| 入口 | 誰能用 | 守門位置 |
|---|---|---|
| 畫面版 `POST /api/1.0/support/diagnostic-bundle` | 登入＋平台管理員 | 網址層 `@jwt_required()`（`diagnostic_bundle_route.py:35`）；服務層第一行 `require_platform_admin()`（`diag_bundle_export_service.py:74`） |
| 指令版 `sudo guidant diag` | 主機 root | `scripts/installer/guidant:658-779`：主機上以 root 收集，再 `docker exec` 進應用容器執行 |
| 開發機 `python -m app.support.diag_cli` | 能登入開發機、讀得到 `.env` 的人 | 作業系統權限（出貨主機上沒有 Python） |

- **沒有簽章網址、沒有第二支路由、沒有背景排程會自動產包。** `api/support/__init__.py` 只登記兩支網址，DI 容器（`di_containers/support/support_containers.py`）也只接了這兩條。
- `RUN_MODE=diag`（`main.py:82-87`）只在啟動時生效，產完包就結束，不會開任何網路埠。
- 「最近錯誤」`GET /support/recent-errors` 同樣是網址層登入＋服務層平台管理員（`recent_error_app_service.py:46`）。
- 產出的包放在只有自己讀得到的暫存目錄（`tempfile.mkdtemp`，權限 0700），包內每個檔案權限 0600，回應送完就刪（`diagnostic_bundle_route.py:51-57`）。我確認了下載套件是先開檔、再在回應結束後刪，不會下載到空檔。**小瑕疵**：產包失敗時（`diag_bundle_export_service.py:97-99`）暫存目錄不會被刪，留下半成品（內容已遮罩）。影響很小，順手修即可。

**額外核對平台管理員的判定**：`require_platform_admin()` 判的是「你的公司是不是最上層公司」（身分套件 `jedi_iam/authz/platform.py:15-32` → `_session_paths_have_root`），**不看角色**。落地版的客戶公司一律建在原廠系統殼（第 1 號公司）底下（`app/setup/service/setup_wizard_service.py:14-16`），所以客戶的管理員**不是**平台管理員，這扇門擋得住客戶。「最上層公司裡的非管理員角色也算平台管理員」是 U1（權限檢查共用層）的範圍，本棒不重報。

### ⑤ 環境變數、授權檔、資料庫摘要、事件逐項看——✅ 除 U10-1 外不成立

- **授權檔**：只收兩個開關的真假值，**不含授權檔內容或任何金鑰**（`diag_bundle_app_service.py:197-233`），開檔確認屬實。
- **資料庫摘要**：資料表筆數用的是估計值，只有數字，沒有資料內容；改版水位只有版號清單。都不含機密。
- **資料庫讀取繞過客戶隔離**：`DiagDbReader` 用 `SET LOCAL app.is_super_admin = 't'` 看全部客戶的紀錄（`infra/support/diag_db_reader.py:63`）。這是刻意的（診斷包要看整台機器），前提是入口只給平台管理員，已由 ④ 確認。
- **降級產包**：應用起不來時，錯誤原文會寫進 `manifest.json`（`diag_cli.py:311-334`），**沒過遮罩**。資料庫連線失敗的錯誤訊息一般帶主機、帳號，不帶密碼，所以風險低。建議順手過一次 `mask_log_lines`。

### ⑥ 「有人守了一半」八種樣式——除 U10-2 外不成立

| 樣式 | 結論 |
|---|---|
| 只驗身分、不驗資料歸屬 | 不適用：診斷包是整台機器的現場，不屬於任何客戶，平台管理員本來就該看全部 |
| 列表有查、單筆沒查 | 不適用：沒有單筆存取 |
| 守門寫在網址層，第二支路由沒掛 | 不成立：守門在服務層，兩支網址都經過 |
| 守門條件用 `or` 串起來 | 不成立：單一條件 |
| 「查不到」和「沒權限」混在一起 | 不適用 |
| 背景或系統身分執行時，假設呼叫者一定是自己人 | 指令版以系統身分執行，但入口是主機 root，本來就是自己人。成立但合理 |
| 防重放、計次只在單一進程內有效 | 不適用：沒有計次 |
| 註解說這裡刻意不檢查，上一層卻沒檢查 | 路由註解說「守門刻意不放在這裡」，服務層確實有檢查 ✅。**但另一句註解說「成敗另記一筆結果，見 `_audit_export`」，這支函式不存在 → U10-2** |

## 逐條細節

### U10-1 — 遮罩只認少數幾種密碼寫法，四條收集路徑的遮罩強度不一樣（中）

- **嚴重度**：🟠 中
- **位置（該補檢查的地方）**：
  - 組裝入口：`app/support/service/diag_bundle_app_service.py:115-120`（收齊所有收集物之後、交給打包器之前，沒有統一的最後一道遮罩）
  - 資料庫紀錄只走弱版：`infra/support/diag_db_reader.py:237`（`mask_json_like`）
  - 最近錯誤只走弱版：`domain/support/service/diag_slice_domain_service.py:245`（`mask_json_like`）
  - 遮罩規則：`infra/support/diag_masking.py:48`（額外名稱清單）、`:57-61`（設定檔規則）、`:164-169`（日誌規則）
- **白話說明**：見「卡片點名的疑點 ①」。簡單說，診斷包把密碼換成 `***` 的方式是「比對長相」，而它認得的長相太少。四條收集路徑裡，有兩條用的是最窄的版本。
- **影響**：系統任何一處用常見寫法把機密寫進日誌或資料庫紀錄，都會跟著診斷包寄給原廠、貼進 AI 對話。`manifest.json` 的遮罩清單只列出「有被遮到的」，讀包的人會誤以為包是乾淨的。
- **觸發前提**：客戶產過診斷包，而且日誌或資料庫紀錄裡剛好有這些寫法的機密。已知來源：第 162 項（AI 金鑰以 `名稱=值` 進日誌，本棒實測確認兩條路徑都沒遮）。**不需要攻擊**。
- **與既有項目的關係**：
  - **第 162 項**的修法欄已寫「主專案：診斷遮罩補認 KEY=值」，那只是本條的一部分（日誌路徑的 `名稱=值`）。**本條新增的是**：資料庫紀錄與「最近錯誤」根本沒走日誌遮罩、單獨出現的 `Bearer`、網址帶帳密、`export`、多行值、`manifest` 遮罩清單會誤導讀包的人。建議首腦判斷要併進第 162 項，還是另開一條。
  - **U2-2**（即時通訊權杖進容器紀錄）是容器紀錄「完全沒過遮罩」，屬 U11 的收集器，本條不含。
- **實測**：本機直接呼叫四支遮罩函式，30 多種寫法逐一比對；另在本機真的產一包，解開確認三個假密碼原樣留在 `env/guidant.env.masked`、且沒列進 `manifest.json`。DEV 資料庫唯讀查證，目前沒有真密碼符合這些寫法。
- **建議修法**：見「卡片點名的疑點 ①」末段四點。**最優先的是第 1 點**（資料庫紀錄與最近錯誤改走日誌那支），改兩行就能拉到跟日誌一樣的強度。

### U10-2 — 稽核事件只記意圖，沒記成功或失敗（低）

- **嚴重度**：🟢 低
- **位置**：`app/support/service/diag_bundle_export_service.py:90-99`（產包成功與失敗兩個分支都沒有寫稽核）；說明文字 `:26-30`
- **白話說明**：程式在開始產包前寫一筆「有人匯出診斷包」的稽核事件，這個設計很好（產包中途當掉也留得下紀錄）。說明文字接著寫「成敗另記一筆結果，見 `_audit_export`」，但全 repo 搜不到 `_audit_export`，程式裡也沒有第二筆稽核。
- **影響**：事後查「誰把系統現場帶走了」時，只知道誰按過按鈕，不知道他有沒有真的拿到包。應用日誌裡有一行「診斷包已產出」（`:101-107`），但那不是稽核事件，會隨日誌輪替被清掉。
- **觸發前提**：平台管理員匯出過診斷包。
- **建議修法**：產包成功後補一筆稽核（帶檔案大小與收錄檔數），失敗時也補一筆（帶錯誤代碼）；或者把說明文字改成跟現況一致。

### U10-3 — 只帶起點（帶時區）不帶終點時回 500（低）

- **嚴重度**：🟢 低
- **位置**：`app/support/service/diag_bundle_export_service.py:120-122`
- **白話說明**：網頁傳來的時間帶時區（`+08:00`），伺服器補的「現在」不帶時區，Python 不允許這兩種時間比大小，直接拋錯。這一行在 try 外面，所以錯誤冒到全站處理、回 500。
- **影響**：平台管理員看到「系統錯誤」而不是「時間範圍不對」；日誌多一筆 ERROR；有接錯誤收集站的話會多一筆假警報。**不會放行、不會洩漏**。
- **觸發前提**：平台管理員身分；請求只帶 `since`、不帶 `until`（前端正常是兩個都帶或都不帶，所以一般使用不會遇到）。
- **實測**：用正式的請求格式解析四種組合，逐一交給 `_resolve_window`：只帶 `since`（有時區）→ `TypeError`；只帶 `until`、兩個都帶、`since` 沒時區 → 正常。
- **建議修法**：比大小前先用打包核心已有的 `_as_naive_local`（`diag_bundle_app_service.py:53-66`）把兩個時間統一格式，或者在補「現在」時帶上時區。

### U10-4 — 產包同步佔住工作程序；日誌切片記憶體用量偏高（資訊）

- **嚴重度**：⚪ 資訊
- **位置**：`app/support/service/diag_bundle_export_service.py:91`（同步產包）；`infra/support/diag_log_reader.py:128-150`（先全部讀完才截）
- **白話說明**：畫面版按下匯出後，伺服器在同一個網頁請求裡把包產完才回應。範圍大時可能要幾十秒，期間這個工作程序不能處理別人的請求。日誌切片先把範圍內所有行讀進記憶體，最後才留尾端 40MB。
- **實測**：造一個 132MB 的日誌檔讓切片去讀，輸出 39MB、正確標示已截斷，記憶體峰值 169MB。
- **影響**：平台管理員選 30 天、日誌量大時，產包那段時間伺服器少一個工作程序、多吃數百 MB 記憶體。
- **觸發前提**：平台管理員身分。
- **建議修法**：不急。之後要改的話，切片改成邊讀邊丟最舊的行（固定大小的佇列），畫面版改背景工作＋完成後下載。

## 這份結果可信到什麼程度

**第一層：工具正式報告（經三人面板驗證）**

- 驗證章：`verified`（stamp `CLAUDE-SECURITY-REVISION-93c57739b77a-dirty.json`）
- 候選 1、面板 3 票（0:3 否決）、研究員派出 2／交回 2、`failed` 0、續跑 0、沒有遺失的候選。
- 正式清單零條，**不代表這 8 支乾淨**。工具的研究員看到了「請求內容原樣進包」，但沒有去實測遮罩函式，所以沒發現遮罩本身的漏洞。

**第二層：runner 自行開檔核對＋實測（未經三人面板投票）**

- 範圍 8 支逐支通讀；卡片「重點看什麼」五條＋「有人守了一半」八種樣式逐條答完。
- 跨讀了範圍外的相關程式（只讀不報）：`infra/support/` 全部（遮罩、打包器、日誌切片、資料庫讀取、三支收集器）、`scripts/installer/guidant` 的 `diag` 子命令、`install.sh` 的設定檔範本、出貨版 compose、身分套件的平台管理員判定、`werkzeug` 的 `send_file`。
- 實測：①四支遮罩函式 × 30 多種寫法；②安裝程式設定檔範本 35 個欄位逐欄過遮罩；③本機真的產一包診斷包、解開檢查；④時間範圍四種組合；⑤132MB 日誌切片的記憶體峰值。
- DEV 資料庫唯讀查證（2026-09-25 約 16:05 +08，唯讀連線、只下 `SELECT`）：`system_logs` 與 `api_logs` 最近 30 天內，符合各種機密寫法的筆數與內容；最近 200 筆 ERROR 裡含堆疊與路徑的筆數。
- **打折處**：
  - 本機應用起不來（平行線的無關錯誤），真包走的是降級路徑，**沒有資料庫紀錄那半**；資料庫紀錄的遮罩結論來自直接呼叫函式，不是從真包裡看到的。
  - 畫面版下載沒有對正在運行的服務實測（本機 BE 未啟動），守門結論來自讀碼。
  - DEV 資料只證明「DEV 今天沒有真密碼進包」，不代表客戶現場。

## 執行概況

| 項目 | 值 |
|---|---|
| 掃描目標 | BE repo `compliance-manager-be`，branch `feature/review` |
| 版本 | `93c57739b`（工作區有平行線未 commit 改動，範圍 8 支檔案本身無改動） |
| 工具 | Claude Code 官方 `claude-security` plugin 0.11.0 |
| 參數 | mode `scan`／effort `low`／focus `attack-surface`／scope 8 檔 |
| run ID | `wf_c03e5bb6-44b` |
| 報告目錄 | `CLAUDE-SECURITY-20260925-074607/`（不入版控） |
| 耗時 | 約 37 分鐘（2,220 秒） |
| 研究員 | 2 派出／2 交回（全範圍 1＋密碼金鑰專掃 1），`failed` 0 |
| 候選／面板票 | 1／3（0:3 否決） |
| 驗證章 | `verified` |
| runner 自行發現 | 4 條（中 1、低 2、資訊 1），皆未經面板 |
