---
title: b4 驗證 V4——主機／版控殘留與日誌內容（18 條）
---

# b4 驗證 V4：主機／版控殘留與日誌內容

卡：CM-2267（母卡 CM-2019）。2026-09-27 在 190（`guidant-ai-be:1.21.0b4`、agent `1.1.0b2`）與本機四個 repo 驗。只驗不修。

## 結論

| 結果 | 條數 | 條號 |
|---|---|---|
| ✅ 通過 | 12 | #10 #11 #26 #71 #72 #82 #93 #96 #103 #122 #132 #185 |
| ⚠️ 修法在出貨線成立，但主線仍有殘留 | 3 | #12（出貨線乾淨）、#73、#74 |
| ❌ 未通過 | 2 | #95、#125 |
| ⚠️ 打折（只驗到一部分） | 1 | #133 |

**最重要的兩件事：**

1. **#95 還沒修好。** AI 儀表板在問「使用者清單」時，後端一行紀錄把六個帳號的完整資料原文寫進容器紀錄，含密碼雜湊、加密鹽值、超級管理員旗標。回給畫面的內容乾淨，但紀錄那一端沒有遮。出處是 jedi-iam `app/service/user_service.py` 的 `get_users` 裡 `logger.info(f"Get users: {users}")`。這行寫到容器標準輸出，沒進 `app.log` 檔也沒進資料庫。
2. **版控殘留的清理全在 `fix/security-b1`，還沒合回 `feature/review` 與 `main`。** 主 checkout 上 Google 四把金鑰與 JWT 簽章金鑰的殘留都還在，main 上也在。只要 `fix/security-b1` 如期合回，這幾條就會通過；但在那之前，任何從 main 或 feature/review clone 的人都拿得到。

## 驗法

- **版控**：四個 repo（BE、FE、evidence-agent、套件 monorepo）用 `git grep -I` 對兩個 ref 各掃一次：`fix/security-b1`（出貨線，190 的 b4 就是從這條出的）與當下 `HEAD`（`feature/review`）。兩種掃法：
  - **字串形狀**：`AKIA…`、`AIza…`、`glpat-…`、`sk-…`、`lsv2_…`、`BEGIN … PRIVATE KEY`、`postgres://帳號:密碼@`、`redis://:密碼@`、`GOCSPX-…`、`ghp_…`、`ntn_…`、JWT 三段式、`password="…"` 這類賦值。
  - **真值比對**：從本機 `.env` 取 19 把現值，以及從修正卡 commit 的刪除行取出被清掉的舊值，逐一 `git grep -F` 計檔數。值只在記憶體比對，本報告只寫檔數。
- **190**：用 API 實打觸發（登入、帳號欄帶換行、密碼帶雙引號、寄測試信到打不通的主機、偽造 `X-Forwarded-For`、叫 AI 儀表板查使用者），觸發字串帶一個本卡專用標記，再到 `docker logs`、`/srv/guidant-ai/log/app.log`、`api_logs`、`system_logs` 找標記與機密。DB 全部唯讀查詢。
- 分支版本：BE `12dee3157`、FE `55ca905`、agent `1b71a0c`、套件 `d94a472d`（皆 `fix/security-b1`）。

## 結果表

