FR-060 檢測基準內容管理 — 全案收尾 SUMMARY

FR-060 檢測基準內容管理 — 全案收尾 SUMMARY

收尾日期:2026-08-03(決策者手測驗收通過後) 2026-08-04 更正:CM-1072(url 型支援內容抽取)經決策者裁定納入本 arc,§2.4/§3/§5/§6/§9 已依實查更新——原先「url 型無法抽取、failed 是設計內終態」的敘述全數作廢。 本檔由 fr060-LOG.md 五個 block + 實查濃縮產生,不憑記憶重建。 現況請看同資料夾 fr060-STATE.md(living);決策軌跡看 fr060-LOG.md(append-only)。


1. 為什麼做這件事

決策者的原話:

「目前只有項目,user 完全不知道這個東西會驗證什麼,我怎麼知道我要選什麼?哪些基準可以參考,我可以先請負責人去判斷、提前先處理,而不是等報告出來才去處理。」

FR-059 做完了「掃描設定檔管理」,但平台把 profile 當成不透明的二進位檔——使用者在管理頁與任務下拉看到的,從頭到尾只有一個名字。

支撐急迫性的實測數字TWGCB-01-014 Ubuntu 22.04 LTS234 條控制項,其中 199 條是人工待判項impact 0.0,掃描時直接 skip),實際自動檢查只有 35 條、覆蓋率 15%。使用者選了、掃了、等了一輪,拿到一份 85% 是 skip 的報告才發現這件事——等於白等一輪掃描。

同樣的形態在另外兩支也成立(收尾日實查):

基準 總控制項 人工待判 自動檢查 自動覆蓋率
TWGCB-01-014 Ubuntu 22.04 LTS v1.2 234 199 35 15%
TWGCB-01-007 Windows Server 2016 v1.3 699 159 540 77%
TWGCB-Windows-2025 708 150 558 79%

差距從 15% 到 79%——這正是「選之前必須看得到」的理由:同樣叫「TWGCB 基準」,選錯一支等於整輪掃描交白卷。

為什麼一個瀏覽功能要順帶拆資料表:現行單表用 (detection_tool_id, tenant_id, name) 當唯一索引——拿 name 當身分,這正是「改名改不動、唯一改名途徑是 fork 複製」的根因;而分類三軸(標的類型/標的產品/基準體系)是「這支 profile 是什麼」的屬性,不是「這一版是什麼」的屬性。當時是最便宜的時機:DEV 僅 13 筆、FR-059 上線才兩天、沒有租戶真的在用。


2. 行為差異(使用者看得到什麼不一樣)

2.1 選基準之前,終於看得到內容

情境 之前 之後
管理頁看一支基準 只有名稱、工具、版本號、上傳時間 點進去看到完整控制項清單(編號/標題/說明/嚴重度/是否人工待判),可搜尋、可篩選
判斷「這支會驗什麼」 無從判斷,只能掃了看報告 摘要區直接給總數/自動檢查數/人工待判數,選之前就知道覆蓋率
人工待判項 完全不可見,混在報告的 skip 裡 清單獨立標示,負責人可提前分派人工判定,不必等報告出來
剛上傳的基準 上傳/版更後自動排入解析,管理頁顯示解析中並輪詢,完成後直接可瀏覽
外部網址型基準 一樣看得到內容(CM-1072):抽取時由 BE 代為下載(含 SSRF 防護),拿到 bytes 後走與壓縮檔完全相同的解析路徑
解析失敗 顯示明確原因並提供手動重抽

2.2 基準終於可以改名與分類

情境 之前 之後
改一支基準的名稱 改不動(name 是唯一索引的一部分),唯一途徑是 fork 複製出一支新的 直接編輯,版本歷程與既有派工引用全部保留
改描述 同上 直接編輯
找一支基準 一長串名字平鋪,靠肉眼掃 三軸分類(標的類型/標的產品/基準體系),列表分成三欄可各自排序
任務下拉選基準 一整條平鋪清單 依標的類型分組顯示
分類選項要增修 存 DB 由 root 維護(不是 FE 寫死),有刪除保護:已被引用的分類不可刪

