# C4b 掃描報告 — 稽核結果 Excel 匯入（CM-2104）

> 範圍：19 檔／1,585 行，跨兩個 repo。套件側 `jedi-compliance-audit` 18 檔／1,469 行，包括 Excel 匯入服務、Excel 解析器與註冊表、框架對照表、評估目標對齊、判定彙總、佐證配對、兩支與 C4a 共用的接縫檔，以及解析工作單的資料層六支（含工作流程與控制項的對照查詢）；主專案側 1 檔／116 行（`api/project/routes/ar_import_route.py`）。
> 掃描工具：Claude Code 官方 `claude-security` plugin，effort low。**兩側各跑一次**，因為工具只認得它所在那個 repo 的檔案。
> 掃描基準：套件側 `eafc7ae511d5`（monorepo 主 checkout，`feature/review`）；主專案側 `c0673705026e`（`feature/review`）。兩邊工作區都有其他 session 還沒 commit 的改動。本機 BE 載入的是修正分支 worktree 裡的套件；本棒三支核心檔（服務、解析器、對照查詢）我逐一比對過，內容與主 checkout 一致。
> 驗證章：兩次都是 **verified**。只掃不修，兩個 repo 的程式碼都沒改。

---

## 1. 一句話結論

**權限檢查跟 C4a 一樣齊全；卡片最擔心的「佐證撈到別的輪次或別家客戶」，追下去三條都不成立。真正的問題還是出在「上傳的 Excel 怎麼被打開」：打開的方式比 Word 還危險，一份不到 5KB 的檔就能吃掉好幾百 MB 記憶體。**

- **工具報的兩個問題**（兩側共 9 票全數投出、全部 3:0）：
  - **Excel 列數沒有上限（中，面板把握度「高」）**：解析器一路讀到「最後一列」，而最後一列在第幾列是上傳者自己決定的。本機實測：**一份 4.9KB 的檔，只在第 20 萬列放一格，就耗時 10.9 秒、吃掉 355MB**。Excel 最多到第 104 萬列，照比例算約 1.9GB、將近一分鐘。
  - **壓縮炸彈（套件側評中、主專案側評低）**：跟 C4a、總表第 124 項同一個病。套件側與主專案側各自掃到。
- **我自己查到、工具沒報的兩條**（未經投票）：
  - **確認匯入時，前端送來的佐證資料照單全收**（第 6.1 節）：檔案編號、連結網址都原樣存進觀察紀錄，沒有核對「是不是這一輪的佐證」。之後有人點開那筆佐證時，連結會直接開、檔案會直接預覽。這不是新的洞，而是一條**把總表第 34 項（任意檔案編號都能下載）變成「讓別人替你點開」的路**，並且多了一個「用連結釣同事」的小口子。
  - **解析工作單表的資料庫隔離規則寫錯**：跟 C4a-3 是同一條，這裡不重報，只補一句「Excel 那張也中」。

卡片點名要追的三件逐一查過，**全部排除**，見第 5 節。

---

## 2. 這一棒在檢查什麼

「稽核結果 Excel 匯入」讓稽核員把外部做好的稽核紀錄表（亞航 CMMC Level 1 格式）上傳進系統。系統會自動讀出每個控制項的判定（符合／不符合）、查核方法、引用的佐證，替每個控制項建觀察紀錄、下判定，不符合的再自動開一筆風險。

流程跟 C4a 一樣分四步：**上傳＋解析 → 預覽 → 捨棄 → 確認匯入**。這一棒比 C4a 多了一件事：**自動配對佐證**。系統會拿 Excel 裡寫的佐證檔名，去比對這個控制項底下已經上傳過的檔案，自動勾選看起來相符的，讓稽核員不用一個個挑。

所以除了 C4a 那三個問題，這一棒還要回答：**自動配對佐證時撈的範圍，有沒有可能撈到別的輪次、別的專案、別家客戶的檔案？**