| n | CM | 一句 | 結果 | 證據 |
|---|---|---|---|---|
| 10 | CM-2048 | GitLab 通行證殘留 | ✅（主線另計） | 四 repo 的 `glpat-` 形狀：出貨線 0 檔。HEAD 剩 2 檔，就是 CM-2048 清掉的那把真值（已撤銷），等合回。 |
| 11 | CM-2048 | 四把 AI 服務金鑰殘留 | ✅（主線另計） | `.env` 現行 OpenAI、Anthropic 兩把：四 repo 兩 ref 全 0。`sk-`／`lsv2_` 形狀在出貨線只剩套件測試檔的假值（見附錄 B）。現行 Google AI 金鑰算進 #74 那一格一起講。 |
| 12 | CM-2049 | 資料庫管理員密碼 249 檔殘留 | ⚠️ 出貨線乾淨 | 從 CM-2049 的刪除行取出被清的那組密碼：四 repo 兩 ref 全 0，含 HEAD。`postgres://帳號:密碼@` 形狀在出貨線剩 30 行，全部是 `{…}` 變數組字串或文件裡的 `**` 遮罩範例，沒有字面密碼。之所以標 ⚠️，是 CM-2049 本身寫明「密碼未換發」，所以這組密碼仍有效。 |
| 71 | — 自擬 | 產生文件腳本寫死展示環境 DB 密碼 | ✅ | `docs/system-design/scripts/generate_db_schema_docx.py` 在出貨線改讀 `DB_SCHEMA_DOCX_DB_PASSWORD`，沒設就中止。被清的舊值在四 repo 兩 ref 全 0。 |
| 72 | CM-2048 | 管理員密碼寫死三支腳本、環境變數沒設就默默用 | ✅ | 五支腳本在出貨線都改成「沒設就 `raise SystemExit` 並提示變數名」。預設測試帳號密碼字串在四 repo 兩 ref 全 0。 |
| 73 | — 自擬 | 套件倉庫與檔案儲存帳密寫在交接文件 | ⚠️ 出貨線乾淨、主線 13 檔 | 被清的舊值在出貨線 0 檔。HEAD 仍有 13 檔，含交接文件原檔、它的 HTML、文件站鏡像、`search_index.json`，另有 8 份對話紀錄。 |
| 74 | CM-2051 | Google 雲端硬碟密鑰與加密金鑰散在 15 檔 | ⚠️ 出貨線乾淨、主線仍在 | `.env` 現值比對，出貨線四把全 0。HEAD（以及 main）還有：雲端硬碟 client secret 12 檔、token 加密金鑰 10 檔、client id 14 檔、Google AI 金鑰 9 檔，全在 `docs/conversation-history/`。 |
| 122 | — 自擬 | JWT 簽章金鑰、DB 密碼、儲存金鑰在同一測試設定檔 | ✅（主線另計） | 出貨線 `.env` 現行 JWT 簽章金鑰 0 檔。190 的簽章金鑰與本機 DEV 的不同（比對雜湊，沒比值），符合「每套各自產生」。HEAD 仍有 30 檔 DEV 金鑰殘留（`docs/conversation-history/`），等合回。 |
| 125 | 卡號對不上 | 兩種紀錄詳細程度寫死最詳細 | ❌ | 見下方「#125 細節」。190 開機時 ORM 一次倒出約 6,000 行 INFO 對映紀錄，佔一小時紀錄量九成。穩態每 5 秒一行背景工作 INFO。表列卡號 CM-2178 講的是範本權限，跟本條無關。 |
| 132 | CM-2226 | 分類器六個套件沒鎖版本沒校驗碼 | ✅ | `guidant-classifier` 映像的建置步驟是 `pip install --require-hashes --no-deps -r requirements.txt`。容器內 `requirements.txt` 24 個套件全帶 `==`，共 542 個 `--hash`。`pip freeze` 25 個套件版本與鎖檔一致。 |
| 26 | — 自擬 | 登入時帳密與通行證原文寫進紀錄檔與 DB | ✅ | 兩輪。第一輪實打 4 次登入（成功、失敗、需 MFA、帶雙引號密碼），`api_logs` 四筆密碼都是 `***`。補驗（2026-09-27）：子租戶 4 的 `blsfrank` 完整走一次「帳密 → email OTP → 拿到通行證 → 帶通行證打 API」。用拿到的 access／refresh token 各取尾段 40 字元、加上密碼，到 `login_logs`、`api_logs`、`system_logs`、`docker logs`、`app.log` 五處找：全部 0。`api_logs` 的 OTP 驗證那筆回應記成 `"access_token":"***","refresh_token":"***"`；全表 JWT 三段式 0 筆。`login_logs` 登入完仍是 0 筆，原因是出貨線 jedi-iam 整條 `login_logs` 寫入鏈已經沒有呼叫端（程式註解寫明「零呼叫端，留著只為維持 metadata」），這張表不再被寫，不是驗不到。`login_tokens` 只存 `jti`，沒有 token 本體。 |
| 82 | — 自擬 | 密碼含雙引號時只遮前半截 | ✅ | 送密碼 `Q1"tail-<標記>-SECRETPART`：`api_logs` 該筆 `request` 裡密碼是 `***`，找不到 `SECRETPART`；`system_logs`、`app.log` 也找不到。 |
| 93 | CM-2055 | 帳號欄塞換行，送到客戶 SIEM 變兩筆 | ✅（本機端） | 不登入，帳號填 `…\nFAKE admin granted super_admin…`。`app.log` 與 `docker logs` 沒有任何一行以偽造內容開頭。`api_logs` 該筆 `user_name` 空白，沒有換行。190 開著 GELF over TCP 轉送，但目標 `192.168.50.150:5140` 拒絕連線，所以**收端沒驗到**。轉送前的跳脫在出貨線 `jedi_api_log/forwarding/common/forwarder.py` 有 `_CONTROL_ESCAPES`。 |
| 95 | CM-2064 | 儀表板回應夾帶鹽值與超級管理員旗標 | ❌ | 用 admin 叫一次 AI 儀表板「列出所有使用者的全部欄位」：回應 244KB、200，裡面 `salt`／`is_super_admin`／`password` 都是 0 次，**回應端修好了**。但同一個請求讓 `docker logs guidant-api` 多了一行 `[user_service.get_users:98] Get users: [UserEntity(...)]`，六個帳號的 `password=`、`salt=`、`is_super_admin=` 都是原值。該行只在容器標準輸出，`app.log` 與 `system_logs` 各 0 筆。送給外部 AI 的樣本本卡看不到（服務沒記送出內容），**沒驗**。 |
| 96 | — 自擬 | 寄信失敗把整組郵件設定含密碼寫進紀錄 | ✅ | admin 對 `/mail/test` 送一組指向打不通主機的設定，密碼帶標記。等 60 秒逾時後回 500。紀錄有兩行帶標記：`smtp_mail_adapter` 那行只有主機、埠、帳號；錯誤處理器的 `5xx context` 那行 body 裡 `secret` 是 `***`。三處（docker logs、app.log、`system_logs`）找不到密碼。附帶發現：打不通的主機要等 60 秒，最後回 500，不是明確的「寄信失敗」訊息。 |
| 103 | — 自擬 | 寫 DB 日誌失敗會連帶弄壞請求 | ✅（讀碼） | 出貨線 jedi-common `logger/db_log/db_handler.py` 的 `emit` 整支包 `try/except`，失敗走 `self.handleError(record)`。打折：190 上沒辦法在不改環境的情況下讓 DB 寫入失敗，所以只讀碼。 |
| 133 | 卡號對不上 | 竄改回報送出主機紀錄最後 50 行 | ⚠️ 打折（讀碼） | 出貨線 `main.py:131` 把 `infra/support/diag_masking.mask_log_lines` 注入給竄改偵測；`jedi_integrity` 送出前經它遮蔽。190 的 `integrity_tamper_events` 0 筆，沒發生過竄改，也不能為了驗而竄改。表列卡號 CM-2180 講的是刪框架版本，跟本條無關。 |
| 185 | CM-2171 | 操作日誌來源 IP 全記成前端代理 | ✅ | 從本機經 VPN 打 API：`api_logs.source_ip` 記 `10.8.0.6`，就是本機的 VPN 位址，不是 172.x 容器位址。再帶 `X-Forwarded-For: 1.2.3.4` 打一次：仍記 `10.8.0.6`，偽造標頭沒被當真。 |