2.3 內部行為修正(使用者不直接看到,但影響正確性)

  • 停用的基準不再派得出去:拆表前 is_active 守門在拆表後會靜默失效(None is False 為假),已停用的 profile 照樣派得出去——這是無聲的授權漏洞,不是 500。已修並補迴歸測試。
  • 通知信不再退化成 UUID:派工通知的「掃描設定」欄位原本在拆表後會靜默變成 profile:<uuid>,且無任何測試會抓到。已修並補測試。

2.4 url 型基準的內容抽取(CM-1072,延伸納入)

url 型原本一律落 failed,理由是 FR-059 的 P5「平台完全不經手檔案」。實測推翻了「做不到」的假設——CINC 原生就吃 URL(cinc-auditor json <tarball 網址> 解得出 59 條),那道限制是產品決策的範圍問題而非技術限制:P5 涵蓋的是「掃描路徑不代管私有 repo 憑證」,而抽取是 FR-060 才有的另一個面向,dev-sec 那兩條是完全公開的 URL。

於是抽取時由 BE 代為下載,拿到 bytes 後完全共用 file 型的既有路徑

🔴 三條不變式(改這塊之前必須先讀懂)

  1. 掃描路徑完全不動——agent 仍原樣把 URL 交給 cinc-auditor exec_load_profile() / build_payload() 未被觸及。掃描抓的永遠是 URL 上的即時內容,不是平台抽取的快照
  2. 下載內容不落儲存——不寫 upload_filessource_type 維持 urlfile_id / sha256 維持 NULL。存成 file 型快照會破壞雙軌語意,且掃描(agent 拉 URL)與抽取(平台存快照)從此可能對不上,對不上時沒人知道該信哪個
  3. 下載必須在 session_scope() 之外——網路 IO 最多等 60 秒,_prepare() 對 url 型只回「待下載的網址」,實際下載由 _run_worker() 在兩段 session 之間執行

⚠️ url 型抽出來的是某一刻的快照master branch 會漂),沒有 sha256 當信任根——FE 已在 ControlExtractionSummary.vue 明示給使用者。偏離問題見 §9 follow-up 第 4 項。


3. commits 清單

