掃描設定檔管理(Scan Profile Management)
事實基準:2026-08-01 從 FE
src/views/detection-profile/DetectionProfileManageView.vue(1,239 行)+ BEapi/detection_tools/routes/detection_tool_profile_route.py/app/detection_tools/service/detection_tool_profile_service.py全鏈 + DEVconfig.detection_tool_profiles實查掃出(FR-059 檢測工具 Profile / Content 庫 T-1.1~T-4.2)
變更紀錄
| 日期 | FR | 內容 |
|---|---|---|
| 2026-08-01 | FR-059 | 新頁建立:檢測工具掃描設定檔(profile /
content)後台管理——公用版(SYSTEM,平台維護全租戶可見)+租戶自有(TENANT)雙軌同頁;上傳(zip
/ tar / tar.gz / tgz 四格式、50MB
上限、結構與安全驗證)或登記外部連結雙軌來源;版更(INSERT
新版不覆蓋)、fork
公版為自有、複製到另一工具池、停用(軟刪)。任務抽屜的 profile
下拉由本庫動態餵(options_source 宣告,見 任務配置) |
1. 功能描述
掃描設定檔管理是檢測工具(CINC Auditor / GCB)掃描設定檔(profile / content)的後台管理頁:把「一支掃描要套用哪套檢測基準」的內容檔收進平台庫,取代原本「進版控 → 人工投放到 Agent 主機 → 改資料庫選項」的維運鏈。上傳或登記後,任務配置頁的 profile 下拉立即列出庫內項目(不經任何部署動作),Agent 執行時透過平台通道取檔——封閉網路(Agent 無外網)環境也能使用檔案型設定檔。
資料模型是公用版 / 租戶自有雙軌(與流程範本管理同一模式):scope='SYSTEM'
公用版由平台(root
租戶)管理員維護、全租戶可見可用但唯讀;scope='TENANT'
自有版各租戶自行上傳維護、他租戶不可見。非 root
使用者對公用版可「複製為自有」(fork)後自行編輯。設定檔按
detection_tool_id
分池:上傳時必選所屬工具,GCB 任務抽屜只見 gcb 池、CINC
Auditor 只見 inspec
池;同一份內容兩工具都要用,走「複製到其他工具」各登記一筆。
來源型態雙軌:檔案型(source_type='file')上傳打包檔存平台儲存,Agent
走 mTLS 通道拉檔+本機
cache;連結型(source_type='url')只登記
URL 字串,Agent
原樣交給檢測引擎自行下載,平台不驗證可達性、不代管憑證(打不打得通是使用端網路與授權問題,設計界線非缺陷)。
主要使用者:持有 detection-profile.*
capability
的角色(角色權限矩陣「掃描設定檔管理」列;比照流程範本管理授予各租戶系統管理員角色);tenant
管理員與 root 用同一頁,能力差異由 BE 回的
can_edit 與三層守門決定。
角色速覽:
| 角色 | 一句話 |
|---|---|
租戶系統管理員(持 detection-profile.create /
delete) |
上傳 / 版更 / 停用自有設定檔;對公用版唯讀但可 fork 成自有副本 |
| 平台管理員(root 租戶) | 同上,另可維護公用版(版更 / 複製 / 停用 SYSTEM 列) |
| 一般使用者 | 不進本頁;在任務抽屜下拉選用庫內設定檔(menu API 人人可讀) |
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 設定檔列表(分頁) | 公用版+本租戶自有合併列出、公用版排前面;欄位:名稱(可點開詳細)/ 來源(公用版・自有 Tag)/ 所屬工具 / 類型(上傳檔案・外部連結)/ 適用平台 / 版本 / 狀態(三態)/ 更新時間・更新者 / 操作 | 主畫面 DataTable(lazy 分頁) | §6.2 |
| 2 | 篩選:來源範圍 | 全部 / 公用版 / 自有 segmented pill | 篩選列 | §6.2 |
| 3 | 篩選:所屬工具 | 下拉(來源=工具目錄 GET /detection-tools) |
篩選列 | §6.2 |
| 4 | 搜尋名稱 | 關鍵字(300ms debounce,後端 name like 過濾) |
篩選列 | §6.2 |
| 5 | 顯示已停用與舊版 | InputSwitch——打開後同時放掉 is_active
與 current_only 兩個條件(見 §12 坑 4) |
篩選列 | §4 |
| 6 | 新增 / 登記 | Dialog:選所屬工具(必選)→ 選來源類型(切換下方欄位)→ 檔案型上傳打包檔 / 連結型填 URL;名稱 / 適用平台 / 描述 | 「新增設定檔」按鈕 → Dialog | UC-DPM-01 |
| 7 | 版更 | 同名 INSERT 新版本+舊版退出現行,不原地覆蓋;檔案型換新檔、連結型換新連結;只改描述也算一次版更 | 列操作「版更」→ Dialog | UC-DPM-02 |
| 8 | 複製為自有(fork) | 公用版複製成本租戶可編輯副本;僅公用版現行啟用列顯示此鈕 | 列操作「複製為自有」→ Dialog | UC-DPM-03 |
| 9 | 複製到其他工具 | 同內容在另一工具池各登記一筆(分池語意);目標工具下拉排除來源工具本身 | 列操作「複製到其他工具」→ Dialog | §6.4 |
| 10 | 停用(軟刪) | confirm 後 is_active=FALSE(同時降
is_current,見 §12 坑
4);任務下拉即不出現,歷史執行紀錄參照可回溯 |
列操作「停用」→ confirm | UC-DPM-04 |
| 11 | 詳細資料 | 唯讀 Dialog:名稱 / 來源 / 工具 / 類型 / 檔名+SHA-256(檔案型)或 URL(連結型)/ 適用平台 / 版本 / 描述 / 建立者・時間 | 點名稱或「檢視」icon | §6.2 |
| 12 | 公版唯讀提示 | 公用版列對非 root 不顯示任何寫入鈕,改顯示鎖 icon+tooltip「公用版由平台管理員維護,您可以『複製為自有』後編輯」 | 列操作區 | §3 / §4 |
UC(§2)展開具後端寫入的核心路徑:新增 / 登記(6,UC-DPM-01)、版更(7,UC-DPM-02)、fork(8,UC-DPM-03)、停用(10,UC-DPM-04)。複製到其他工具(9)與 fork 同構(INSERT 新列),於 §6.4 說明;純顯示 / 篩選類(1~5、11、12)於 §5 / §6.2 說明。
2. Use Case
角色速覽:見 §1。
流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。
UC-DPM-01 上傳 / 登記掃描設定檔
| 項目 | 內容 |
|---|---|
| 角色 | 持 detection-profile.create 的角色 |
| 前置條件 | 所屬工具存在於工具目錄(上傳時必選,P7 分池) |
| 產出 / 後置條件 | config.detection_tool_profiles
新增一列(version=1, is_current=TRUE);檔案型另落
public.upload_files 一筆實體+回填 sha256;任務抽屜 menu
立即可選 |
流程:按「新增設定檔」開 Dialog → 選所屬工具(必選)+來源類型+名稱
→ 檔案型選擇打包檔(FE
accept=".zip,.tar,.tar.gz,.tgz"、maxFileSize=52428800)走
multipart 送出;連結型填 URL 走 JSON
送出(同一 endpoint 依 content-type 分流,_payload())→ BE
capability 檢查 →
同工具同租戶同名檢查(DETECTION_TOOLS_409006)→
檔案型跑驗證管線(四格式偵測 / 50MB / 安全 / 結構,四種失敗各自獨立
error code,見 §6.3)→ 計算 sha256 → 存平台儲存 → INSERT。連結型只驗 URL
形狀(須 http(s):// 開頭,否則
DETECTION_TOOLS_400012),不驗可達性。
建立公用版(
scope='SYSTEM')目前無 FE 入口:新增 Dialog 的scope固定送TENANT(DetectionProfileManageView.vue::openCreate:188-202),API 雖收scope欄位且 service 有對應守門,公用版新增現況走 seed script(見 §11)。詳見 §12 坑 1。
UC-DPM-02 版更
| 項目 | 內容 |
|---|---|
| 角色 | 持 detection-profile.create 的角色;SYSTEM
列限平台管理員 |
| 前置條件 | 該列 can_edit=true
且為現行啟用版(canWriteRow,見 §4) |
| 產出 / 後置條件 | 同名 INSERT version+1 新列+舊版
is_current=FALSE(不原地 UPDATE);menu /
任務下拉即指新版;舊版留表供歷史執行紀錄回溯 |
流程:按「版更」開 Dialog(顯示「為『{name}』建立新版本(目前
v{n})」)→ 檔案型換新檔 /
連結型換新連結(名稱與所屬工具沿用舊版不可改——版更的定義是「同一支
profile 的新內容」)→ BE guard(SYSTEM 列非平台管理員 →
DETECTION_TOOLS_403001)→
帶了新來源就重跑驗證管線;未帶新來源(只改描述 /
適用平台)也算一次版更,沿用舊版的檔案與
sha256,讓每一列自身資訊完整 → 先 demote_current(舊版降
FALSE)再 INSERT——順序不可顛倒,uq_dtp_current_name
唯一索引擋兩筆 current。
UC-DPM-03 複製為自有(fork 公用版)
| 項目 | 內容 |
|---|---|
| 角色 | 持 detection-profile.create
的角色(不需平台管理員——fork
寫的是新租戶列,不是改公用版) |
| 前置條件 | 來源列為公用版現行啟用版(FE
scope==='SYSTEM' && is_active && is_current
才顯示按鈕) |
| 產出 / 後置條件 | 本租戶新增一列
scope='TENANT', tenant_id=本租戶, version=1
的可編輯副本;共用來源列的檔案實體(file_id
/ sha256 沿用,不複製 binary) |
流程:按「複製為自有」→ Dialog 預填來源名稱(可改)→ 送出 → BE
檢查本租戶同工具同名(DETECTION_TOOLS_409006)→
INSERT——scope / tenant_id 由 BE
硬寫成 TENANT /
本租戶,不沿用來源列(沿用會讓副本仍是公用版,等於任何人 fork
一次就多一份全站可見資料;detection_tool_profile_service.py::fork_profile:256-294)。
UC-DPM-04 停用
| 項目 | 內容 |
|---|---|
| 角色 | 持 detection-profile.delete 的角色;SYSTEM
列限平台管理員 |
| 前置條件 | 該列 canWriteRow=true |
| 產出 / 後置條件 | is_active=FALSE 且
is_current=FALSE(軟刪;同時降 current
讓同名後繼者建得起來,見 §12 坑
4);任務下拉即不出現;歷史執行紀錄參照仍可回溯,已綁定舊參照的任務再派工會被擋(見
§11 派工展開) |
流程:按「停用」→
confirm(「停用後任務下拉不再出現此設定檔,既有的執行紀錄不受影響」)→
PUT /<uid>/deactivate → guard → 更新兩旗標。
3. 權限矩陣
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入本頁 | ui_routes web-menu
可見性(detection-profile.read=ALL;寫入三能力=ANY) |
N/A(僅 JWT) | migration
scripts/sql/2026-08-01-fr059-3-detection-profile-menu-route.sql |
| 列表 / 下拉 menu / 詳細 | 無 | 僅 @jwt_required()(無 capability
守門,menu 供任務抽屜全員取用;可見範圍由查詢收斂+RLS 決定,見
§12 坑 3) |
api/detection_tools/routes/detection_tool_profile_route.py::DetectionToolProfileListRoute
/ MenuRoute / DetailRoute |
| 新增 / 版更 / fork / 複製到其他工具 | hasCap('detection-profile.create')——無權時按鈕
disabled+tooltip「無權限」 |
route 層
@require_capability("detection-profile.create")(POST
家族全對 create——版更 / fork / 複製都是 INSERT 新列) |
detection_tool_profile_route.py:39+各 route
decorator |
| 停用 | hasCap('detection-profile.delete') |
route 層
@require_capability("detection-profile.delete")(軟刪非真刪,但「讓它從下拉消失」的權限語意等同刪除) |
detection_tool_profile_route.py:40 /
DeactivateRoute |
| 對公用版(SYSTEM)寫入 | 依 BE 回的 can_edit——false 時不顯示寫入鈕、顯示鎖
icon(FE 不自己重算「我是不是
root」,避免兩邊判定漂移) |
service 層 guard
_guard_system_writable():scope=='SYSTEM' and not viewer_is_platform_admin()
→ 403 DETECTION_TOOLS_403001(版更 / 複製到其他工具 /
停用適用;fork 不走 guard——寫的是新租戶列) |
app/detection_tools/service/detection_tool_profile_service.py:377-386;root
判定
canonical=common/authz/platform.py::viewer_is_platform_admin() |
| 跨租戶隔離 | 無(列表本來就查不到他租戶) | repo 查詢顯式收斂
or_(scope=='SYSTEM', tenant_id==ctx.tenant_id)+DB
RLS 四段 policy 兜底(SELECT 公用版人人可讀;INSERT / UPDATE
WITH CHECK 堵租戶偽造 / 竊升 SYSTEM;DELETE 非 super 不能刪
SYSTEM) |
infra/detection_tools/repository/detection_tool_profile_repo_impl.py:45-56;RLS
scripts/sql/2026-08-01-fr059-1-detection-tool-profiles.sql |
三層防禦分工(capability 管「能不能碰這個功能」、service guard 管「能不能碰公用版」、RLS 兜底):四個 capability 全部
is_platform=false——租戶管理員必須能建自己的 TENANT 設定檔與 fork 公用版,這正是雙軌功能的核心;公用版保護不在 capability 層鎖。與檢測工具管理(純平台目錄頁)的守門模型刻意不同。⚠️
detection-profile.updatecapability 已 seed 且掛在路由節點,但目前沒有任何端點使用(見 §12 坑 2)。
4. 狀態機與前置條件
本頁無輪次 / 階段狀態機,僅有每列自身的 is_active ×
is_current 二旗標,FE 呈現為三態:
| 狀態 | 條件 | Tag | 語意 |
|---|---|---|---|
| 啟用中 | is_active && is_current |
success | 現行版——任務下拉可選、可版更 / 停用 |
| 舊版 | is_active && !is_current |
warning | 版更後的歷史版本——下拉不出現,但已綁定它的任務照常可派工(派工不檢查
is_current,見 §11) |
| 已停用 | !is_active |
secondary | 軟刪——下拉不出現、再派工被擋、不可再異動 |
頁面 gate 表:
| 條件 | 效果 | 出處 |
|---|---|---|
row.can_edit === false(公用版 × 非平台管理員) |
版更 / 複製到其他工具 /
停用按鈕整組不顯示(v-if="canWriteRow(data)"),改顯示鎖
icon+tooltip |
DetectionProfileManageView.vue::canWriteRow:32、鎖 icon
:727-733 |
row.is_active === false 或
row.is_current === false |
同上——停用列與舊版列不可再異動(要改請對現行版版更) | 同 canWriteRow:32 |
scope==='SYSTEM' && is_active && is_current |
才顯示「複製為自有」按鈕(自有列 fork 自己沒有意義;停用 / 舊版不該再被複製出去) | DetectionProfileManageView.vue:686 |
!hasCap('detection-profile.create') /
!hasCap('detection-profile.delete') |
對應按鈕 disabled+tooltip「無權限」(按鈕仍渲染,與
canWriteRow 的「不渲染」是兩層不同 gate) |
DetectionProfileManageView.vue:27-28 |
createDialog.source_type |
切換 Dialog 下半欄位:file →
FileUpload(accept=".zip,.tar,.tar.gz,.tgz"、maxFileSize=52428800);url
→ URL 輸入框 |
DetectionProfileManageView.vue:810-853 |
| showHistory switch 打開 | 列表 filters 同時放掉 is_active 與
current_only(必須一起放,見 §12 坑
4) |
DetectionProfileManageView.vue::fetchProfiles:109-125 |
| 版更 Dialog | 沿用來源列的 source_type 分流欄位(檔案型換檔 /
連結型換連結),名稱與工具不可改 |
DetectionProfileManageView.vue::openNewVersion:284-295 |
5. UI 設計
版面骨架(篩選列 + DataTable + 四組 Dialog):
📸 實機截圖待補:列表(公用版 10 筆排前 + 自有列、三態狀態 Tag、公用版鎖 icon)、新增 Dialog(file / url 兩型態各一張)、版更 Dialog、複製為自有 Dialog、詳細資料 Dialog(sha256 等寬字)、任務抽屜 profile 下拉動態選項(GCB 池)。
狀態呈現:來源欄 Tag——公用版(info)/ 自有(success);類型欄
icon+文字——上傳檔案(pi-file)/
外部連結(pi-link);狀態欄三態 Tag(見 §4)。名稱 cell
可點開詳細資料 Dialog(hover
底線);描述截兩行顯示於名稱下方。互動模式沿用流程範本管理頁(segmented
pill 篩選、DataTable lazy 分頁、confirm 停用),不另發明。無
socket;各寫入動作成功後重打
fetchProfiles()(新增另重置回第 1 頁)。首次載入以
LoadingState 蓋住整個表格區。錯誤呈現:具體驗證錯誤(格式 /
大小 / 安全 / 結構)由 axios interceptor 依 error code 出
toast(error-code.json 已補
DETECTION_TOOLS_4000xx 對應文案),Dialog 端 catch
留空不重複出錯。
6. API 規格
Envelope:成功 {"code": 1, "data": …}、失敗
{"code": 0, "msg": "…"}(return_response)。route
註冊:api/detection_tools/__init__.py:48-68(掛
/detection-tool-profiles,不掛工具目錄底下——設定檔是獨立資源家族)。
6.1 總清單(本頁呼叫的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 讀 | POST /detection-tool-profiles/list |
分頁列表(公用版+本租戶自有,公用版排前面) | §6.2 |
| 讀 | GET
/detection-tool-profiles/menu?detection_tool_uid= |
下拉 menu(只回現行+啟用,依工具分池)——本頁不直接用,任務抽屜與執行紀錄消費(見 §11) | §6.2 |
| 讀 | GET /detection-tool-profiles/{uid} |
單筆詳細(本頁詳細 Dialog 用列表資料即足,此端點供他處 / 除錯) | GAI-SD-02 |
| 寫 | POST /detection-tool-profiles |
新增 / 登記(file 型 multipart、url 型 JSON) | §6.3 |
| 寫 | POST /detection-tool-profiles/{uid}/new-version |
版更(INSERT 新版+舊版退現行) | §6.4 |
| 寫 | POST /detection-tool-profiles/{uid}/fork |
fork 公用版成本租戶自有副本 | §6.4 |
| 寫 | POST /detection-tool-profiles/{uid}/copy-to-tool |
複製到另一工具池 | §6.4 |
| 寫 | PUT /detection-tool-profiles/{uid}/deactivate |
停用(軟刪) | §6.4 |
| 讀(輔) | GET /detection-tools |
工具目錄——所屬工具下拉與工具名對照的來源 | 檢測工具管理 §6.2 |
6.2 列表 / 下拉 menu
[POST] /detection-tool-profiles/list
Request(RequestMetaSchema 分頁慣例):
{
"pager": {"page": 1, "page_size": 10, "with_total": true},
"sort": [],
"filters": {
"detection_tool_uid": "<uid>",
"source_type": "file",
"scope": "SYSTEM",
"name": "TWGCB",
"is_active": true,
"current_only": true
}
}| filters 欄位 | 型別 | 說明 |
|---|---|---|
| detection_tool_uid | string|null | 依所屬工具過濾 |
| source_type | string|null | file / url |
| scope | string|null | SYSTEM / TENANT(FE 來源範圍
pill:全部=不帶) |
| name | string|null | 名稱 like 過濾 |
| is_active | bool|null | load_default=true(預設只看未停用);FE 歷史開關打開時送 null |
| current_only | bool | load_default=true(預設只看現行版);歷史開關打開時送 false |
Response
data[](DetectionToolProfileResponseSchema):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string | 對外識別 |
| scope | string | SYSTEM / TENANT——FE 來源 Tag 與 fork
鈕條件的依據 |
| detection_tool_id / detection_tool_code | int / string|null | 所屬工具;code 由 service 補算(FE 以工具目錄對照顯示名) |
| name / description / platform_hint | string / string|null / string|null | 顯示名稱 / 描述 / 適用平台標記(純顯示用,不參與過濾邏輯) |
| source_type | string | file / url |
| file_id / file_name | int|null / string|null | 檔案型的 upload_files soft-ref 與原始檔名(service
批次補算) |
| url | string|null | 連結型的登記 URL |
| sha256 | string|null | 檔案型對原始壓縮檔 bytes 計算——Agent 取檔對帳的信任根;詳細 Dialog 以等寬字顯示 |
| version / is_current / is_active | int / bool / bool | 版本模型三件套(§4 三態) |
| can_edit | bool | BE
算好的結論(scope != 'SYSTEM' or 平台管理員,與
service guard 同一條規則)——FE 直接用,不自己重算 |
| created_user / created_user_name / updated_user / updated_user_name | string|null | 審計欄位規範:login_name 配 nickname 成對回傳(app service
層批次查,_build_nickname_map:519-531;system
映「系統」) |
| created_at / updated_at | datetime | %Y-%m-%d %H:%M:%S |
排序固定:scope asc, name asc——'SYSTEM' < 'TENANT'
字典序,公用版天然排前(detection_tool_profile_repo_impl.py:89-92),FE
不另排序。
可見範圍是業務語意不是只靠 RLS:root 租戶的
allowed_tenant_paths 前綴涵蓋所有子租戶,只靠 RLS
的話平台管理員會看到全站每個租戶的私有設定檔;repo 顯式收斂
or_(scope=='SYSTEM', tenant_id==ctx.tenant_id)(:45-56),RLS
只是兜底。
出處:route
detection_tool_profile_route.py:54-77、schema
api/detection_tools/serializers/detection_tool_profile.py:13-61、service
detection_tool_profile_service.py::list_profiles:84-112。
[GET] /detection-tool-profiles/menu?detection_tool_uid=
detection_tool_uid 必帶(P7 分池:GCB
抽屜只該見 gcb 池,未帶回 400
DETECTION_TOOLS_400007)。只回
is_current=TRUE AND is_active=TRUE
的列(公用版+本租戶自有)。Response
data[](DetectionToolProfileMenuResponseSchema):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid / name | string | 識別與顯示名 |
| value | string | 已組好的 profile:<uid>
下發格式——前綴規則只有 BE
一處知道(common/util/detection_profile_ref.py::PROFILE_REF_PREFIX),FE
直接放進任務 params.profile,不自己拼 |
| scope / source_type / platform_hint | string / string / string|null | 供下拉呈現輔助 |
出處:route :80-97、service
list_menu:114-125、repo
list_menu:114-123。
6.3 新增 / 登記
[POST] /detection-tool-profiles
同一 endpoint 收兩種 content-type(route
_payload():43-51 判 multipart/form-data 取
request.form,否則取 JSON):檔案型走 multipart(檔案在
file 欄位、其餘欄位在 form;FE 送 FormData
時不手動設 Content-Type,讓 axios 自帶
boundary);連結型走 JSON。
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| detection_tool_uid | string | 是 | 所屬工具(P7 必選;查無 → 404
DETECTION_TOOLS_404001) |
| name | string | 是 | 同工具同租戶同名(現行)已存在 → 409
DETECTION_TOOLS_409006 |
| source_type | string | 是 | file / url |
| file | multipart file | file 型是 | 打包檔——走下方驗證管線 |
| url | string | url 型是 | 只驗 http(s)://
開頭形狀;不驗可達性、不代管憑證 |
| description / platform_hint | string | 否 | 描述(≤2000 字 FE 限)/ 適用平台(≤50 字) |
| scope | string | 否 | 預設 TENANT;SYSTEM
僅平台管理員送得動(service guard)——FE 現況固定送
TENANT(§12 坑 1) |
檔案型驗證管線(common/util/detection_profile_archive.py::ProfileArchiveValidator,streaming
全程不落地解壓——只讀 entry metadata、不呼叫任何
extract):
| 順序 | 檢查 | 失敗碼 |
|---|---|---|
| 1 | 副檔名白名單
.zip / .tar / .tar.gz / .tgz(最長後綴優先比對)+
magic number 雙重確認(zip=PK\x03\x04
含空包 / spanned 變體、gzip=\x1f\x8b、tar=offset 257
ustar)——防改副檔名的假檔 |
400 DETECTION_TOOLS_400008 |
| 2 | 大小 ≤ 50MB(Content-Length 預檢+實讀累計雙保險) | 400 DETECTION_TOOLS_400009 |
| 3 | 逐 entry 安全檢查:路徑不得為絕對 / 含 .. 逸出(防
zip-slip);tar 的 symlink / hardlink 一律拒收;entry 數
≤10,000、宣告解壓總量 ≤500MB、壓縮比 ≤200(1MB
以下小檔不看比值,防誤殺)。四類問題共用一個碼——細分會把防護細節洩漏給攻擊者 |
400 DETECTION_TOOLS_400010 |
| 4 | 結構驗證:頂層或一層目錄內須有
inspec.yml。只驗結構不驗語意正確性——壞
profile 掃了會
fail、錯誤可回溯,平台不做「上傳即保證能掃」的承諾(設計界線,非缺陷) |
400 DETECTION_TOOLS_400011 |
| 5 | sha256 計算(對原始壓縮檔 bytes)→ 落
detection_tool_profiles.sha256 並回填
upload_files.sha256(上傳 adapter 只算
md5,_backfill_sha256:460-470) |
— |
| 6 | 存儲:SYSTEM 走 upload_system_file()(root
租戶儲存+storage_scope=system)、TENANT
走一般上傳;save_dir="detection-profiles" |
— |
tar 系與 zip 的安全檢查共用同一套 entry
檢查器(僅迭代介面不同)——「按格式分支處理」正是 zip
路徑漏防的典型成因。source_type 與來源欄位互斥(file 無檔 /
url 無字串 / 未知型態)→ 400 DETECTION_TOOLS_400012(DB
chk_dtp_source_exclusive CHECK
是兜底,業務層先擋讓錯誤可讀)。
出處:route :100-125、service
create_profile:133-194 /
_resolve_source:404-432 /
_store_file:434-458。
6.4 版更 / fork / 複製到其他工具 / 停用
[POST] /detection-tool-profiles/{uid}/new-version
Request(全部選填;multipart / JSON 同 §6.3
分流):source_type / file / url
/ description /
platform_hint。名稱與所屬工具不收(沿用舊版)。來源型態允許換(file
→ url 或反向);帶了新來源重跑驗證管線;全部未帶=只改 metadata
也算一次版更,沿用舊版 file_id /
sha256 /
url——讓「這一版對應哪個檔」在每一列上都是完整的(Agent 拿
payload 不必回溯前版)。寫入順序固定:先 demote_current 再
INSERT version+1(uq_dtp_current_name 擋兩筆
current,順序是必要條件不是風格)。出處:service
create_new_version:196-254。
[POST] /detection-tool-profiles/{uid}/fork
Request:{"name": "<新名稱>"}(未帶沿用來源名)。scope='TENANT'
/ tenant_id=本租戶 硬寫;file_id /
url / sha256 沿用來源列(共用同一
upload_files 實體——profile
檔不可變、版更=新列新檔,複製 binary
只會同內容存兩次)。同名檢查對本租戶池。出處:service
fork_profile:256-294。
[POST] /detection-tool-profiles/{uid}/copy-to-tool
Request:{"detection_tool_uid": "<目標工具>", "name": "<名稱>"}。目標工具=來源工具
→ 400(FE
下拉已排除來源工具,copyToolOptions:387-389)。scope
沿用來源——把公用版複製到另一工具,結果仍是公用版(受眾沒變,只是換池),故此路徑走
_guard_system_writable(與 fork
的語意差異正在於此)。同名檢查對來源列的
tenant_id+目標工具池。出處:service
copy_to_tool:296-343。
[PUT] /detection-tool-profiles/{uid}/deactivate
無 body。is_active=FALSE 且
is_current=FALSE(後者讓同名後繼者建得起來——留著 current
會撞
uq_dtp_current_name;deactivate_profile:345-360)。
Error
code
一覽(本頁新增,common/code/detection_tools_error_code.py:56-90)
| 情境 | HTTP | code |
|---|---|---|
| 設定檔不存在 | 404 | DETECTION_TOOLS_404005 |
| 同工具同名已存在 | 409 | DETECTION_TOOLS_409006 |
| 壓縮格式不支援(副檔名 / magic number) | 400 | DETECTION_TOOLS_400008 |
| 超過 50MB | 400 | DETECTION_TOOLS_400009 |
| 壓縮檔不安全(slip / bomb / entry 數 / symlink,合併一碼) | 400 | DETECTION_TOOLS_400010 |
結構驗證失敗(找不到 inspec.yml) |
400 | DETECTION_TOOLS_400011 |
| source_type 與內容互斥違反 / URL 非 http(s) 形狀 | 400 | DETECTION_TOOLS_400012 |
| 非平台管理員寫公用版 | 403 | DETECTION_TOOLS_403001 |
FE error-code.json(zh-tw /
en)已補齊上列八碼文案,上傳失敗由 axios interceptor 統一出 toast。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/detection-profile/DetectionProfileManageView.vue |
主檢視(1,239 行):篩選列(scope pill / 工具下拉 / 搜尋 debounce /
歷史開關)、lazy 分頁 DataTable、新增(source_type 切換 file / url
欄位、multipart vs JSON 分流)、版更、fork、複製到其他工具、停用
confirm、詳細資料 Dialog、canWriteRow / 鎖 icon 呈現 |
src/service/DetectionToolProfileService.js |
API 包裝:listProfiles / listMenu /
createProfile / createNewVersion /
forkProfile / copyToTool /
deactivateProfile——FormData 不手動設 Content-Type(缺
boundary 會 400) |
src/composables/useDetectionProfileOptions.js |
任務抽屜下拉用(非本頁):options_source: "profile_library"
欄位的 menu 選項載入——模組層級快取(同工具只打一次
API、in-flight promise 去重;規劃頁 N
個任務列共用)+invalidate();isProfileLibraryField()
宣告判定 |
src/components/detection-tools/DetectionConfigField.vue |
動態欄位渲染元件(與檢測工具管理
/ 任務配置共用):FR-059
起 options_source 宣告欄位的 options 改接 profile menu
API(帶 detectionToolUid prop 分池);已停用參照補唯讀
fallback 選項、庫拉不到顯示警示但手填照常(見 §11) |
src/config/router/index.js:630-634 |
route detection-profile-manage,path
/plugin/detection-profile-manage |
src/config/api/api.js:353-362 |
DETECTION_TOOL_PROFILE_LIST / _MENU /
DETECTION_TOOL_PROFILES / _DETAIL /
_NEW_VERSION / _FORK /
_COPY_TO_TOOL / _DEACTIVATE 常數 |
src/config/locales/i18n/{zh-tw,en}/detection-profile-manage.json |
頁面 i18n(detection_profile namespace,獨立檔) |
src/config/locales/i18n/{zh-tw,en}/menu.json |
選單標題「掃描設定檔管理」+描述 |
src/config/locales/i18n/{zh-tw,en}/error-code.json:186-193 |
上傳驗證八碼的使用者文案 |
8. 後端檔案地圖(本頁核心鏈路)
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 列表 / menu / 詳細 | api/detection_tools/routes/detection_tool_profile_route.py::DetectionToolProfileListRoute
/ MenuRoute / DetailRoute |
app/detection_tools/service/detection_tool_profile_service.py::list_profiles
/ list_menu / get_profile(皆
@transaction) |
domain/detection_tools/service/detection_tool_profile_domain_service.py
→
infra/detection_tools/repository/detection_tool_profile_repo_impl.py(繼承
BaseRepositoryImpl,session lazy) |
| 新增 / 登記 | DetectionToolProfilesRoute.post |
create_profile(驗證管線 → 存儲 → INSERT) |
同上+common/util/detection_profile_archive.py::ProfileArchiveValidator(DI
注入,di_containers/detection_tools/detection_tools_containers.py:53,81)+app/upload_file/service/managed_file_upload_service.py(canonical
儲存,upload_system_file / upload_files) |
| 版更 / fork / 複製 | NewVersionRoute / ForkRoute /
CopyToToolRoute |
create_new_version / fork_profile /
copy_to_tool |
同 repo(demote_current /
find_by_exact_name) |
| 停用 | DeactivateRoute.put |
deactivate_profile |
同 repo |
| 下發參照契約 | —(非 HTTP) | common/util/detection_profile_ref.py——PROFILE_REF_PREFIX
/ PROFILE_PARAMS_KEY / build_profile_ref() /
extract_profile_uid() /
build_payload(),前綴判斷只有這一份,派工端與
menu DTO 都 import 這裡 |
— |
| ORM Model | — | — | infra/detection_tools/model/detection_tool_profile.py(BaseModel, TenantScopedMixinModel——before_flush
自動填 tenant_id) |
DDD 提醒:route 不碰 session、app service 全數
@transaction、repo 繼承
BaseRepositoryImpl;寫入守門 route 層 capability
decorator(主體域)+service 層 scope guard(資源域)雙軌,符合 FR-048
規範。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 欄位詳述 |
|---|---|---|---|
| config.detection_tool_profiles | 寫 | 掃描設定檔庫本體(雙軌 scope+版本模型+來源雙軌) | §9.3 |
| public.upload_files | 寫(檔案型上傳時) | 檔案實體 metadata(file_id soft-ref 目標;sha256
回填) |
證據管理群 |
| config.detection_tools | 讀 | 工具目錄(所屬工具綁定與 code 對照) | 檢測工具管理 §9.3 |
| config.detection_tool_param_schemas | 不讀寫(關聯機制) | profile 欄位的 options_source: "profile_library"
宣告落在此表新版 schema——本頁資料成為該宣告的下拉來源 |
任務配置 §9.3 |
9.2 ER 圖
9.3 核心表欄位(2026-08-01 DEV 實查)
config.detection_tool_profiles — 掃描設定檔庫
| 欄位 | 型別 | 說明 |
|---|---|---|
| id / uid | bigserial / varchar(36) NOT NULL UNIQUE | 主鍵+對外識別 |
| scope | varchar(20) NOT NULL default 'TENANT' |
SYSTEM(公用版,owner 掛 root 租戶)/
TENANT(租戶自有)。CHECK
chk_dtp_scope;tenant_id IS NULL
永遠是壞資料(SYSTEM 列掛 root,不是 NULL) |
| tenant_id / org_unit_id | bigint NOT NULL / bigint | 擁有者租戶(TenantScopedMixinModel
自動填);org_unit_id 預留部門層級,本版邏輯只做租戶層 |
| detection_tool_id | bigint NOT NULL | soft-ref
config.detection_tools.id——分池綁定,上傳必選 |
| name / description / platform_hint | varchar(255) NOT NULL / text / varchar(50) | 顯示名稱 / 描述 / 適用平台標記(純顯示用不參與過濾) |
| source_type | varchar(10) NOT NULL | file / url。CHECK
chk_dtp_source_type;互斥 CHECK
chk_dtp_source_exclusive(file 必有 file_id 無 url;url
反之) |
| file_id | bigint | soft-ref public.upload_files.id(file 型必填) |
| url | text | url 型必填;平台不驗證可達性 |
| sha256 | varchar(64) | file 型對原始壓縮檔 bytes 計算——Agent 對帳信任根 |
| version / is_current | integer NOT NULL default 1 / boolean NOT NULL default TRUE | 版本模型:版更=INSERT 新版+舊版降 FALSE |
| is_active | boolean NOT NULL default TRUE | 軟刪旗標(與 is_current 正交,但停用會同時降兩者,見 §12 坑 4) |
| created_user / updated_user / created_at / updated_at | varchar(255) / varchar(255) / timestamptz / timestamptz | 稽核欄位 |
唯一索引三組(皆含 version 或以 is_current 收斂——版更後同名多列並存是常態,全域唯一會在第一次版更撞索引):
uq_dtp_tool_tenant_name_version(detection_tool_id, tenant_id, name, version)uq_dtp_system_tool_name_version(detection_tool_id, name, version) WHERE scope='SYSTEM' AND is_active(公用版跨租戶防撞名的防禦性第二道)uq_dtp_current_name(detection_tool_id, tenant_id, name) WHERE is_current AND is_active——「同名只有一個現行版」的真正保證
查詢索引:idx_dtp_tool_current_active(menu)、idx_dtp_tenant_scope(列表)。
RLS 四段
policy(detection_tool_profiles_select / insert / update / delete):SELECT
公用版分支放最前(人人可讀)+super_admin+自己子樹;INSERT WITH CHECK
堵 SYSTEM 偽造;UPDATE USING+WITH CHECK 兩段都有(缺
WITH CHECK 的話租戶可把自己的列 UPDATE 成 scope='SYSTEM'
升格公用版);DELETE 非 super 不能刪
SYSTEM。app_tenant_allowed_for_session() 只接
integer,呼叫處對 bigint tenant_id 顯式 ::integer
cast。
建表
migration:scripts/sql/2026-08-01-fr059-1-detection-tool-profiles.sql(含
GRANT cm_app+sequence);capability
seed:2026-08-01-fr059-2-detection-profile-capability.sql;選單路由:2026-08-01-fr059-3-detection-profile-menu-route.sql。三支皆只套
DEV,STG / POC 依環境異動鐵律等放行(§12 坑 12)。
10. 頁面邏輯與資料對應
載入時序:onMounted 先
fetchTools()(工具目錄——所屬工具欄顯示名與新增下拉的來源;拿不到不擋列表,只是工具欄退成
code、新增下拉為空)→ fetchProfiles()。搜尋 300ms debounce
重查並跳回第 1 頁;scope / 工具 / 歷史開關任一變動同樣重查跳頁。
關鍵欄位對應(畫面 ↔︎ API):
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| 列表 | profiles / totalRecords |
POST /list 回 data /
meta.total |
| 來源 Tag | 直接綁定 | scope |
| 所屬工具欄 | toolNameByCode[data.detection_tool_code] |
detection_tool_code+GET /detection-tools
對照 |
| 狀態三態 Tag | rowStatus(row) |
is_active × is_current |
| 操作鈕顯示 | canWriteRow(row) / fork 條件 |
can_edit+is_active+is_current+scope |
| 操作鈕 disabled | canCreate / canDelete |
登入者 permissions(hasCap) |
| 詳細 Dialog | detailDialog.row(列表列原資料,不另打 API) |
同列表欄位(含 file_name / sha256 /
url) |
| 更新者欄 | 直接綁定 | updated_user_name(fallback
updated_user) |
儲存流程:新增依 source_type 分流——file
組 FormData(欄位逐一 append、空值不帶)、url 組 JSON(空值帶 null)→
createProfile() → 成功 toast+關 Dialog+清
FileUpload+跳回第 1 頁重查。版更 / fork / 複製 /
停用成功後重查當前頁。
錯誤對應:BE error envelope → axios interceptor 依
error code 查 error-code.json 出 toast(八碼文案見
§6.4);Dialog 的 catch 留空不重複出錯。列表載入失敗另有本地
toast(toast_load_failed)並清空列表。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | 無——本頁不觸發、不消費任何通知 |
| Socket | 無 socket / 輪詢;動作後重查 |
| 任務抽屜下拉動態化(本庫的消費端) | param_schema 的 profile 欄位帶
"options_source": "profile_library" 宣告(inspec / gcb 已升
v2 帶宣告並拿掉靜態 options,migration
2026-08-01-fr059-3-param-schema-options-source.sql)→ FE
DetectionConfigField 偵測宣告改呼 menu API(帶工具 uid
分池)動態組選項;機制 tool-agnostic,新工具 seed 宣告即接庫、FE
零改。詳見 任務配置 §5 |
派工展開(profile:<uid> →
_profile) |
使用者選庫內設定檔發起執行時,params.profile 存
profile:<uid>;派工當下(detection_orchestration_service.py:197-207,非心跳組裝時)展開
_profile 內部 payload——file 型
{uid(=upload_files uid), version, sha256, source_type, file_name}、url
型 {uid, source_type, url}。params.profile
原值保留(FE 執行紀錄顯示用);_profile
底線前綴被既有剝除機制擋掉不出 API(同 _credentials
慣例)。參照失效(查無 / 已停用)→ 400
明確報錯不靜默;is_current
不檢查——歷史版本照使用者所選執行,凍結快照(version /
sha256)正是稽核回溯要的 |
| Agent 取檔授權 | profile_ref
resolver(app/detection_tools/service/agent_file_access_service.py:79-85):uid
是該 agent 名下 pending / running 任務 params._profile
引用的檔 → 放行 mTLS 取檔端點
GET /api/1.0/agents/files/<uid>(FR-058.7
通道,可插拔 resolver 清單) |
| Agent 端 cache | evidence-agent
core/profile_cache.py+inspec.py::_resolve_profile():file
型查 /data/content/cache/<uid>/v<version>/
命中直用;miss → 拉檔 → sha256 對帳(不符即 fail)→ 安全解壓(agent
端同防 slip / bomb)→ 解壓後目錄餵
cinc-auditor(四格式一條路徑)。url 型 / 手填原樣入
argv(現行為零變化)。cache key 含 version →
版更自然失效;第一期無上限+手動清理 |
| 執行紀錄標籤對照 | JobExecutionDrawer.vue::fetchProfileLibraryLabels:273-289:宣告欄位的值→顯示名對照另向
menu API 拿(param_schema 已無靜態 options),否則執行紀錄會露出
profile:<uuid> 裸值;拿不到退回顯示原值不擋畫面 |
| 搬遷 seed | scripts/seed_2026-08-01_fr059_detection_profiles.py——8
支 TWGCB 打包走與使用者相同的上傳管線落 SYSTEM file
型+dev-sec linux / windows 基準 2 條落 SYSTEM url
型(名稱沿用原靜態選項 label,下拉零回歸);DEV 現況公用版 10 筆 |
| jedi-* 套件 | 無新增依賴(@transaction /
BaseRepositoryImpl / exception 為既有 jedi_common) |
| 系統參數 | 無 feature flag;驗證上限(50MB / 500MB / 10,000 entry / 比 200)為
ProfileArchiveValidator 建構參數預設值,DI 可帶 config
覆寫 |
12. 邊界情況與已知坑
- 公用版新增無 FE 入口(現況):新增 Dialog 的
scope寫死TENANT——API 與 service guard 都支援scope='SYSTEM'(平台管理員),但 FE 沒有渲染 scope 選擇欄位(i18n 已備field_scope/hint_scope_systemkey 未使用)。公用版建立現況走 seed script;root 對既有公用版的版更 / 複製 / 停用則正常可用(can_edit=true)。要補公用版新增 UI 時 i18n 與 BE 都是現成的。 detection-profile.updatecapability 是預留孤兒:四能力點全數 seed 且掛路由節點(read=ALL、寫入三個=ANY),但版更 / fork / 複製全是 INSERT 對create、停用對delete——沒有任何端點消費update。日後若出現原地編輯類端點(如改描述不升版)再啟用;在那之前對角色勾 update 沒有實際效果。- 讀端點僅 JWT 無 capability 守門:列表 / menu /
詳細只
@jwt_required()——menu 是任務抽屜全員要用的(刻意開放),列表與詳細跟著同粒度。可見範圍靠查詢收斂+RLS,不靠 capability;與寫入端點的守門粒度不一致,改動時勿混為一談(同 檢測工具管理 §12 坑 2 模式)。 - 停用同時降
is_current,FE 歷史開關必須一起放兩個條件:deactivate_profile把is_active與is_current一起降 FALSE(留著 current 會讓同名後繼者撞uq_dtp_current_name建不起來)。因此 FE「顯示已停用與舊版」開關必須同時放掉is_active與current_only——只放前者的話停用列仍被 current 條件濾掉、開關等於沒作用。代價是版更後的舊版一起現身,狀態欄因此做三態(啟用中 / 舊版 / 已停用)不把舊版誤標成啟用中。 - fork 與複製到其他工具共用檔案實體不複製
binary:
file_id/sha256沿用來源列——profile 檔不可變(版更=新列新檔),同內容存兩次沒有意義。含意:來源公用版停用不影響 fork 列(upload_files實體仍在);反向也一樣。 - fork 不走 SYSTEM guard、複製到其他工具走:fork 寫的是新租戶列(scope 硬寫 TENANT);複製沿用來源 scope——公用版複製到另一池結果仍是公用版,故需平台管理員。兩者語意差異就在這裡,改動守門時別對齊成同一套。
- 派工不檢查
is_current只檢查is_active:任務綁定存的是使用者當初選的那一版 uid,版更後舊版仍是合法歷史版本,掃描照使用者所選執行(要換版是使用者自己回任務改選,不是 BE 偷偷跳版);停用才是「不該再被使用」的管理決策。參照失效(查無 / 停用)→ 派工當下 400 明確報錯,不原樣下發(agent 會拿到無法解析的profile:<uid>變成 agent 端神祕失敗)、不默默拿掉(使用者以為在掃 A 基準實際參數沒了)。 profile:前綴契約單點:menu 的value已是 BE 組好的profile:<uid>,FE 不自己拼前綴;BE 端前綴判斷只有common/util/detection_profile_ref.py一份,派工展開不可自己寫startswith。手填值不可能以profile:開頭——這是三種取值來源(庫內 / 手填 / 舊靜態選項值)零歧義共存的基礎。_profile.uid對 file 型是upload_files的 uid 不是 profile 列的 uid:agent 拿它直接打取檔端點(該端點認的就是 upload_files uid);url 型無檔案實體,uid 才是 profile 列自己的。讀 payload 時別搞混兩種 uid。- 上傳驗證是第一道不是唯一一道:bomb 檢查靠 entry
自報的宣告 size(header 值理論上可偽造)——偽造後 agent
端真解壓時與宣告不符會失敗,且 agent 端另有一套同等的 slip / bomb
防護(BE 驗過不代表可信任傳輸後內容)。本驗證器是全 repo
第一個壓縮檔安全實作,之後任何要收壓縮檔的功能應來
common/util/detection_profile_archive.py復用,不要各自再寫一套。 - url 型平台不驗證可達性、不代管憑證:登記時只驗
http(s)://形狀。連結打不打得通、要不要憑證,是使用端網路與授權問題——此界線在設計即明寫(含 FE hint 文案),別當缺陷回報;需求出現再立案。 - ⚠️ 本功能現況只在 DEV:表 / capability / 選單路由 / param_schema 升版 / 搬遷 seed 全數只套 DEV(環境異動鐵律)。param_schema 升版 migration 尤其不可單獨誤套 STG / POC——它拿掉靜態 options,而該兩環境沒有搬遷 seed 資料,套下去任務抽屜下拉會直接空掉;必須整批依序等決策者放行。
- 同名檢查是業務層前置、唯一索引是兜底:
find_by_exact_name查同租戶同工具現行名稱;公用版與某租戶自有版可以同名(不同 tenant_id 池)——fork 預設沿用原名能成立正是因為這點。
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟 - 測試帳號:dev 環境持
detection-profile.create/deletecapability 的帳號(比照流程範本管理授予的租戶系統管理員角色;公用版維護需 root 租戶平台管理員),密碼見.env/ 部署文件 - 導航路徑:登入 → 合規稽核 →
「掃描設定檔管理」(
/plugin/detection-profile-manage,選單掛在檢測工具管理與流程範本管理之間) - 前置資料:
config.detection_tools已 seed(工具下拉來源);公用版 10 筆由scripts/seed_2026-08-01_fr059_detection_profiles.py落庫(需 BE 環境可連 DB 執行);上傳測試可用content/detection-profiles/下任一 TWGCB 目錄自行打包 tar.gz - 驗證要點:上傳四格式正常樣本全過、惡意樣本(假副檔名 / 超大小 / traversal / 無 inspec.yml)各回獨立 error code;版更後 menu 指新版且舊版留存;fork 後副本可編輯;非 root 對公用版寫入 403;租戶 A 看不到租戶 B 自有列(RLS);任務抽屜 GCB 池只見 gcb、CINC 只見 inspec
- E2E:
compliance-manager-test/repo(Cucumber + Playwright);以detection-profile/掃描設定檔搜尋 - 相關文件:需求討論稿(決策脈絡與 mermaid
流程圖)
docs/features/FR-059-2608-detection-profile-library/discussion.html;設計決策全文(P1–P7 + D1–D10)同資料夾design.md;下拉動態化與三層取值來源見 任務配置;執行紀錄的 profile 標籤對照見 我的任務 §11.1;工具目錄與 param_schema 兩層 schema 關係見 檢測工具管理;使用手冊docs/user-manual/scan-profile-guide.md