## #125 細節

190 的 api 容器 `ENV=PRD`、`RUN_ENV=prod`、`DEBUG=false`，沒設 `LOG_LEVEL`，所以走 jedi-common `config_prod.py`。

| 量 | 數字 |
|---|---|
| 開機後一小時 `docker logs` 行數 | 7,191 |
| 其中 `sqlalchemy.orm.*` INFO | 約 6,050 |
| 穩態十分鐘行數 | 367 |
| `app.log` 今天 | 5.3MB |
| 昨天整天 `app.log` | 7.0MB |

- **大宗是 ORM 對映紀錄**：`sqlalchemy.orm.mapper.Mapper`、`relationships`、`strategies.LazyLoader` 在開機那一分鐘倒出約 6,000 行，全是 INFO。`config_prod.py` 裡 `sqlalchemy.orm` 設的是 `INFO`，註解說「調回 INFO」，但 SQLAlchemy 的對映設定紀錄本身就是 INFO 級，這個級別擋不掉。要擋得設 `WARNING`。
- **穩態主要是背景工作**：`drive_sync_worker` 每 5 秒一行「system context entered」。
- **`app` handler 級別寫死 `DEBUG`**：`config_prod.py` 的 `handlers.app.level` 是 `DEBUG`，目前靠 logger 的 `LOG_LEVEL` 擋著所以沒冒出 DEBUG 行（`app.log` 內 DEBUG 0 行）。
- 本條原文說「三處已改一處，還剩兩處」，SUMMARY 標已修。實況是 pymongo 那處改了，`sqlalchemy.orm` 那處改成 INFO 仍不夠，是否算「還剩」的那兩處之一，要對原報告 M04-9 確認。