---

## 3. 掃到什麼：總覽

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度 | 誰掃到的 | 跟總表的關係 |
|---|---|---|---|---|---|---|---|
| C4b-1 | 解析器讀到「最後一列」為止，最後一列由上傳者決定 | 幾 KB 的檔吃掉 GB 級記憶體、卡住一分鐘；幾份同時送，服務程序被砍，所有客戶一起斷線 | 某個專案的稽核員或管理者，而且那一輪要在「稽核中」 | **套件側** `ar_report_parser/airasia_cmmc_l1_ar_v1.py:159-163`（`_parse_rows` 起點與迴圈條件） | **中** | 套件側（面板把握度高），**本機實測** | **新**，Excel 解析特有，C4a 沒有 |
| C4b-2 | 上傳的 Excel 只量壓縮後大小，打開時整份載進記憶體（壓縮炸彈） | 同上 | 同上 | **套件側** `ar_report_parser/registry.py:16-17`（打開方式），以及 `import_adapter/registry_base.py:23-24`（與 C4a 共用、該加檢查的地方） | **中**（套件側）／**低**（主專案側） | 套件側、主專案側**各自掃到** | **新入口**，與第 124／127 項、C4a-1 同病同修法 |
| C4b-3 | 確認匯入時，佐證資料照前端送來的存，沒核對歸屬 | 稽核員可以在觀察紀錄裡放任意檔案編號或任意連結；同專案的人點開時，檔案直接預覽、連結直接開 | 同上 | **套件側** `ar_import_app_service.py:225`（`confirm_import` 起點），以及 `assessment_result_app_service.py:552`（`create_observation`，一般新增觀察也走這裡） | **低** | **runner 自行核對，未經投票** | 新；影響面依附在第 34 項上 |

---

## 4. 工具報的兩條（經三人面板投票）

### 4.1 C4b-1 一份不到 5KB 的 Excel，就能吃掉 GB 級記憶體

**現況**：已修（M12-3，FR-114 CM-2181，commit BE `48638cbdb`／套件 `2cb9d2cd`，1.21.0 出貨）

**場景**

稽核員在稽核輪次頁按「匯入稽核紀錄」，選了一份自己做的 .xlsx。表面上它就是一張正常的亞航稽核表：工作表名稱是「檢查紀錄」，第 5 列有「索引號」「稽核結果」這些欄名，騙得過格式檢查。但他在第 1,048,576 列（Excel 的最後一列）的 A 欄隨便填了一個字。整份檔只有幾 KB，遠低於 10MB 上限。

系統收到檔案後，從第 6 列開始一列一列往下讀，每列讀 8 格，一直讀到「最後一列」。對系統來說，最後一列就是第 104 萬列。中間一百萬列全是空的，但每讀一格空格，openpyxl 這套程式庫都會在記憶體裡新建一個儲存格物件。算下來大約 840 萬個物件、將近 2GB 記憶體，還要花將近一分鐘。這段時間裡，資料庫交易一直開著。幾份同時送，服務程序被作業系統砍掉，所有客戶一起斷線。

**本機實測**（只在本機跑解析器那段迴圈，沒有打任何網址）：

| 檔案大小 | 最後一列 | 耗時 | 記憶體高峰 |
|---|---|---|---|
| 4,914 bytes | 200,000 | 10.9 秒 | 355 MB |
| （推算） | 1,048,576 | 約 57 秒 | 約 1.9 GB |

記憶體與耗時都跟列數成正比，所以推算是直接等比放大。服務逾時設定是 120 秒（`main.py:267`），一份就能卡住一個服務程序將近一分鐘。

**為什麼會這樣**