BE(feature/e2e-env-build,共 11 支,🔴 全數未 push

commit 內容
7e3e7b97 設計定案 design.md(D1–D19 全數拍板,1030 行)+ FR 登記表
7124a473 討論稿補 D19 多工具擴充邊界
fce13bcb 索引頁狀態改「設計定案,待實作」+重生反映 design.md
7a7a3b41 建三張子需求卡 CM-1055~1057 + STATE/LOG 雙檔交接
61eeb2a8 建卡收口 + T-1.1 落地後的 STATE/LOG 更新
d7cf9abc CM-1060 T-1.1 資料模型:三表 DDL + RLS 八段 + 13 筆搬遷 + 舊表 rename(662 行 SQL,零 Python)
01d0909b CM-1061 T-1.2+1.3 資料層三組(model/mapper/repo)+ domain/app service + 讀取端 API(38 檔,+2925/−1589)
40fcc38d CM-1062 T-1.4 派工鏈兩處破口改走 get_version_with_profile() + 迴歸測試(2 檔,+137/−7)
a2f2512b CM-1063 T-2.1+2.2 分類 enum 表 + seed + 13 筆補登 + PUT 編輯端點 + 刪除保護(22 檔,+2028)
ec4e70b7 CM-1065 T-3.2+3.3 抽取器註冊表 + InSpec extractor + 非同步抽取管線(6 檔,+1170/−6)
25045758 CM-1072 url 型基準支援內容抽取(BE 代下載 + SSRF 防護):新增 common/util/safe_http_fetch.py(347 行,五道防護)、改抽取服務(+120)/序列化(+4)/design.md(+28)。零 SQL migration,STG/POC 未動

FE(~/Projects/Billows/Audit-Manager/compliance-manager-fe/,同 branch,🔴 全數未 push

commit 內容
28f061f CM-1063 補 8 個 detection-tools error code i18n 對照(2 檔)
97f9ac8 CM-1064 T-2.3+2.4 管理頁重構(1,239→639 行,拆六支子元件)+ 下拉分組 + fallback 攤平(15 檔,+1912/−868)
bbcb882 CM-1066 T-3.4 控制項詳細頁 + 摘要區 + 抽取狀態輪詢(12 檔,+1136/−18)
07b8a7d 手測後:列表分類欄拆成標的類型/標的產品/基準體系三欄
e4e7967 手測後:控制項展開列的中繼資料標籤中文化
7274d13 手測後:修多列展開時只剩最後一筆有資料

4. 改動範圍

資料模型(三支 migration,只套 DEV

行數 內容
scripts/sql/2026-08-03-fr060-1-detection-profile-split.sql 662 單表拆主檔/從檔+控制項表;RLS 主從各 4 段共 8 段;13 筆資料搬遷;舊表 rename 為 detection_tool_profiles_deprecated_20260803未 DROP
scripts/sql/2026-08-03-fr060-2-detection-profile-taxonomies.sql 272 分類字典表 config.detection_profile_taxonomies(兩軸 CHECK + (axis,key) 唯一 + slug 格式檢查)+ seed + 13 筆既有資料補登分類
scripts/sql/2026-08-03-fr060-3-param-schema-extraction-declaration.sql 91 參數 schema + 抽取宣告欄位

新表:

  • config.detection_profiles(主檔,17 欄,含 target_type / target_product / benchmark_family 三軸)
  • config.detection_profile_versions(從檔,含 extraction_status / extraction_error
  • config.detection_profile_controls(控制項,17 欄,含 severity_raw / severity_norm / is_pending / attributes jsonb)
  • config.detection_profile_taxonomies(分類字典,兩軸)

控制項表刻意不掛 RLS(判準「不會被當獨立查詢入口」,比照 oscal.catalog_controls 等九張明細表慣例)。

BE 程式碼

  • 資料層三組全數改寫:infra/detection_tools/{model,mapper,repository}/ 主從各一套 + 分類字典一套
  • domain 層:entity/query entity/repository interface/domain service 主從分立,新增 get_version_with_profile(uid) 合成 entity
  • app 層:detection_profile_service.py(原 551 行 profile service 改寫)+ detection_profile_taxonomy_service.pydetection_profile_extraction_service.py
  • API:api/detection_tools/routes/detection_profile_route.py 十一個 Resource(列表/menu/建立/明細/版本/版更/fork/跨工具複製/停用/控制項清單/抽取狀態與重抽)
  • 抽取器:common/util/profile_extractor/base.py(95 行,註冊表 + 抽象基底)+ inspec.py(439 行)
  • url 型代下載:common/util/safe_http_fetch.py(347 行,SSRF 五道防護),由 detection_profile_extraction_service.py 在兩段 session 之間呼叫(CM-1072)
  • error code:common/code/detection_tools_error_code.py 新增 10 條
  • 派工鏈:app/detection_tools/service/detection_orchestration_service.py 兩處呼叫點改走 get_version_with_profile()
  • 測試:test/test_detection_profile_version_alloc.py(重寫 +334/−…)、test_detection_profile_dispatch.py(+133)、test_detection_profile_taxonomy.py(340 行,新增)、test_detection_profile_archive.py

FE 程式碼

管理頁從單檔 1,239 行拆成一頁 + 六支子元件:

src/views/detection-profile/
├── DetectionProfileManageView.vue        688 行(原 1,239)
├── DetectionProfileControlsView.vue      632 行(新,控制項詳細頁)
├── profileDisplay.js                      顯示名解析共用
└── components/
    ├── ProfileFormDialog.vue             433
    ├── ControlExtractionSummary.vue      279
    ├── ProfileVersionTable.vue           249
    ├── ProfileDetailDialog.vue           170
    ├── ProfileNewVersionDialog.vue       175
    └── ProfileCopyDialog.vue             149

另動 src/components/detection-tools/DetectionConfigField.vue(下拉分組)、src/config/api/api.js(新增 controls / extraction / taxonomies 端點常數)、i18n error code 對照。


5. DEV 最終狀態(2026-08-04 協調者實查,含 CM-1072 生效後)

主檔               13
版本               13
控制項           2,131
分類字典           13(target_type 8 + benchmark_family 5)
抽取 succeeded      5
抽取 failed         0
抽取 pending        8

⚠️ 本段數字已兩度修正: ① 協調者交付的事實基準寫「控制項 942/已抽取 2 支」,收尾日(08-03)實查為「1,641/succeeded 3/failed 1/pending 9」,差異來自手測期間又觸發了一支抽取(TWGCB-Windows-2025,708 條)。 ② CM-1072(25045758)讓 url 型可抽取後,08-04 實查為 2,131/succeeded 5/failed 0/pending 8——原本那支 failed 的 dev-sec 基準已抽取成功,failed 歸零。 三環境未動的結論兩次都不變。

🔴 原本記在此處的「url 型無法抽取、failed 是設計內終態」已被推翻,正確行為見 §2.4。CM-1072 之前的錯誤訊息「平台不代為下載…請改以上傳壓縮檔的方式建立版本」已不再出現

STG/POC 完全未動config.detection_profile% 新表數皆為 0;CM-1072 零 SQL migration,不改變此結論)。


6. Notion 卡片樹

層級 Case No 狀態
母案 FR-060 CM-1054 In progress
FR-060.1 資料模型重構 CM-1055 子需求卡
FR-060.2 分類體系+編輯 CM-1056 子需求卡
FR-060.3 抽取+瀏覽 CM-1057 子需求卡
T-1.1 拆表 migration CM-1060 修正待驗證
T-1.2+1.3 資料層/服務層 CM-1061 修正待驗證
T-1.4 派工鏈破口 CM-1062 修正待驗證
T-2.1+2.2 分類 BE CM-1063 修正待驗證
T-2.3+2.4 管理頁 FE CM-1064 修正待驗證
T-3.2+3.3 抽取 BE CM-1065 修正待驗證
T-3.4 詳細頁 FE CM-1066 尚未回寫
url 型支援內容抽取(延伸) CM-1072 狀態待標記

子任務卡採乙案共 7 張(CM-1060~1066),與執行 session 一對一——非原規劃的 12 張(理由見 §8)。原 T-3.1(裝 CINC Auditor)未單獨建卡,安裝屬環境動作由決策者決定時機,程式碼與文件部分併入 CM-1065。

🔴 CM-1072 原被劃在 FR-060 之外,2026-08-04 由決策者裁定納入本 arc——它改的就是 FR-060.3 的抽取服務、程式碼已在裡面、DEV 已生效。決策者原話:「那看起來 1072 必須納入 fr-60 對吧,算是延伸題目…既然要把功能做完整」。


7. 規範文件清單

文件 狀態
design.md(1030 行,D1–D19) ✅ 已 commit(7e3e7b97
discussion.md / discussion.html(討論稿,18 項 D 項 callout + 4 張 mermaid) ✅ 已 commit
README.md / README.html(需求索引頁) ✅ 已 commit
fr060-STATE.md(living 交接檔) ✅ 收尾日已更新
fr060-LOG.md(append-only) ✅ 已追加第 5 棒 block(CM-1072 納入 + 收尾文件更正)
docs/claude/host-dependencies.md 第三項 CINC Auditor 已補ec4e70b7,含授權紅線/路徑解析順序/缺了會怎樣/各主機狀況表)
docs/specs/current/system-admin/detection-profile-manage.md ✅ 已由另一支 agent 更新,內容以程式碼為準(含 CM-1072 的 url 型抽取)
docs/user-manual/scan-profile-guide.md ⏳ 另一支 agent 同步進行中
Notion 母卡 CM-1054 收口(狀態 Done + 完整五段) ⏳ 待做

8. 關鍵決策(濃縮自 LOG 四個 block)

# 決策 被排除的選項與原因
D5 分類 enum 值存 DB 由 root 維護 被排除:FE 寫死。決策者當場推翻原建議,原話「這類最好都要可以維護」。連帶多出三塊必做工作(顯示名不落庫/刪除保護/維護介面),FR-060.2 範圍因此比討論稿估的肥
D19 多工具擴充採三層結構,擴充點一律「加資料/加實作類別」 被排除:加欄位/改欄位語意。決策者原話「不能像這次一樣又大幅度更新,上線後很危險」。連帶改寫 D12 欄位定義(嚴重度欄不叫 impact,用 severity_rawseverity_norm
D20 名稱與描述多語化砍出本期範圍 決策者:「我不想把需求拉複雜,名稱跟描述之後再補吧」。記在 design.md §6.3 免得半年後有人重新發現一次
子任務卡改乙案 7 張(卡與執行 session 一對一) 被排除:第 2 棒規劃的 12 張。原 T 之間依賴過緊(T-1.2 與 T-1.3 必定同一 session 做完),卡切得比執行單位細只會讓回寫時出現「一張卡改一半」
DEV 階段不加相容 view 拆表後 BE 會有一段壞掉的過渡期。決策者裁示「開發階段就是開發,直接往下做」
人工待判判準用 impact == 0.0 被排除:tags.check_type == 'pending'——會漏 6 條(twgcb_01_014_0079~0084check_type: 'service' 但實為 impact 0.0 且只有 skip)。impact 是 InSpec 原生語意,外來 profile 也一定有
控制項路徑前綴用 **-versions 控制項屬於某一版而非一支基準(同支 v1/v2 內容可以完全不同),uid 收的是從檔** uid

9. 已知 follow-up

🔴 阻斷級(上版前必處理)

  1. 全案未 push——BE 15 支未 push commit(含其他線:E2E arc、文件產出泛化 CM-1049~1053/CM-1067)、FE 6 支。 ⚠️ push 是全 branch 的事,不是 FR-060 單獨能決定的,等決策者明示。

  2. STG/POC 完全未部署——三支 migration 只套 DEV(收尾日實查兩環境新表數皆 0)。 上版是另一件事,需決策者放行。 design.md §8.2 有上版前條件清單:

    🔴 派工單或交接文件若寫「三環境都套」,該指令本身可能就是錯的,停下問決策者。

待決 / 待補

  1. CINC Auditor 部署機安裝未確認——本機 mac 已裝 7.1.7(/usr/local/bin/cinc-auditor)。host-dependencies.md 的主機狀況表顯示三台部署機(188/189/190)皆為「未確認」,欄位註記「安裝等決策者放行」。上版前必須逐台裝好並實測。 ⚖️ 授權紅線:必須是 CINC Auditor(Apache 2.0),絕不可換官方 InSpec 6+ 商業 binary 或 inspec-core gem(均受 Chef EULA)。

  2. 🔴 url 型明細與來源內容會靜默偏離——掃描抓的是 URL 上的即時內容,而控制項明細是某次抽取的快照。git 上改了平台不會知道,明細就一直是舊的。目前沒有任何自動更新機制,只有兩個抽取時機:建立版本時自動排一次、手動按重抽。

    決策者已裁定方向(甲案):開明細頁時對來源 URL 發輕量請求(HEAD/If-None-Match)比對 ETag/Last-Modified,一樣就直接顯示現有清單(毫秒級),不一樣才顯示「有更新版本,正在重新解析」的讀取畫面並觸發背景重抽。下次再進來若無變更就直接讀資料。

    ⚠️ 可行性未驗證:GitHub 的 archive/refs/heads/master.tar.gz 是動態產生的,不一定給穩定的 ETag,這要實測才知道甲案成不成立。

    被排除的方案:

    • 乙案(開頁無條件背景重抽):每次開頁都跑一次 CINC(28~117 秒),被開頁頻率放大,浪費
    • 丙案(偵測到變更就自動長 v2):稽核上漂亮,但 url 型會出現同一 URL 有 v1/v2/v3,而掃描永遠拉最新 git 內容 → 任務綁的版本 uid 指不到掃描實際用的東西,讓「綁版本」這個既有契約自我矛盾

    已另開 Notion 卡追蹤。

  3. 13 支基準中 8 支仍 pending 未抽取(另 5 支 succeeded、0 支 failed)。需人工逐一觸發,或評估寫一支批次抽取。

  4. 三個欄位的「值」仍是英文——決策者已知悉,未決定是否翻譯

    • 檢查類型:kernel_module 等 14 種
    • 適用角色:baseline / dc / dns / web
    • 狀態:pending-mapping
  5. 一支端點不在任何規格書裡POST /api/1.0/detection-tool-profile-versions/<uid>/extraction(手動重抽,失敗後的補救途徑),是 a2f2512b(CM-1063)順手帶進來的,FE 已在用(DETECTION_TOOL_PROFILE_VERSION_EXTRACTION)。 同路徑的 GET 是抽取狀態輪詢(建議 5 秒一次)。權限:POST 對 create capability(與版更同一組,兩者都是「產生這一版的內容」),GET 不設 capability 與其他讀取端一致。 補進 spec 時要一併寫這兩支。

  6. 舊表 config.detection_tool_profiles_deprecated_20260803 未 DROP——觀察期後另案處理(design.md §8.3)。


10. 部署 handover

項目 內容
DB 環境 DEV 192.168.50.188:25432 / guidant_ai_dev;STG 同機 guidant_ai_stg;POC 192.168.50.189:25432 / guidant_ai_poc
帳號 cmmgr(繞 RLS,跑 migration 用)/ cm_app(受 RLS)。密碼查 BE .envDB_SECRET
套 migration 指令 psql --single-transaction -v ON_ERROR_STOP=1 -f <檔>,依序 fr060-1fr060-2fr060-3
主機層新依賴 CINC Auditor 7.x(275MB,其中約 200MB 是本案用不到的雲端 SDK)。安裝 curl -fsSL https://omnitruck.cinc.sh/install.sh | bash -s -- -P cinc-auditor -v 7
新環境變數 CINC_AUDITOR_CMD(選配)——binary 裝在非標準路徑時指向絕對路徑。不要硬編,解析順序:env var → PATH → /usr/local/bin/opt/cinc-auditor/bin/usr/bin
缺 CINC 會怎樣 基準上傳/版更仍然成功,派工掃描完全不受影響(agent 吃的是壓縮檔本身);只有內容解析落成 extraction_status = failed,管理頁控制項清單看不到東西
BE 重啟 資料層全面改寫,部署後必須重啟 BE
FE 版本對齊 上版時 FE package.json 需 bump 到同一版號

11. 這一棒真正學到的(詳見 LOG 第 4/5 棒)

  1. 🔴 派工前要查當前 HEAD 有沒有相關異動,不能只憑幾小時前的盤點——協調者把「url 型無法抽取」當既定事實寫進三份派工單,而該事實在派工當下已被 commit 25045758 推翻。SPEC agent 以程式碼為準沒照抄簡報,是對的判斷——這也印證了派工單該寫「去讀什麼」而不是「事實是什麼」。(LOG 第 5 棒)

  2. 🔴 驗收只查 DB 沒起 BE,會漏掉「服務起不起得來」——CM-1060 完成後 BE 其實是壞的(__tablename__ 仍指已 rename 的舊表),是 runner 自己在回寫裡揭露的,協調者的驗收沒抓到。拆表類任務的驗收必須含「服務還活著」。

  3. 🔴 401 分不出「端點存在」與「端點不存在」——@jwt_required() 在路由解析前就擋下,用 401 驗端點是無效驗證。正解是 OPTIONS 探路由(回 200 + Allow 標頭)。

  4. 🔴 把共通紀律寫進卡片本身,派工單縮成一行——決策者原話「不要每次都用攏長 prompt,很浪費 token 也會斷掉」。七張卡各附「必讀清單 + 作業紀律 + 完成後四步」後,派工單只剩「請處理 CM-10xx,照卡做」。

  5. PrimeVue 3 的展開列會整個銷毀重建——BodyRow<component :is="templates['expansion']">,而 templates.expansion 是父層插槽函式本身,父層每次 re-render 都是新實例 → :is 視為新元件型別 → 已展開的列全部銷毀重建。:key 或改具名函式 ref 都擋不住,只能讓子元件自己管載入 + 模組層快取。這是會再遇到的框架坑。