## 表列卡號對不上的兩條

驗證清單把 #125 對到 CM-2178、#133 對到 CM-2180。兩張卡的標題分別是「合規範本七個寫入入口補權限」與「刪框架版本前改查資源庫」，都跟本條無關，應該是「總表第 N 項」與「SUMMARY 第 N 條」兩套編號撞號。本卡這兩條照白話自擬測法。

## 範圍外發現（不修，記下）

- **OTP 驗證碼原文留在操作日誌**：`api_logs` 的 `/otp-verify` 那筆 `request` 欄記的是完整六位數驗證碼，沒遮。驗證碼一次性、用過即失效，風險低於密碼；但它和帳號 uid 同一筆，在有效期內看得到操作日誌的人理論上可以搶先用。遮罩名單可考慮加 `code`。

- **`/mail/test` 打不通時回 500**：對方主機不回應時，要等整整 60 秒才回 `COMMON_500001`，使用者看不到「郵件伺服器連不上」。SMTP 轉接器對 `TimeoutError` 回 `False`，但例外還是往上冒到錯誤處理器。
- **jedi-iam 登入成功會印整份 capability 清單**：`capability_service.get_capabilities:55` 每次把所有能力點的完整物件寫成一行 INFO，一次約數 KB。屬 #125 的紀錄量問題。
- **jedi-iam harness 有寫死的 smoke 密碼**：`jedi-iam/harness/dev_app.py:330`。註解寫明「harness 本機專用，非任何環境憑證」，值跟 DEV 測試帳號不同。不是殘留憑證，記下避免之後掃描誤判。

## 附錄 A：主線（HEAD＝feature/review）仍在的殘留

出貨線全部是 0。以下是等 `fix/security-b1` 合回才會消失的：

| 項目 | 條號 | HEAD 檔數 | 位置 |
|---|---|---|---|
| Google 雲端硬碟 client secret | #74 | 12 | `docs/conversation-history/` |
| Google 雲端硬碟 token 加密金鑰 | #74 | 10 | `docs/conversation-history/` |
| Google 雲端硬碟 client id | #74 | 14 | `docs/conversation-history/` |
| Google AI 金鑰 | #11／#74 | 9 | `docs/conversation-history/` |
| GitLab 通行證（已撤銷） | #10 | 2 | `docs/conversation-history/` |
| DEV JWT 簽章金鑰 | #122 | 30 | `docs/conversation-history/` |
| 套件倉庫／檔案儲存帳密 | #73 | 13 | FR-039 交接文件的 md、html、文件站鏡像、`search_index.json`，另有 8 份對話紀錄 |

`main` 上 Google 四項的檔數與 HEAD 相同。

## 附錄 B：出貨線上被形狀掃描抓到、判定不是憑證的

| 檔:行 | 形狀 | 判定 |
|---|---|---|
| BE `scripts/build/build_image.sh:159`、`:164` | 私鑰標頭 | 註解，說明格式 |
| BE `test/test_detection_secret_params.py:194`、`test/test_diag_log_line_masking.py:45`、`:56` | 私鑰標頭 | 測試假值 |
| BE `infra/support/diag_masking.py:137`、`:292` | DB 連線字串 | 註解，說明遮罩規則 |
| BE `docs/conversation-history/` 下 28 行 | DB 連線字串 | 全部是 `{creds[...]}`／`{secret[...]}` 變數組字串 |
| BE `docs/features/FR-120-*/scan-U10.md`、`scan-U11.md` | DB 連線字串 | 文件用 `**` 表示遮罩 |
| agent `test/test_*_connector.py` 四處 | 私鑰標頭 | 內容是 `fake`／`fakekeydata` |
| 套件 `jedi-*/harness/dev_app.py` 十處 | DB 連線字串 | 本機 harness 專用庫，連 `localhost:55xx`／`54xx` |
| 套件 `jedi-evidence-classification` 三個測試檔 | `sk-ant-…` | 測試用假金鑰，配 `t0ken-for-tests-1234` |
| FE `JobExecutionDrawer.vue:1059`、agent `install.sh:2066`、套件多處 `_NOT_MET_TOKEN` 等 | `TOKEN = "…"` | 值是識別字，不是密鑰 |
| BE、FE、套件都有 `TURNSTILE_SECRET_KEY` 同值 | `.env` 比對命中 | Cloudflare 公開測試金鑰，CM-2048 已判定不清 |