- 套件側 `airasia_cmmc_l1_ar_v1.py:162` 取 `max_row = ws.max_row`，`:163` 開始 `while r <= max_row`，`:164` 每列呼叫 8 次 `ws.cell(...)`。openpyxl 的 `max_row` 是「檔案裡任何一格出現過的最大列號」，上傳者要多大就有多大。`ws.cell()` 讀到空格時會新建並保存一個物件。
- 讀檔用的是一般模式（`registry.py:17` 的 `load_workbook(file_path, data_only=True)`），不是逐列串流的唯讀模式。
- 空白列只會被跳過（`:165-167`），不會讓迴圈提早結束；也沒有「最多讀幾列」的上限。

**怎麼修**

改用唯讀模式打開，用 `ws.iter_rows(min_row=6, max_col=8, values_only=True)` 逐列讀。這個寫法遇到空格不會建物件。再加上「最多讀幾列」的硬上限：真實的亞航表只有一百多列，上限設幾千就很寬鬆了。另外，「連續 N 列全空就停」也順手加上。

**嚴重度為什麼是中**：要先是某個專案的稽核員或管理者，而且那一輪要在「稽核中」階段；但一旦條件成立，打的是所有客戶共用的服務，而且成本極低（幾 KB 的檔，不用做壓縮炸彈）。

### 4.2 C4b-2 壓縮炸彈：Excel 整份被載進記憶體

**現況**：已修（M12-2，FR-114 CM-2181，同上）

**場景**

跟 C4a-1 一樣，只是換成 Excel。上傳一份不到 10MB 的 .xlsx，裡面放工作表內容的那一段解開後有幾 GB，塞滿重複的儲存格。系統打開檔案時，會把每一格都建成物件放進記憶體，還沒輪到格式檢查，服務程序就先被砍了。

**為什麼會這樣**

- 唯一的大小檢查在套件側 `ar_import_app_service.py:44`（`_XLSX_MAX_SIZE = 10MB`）與 `:78-80`，量的是**壓縮後**的檔案大小。
- 打開檔案在套件側 `ar_report_parser/registry.py:17`（一般模式，不是唯讀模式），經由與 C4a 共用的 `import_adapter/registry_base.py:24` 呼叫。
- 主專案側面板補查了一件事：openpyxl 預設會用 defusedxml 讀 XML，所以**不會有外部實體注入（XXE）的問題**，這條是單純的記憶體耗盡。

**兩側評等不同的原因**：套件側面板評中，主專案側評低。主專案側的研究員看的是「路由把檔案原樣交下去」這一段，面板在評影響時比較保守。兩邊都 3:0 確認問題存在。建議以套件側的中為準，因為病灶就在套件。

**怎麼修**：跟 C4a-1、總表第 124／127 項同一個修法：打開之前先用壓縮檔工具檢查解開後的大小與膨脹比。**放在共用的 `RegistryBase.parse_file` 裡，C4a 的 Word 與這裡的 Excel 可以一次補好。**再配合 4.1 的唯讀模式，一起降低記憶體用量。

---

## 5. 卡片點名的問題，逐條回答（runner 自行開檔核對，未經三人面板投票）

### 5.1 🔴 `get_wf_ids_by_control_ids` 的輪次編號可以不帶：**呼叫端到不了「不帶」那條路，不成立**

**背景**：這支查詢（套件側 `wf_control_mapping_lookup_query.py:30-53`）用控制項代號（例如 `AC.L1-b.1.i`，所有客戶都一樣的字串）查「哪些工作流程屬於這個控制項」。那張對照表 `public.workflow_execution_control_mapping` **沒有客戶欄位、也沒開資料庫隔離**（DEV 實查 18:15），欄位只有 `workflow_execution_id`、`version_id`、`control_id`、`ao_part_id`、`round_id`。不帶輪次編號時，查詢條件就只剩控制項代號，會撈到全系統所有客戶、所有輪次的對照。

**追呼叫鏈**：

1. 整個系統只有一個呼叫端：`ar_import_app_service.py:430`，由 `_get_allowed_wf_ids_by_control` 呼叫（我 grep 過兩個 repo 與主專案的 DI 組裝檔）。
2. `_get_allowed_wf_ids_by_control` 只被 `_build_evidence_pool`（`:364`）呼叫；`_build_evidence_pool` 只被 `upload_and_parse`（`:175-177`）呼叫。
3. 輪次編號是 `getattr(round_entity, "id", None)`，其中 `round_entity` 是 `:74` 的 `resolve_round_for_import(round_uid, user_context.id)` 回傳值。
4. 那支守門（`assessment_result_app_service.py:131-142`）第一行 `_require_round` 找不到輪次就直接丟 404，找到才回傳。所以 `round_entity` **不可能是空值**，`id` 是資料庫主鍵，也不會是空值。

**結論**：這條路徑走不到「不帶輪次」的分支。將來如果有人新增呼叫端卻不帶輪次，就會出事，建議把參數改成必填（見第 10 節）。

**補充**：就算真的撈到別家客戶的對照，後果也只是「過濾範圍變寬」，不是「多給佐證」。因為對照結果只被拿來**刪掉**候選佐證（`:385-389`），候選佐證本身是從這份計畫自己的控制項樹走訪出來的（見 5.2）。

### 5.2 兩處「查詢失敗就回空、不擋解析」：**失敗時是少給或維持原範圍，不會多給，不成立**

- **`_build_evidence_pool`（`:341`）**：建控制項樹失敗（`:356-360`）就回空池，也就是「沒有任何佐證可配對」，預覽照常，佐證要稽核員自己挑。**少給，不是多給。**
- **`_get_allowed_wf_ids_by_control`（`:424`）**：查對照失敗（`:429-433`）也回空，這時 `:385` 的 `if control_allowed_wfs:` 不成立，**不過濾**。這個「不過濾」看起來像多給，但我追了候選佐證的來源：
  - 候選是從 `build_control_tree_by_ssp_id(ssp_id)`（`:357`）走訪出來的。這裡的 `ssp_id` 是這一輪的凍結快照計畫（`round_entity.ssp_id`，`:176`），伺服器自己解析、使用者塞不進來。
  - 所以候選池本來就只包含**這份計畫底下、這個控制項的任務佐證**。對照查詢只是二次過濾，用來排除「同一份檔案也掛在別的控制項」造成的誤配（檔頭註解寫的 2026-07 STG 案例）。
  - 失敗時退回的是「同一份計畫內可能有跨控制項的誤配」，**不會跨計畫、跨客戶**。

另外，對照表裡沒有對應資料的控制項同樣不過濾（例如 `round_id` 為空的舊資料，查詢註解寫明「寧缺勿錯」）。性質同上：只影響同一份計畫內的配對準確度，不是資安問題。

### 5.3 `is_admin` 放行的是誰：**同 C4a 第 5.1 節，帳號層超級管理員旗標，不成立**

`_require_job`（套件側 `ar_import_app_service.py:569-576`）跟 C4a 是逐字相同的寫法，結論也相同，這裡不重複。

### 5.4 四步是不是都有兩道檢查：**都有，而且比 C4a 更嚴**

守門 10 行，對上公開方法 4 支。

| 步驟 | 公開方法（套件側起點） | 「是你們公司的」 | 「你在專案裡的角色」 | 階段限制 |
|---|---|---|---|---|
| 上傳＋解析 | `upload_and_parse`（`:68`） | 工作單由伺服器建，公司取自登入身分 | `resolve_round_for_import`：稽核員或管理者 | 只有「稽核中」，而且要已開始稽核 |
| 預覽 | `get_parse_result`（`:186`） | `_require_job` | 同上：稽核員或管理者 | 同上 |
| 捨棄 | `discard_parse`（`:217`） | `_require_job` | 同上：稽核員或管理者 | 同上 |
| 確認匯入 | `confirm_import`（`:225`） | `_require_job` | `resolve_round_for_import`，底下每個寫入方法再各查一次 | 同上 |

**跟 C4a 的差別**：Excel 這邊的預覽與捨棄也要求是稽核員或管理者（C4a 只要求是專案成員）。C4a 第 5.2 節提到的「檢視者也能丟掉工作單」，在這裡不存在。

輪次歸屬同樣由伺服器決定：上傳時把網址上的 `round_uid` 寫進工作單的 `source_uid`（`:82-89`），之後每一步都用 `job.source_uid` 重查角色。

主專案側四條路由都掛了「要登入」＋「客戶有買稽核模組」，並把 `get_user_context()` 傳進服務，接縫沒有漏接（主專案側面板也確認了）。

### 5.5 「檢查甲、動乙」：**判定與觀察都限定在「這一輪」底下找，不成立**

`confirm_import` 會用到三種編號，我逐一追過：

- **判定編號**：不是從前端拿的。`:262-267` 由伺服器依「這一輪的判定清單」建控制項對判定的對照，前端只送控制項代號。代號在這一輪找不到就跳過（`:286-289`）。
- **觀察編號**（綁到判定上）：是這次剛建立的觀察，而 `judge_finding` 綁定前會用 `_resolve_observation(r.ar_result_id, ...)`（`assessment_result_app_service.py:486-490`）在**這一輪**的觀察裡找，找不到就 404。
- **重匯時要刪掉的舊觀察**：來自這一輪先前完成的工作單（`get_completed_by_source(round_uid)`，repo 側 `ar_xlsx_parse_job_repo_impl.py:56-65`），刪除時同樣用 `_resolve_observation` 限定在這一輪。

唯一沒核對歸屬的是佐證資料，見第 6.1 節。

### 5.6 Excel 公式注入：**本棒範圍內不成立**

讀檔時用了 `data_only=True`（`registry.py:17`），公式儲存格讀到的是 Excel 上次存檔時算好的值，不會執行公式。寫出方向，Excel 裡的文字最後寫進觀察說明與風險說明；我在兩個 repo 找過，稽核結果與改善計畫都沒有匯出 Excel 或 CSV 的功能（有 openpyxl／csv 的服務都跟稽核結果無關）。這些文字如果之後流進別的匯出，屬於那一棒的範圍。

---

## 6. 本棒另外查到的（runner 自行開檔核對，未經投票）

### 6.1 C4b-3 確認匯入時，佐證資料照單全收

**現況**：已修（M12-9，FR-114 CM-2174，commit BE `a8f1a24b7`／套件 `b7022b8b`，1.21.0 出貨）

**場景**

某專案的稽核員 A 匯入一份稽核紀錄。預覽畫面上，系統自動替每筆觀察勾好佐證。A 按「確認匯入」前，把送出的資料改掉（用瀏覽器開發者工具，或直接打網址），在某筆觀察的佐證裡塞兩樣東西：

1. **一個不屬於這一輪的檔案編號**：例如他從別處（總表第 49 項那種讀取漏洞）拿到的某份檔案編號。
2. **一個自己的網址**：看起來像「稽核證據連結」，實際是釣魚頁。

伺服器照存。之後同專案的管理者 B 打開「稽核判定」頁或「改善計畫」頁，看到這筆觀察，點了佐證：

- 點到檔案：前端直接拿那個編號去預覽（`EvidencePreviewDialog.vue:98-102` → `/file/pdf-preview/<編號>`），B 看到的是 A 指定的檔案。
- 點到連結：前端直接開新分頁打開 A 的網址（`EvidencePreviewDialog.vue:104-105`，有 `noopener`）。

**為什麼會這樣**

- 前端送上來的 `controls[].observations[].relevant_evidence`（主專案 `api/project/serializers/ar_import.py:38` 是 `fields.List(fields.Dict())`，內容不驗）在套件側 `ar_import_app_service.py:301` 被原樣傳給 `create_observation`。
- `create_observation`（套件側 `assessment_result_app_service.py:552-580`）直接存進觀察紀錄的 `relevant_evidence`，不核對裡面的檔案編號是不是這份計畫的佐證、連結是不是合法網址。
- 預覽時伺服器算好的候選池（`parsed_result`）存在工作單裡，但確認時**沒拿來比對**。

**影響到哪裡、為什麼只評低**

- **不是新的洞**：一般的「新增／修改觀察」網址（主專案 `api/project/routes/audit_round_route.py:286-311`）也收 `relevant_evidence` 且不驗，所以這不是 Excel 匯入特有的問題。
- **檔案那一半依附在總表第 34 項上**：預覽端點本身只驗登入、不驗歸屬（第 34 項，仍是「部分修」）。A 自己本來就能直接打那支端點拿檔案，塞進觀察紀錄只是讓 B 替他點開。好在 `upload_files` 已開資料庫隔離（DEV 實查 18:28，用的是標準判斷函式），**跨客戶的檔案在 B 那邊一樣打不開**；能被指到的只有同一家客戶的檔案。
- **連結那一半是新的小口子**：稽核系統裡的「佐證連結」天然容易讓人信任，可以拿來釣同事。前端用的是 `window.open`，而不是 `href` 直接渲染，所以 `javascript:` 這類網址在新分頁裡不會在本站的網域下執行。
- 要先是這個專案的稽核員或管理者，而且這一輪在「稽核中」。

**怎麼修**：確認匯入時，把每筆佐證拿去跟工作單裡存的候選池比對，只接受池子裡有的，或是前端手動從「這份計畫的佐證清單」挑出來的。連結欄位只接受 `http`／`https`。一般新增觀察的網址也要同樣處理（屬於 C3 那條線，建議一起修）。

### 6.2 解析工作單表的資料庫隔離規則寫錯（＝C4a-3，不重報）

`oscal.ar_xlsx_parse_jobs` 跟 C4a 那張 Word 工作單表用的是同一條壞規則（`scripts/sql/2026-07-03-ar-xlsx-parse-jobs.sql`，出貨基線 `02-schema.sql` 緊接在 AP 那條之後）。DEV 實測結果已經寫在 C4a 報告第 6.1 節：子公司 152 看得到母公司 102 的 4 筆 Excel 工作單。兩張表應該一起修。

---

## 7. 資料庫隔離現況（DEV 唯讀實查，2026-09-24 18:15／18:28，查完 ROLLBACK）

| 表 | Schema | DEV 隔離開了嗎 | 規則數 | 備註 |
|---|---|---|---|---|
| `ar_xlsx_parse_jobs`（Excel 解析工作單） | `oscal` | 開 | 1 | **規則是壞的**，見 C4a 報告第 6.1 節 |
| `workflow_execution_control_mapping`（工作流程與控制項對照） | `public` | **關** | 0 | 沒有客戶欄位。本棒唯一呼叫端一定帶輪次，現在不會出事（5.1） |
| `job_evidences`（任務佐證） | `compliance` | **關** | 0 | 佐證池由這份計畫的控制項樹走訪，不經全表查詢 |
| `assessment_observations`／`assessment_findings`／`assessment_risks`（確認匯入實際寫入的三張） | `oscal` | **關** | 0 | 已由 C3 登記（總表 927 行），本棒不重報 |
| `upload_files`（上傳檔案） | `public` | 開 | 4 | 用標準判斷函式，正確；讓 6.1 的檔案那一半擋在同一家客戶內 |

---

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

**「工具報的兩條存在嗎」：可信度高。**兩次掃描共 3 個候選，9 票全數投出、全部 3:0 成立，嚴重度都沒有被調降。壓縮炸彈兩側各自掃到，列數無上限那條我另外在本機實測過。

**「第 5、6 節」是 runner 自行開檔核對的，沒有經過三人面板投票。**關鍵事實都實查過：

- 輪次編號不帶的路徑：兩個 repo 加 DI 組裝檔 grep 呼叫端，逐層追到守門。
- 「失敗回空」的後果：追過候選池的來源（`ssp_id` 由伺服器解析）。
- 佐證照單全收：追過前端送出、後端存入、前端讀回點開三段。
- 資料庫隔離：DEV 唯讀實查（18:15、18:28）。

**「只有這些嗎」不保證。**

1. 用的是最快的檔位（effort low），只跑研究員一輪加投票一輪，沒有威脅建模和廣度掃描。
2. 兩側研究員彼此看不到對方。主專案四條路由與套件四支服務方法之間的接縫、套件與 `AssessmentResultAppService` 六支寫入方法之間的接縫，都是我手動核對的。
3. 列數無上限只在本機單獨跑解析器那段，壓縮炸彈沒有實際做檔去打。全程沒有打任何網址。

---

## 9. 執行概況（數字，給工程師看）

| 項目 | 套件側 | 主專案側 |
|---|---|---|
| 掃描範圍 | 18 檔／1,469 行 | 1 檔／116 行 |
| 基準 commit | `eafc7ae511d5`（dirty） | `c0673705026e`（dirty） |
| 檔位 | effort low | effort low，focus 生產程式碼 |
| 研究員 | 派 1 支，回 1 支 | 派 2 支（含密鑰專項），回 2 支 |
| 候選 | 2 條，去重後 2 條 | 1 條 |
| 投票 | 6 票全數投出 | 3 票全數投出 |
| 票型 | F1 3:0 中（列數無上限，把握度高）；F2 3:0 中（壓縮炸彈） | F1 3:0 低（壓縮炸彈） |
| 驗證章 | **verified**（`CLAUDE-SECURITY-REVISION-eafc7ae511d5-dirty.json`） | **verified**（`CLAUDE-SECURITY-REVISION-c0673705026e-dirty.json`） |
| 工具 run ID | `wf_153effec-460` | `wf_c7e16145-981` |
| 耗時 | 約 18 分鐘（7 個 agent，零失敗） | 約 9 分鐘（5 個 agent，零失敗；1 個回空結果，不影響候選數） |
| 工具原始報告 | 套件 repo `CLAUDE-SECURITY-20260924-100504/`（未入版控） | BE repo `CLAUDE-SECURITY-20260924-102701/`（未入版控） |

工具報告的 F 編號對到本報告：套件側 F1＝第 4.1 節，套件側 F2 與主專案側 F1＝第 4.2 節。密鑰專項沒有撿到任何東西。

---

## 10. 待首腦裁決

1. **C4b-1（列數無上限）要不要單獨登記**：建議要。這是 Excel 解析特有的問題，而且成本比壓縮炸彈還低（不用做特殊檔案，幾 KB 就夠）。修法（唯讀模式、`iter_rows`、列數上限）可以跟 C4b-2 開同一張卡。
2. **C4b-2 壓縮炸彈併進第 124／127 項那張卡**：與 C4a 報告第 10 節第 1 項一起裁。建議卡上寫明「四個入口、兩個 repo」：SSP Word 與 SSP Excel 在主專案，稽核計畫 Word 與稽核結果 Excel 在套件的 `RegistryBase`。
3. **C4b-3（佐證照單全收）要不要登記**：建議登記為低，並寫明依附第 34 項。檔案那一半要等第 34 項補上歸屬檢查才真正關得起來；連結那一半只收 `http`／`https` 就夠了。一般新增觀察的網址是同一個病，建議一起修。
4. **（可選）`get_wf_ids_by_control_ids` 的 `round_id` 改成必填**：現在不會出事（5.1），但那張表沒有客戶欄位、也沒開隔離。將來新增呼叫端時忘了帶，就會變成全系統查詢。改成必填的成本是一行。
5. **兩張解析工作單表的隔離規則**：與 C4a 報告第 10 節第 3 項合併裁，不另開。
