跳轉到

AO 任務配置與啟動(Task Config)

功能群:專案管理|上層見 專案規劃頁(本頁=其 Tab0「控制項實作」的 AO 任務配置,規劃頁功能總覽第 4/5/6 項)

事實基準:2026-07-06 從 FE / BE code 掃出;表結構對 db_schema.json、endpoint 對 routes.json、欄位對 marshmallow serializer + service 組裝碼逐條核

變更紀錄: - 2026-10-14(FR-137):任務可設週期——任務表單新增「週期」摘要列與「週期設定」Dialog(重複開關、頻率讀系統選單 task_frequency、每 N 個單位、下次啟動可選到時分;檢測工具任務另有自動執行與高風險額外通知對象)。時間到就在同一筆任務上再跑一輪,不產生新任務;建專案時資源庫的頻率帶成預選(未啟用)。見 §5「任務週期」、§6.2 recurrence、§9.3、§11 - 2026-10-05(FR-129):分派列的掃描目標不再收自由文字——逐台型欄位(CINC Auditor/GCB/OpenSCAP 的 hosts、Nmap 的執行主機 hosts)改設備多選、存設備 uid 陣列,派工時才換成 IP;網段型欄位(Nmap targets、OpenVAS hosts)仍填 CIDR/範圍,但每段必須整段落在授權掃描網段的已授權網段內(DETECTION_TOOLS_400024),主機名稱一律不收。存檔、開始執行、重跑都驗。升級時既有任務的手打 IP 自動搬進設備清冊,網段外的網段自動建「待確認」網段 - 2026-09-19(FR-112):一個 AO 可以有多筆任務,且可在規劃頁直接新增/刪除——新增只填名稱即可存,後端自己在流程圖上掛成一條平行分支(誰先做都行,全部做完該 AO 才算通過);刪除兩個入口(規劃頁刪除鈕/流程圖編輯器刪方塊存檔)共用同一支刪除邏輯。兩者只在規劃期開放(GRC_412054)、每個 AO 至少保留一筆(FLOW_ENGINE_412003)。版面改 Master-Detail(籤列選一筆、右側單一表單)。連帶:流程圖只存拓樸與任務編號,任務資料一律以 DB 為準(圖上舊 camunda 屬性不再寫也不再讀);流程圖編輯器屬性面板只剩「名稱」、存檔改由後端三向 diff 決定增刪(§5「新增與刪除任務」、§6.2、§12 坑 16~19) - 2026-09-14(FR-094.4a):指派人員下拉只列本租戶帳號——/users/menu 預設帶作用租戶條件,跨租戶帳號不再出現在候選名單(不支援跨租戶指派,見 §5 欄位 5 備註) - 2026-08-26(FR-067 驗收回饋,v1.16.0 後續補項,歸下版):分派列摘要與執行確認框的目標顯示改「原始寫法+真實台數」(192.168.50.1-123(123 台))——台數由 BE 算好供應(scan_target_spec.display_counts_of() 唯一入口,FE 不複製展開規則;CIDR 依欄位分流:展開型算可用主機 /24=254、透傳型算整段 256;range 無歧義一律算;逐台寫法不給值);FE 以 BE 回的 scan_target_spec 字串比對畫面現值做過期防護(改了目標未存檔退回段數顯示)。詳見 §5(CM-1395) - 2026-08-25(FR-067.8):檢測任務改「每列自足」模型——原本「任務層共用掃描參數+分派列覆寫」的兩層形狀取消,改為每列=一份完整獨立的工作設定:任務層掃描參數區整塊移除(只留完成模式),每個 Agent 分派列展開後直接渲染該工具的整份掃描參數(目標/連線方式/掃描基準/SonarQube 模式各列自填),「留空沿用任務層」語意全數消失。連帶:分派列改摘要列+點開展開卡片(設定項太多,攤平認不出哪列在做什麼);每列可「複製此列」、新增列預設帶第一列的值(密碼欄刻意不抄);最少保留一列,agent 留空=執行時自動挑一台在線的(原「零列=自動分派」的載體改變);SonarQube 同一任務可一列 pull、兩列 upload 各掃各的源碼包(上傳槽逐列,見我的任務 §11.1)。新增驗證:逐列必填(依該列當下模式判斷該不該驗,GRC_400113)+project_key_suffix 跨列唯一(兩列落同一 SonarQube 專案會互相覆蓋結果,GRC_400114)。舊資料相容走讀取端合併,不做 backfill。詳見 §5「Agent 分派區塊」 - 2026-08-25(FR-067,驗收第二輪):分派列新增連線方式(transport)覆寫——混合 OS 場景下同一任務的不同列可各走各的通道(Linux 走 SSH、Windows 走 WinRM),不必為每種 OS 各開一個任務;只對 param_schema 真有 transport 欄位的工具(CINC Auditor/GCB)出現,工具不支援 GRC_400109、值不在選項內 GRC_400110。掃描目標欄支援 CIDR 與末段範圍(192.168.50.0/24、192.168.50.1-50 可與單一 IP 混用)——⚠️ 展開與否依「欄位」而非「工具」:逐台 SSH 登入型的欄位(CINC/GCB/OpenSCAP 的 hosts,以及 Nmap 的 hosts——那是執行主機不是掃描目標)BE 展開成逐台清單且有台數上限(預設 256,DETECTION_SCAN_TARGET_MAX_HOSTS);原生解析型(Nmap 的 targets、OpenVAS 的 hosts)原樣透傳且不套上限。格式錯 GRC_400111(訊息帶出錯的那一段原文)、超上限 GRC_400112。工具參數區的 hosts/targets 欄說明由 param_schema.hint 承載(migration f10e45eb,措辭依上述分流分三段) - 2026-08-25(FR-067):檢測任務多 Agent 分派——檢測工具下拉正下方新增「Agent 分派」區塊(每列=agent+掃描目標+可選 profile 覆寫+⏱ 排程 chip;零列=自動分派 fallback),hosts 型工具有分派列時參數區 hosts 欄隱藏(掃描目標單一入口);單一目標型工具(ZAP/SonarQube)分派列不渲染目標欄(依 param_schema 有無 hosts 判斷);同一 agent 可開多列各掃各基準(CM-1379,BE 擋完全相同的重複列);openscap/inspec/gcb 的 profile 欄位改純下拉(select_or_text → select,手填實務上只產生錯誤輸入);測試連線可指定 agent(CM-1377)。詳見 §5「Agent 分派區塊」 - 2026-08-01(FR-059):profile 下拉動態化——CINC Auditor / GCB 的 profile 欄位改由「掃描設定檔庫」動態餵選項:param_schema 升 v2 加 "options_source": "profile_library" 宣告並拿掉靜態 options,FE 偵測宣告改呼 profile menu API(帶工具 uid 過濾,GCB 池與 CINC 池分開);型別維持 select_or_text、手填能力保留。取值來源三層並存:庫內檔案型(profile:<uid> 參照)/ 庫內連結型 / 現場手填(既有任務存的舊靜態選項值照常可用)。設定檔的上傳與維護見新頁 掃描設定檔管理 - 2026-08-01(FR-058.5/.6/.7):檢測工具下拉新增 SonarQube(原始碼安全檢測 SAST),三種取源模式以 scan_mode 參數分流——pull 讀取既有結果/scan 掃描公開 Git repo/upload 上傳源碼壓縮包(各自參數與適用情境見 §5「SonarQube 的掃描參數與三種執行模式」);scan/upload 為寫入行為(會在客戶 SonarQube 建立/更新 Guidant-AI- 前綴專案),任務卡加橘色警語區塊事前告知(D24 配套);upload 模式的壓縮包不在本頁上傳(於任務抽屜發起執行時提供,D26);upload 任務不進「開始執行任務」自動派發(D33,批次發佈 confirm 文案加註提示);condition.value 語法擴充支援陣列形式(一欄可同時綁多個模式值)。另 GCB 的 profile 下拉由 1 筆示範擴為 8 支 TWGCB 作業系統類基準(fr058-18;後續已由 FR-059 改為設定檔庫動態餵) - 2026-07-31(FR-058):檢測工具下拉新增四款(ZAP / CINC Auditor / Nmap / 政府組態基準 GCB),各自的掃描參數見 §5「四款新工具的掃描參數」;任務層敏感參數加密(param_schema 的 secret:true 欄位如 ZAP 登入密碼,DB 存密文、API response 整個 key 剝除、FE 靠 secret_keys_set 顯示「已設定」);掃描參數必填驗證(原本完全不驗、缺參數的任務照樣存檔到 agent 執行期才失敗,改為存檔前擋下且 condition-aware);CINC Auditor 的 profile 參數由 text 改 select_or_text - 2026-07-29(FR-057):檢測工具任務新增 select_or_text 掃描參數型態(下拉常用選項 + 自由輸入,OpenSCAP profile 欄位用);選定工具後有 default 值的參數欄位自動帶入預設值(onSelectDetectionTool 新行為,見 §5 / §10) - 2026-07-27(FR-056):任務類型新增 detection_tool(檢測工具執行)——選定工具後動態渲染掃描參數 + 完成模式(manual/auto);「開始執行任務」批次動作新增自動派 detection 任務首次掃描(見 §6.3) - 2026-07-06 FR-047 從 project-planning.md 拆出並納入快速配置 / 開始執行

1. 功能描述

AO 任務(prep-job)是「為每個控制項評估目標(AO)蒐集證據」的工作項。一個 AO 可以掛多筆任務,預設彼此平行——誰先做都行,該 AO 要全部任務做完才算通過。本頁涵蓋任務域的四件事:①新增/刪除任務、②單一任務逐欄配置、③快速配置(一鍵把多位人員批次併入所有可編輯任務)、④開始執行(PM 把已指派任務批次轉 PROCESSING 並通知)。

規劃頁 Tab0 的左樹選到某個 AO 後,右面板下方是任務區塊:上方籤列一籤一筆任務,下方顯示選中那一筆的完整表單(任務名稱 / 描述 / 指南、任務類型、指派人員、部門、設備、問卷、檢測工具設定)。只有一筆任務時籤列不顯示、表單佔滿。儲存以 PUT job 一次寫回,關聯欄位採 full-replace 語意(送 [] 清空、省略 = 不動)。快速配置與開始執行是 Header 的批次動作:前者 FE 端逐一 PUT 合併、後者呼叫專屬 start-task-execution endpoint。

任務資料的真相在資料庫 job_executions 與其關聯表;流程圖 BPMN 只存兩件事——誰先誰後的拓樸,以及每個方塊上指回任務的 job_execution_uid。圖上的 name 是後端單向寫的顯示副本;其餘 camunda 屬性(workDescription/guide/actionType…)是舊版留下的殘留,既不寫也不讀(見 §12 坑 16)。與 SSP 編輯不同,任務指派不受 SSP 凍結影響,只要輪次在 planning 階段、使用者對該節點有三層遞進的 manager 權即可編。

主要使用者:對該控制節點有 manager 權者(專案 / 群組 / 控制項任一層 manager)。

角色速覽(完整定義見 _overview §3):

角色 一句話
manager(三層任一) 專案 / 群組 / 控制項 manager — 可編此節點下的任務(canEdit 遞進,見規劃頁 §3)
參與者 auditor / reviewer / viewer — 可讀不可寫

1.1 功能總覽(本頁全部功能)

# 功能 說明 位置 詳述
0 新增任務(FR-112) 只填名稱即可存;後端自己在該 AO 的流程圖上插一個方塊、接成平行分支、回寫任務編號。限規劃期 任務區塊標題列「新增任務」(空狀態也有) §5「新增與刪除任務」/§6.2
0a 刪除任務(FR-112) 帶確認;清五張關聯表與問卷快照後硬刪,並把方塊從流程圖上移掉。限規劃期、每個 AO 至少保留一筆 任務表單內 §5「新增與刪除任務」/§6.2
0b 切換任務(FR-112) 多筆任務以籤列切換,選中那筆才顯示表單;只有一筆時籤列不顯示 任務區塊籤列 §5
1 編輯任務基本文案 名稱 / 描述 / 指南 + 類型(general / survey) 任務表單 UC-TE-01
2 指派人員(含審核人) MultiSelect 指派;survey 型可標審核人(is_approver) 任務卡 UC-TE-01
3 部門 / 設備關聯 MultiSelect 綁部門(org_unit)/ 設備(device) 任務卡 UC-TE-01
4 問卷關聯(含審批) 僅 survey 型;MultiSelect 選問卷,每張可標「審批要求」 任務卡 UC-TE-01 / §12
5 儲存任務 一次 PUT,關聯 full-replace,同步 BPMN 每卡「儲存」 §6.2
6 快速配置 多選人員一鍵 additive 併入所有可編輯 AO 的任務(FE 逐一 PUT) Header 按鈕 → dialog UC-PP-02 / §12
7 開始執行任務 PM 把「已指派且 TODO」的任務批次轉 PROCESSING 並通知(idempotent);FR-056:detection_tool 型任務一併自動派第一次掃描(scan_mode=upload 者除外——自動路徑不帶檔案,D33,見 §6.3) Header 按鈕(限 PM) UC-PP-02 / §6.3
8 檢測工具綁定與完成模式(FR-056) 任務類型選「檢測工具執行」後:選定工具(config.detection_tools)+完成模式(manual/auto)。⚠️ FR-067.8 起掃描參數不在任務層——整份參數搬進每個 Agent 分派列(見 8a),任務層只剩這兩項 任務卡(jobType==='detection_tool' 條件層) §5 / §6.2
8a Agent 工作指派(FR-067/FR-067.8) 每列=一份完整獨立的工作設定:agent+該工具整份掃描參數(目標/連線方式/掃描基準/SonarQube 模式各列自填)+⏱ 排程時間。收合為摘要列、點開展開卡片;可複製列;最少一列(agent 留空=執行時自動挑)。同 agent 可多列(混合 OS 各掃各基準);secret 參數加密存放、必填驗證逐列且 condition-aware(FR-058 機制對到列上) 任務卡「Agent 工作指派」區塊(檢測工具下拉正下方) §5「Agent 分派區塊」
8b 任務週期(FR-137) 摘要列顯示「每月 · 第 N 輪 · 下次啟動 …」;按「設定週期/設定」開 Dialog 設重複、頻率、間隔、下次啟動;檢測工具任務另設自動執行與高風險額外通知對象。Dialog 確定只改表單,仍要按「儲存」 任務表單(一般任務在描述/指南後;檢測任務在檢測設定區) §5「任務週期」/§6.2

UC(§2)展開兩條核心路徑:UC-TE-01(單一任務逐欄 inline 編輯,含 M2M / 審核人坑)與 UC-PP-02(快速配置 → 開始執行的批次流)。快速配置 ⚠️ 有清空關聯的坑(見 §12)。

2. Use Case

角色速覽(同 §1)。流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。

UC-TE-01 編輯 AO 任務

項目 內容
角色 對該節點有 manager 權者
前置條件 輪次 AP=planning;已載入 control-tree(任務內嵌於樹);canEdit 為真
產出 / 後置條件 寫 job_executions(本體)+ reconcile 四關聯表(指派 / 部門 / 設備 / 問卷);同步 BPMN userTask;名稱變更 best-effort 觸發 Drive 改名

UC-TE-01 流程:展開任務卡 inline 編輯(canEdit 三層 manager,否則 disabled)→填基本欄+指派/部門/設備→survey 型顯示問卷+審批+審核人 switch→儲存時 FE 檢查審批問卷需審核人(缺 toast 不送)→PUT job(route 僅 JWT 無角色守門)→job 不存在 GRC_404005→full-replace 寫 job_executions + 四關聯表→同 transaction 同步 BPMN→回 DTO、名稱變更觸發 Drive 改名

UC-PP-02 快速配置與開始執行

項目 內容
角色 快速配置:有編輯權者;開始執行:專案 manager
前置條件 輪次 = planning;AO 下已有任務(BPMN 範本衍生的 prep-job)
產出 / 後置條件 快速配置:對每個可編輯任務 additive 併入指派人員後逐一 PUT job;開始執行:compliance.task_assignees 等已備妥後 job_executions.status TODO→PROCESSING、站內通知送出

UC-PP-02 流程:指派→儲存(full-replace)→PM 批次啟動(idempotent 可重按);審核人檢查只在 FE

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
讀任務 參與者即可(隨 control-tree 一起載) require_participant(讀樹) 規劃頁 control-tree 路徑
編輯 / 儲存任務 三層遞進 canEdit(專案 → 群組 → 控制項 manager,ProjectPlanningView.vue:663-679) ⚠️ 無角色守門(route 僅 @jwt_required()) api/grc/routes/job_route.py:98(route);service app/grc/service/job_service.py
新增 / 刪除任務(FR-112) 同三層 canEdit,且當前輪須在規劃期、刪除時該 AO 須多於一筆 ✅ 專案 manager(_require_manager)+規劃期守門(_assert_round_in_planning) app/flow_control/service/job_service.py::create_job:166-167 / delete_job:212-213
流程圖編輯器存檔(連帶增刪任務,FR-112) 能開編輯器即可存 ✅ 沿用端點既有的 module-frame.update 能力守門,不問專案 manager app/flow_control/service/workflow_xml_sync_service.py::sync(),接在 PUT /module-frame/item/xml/{uid}

存檔端點為什麼不問專案 manager:那個端點只拿得到範本 uid,而範本反查專案在現況資料上多半查不到(DEV 實查:有流程實例的 11,727 張範本只有 1,138 張反查得回專案)。硬要反查等於讓大多數存檔直接 404。租戶隔離由 RLS 負責。 | 快速配置 | 同編輯(逐一 PUT job) | 同上(每筆 PUT 無角色守門) | FE executeQuickConfig(ProjectPlanningView.vue:1192) | | 開始執行任務 | isProjectManager 才顯示按鈕 | _check_manager(須專案 manager,否則 GRC_403002) | app/grc/service/task_execution_service.py(_check_manager:8-17) |

⚠️ FE / BE 權限不對齊:FE 用三層遞進的 canEdit 擋按鈕,但 PUT job route 層只有 JWT 登入、無任何角色檢查——任一登入者繞過 FE 直接打 API 都能改任務指派。與規劃頁 §3 / §12 記載的同一 finding。唯有「開始執行」有 BE manager 守門(GRC_403002)。

4. 狀態機與前置條件

承接輪次階段 gate(見 規劃頁 §4):

條件 效果 出處
apStatus === 'planning' 任務可編(不受 SSP 編輯凍結影響) 規劃頁 FE 行為
!canEdit(非三層 manager) 任務表單全部欄位 disabled AoTaskForm.vue(canEdit 綁 :disabled)
類型 = survey 才顯示問卷 MultiSelect + 審批 / 審核人 switch 同檔(jobType==='survey' gate)
當前輪不在 planning(FR-112) 新增/刪除鈕停用,原因常駐顯示;直打 API 回 412 GRC_412054 FE taskMutationBlockedReason(ProjectPlanningView.vue:1467-1474);BE job_service._assert_round_in_planning()
該 AO 只剩一筆任務(FR-112) 刪除鈕停用,原因「查核項目至少要有一個任務」;直打 API 回 412 FLOW_ENGINE_412003 FE canDeleteTask(aoId)(同檔 :1483-1485);BE 套件 remove_user_task_from_xml()

「當前輪」=round_no 最大的那一輪,不是清單第一筆。第一輪已結案、第二輪在規劃期時要放行;判斷規則與既有兩處凍結判定(workflow_execution_service._assert_round_not_frozen_for_workflow、flow_control_job_repo._is_round_frozen_for_job)同源,沒有第二套。查不到專案或專案還沒有任何輪次 → 放行(規劃早期本來就沒有輪次可凍結)。

🔴 凍結後「調線」仍然存得下去:改走向不動稽核範圍,只有增減任務才擋。流程圖存檔那道守門因此寫成「這次存檔真的要增減任務時才問」(workflow_xml_sync_service.py:78-79)——寫成無條件問的話,凍結後連純粹調整順序都會被擋掉。

5. UI 設計

版面骨架(Header 批次動作 + AO 任務卡 inline 編輯,含 survey 型條件層 + 代表性 API):

AO 任務配置版面骨架:Header 快速配置/開始執行+任務卡 inline 編輯(基本欄/指派/部門/設備/survey 型問卷審批審核人)+PUT job 代表性 API

📸 截圖待補(亮色模式):AO 面板 + 任務卡展開(general 型)taskedit-general.png、survey 型(含問卷 + 審批 + 審核人 switch)taskedit-survey.png。(規劃頁 §5 已有 planning-ao-tasks.png 概覽可參考)

任務區塊的版面(Master-Detail,FR-112)

一個 AO 底下可能有多筆任務,版面是「上籤列、下表單」:

區 內容 檔
標題列 「任務 (N)」+「新增任務」鈕 AoTaskSection.vue:161-165(空狀態時另一顆在 :186-190)
提示條 不能增減任務時的原因;任務超過 20 筆時的可讀性提示 同檔 :171-177
籤列 PrimeVue TabView,一籤一筆(狀態點/名稱/指派摘要/類型)。只有一筆時不顯示 同檔 :213-224(showList 判 tasks.length > 1,:66)
表單 AoTaskForm.vue,只 mount 選中那一筆 同檔 :225-241

選取行為:每個 AO 各記一筆選取,回到看過的 AO 會停在上次那筆;沒選過的選第一筆;新增後自動選中新那筆並聚焦名稱欄;刪除後選相鄰一筆(回第一筆會讓人在長清單裡失去位置)。任務順序依建立時間(ORDER BY je.created_at ASC, je.id ASC,ssp_control_implementation_query.py:77/113,id 為同秒 tie-break)——少了這個排序,順序就看 DB 回傳,平行新增多筆後畫面上會跳。

資料生命週期留在父頁:建立/刪除/儲存與所有 API 呼叫都在 ProjectPlanningView.vue,子元件一律 emit——taskMap 只能有一個寫入者。分派列的參數組裝與比對抽成 src/utils/detectionAssignmentParams.js:存檔擋門在父頁、列內渲染在表單,同一份規則寫兩處一旦漂移,畫面顯示的參數會與實際派工的不同而且沒人會發現。

任務週期(FR-137)

任務表單有一列「週期」摘要(TaskRecurrenceSummary.vue):未啟用顯示「不重複」+「設定週期」鈕;啟用後顯示「↻ 每月 · 第 N 輪 · 下次啟動 YYYY-MM-DD HH:mm」,檢測工具任務再接「自動執行」「高風險額外通知 N 個對象」,旁邊「設定」鈕。按鈕開「週期設定 — 任務名」Dialog(TaskRecurrenceDialog.vue):

欄位 控制項 規則
重複執行 開關 關=不重複;關閉時頻率保留當預選,下次啟動清空
頻率 SelectButton 選項讀系統選單 task_frequency:每小時/每日/每週/每月/每季/每半年/每年;開啟時必填
間隔 「每 [n] 個週期一次」,1~99 預設 1
下次啟動時間 日期+時分(24 小時制) 開啟時必填;改這格只影響下一輪,之後依頻率往後推
自動執行 開關(限檢測工具任務) 開=到時直接開掃。SBOM 上傳型分派列還沒上傳過源碼包時,Dialog 警示「需先手動執行一次」
掃到新出現的 high(含)以上時,額外通知 對象種類(專案參與人員/系統責任角色)+對象下拉+「加對象」,已加的以 Chip 列出可移除(限檢測工具任務) 同一對象不重複加;最多 50 個

非檢測工具任務的 Dialog 只有前四欄,底部提示「到了啟動時間,這筆任務會重新開啟並通知指派人員;自動執行與高風險通知只有檢測工具任務有。」Dialog「確定」只回寫表單 state,存檔仍走任務表單的「儲存」(PUT job 帶 recurrence)。

建專案時的預選:資源庫的「頻率」在建專案生成任務時帶進每筆任務的 recur_unit,但下次啟動留空=未啟用,摘要列仍顯示「不重複」,打開 Dialog 頻率已選好(prep_job_generation_service.py,覆核輪不帶)。資源庫基本資料的頻率下拉與本 Dialog 讀同一組系統選單。

到期後發生什麼(背景,見 §11):在同一筆任務上再跑一輪——已完成先重開成進行中;開了自動執行的檢測任務直接開掃;寄信提醒執行人;下次啟動往後推一格。不會產生新任務。抽屜上怎麼顯示見我的任務 §11.4。

新增與刪除任務(FR-112)

新增:只填名稱即可存。前端不帶流程圖節點編號,後端自己做四件事(同一交易)——查該 AO 的範本 XML → 呼叫套件把新任務掛成一條平行分支 → 建 job_executions 一筆 → 把新任務編號注回那個方塊、寫回範本 XML。

新方塊掛在哪,由圖現在長什麼樣決定(套件的 append_parallel_user_task_to_xml() 自己判斷,呼叫端不必先分辨圖形):

圖現在長什麼樣 掛法 結果
只有一個任務(專案啟動後的預設樣子) 頭尾補上分叉/合流閘道 開始 → 分叉 → 兩個任務並排 → 合流 → 結束
已是標準平行 沿用既有閘道 分叉出線+1、合流入線+1
使用者手動排成一條龍 頭尾補閘道,掛成合流前的新平行分支 既有先後順序完全不動

為什麼不一律重排成標準平行:使用者若刻意排成「先做 A 才能做 B」,一按新增就被重排回全平行,等於系統擅自推翻他的設計,而且沒有任何提示。

既有節點的 id 與任務編號全程不動:引擎是用節點 id(template_job_id)查任務的,重新產 id 等於既有任務與圖失聯,且沒有錯誤訊息。

新任務的初始狀態依上游拓樸判定(不是依數量):

新節點的上游 初始狀態
開始事件,或分叉閘道 PROCESSING(可以馬上做)
另一個 userTask(使用者排的一條龍中段),或合流閘道之後 TODO(還沒輪到)
拓樸解不出來(圖壞了/孤兒節點) TODO(寧可讓人看到「還沒輪到」,也不要把接在別人後面的任務直接開成進行中)

刪除:帶確認。清 task_assignees / job_execution_org_units / job_execution_devices / job_execution_surveys / job_execution_comments 五張關聯表與問卷快照、survey.task_surveys,再把方塊從圖上移掉,最後硬刪 job_executions 那列(不是改成 CANCEL)。平行分支刪到只剩一條時,分叉與合流閘道一併收掉、接回線性。

兩個入口、同一支刪除邏輯:規劃頁的刪除鈕與流程圖編輯器「刪方塊後存檔」走的是同一支——差別只有守門(見 §3)。繞過去自己寫一套的症狀是「任務不見了但關聯資料還在」,而且沒有錯誤訊息。

現況允許刪掉有進度的任務:PROCESSING/COMPLETED、已有證據、已指派負責人都刪得掉——本頁只列現況,此規則未隨 FR-112 改變。

流程圖編輯器存檔的三向 diff(FR-112)

編輯器存檔時只送一份新 XML 上來,任務的新增與刪除都由後端比對出來。後端拿新 XML 的 userTask 節點,對照該流程實例現有的任務清單,逐一歸成五種處置:

圖上的節點 DB 有沒有對應任務 判定 後端做什麼
有節點、沒有任務編號 — 新增意圖 建一筆空任務(名稱取圖上的 name)→ 把編號注回節點
有節點、有編號、查得到 ✅ 保留 只存拓樸,任務資料一個欄位都不動
— ✅ DB 有、新 XML 裡找不到它的編號 刪除意圖 呼叫同一支刪除邏輯
有節點、有編號、但 DB 查不到 ❌ 錯誤 擋下存檔(GRC_400125),訊息指出是哪個節點
一個節點對到多筆任務 ✅ 多筆 錯誤 擋下存檔(GRC_400127),訊息指出是哪個節點對到哪幾筆

🔴 「XML 裡少了某個編號」是刪除,不是孤兒。把它當錯誤擋下等於宣告「在圖上刪方塊沒有用」,而編輯器的定位就是管走向、刪方塊正是改走向。孤兒的定義因此收窄成上表第四列:帶著一個查不到的編號。

一個節點對到多筆任務不可判成刪除意圖靜默清掉:圖上一個節點只放得下一個編號,同一節點的其餘那幾筆在新 XML 裡找不到自己的編號,會被判成刪除——而哪一筆才是有證據的那筆要由人決定。這種資料是舊版逐節點打 API 的編輯器留下的殘留。

另外兩項驗證,任一不過就擋下並指出哪個節點有問題:

檢查 不擋的症狀 error code
任務編號不重複 使用者複製貼上方塊時連帶複製了別人的任務編號 → 兩個方塊指向同一筆任務,完成其中一個另一個跟著變狀態 GRC_400124
每個任務節點從開始事件走得到 接線接錯、方塊沒連上 → 完成前一個任務時引擎找不到它,流程卡住 GRC_400126

整個 diff(建/刪/寫 XML/寫翻譯表)在同一交易內——中途失敗時圖與任務一起回到存檔前的狀態,不會出現「任務刪了但 XML 沒存」。「刪完會剩零任務」是在動任何東西之前先算的,被擋下時一筆都不刪。

流程圖編輯器的能力範圍(FR-112)

還能做 不能做
改線(把平行拉成一條龍、加二選一分支、調整順序) 填任務類型、問卷、設備、部門、工具參數(回規劃頁填)
拖新方塊進來(只填名稱) 直接改任務的負責人與狀態
刪方塊(=刪任務,與規劃頁刪除等效) —

屬性面板只剩「名稱」一個欄位(WorkflowSetupEditor.vue:486-501)。存檔只做一件事:把 XML PUT 上去,拿後端回的新 XML 重新載入圖(:336-361)——前端不再逐節點打新增/更新任務端點。刪了方塊的存檔會先跳確認,告知那些任務會一併刪除(:295-311,比對載入時的任務編號與當前圖上的)。

任務卡表單(AoTaskForm.vue)。欄位:

# i18n label 型別 必填 列舉 / 來源 payload 欄位 disabled 備註
1 task_name_label 任務名稱 InputText 否 — name !canEdit
2 task_desc_label 描述 Textarea(autoResize) 否 — description !canEdit
3 task_guide_label 指南 Textarea(autoResize) 否 — guide !canEdit
4 任務類型 SelectButton 否 general / survey job_type !canEdit 切 survey 才顯示問卷區
5 assignees_label 指派人員 MultiSelect(chip) 否 /users/menu({id,name}) assignees[{uid,is_approver}] !canEdit dataKey=id;候選名單只列作用租戶帳號(不支援跨租戶指派,FR-094.4a)——要讓別租戶的人參與,由該租戶管理員把人加進租戶並給角色,切租戶操作
5a is_approver_label 審核人 InputSwitch(每位人員) 否 — assignees[].is_approver !canEdit 僅 survey 型且有問卷開審批時顯示
6 departments_label 部門 MultiSelect 否 /org-units/menu({id,name}) department_uids[] !canEdit
7 devices_label 設備 MultiSelect 否 POST /devices({id,name}) device_uids[] !canEdit
8 surveys_label 問卷 MultiSelect 否 /surveys/menu({id,name}) surveys[{uid,approval_required}] !canEdit 僅 survey 型顯示
8a approval_required_label 審批 InputSwitch(每張問卷) 否 — surveys[].approval_required !canEdit 與問卷名並列
9 檢測工具(FR-056) Dropdown 是(detection_tool 型必選) 工具目錄選單(menuStore.detectionToolMenu,來源 GET /detection-tools) tool.uid !canEdit 僅 job_type==='detection_tool' 顯示;未選不可儲存(toast warn_tool_required)
9a 掃描參數 依 param_schema 動態渲染(DetectionConfigField,型態含 text/number/select/select_or_text/password) 依欄位定義,存檔前實際驗證(FR-058;FR-067.8 起改逐列驗、condition 依該列參數求值) GET /detection-tools/{uid}/param-schema FR-067.8 起:tool.agent_assignments[].scan_targets(每列一份完整參數);tool.params 僅存量資料的讀取合併底與內部 sticky 欄位承載,FE 存檔時刻意整個不送 !canEdit ⚠️ 不在任務層渲染——整份參數在每個 Agent 分派列的展開卡片內(見下方「Agent 分派區塊」)。選定工具當下欄位 default 值自動帶入的行為(FR-057)仍在,作用於列;各工具參數見下方「四款新工具的掃描參數」與 檢測工具管理 §6.2
9b 完成模式 SelectButton 否(預設 manual) manual / auto tool.completion_mode !canEdit manual=掃完留 PROCESSING 等人工;auto=掃完自動完成
9c Agent 工作指派(FR-067/FR-067.8) 列表(每列收合=摘要列:agent 名/目標/transport・profile chip/⏱ 排程 chip/複製列/刪除;展開=整份 param_schema 表單)+「新增一組」 最少一列(剩一列時不可刪);列內必填逐列驗(GRC_400113)、project_key_suffix 跨列唯一(GRC_400114)。agent 留空合法=執行時自動挑一台在線的 GET /remote-agents?capability=detection_scan + GET /detection-tools/{uid}/param-schema tool.agent_assignments[]({uid?, agent_uid, scan_targets, sort_order, scheduled_at?},scan_targets 承載該列完整參數) !canEdit 僅 detection_tool 型顯示,位於檢測工具下拉正下方;詳見下方「Agent 分派區塊」

select_or_text 型態(FR-057 新增):PrimeVue editable Dropdown——下拉列常用選項,也可直接輸入清單外的自由字串。FR-058 把 CINC Auditor 的 profile 由 text 改為 select_or_text。渲染元件與檢測工具管理 §5 設定表單共用同一 DetectionConfigField.vue。 ⚠️ FR-067(2026-08-24 拍板)起 openscap/inspec/gcb 三支的 profile 欄位改回純 select:手填實務上只產生錯誤輸入——PrimeVue editable Dropdown 打字不會過濾選項(清單不動、看起來像壞掉);openscap 的值是 XCCDF profile id,必須存在於目標主機的 SSG content 內,手打幾乎必錯;庫(含自行上傳)已是主要來源。migration 用 jsonb_agg 只改 type 保住欄位順序,FE 零改(select 與 select_or_text 共用同一份 selectOptions)。select_or_text 型態本身保留供其他欄位使用。

profile 下拉動態化(FR-059):欄位定義帶 "options_source": "profile_library" 宣告時(CINC Auditor / GCB 的 profile 欄位,param_schema v2 起),DetectionConfigField 不再讀欄位內的靜態 options 陣列,改呼 GET /detection-tool-profiles/menu?detection_tool_uid=(帶當前工具 uid——GCB 抽屜只見 gcb 池、CINC 只見 inspec 池)動態組選項;選項的 value 是 BE 組好的 profile:<uid> 庫內參照,派工時展開。選項載入走 src/composables/useDetectionProfileOptions.js 的模組層級快取(規劃頁 N 個任務列共用、同工具只打一次 API);menu 拉不到時顯示警示但欄位照常可手填(不擋畫面)。取值來源三層並存:庫內檔案型 / 庫內連結型 / 現場手填(URL、Agent 本機路徑、Supermarket 名稱——不進庫、單次執行用;既有任務存的舊靜態選項值屬手填路徑,升級後照常可用)。回顯相容:值不在選項內時 editable Dropdown 原樣顯示該字串;已停用的 profile:<uid> 參照補一個唯讀 fallback 選項標示「庫裡已沒有的項目」,不露裸 UUID(DetectionConfigField.vue::profileOptions:82-92)。沒有宣告的欄位維持靜態 options 行為零影響;機制 tool-agnostic,新工具 seed 宣告即接庫、FE 零改。設定檔的維護見 掃描設定檔管理。

Agent 分派區塊

檢測任務以「Agent 工作指派」清單設定「哪台 agent 用什麼參數掃哪段目標」,支援多台 agent 各管一個網段、混合 OS 各掃各基準、SonarQube 多包混掃。區塊位於「檢測工具」下拉 正下方,其下只剩「完成模式」——任務層的共用掃描參數區已於 FR-067.8 整塊移除。

🔴 每列=一份完整獨立的工作設定(FR-067.8「每列自足」):列上帶該工具整份掃描參數, 沒有「留空沿用任務層」這回事。原本兩層合併(任務層墊底+列覆寫)的心智模型已取消—— 使用者看一列就是那一列實際會用的全部設定。

列的形狀:收合摘要 + 點開展開卡片

狀態 內容
收合(摘要列) agent 名稱/目標(含 CIDR 或範圍時顯示原始寫法+BE 供的真實台數如「192.168.50.1-123(123 台)」,CM-1395;逐台寫法顯示「N 台」)/連線方式與掃描基準 chip/⏱ 排程 chip(× 可移除)/「複製此列」鈕/刪除鈕
展開(卡片) 以 DetectionConfigField 渲染該工具整份 param_schema——condition 依本列參數求值(SonarQube 選 upload 就出現上傳相關欄位、選 pull 就出現 project_key)、secret 欄 per 列顯示「已設定」、profile 庫動態選項、advanced 分區照舊
  • 新增一組=新列直接展開;存檔驗證沒過的列強制展開+整列紅邊(錯誤欄位藏在收合區 裡的話,toast 說「第 2 組有問題」但畫面上那組看起來一切正常)。
  • 最少一列:剩一列時刪除鈕 disabled。agent 留空是合法值=執行時自動挑一台在線的 (原「零列=自動分派」的 fallback 載體改變,便利性保留)。
  • 複製此列+新增列預設帶第一列的參數值(非空白表單);密碼等 secret 欄刻意不抄—— 抄 key 名會讓新列誤顯示「已設定」,使用者不填就存進空密碼。
  • 摘要列的台數由 BE 供、FE 不算(CM-1395):FE 刻意不算展開後的總台數,展開規則 (是否排除 network/broadcast、哪些欄位不展開)是 BE 的分流邏輯,在 FE 複製一份就是第二套 真相——初版因此只顯示「N 段」,CM-1395 改由 BE display_counts_of() 算好台數隨分派列回傳 (含 scan_target_spec 原始寫法字串),FE 只負責印。只在「一段代表多台」時給台數 (CIDR/range);CIDR 依欄位分流:展開型算可用主機(/24=254)、透傳型算整段(256, 工具實際處理的量);逐台寫法段數即台數不另給。過期防護:FE 以 scan_target_spec 字串 與畫面現值比對,使用者改了目標未存檔時退回段數顯示,不顯示過期台數。執行前確認框同一原則 (agent(192.168.50.1-123(123 台))+ 立即/⏱ 時間開始)。

欄位重點(存 config.job_execution_detection_tool_agents 子表,PUT 走 tool.agent_assignments[] diff-sync;列 params 存該表的 scan_targets JSONB——欄位名沿用 未改,承載內容已從「覆寫子集」擴張為「完整列參數」):

欄 說明
Agent 下拉 資料源 GET /remote-agents?capability=detection_scan;離線機照列標「(離線)」不可選。同一 agent 可開多列(混合 OS 場景);BE 擋的是「(agent, 列參數內容) 完全相同」的重複列(GRC_400107)。留空=執行時自動挑一台在線的
掃描目標(逐台型) CINC Auditor/GCB/OpenSCAP 的 hosts、Nmap 的 hosts(執行主機):設備多選(param_schema type: device_multiselect,元件 DeviceMultiSelect,選項來自 GET /devices/menu,顯示「設備名(IP)」)。存的是設備 uid 陣列;存檔後設備被刪,畫面保留一項「已不在清冊」讓使用者看得到要移除。無 device.read 時下拉明講「沒有權限讀設備清冊」
掃描目標(網段型) Nmap 的 targets、OpenVAS 的 hosts:支援單一 IP/CIDR/末段範圍混用(192.168.50.0/28, 192.168.60.1-20, 192.168.70.5),分隔符吃逗號/分號/空白/換行;每段必須整段落在已授權網段內,主機名稱不收。欄位下方給語法說明
連線方式(transport) SSH/WinRM,per 列(同一任務 Linux 列走 SSH、Windows 列走 WinRM)。只對 param_schema 真有此欄位的工具出現(CINC Auditor/GCB)
掃描基準(profile) 選項源同工具參數區(FR-059 設定檔庫動態餵);content_path 收在該列 advanced 分區
SonarQube scan_mode 等 各列自選模式與對應欄位——同一任務可一列 pull、兩列 upload,兩個 upload 列各掃各的源碼包(上傳槽在我的任務抽屜逐列提供)
⏱ 排程 chip per-列排程時間(scheduled_at)——預設無時間欄位,按 icon 彈 datetime picker,選定顯示實心 chip 帶 ×。時間已過附註「(已過,將立即開始)」

目標授權(FR-129):分派列的目標只能來自系統內已登記的資料,兩種欄位兩條規則——

型態 欄位 規則 不合格
逐台型(device) inspec/gcb/openscap 的 hosts、nmap 的 hosts 每個 uid 必須屬本租戶、存在於設備清冊、IP 為內網位址;派工時換成逗號分隔的 IP 交給 agent 400 DETECTION_TOOLS_400025,訊息列出不合格的設備
網段型(zone) nmap 的 targets、openvas 的 hosts 每段(單一 IP/CIDR/範圍)必須是內網位址且整段落在本租戶 active 授權網段內;待確認網段不算 400 DETECTION_TOOLS_400024,訊息列出不在範圍內的那幾段
  • 哪個欄位是哪一型寫死在後端(jedi-detection common/target_field_kinds.py),不讀 param_schema 的 type:schema 是可改的 seed 資料,若由它決定要不要驗,改一筆 schema 就能把欄位變回自由文字。schema 的 type 只決定畫面怎麼畫。新工具帶目標欄位進來時兩邊都要登記。
  • 驗三次:存檔(validate_params)、開始執行與重跑(resolve_params,設備可能已刪、網段可能已收回)。判不出工具時,參數裡只要出現任何目標欄位鍵就拒。
  • 逐台型不另要求落在授權網段內:只驗「在清冊內且為內網位址」。存量任務遷移時自動建的設備可能不在任何網段內,要求落在網段內會讓存量任務整批無法執行。
  • agent 端再比一次:agent 1.4.0 起連線前比對心跳下發的准連清單(清冊內網 IP+已授權網段),雲端與 agent 用同一套寬窄,見檔案 Agent 管理 §6.6。
  • 給 agent 的 payload 形狀不變:hosts 仍是逗號分隔的 IP 字串,agent 不認得 device uid。

掃描目標的展開規則(BE,⚠️ 判準是「欄位」不是「工具」):同一份語法對不同欄位意義不同, BE 依「這個欄位的值最後被 agent 拿去做什麼」分流——

工具 欄位 BE 行為 為什麼
CINC Auditor/GCB/OpenSCAP hosts 設備 uid 換成逐台 IP 清單 agent connector 逐台建 SSH 連線
Nmap hosts 設備 uid 換成逐台 IP 這是 agent 登入去跑 nmap 的執行主機,不是掃描目標
Nmap targets 原樣透傳 nmap 原生解析 CIDR,併發探測遠比幾百個參數高效
OpenVAS hosts 原樣透傳 丟給 GVM 的目標規格,GVM 原生支援
  • 以下 CIDR/上限規則只適用網段型欄位的寫法解析與既有展開邏輯(逐台型已是設備清單)。CIDR 語意刻意與 nmap 不同:/24 展開成 254 台(排除 network/broadcast——用途是逐台 SSH 登入,那兩個位址登入必定失敗、在報告留下兩筆假失敗)。/31、/32 例外全數視為主機。
  • 台數上限只對展開型套用(預設 256,DETECTION_SCAN_TARGET_MAX_HOSTS;剛好容納一個 /24。要掃更大範圍的正解是拆成多列平行跑,不是調高)。OpenVAS 的 /16 是正常用法故不套。
  • 驗證只在寫入端(存檔時擋):格式錯 GRC_400111(訊息帶出錯的那一段原文)/ 超上限 GRC_400112。派工端刻意不擋只展開——擋在那裡使用者會在「按下執行」才看到 幾天前存的設定有問題,且存量資料或上限調小時舊任務會突然執行不了。
  • 跨段範圍(192.168.1.1-192.168.2.5)不支援但明確報錯,不會被誤當成主機名放行。
  • 工具參數區那格的說明走 param_schema.hint(措辭依上述分流分三段給:展開型講上限與拆列的 出路/Nmap hosts 強調是執行主機/透傳型刻意不提上限),與分派列的說明措辭一致。

逐列驗證(FR-067.8):

  • 必填 condition-aware 且逐列判:GRC_400113,訊息帶「第 N 列(缺哪些欄位)」——SonarQube 的 repo_url 只在 scan 模式必填,對 upload 列驗它會讓合法設定存不進去。FE 與 BE 用同一套 判定規則(BE common/util/detection_assignment_params.field_visible/FE src/utils/detectionFieldValidation.js),避免「畫面沒這個欄位、存檔卻說它必填」。
  • project_key_suffix 跨列唯一:GRC_400114。兩列落到同一 SonarQube 專案代碼,後跑完的 那次會覆蓋前一次的結果,使用者看到的是「有一列的掃描結果莫名其妙不見了」。
  • 測試連線可指定 agent(CM-1377):測試連線對話框加 agent 下拉(首項「自動選擇」=原 行為;離線機照列 disabled),開窗門檻=可選 agent >1 台;成功訊息帶實際執行 probe 的 agent 名稱。BE 對指定台走可派性診斷,不可派 400 一次列全原因(DETECTION_TOOLS_400018)。

存量相容(不做 DB backfill):舊資料是「任務層參數+列覆寫(或零列)」,讀取端做一次性 語意合併——每列讀出時任務層 params 墊底;零列綁定合成一列(agent 留空)。合成列沒有 uid, 使用者第一次存檔時才真正落庫(把隱含的 fallback 顯性化)。存檔時 FE 刻意整個不送 tool.params:BE 對未帶欄位是 None-skip,而任務層那份現在只剩兩個作用——存量讀取的合併底、 以及承載 _source_file(upload 源碼包 sticky 參照);送 {} 會把後者清掉,症狀是 upload 任務重掃時找不到源碼包,且要到執行當下才發現。

執行展開:每列直接以該列 params 派工(不再合併);profile:<uid> 庫內參照的展開仍在 派工當下做。執行紀錄的群組化呈現見 我的任務 §11.1。

四款新工具的掃描參數(FR-058,DEV 實查 config.detection_tool_param_schemas 當前生效版)

工具(code) 參數欄位
ZAP(zap) target_url(text,必填)/scan_mode(select,必填,預設 passive:被動掃描 / 主動掃描 / 登入後主動掃描)/login_url・login_username(text,必填,condition: scan_mode=authenticated)/login_password(password,secret: true,必填,同 condition)/logged_in_indicator(text,必填,同 condition)/timeout_sec(number,預設 3600)
CINC Auditor(inspec) hosts(device_multiselect,必填,從設備清冊多選、逐台各產一份報告、單台失敗不影響其餘)/transport(select,必填,預設 ssh:SSH(Linux 目標)/WinRM(Windows 目標))/profile(select_or_text,必填,FR-059 起帶 options_source: "profile_library" 宣告、選項改由掃描設定檔庫動態餵——原靜態預置的 dev-sec Linux/Windows 基準已搬入庫成公用版連結型,下拉內容零回歸)/timeout_sec(number,預設 3600,逐台各自計時非總時間)
Nmap(nmap) hosts(device_multiselect,必填,「執行主機」=Agent 登入並執行 nmap 的機器,從設備清冊選)/targets(text,必填,「掃描目標」=IP/網段,必須落在授權掃描網段內,不收主機名稱)/scan_type(select,必填,預設 connect:TCP 連線掃描(不需提權)/SYN 掃描(較快,需 sudo)/服務版本偵測(探測流量較多,需確認已獲授權))/port_range(text,預設 1-1024)/timeout_sec(number,預設 3600)
GCB(gcb) 與 CINC Auditor 同構(hosts/transport/profile/timeout_sec)。profile 同樣帶 options_source 宣告(FR-059)——下拉列掃描設定檔庫 gcb 池內容:8 支 TWGCB 公用版(Windows Server 2016/2019/2022、Windows 11、RHEL 8/9、Ubuntu 22.04 等,原 FR-058 靜態容器路徑選項搬遷而來)+租戶自行上傳的自有版

Nmap 的 hosts ≠ targets:其餘工具「登進去掃自己」,hosts 同時是登入對象與掃描對象;Nmap 是 Agent 以 SSH 登入 hosts 這台執行主機,再由該主機對 targets(可能是整個網段)送出探測封包。兩個欄位語意不同,任務執行抽屜 的摘要卡與執行詳情視窗因此分開顯示兩者。

ZAP scan_mode='authenticated' 對單頁式應用(SPA)不可使用:不只是掃不到——會引發登入無限重試,累積到 ZAP 保護門檻後 daemon 主動停止,連帶中斷其他租戶正在執行的掃描。詳見 檢測工具管理 §12 坑 14;ZAP 卡片的部署前提說明已明白標示。

profile 下拉一律用 tarball 形式(.../archive/refs/heads/master.tar.gz)而非裸 GitHub 網址:裸網址走 CINC 的 git fetcher,需 shell out 呼叫 git 查 default branch;實測把 agent 容器內的 git 移走後裸網址直接失敗、tarball 照常可用。下拉是平台預置的建議值,選少一個故障點的形式。

GCB 的 profile 選項演進:fr058-13 上架時僅 1 筆 Windows 最小示範(2 項);fr058-14 加入 TWGCB-01-011 Server 2022 完整基準後,fr058-16 把示範筆移出下拉(select_or_text 手填仍可用);fr058-18 擴為 8 支 TWGCB 作業系統類基準——Windows 組 4 支(007 Server 2016/009 Server 2019/010 Windows 11/011 Server 2022,各 540/539/108/558 項自動檢查)+ Linux 組 4 支(008 RHEL 8/012 RHEL 9/013 RHEL 9/014 Ubuntu 22.04 LTS,各 53/74/60/35 項)。排序先 Windows 後 Linux、組內照編號;012 與 013 是 NICS 官方兩份不同的 RHEL 9 基準(非重複),label 一律帶 TWGCB 編號區分。來源集合共 23 支,其餘 14 支(macOS 15、browser/application/network/cloud 類)可自動檢查項數皆 0 或標的形態接不上 SSH/WinRM transport,不上架。2026-08-01 起選項來源再改為掃描設定檔庫動態餵(FR-059 options_source,見變更紀錄),上述 8 支已搬遷為庫內 SYSTEM 公版。

SonarQube 的掃描參數與三種執行模式(FR-058.5/.6/.7,DEV 實查 param_schema v4 當前生效版)

SonarQube 是平台第一個拉取快照(pull-snapshot)與平台發動掃描並存的工具,以 scan_mode 參數在同一張工具卡內分流三種取源模式(D17 ZAP 式單卡多模式;憑證兩模式共用同一組 base_url+User Token):

模式(scan_mode) 誰發動掃描 源碼來源 對客戶 SonarQube 適用情境
scan 原始碼掃描(預設,D24) 平台(Agent 執行 sonar-scanner) 公開 Git repo(D19) 寫入——推分析結果,專案不存在自動建立 沒有 CI 整合、要掃「稽核當下」的源碼狀態
upload 上傳原始碼壓縮包 平台(同上) 使用者上傳的壓縮包(FR-058.7) 寫入(同上) 源碼在私有 repo 或不在 git(委外交付源碼包、特定版本快照)——客戶打包上傳即可,不必把 git 憑證交給平台
pull 讀取既有結果 客戶自己的 CI / 開發者機器 平台不碰源碼 唯讀——只取回最近一次分析結果 已有成熟 CI、SonarQube 天天在跑,只要把結果收進證據池

各模式的參數欄位(condition 綁 scan_mode,不可見欄位不必填也不送出):

key 型別 顯示條件 說明
scan_mode select・必填・預設 scan 永遠顯示 三選項如上表;hint 註明「上傳模式的檔案於發起執行時提供,設定頁不需填寫」
repo_url text・必填 僅 scan 公開 Git repo 的 HTTPS 網址;不支援私有 repo 認證(要掃私有源碼改走 upload)
project_key_suffix text・必填 scan 與 upload(condition.value 陣列形式 ["scan","upload"],FR-058.7 語法擴充) 只填後半段識別字;實際推上客戶 server 的專案代碼是 Guidant-AI-<此欄>,前綴由平台組上(D21——防撞號、可辨識來源)
project_key text・必填 僅 pull 客戶 SonarQube 上的既有專案代碼
branch text・選填 僅 pull 多分支為付費版(Developer Edition 起)功能;Community Build 留空即可,填了而 server 不支援回明確錯誤(D14)

三個必知限制與配套:

  • scan/upload 是寫入行為且沒有 dry-run——每次掃描都把結果推上客戶 SonarQube,專案不存在會自動建立。FE 於 scan_mode 為寫入模式時(含舊 v1 任務 scan_mode 未存值、實際生效預設 scan 的情況)在參數區塊顯示橘色警語(D24 配套①,ProjectPlanningView.vue::isWritingScanMode);setup_guide 另載收緊做法(預先建好專案+移除 token 帳號的 Create Projects 全域權限)。
  • 支援語言僅七種 A 類純源碼語言(Python/JS/TS/Go/PHP/Ruby/Kotlin,D18)——不需編譯即可分析。C#/VB.NET 永遠只能走 pull:這不是取捨而是架構限制——.NET 的分析發生在編譯器內部(Roslyn analyzer 隨 MSBuild 編譯執行),SonarScanner CLI 明文不支援,且送進去不會報錯、只會靜默淺掃(產出一份看似正常實際幾乎沒分析到的報告),故 setup_guide 以醒目警語要求改用 pull。Java/C/C++ 目前亦不支援。
  • 同一任務可混用三種模式(FR-067.8):scan_mode 是每列各自的參數,一列 pull、兩列 upload 各掃各的源碼包是典型用法(前後端各一包);project_key_suffix 因此必須跨列唯一,否則兩列落到同一 SonarQube 專案、後跑完的覆蓋前一次(GRC_400114 於存檔時擋)。
  • upload 的壓縮包不在本頁提供(D26)——任務設定只選模式;檔案於任務抽屜發起執行時上傳(壓縮包是「這一次要掃的源碼快照」,不是「任務怎麼掃」的設定),已上傳的檔案 sticky 保留供重掃沿用(D29)。格式與限額見任務抽屜頁 §11.1。

出處:param_schema v4 seed scripts/sql/2026-08-01-fr058-21-sonarqube-upload-scan-mode.sql(v1→v4 演進:2026-07-31-fr058-17(pull 兩欄)→ fr058-19(scan 模式+setup_guide 改寫)→ 2026-08-01-fr058-20(標籤精簡)→ fr058-21(upload 選項));FE 警語 ProjectPlanningView.vue:1008-1023;condition.value 陣列支援 src/utils/detectionFieldValidation.js+DetectionConfigField.vue(純擴充,字串形式行為不變)。

任務層敏感參數加密(FR-058.0)

⚠️ FR-067.8 起載體改為「列」:secret: true 欄位跟著 Agent 分派列走(DB 密文存在該列的 scan_targets、API response 剝除、FE secret_keys_set per 列顯示「已設定」)。下表機制逐項不變,只是作用範圍從「一個任務一份」變成「一列一份」。另複製此列/新列帶預設值刻意不抄 secret 欄——抄 key 名會讓新列誤顯示「已設定」,使用者不填就存進空密碼(與換工具時清空 toolSecretKeysSet 同一個理由)。

param_schema 的 secret: true 欄位(現況只有 ZAP 的 login_password)屬某一次掃描任務的機密,不是工具連線設定,只能落在 config.job_execution_detection_tools.tool_params(JSONB,與非機敏欄位混雜)。租戶層那套「整欄加密」無法直接套用,故另做「同一個 JSONB 內挑欄位加密」的機制(common/util/detection_secret_params.py):

環節 行為
寫入 job_service._encrypt_tool_secret_params() 把 secret 欄位包成自我描述 envelope {"__enc__": "fernet", "value": "<密文>"};欄位留空時從既有綁定沿用舊密文(否則使用者改任何其他參數都會清空密碼,且症狀要到掃描時才出現)
讀取(API response) JobToolResponseSchema.params 整個 key 不出現(非遮罩成 ****);另回 secret_keys_set(已設定值的 secret 欄位 key 名清單,只回 key 不回值)
FE 顯示 DetectionConfigField 以 hasExistingValue(secret_keys_set.includes(field.key))顯示「已設定,留空=沿用原值」,而非空白必填紅框——逼使用者重打密碼才是真正的洩漏誘因
派工 decrypt_secret_params() 還原明文下發給 agent;agent 用完即丟。解密失敗(金鑰輪替、資料損毀)剝除該 key 而非落密文——送密文過去 agent 會拿它當密碼登入,症狀是「掃描跑完但沒進到登入後頁面」,比缺欄位更難查
稽核 log 同樣走 strip_secret_params() 剝除

為何用 envelope 而非裸密文:密文存進 JSONB 後與明文同為 string,無法從值本身判斷是否已加密。若靠「查 schema 哪些 key 是 secret」決定要不要解密,正確性就綁在 schema 上——而 param_schema 是版本化的(is_current 切換),改版後舊任務會解錯。改存 envelope 後值本身就說明自己加密與否,schema 只用來決定「新寫入的值要不要加密」,不參與解密判定。換工具時 FE 會一併清空 toolSecretKeysSet(否則新工具的同名 secret 欄位會誤顯示「已設定」,使用者不填就存 → 存進空密碼)。

掃描參數必填驗證(FR-058.2)

⚠️ FR-067.8 起改逐列驗:每個 Agent 分派列各驗一次,condition 依該列的參數求值(SonarQube 的 repo_url 只在該列 scan_mode='scan' 時必填,對 upload 列驗它會讓合法設定存不進去)。BE 端同一套判定在 common/util/detection_assignment_params.py::field_visible(),與 FE 的 isDetectionFieldVisible() 規則一致——兩邊漂移會出現「畫面沒有這個欄位、存檔卻說它必填」。BE 保底錯誤碼 GRC_400113(訊息帶第幾列缺哪些欄位,FE 列入 CODES_KEEPING_BE_DETAIL 併陳明細)。

原本 FE 對掃描參數完全不驗,缺參數的任務照樣存檔 → 派工 → 到 agent 執行期才失敗(ZAP 缺 logged_in_indicator 實測踩過兩次)。改為 saveTask 前檢查,缺漏時 toast 列出欄位名稱並不送出。判斷是 condition-aware 的:因 condition 不成立而隱藏的欄位不算必填(被動掃描時 ZAP 的登入四欄不該被誤擋);secret 欄位已設定過(在 toolSecretKeysSet 內)留空=沿用原值、不算缺。與 DetectionConfigField 的 visible / showError 同源——三處都呼叫 src/utils/detectionFieldValidation.js 的 isDetectionFieldVisible() / findMissingRequiredDetectionFields(),不各判一套。出處:ProjectPlanningView.vue::saveTask:1037-1057。

FE 別名坑:指派人員的審核旗標,FE 早期送 is_admin,serializer AssigneeInputSchema 以 @post_load coalesce 進 is_approver(api/grc/serializers/job.py:105-114)。新資料一律 is_approver。

狀態呈現:無 socket;儲存中 per-任務 spinner(taskSavingSet Set);儲存後前端 state 更新(不整頁重載)。

6. API 規格

Envelope:成功 {"status": true, "data": …}、失敗 {"error_code": "…", "msg": "…"}。

6.1 總清單

分類 Method + Path 說明 完整規格
任務 POST /grc/project/{project_uid}/assessment-object/{ao_uid}/jobs 新增任務(FR-112 起不必帶節點編號) §6.2
任務 PUT /grc/project/{project_uid}/job/{job_uid} 任務更新(關聯 full-replace) §6.2
任務 DELETE /grc/project/{project_uid}/job/{job_uid} 刪除任務(硬刪) §6.2
流程圖 PUT /module-frame/item/xml/{template_uid} 編輯器存檔;後端三向 diff 決定增刪任務,回傳含新編號的 XML §6.2
picker GET /users/menu・/org-units/menu・/surveys/menu、POST /devices 指派 / 部門 / 設備 / 問卷下拉 GAI-SD-02

批次啟動 POST /grc/project/{uid}/start-task-execution 屬規劃頁功能 6,見 規劃頁 §6.4。

6.2 核心 endpoint

[POST] /grc/project/{project_uid}/assessment-object/{ao_uid}/jobs

新增任務。Request(JobCreateRequestSchema,api/flow_control/serializers/job.py:306-326,Meta.unknown=EXCLUDE):只有 name 必填,其餘 description / guide / job_type / control_uid / assignees[] / department_uids[] / device_uids[] / surveys[] / tool / job_element_uid 皆 load_default=None。

job_element_uid(流程圖節點編號)是兩種入口的唯一差別:

入口 帶 job_element_uid 嗎 後端做什麼
規劃頁按「新增任務」 不帶 自己在圖上插節點、接成平行分支、回寫任務編號(見 §5)
流程圖編輯器拖新方塊後存檔 不走這支,走 XML 存檔端點的三向 diff 用圖上那個節點建一筆空任務、把編號回寫圖上

守門:_require_manager(project_uid) → _assert_round_in_planning(project_uid)(job_service.py:166-167),順序固定——先確認你是 manager 再談輪次。Response data=任務 DTO(同 GET job detail 形狀)。副作用:route 層 best-effort 在 Drive 上替新任務建資料夾(沒接 Drive 就靜默跳過,job_route.py:205-211)。

[DELETE] /grc/project/{project_uid}/job/{job_uid}

刪除任務(硬刪)。無 body。守門同新增(manager +規劃期)。清完五張關聯表、問卷快照與 survey.task_surveys 後移除圖上方塊、刪 job_executions 那列。Response data=布林。副作用:route 層 best-effort 把該任務的 Drive 資料夾封存進 _Archive/(job_route.py:160-166)。

擋下的情形:該 AO 只剩這一筆 → 412 FLOW_ENGINE_412003(「查核項目至少要有一個任務」);當前輪已離開規劃期 → 412 GRC_412054。

圖上的移除發生在清關聯表之前:套件在「圖上剩最後一個 userTask」時會拋錯,擺前面才能保證被擋下時關聯表一列都還沒刪。同一交易本來就會回滾,但不依賴回滾更穩。

兩道守門守的不是同一件事,刻意都留:套件那道擋「圖上不能一個 userTask 都不剩」,任務筆數那道擋「任務不能歸零」。正常情況兩者一致,髒資料會讓它們對不上(同一個節點掛兩筆任務時,圖那道已經擋不住,但任務可能已被刪到零)。

[PUT] /module-frame/item/xml/{template_uid}

流程圖編輯器存檔。Request:新的 BPMN XML。後端跑三向 diff(規則見 §5),回傳寫進 DB 的最終 XML(頂層欄位 xml,含後端替新方塊補上的任務編號)——前端拿它重新載入圖,不必自己改 XML 字串。

該範本底下沒有流程實例時(流程範本管理頁那類)diff 回 None、照舊直接存,行為不變。

錯誤碼(四支的訊息在拋出前都會換成帶節點編號的具體句子):

code 意思
GRC_400124 流程圖有節點重複指向同一筆任務
GRC_400125 流程圖上有節點帶著查不到的任務編號
GRC_400126 流程圖上有節點從開始事件走不到
GRC_400127 流程圖上有節點對應到多筆任務
GRC_412054 本輪已離開規劃階段,不可增減任務(純調線不受影響)
FLOW_ENGINE_412003 這次存檔會把任務刪到零

⚠️ 前四支的具體訊息要靠 FE 配合才看得到:前端全域攔截器預設用 i18n 翻譯蓋掉後端訊息,只留一句籠統的話——圖上十幾個方塊,使用者無從得知要改哪一個。四支碼都加進 App.vue 的 CODES_KEEPING_BE_DETAIL,顯示成「翻譯當抬頭(後端明細)」。只做後端這半等於白做。

[PUT] /grc/project/{project_uid}/job/{job_uid}

任務更新。全部欄位選填(load_default=None);關聯陣列 full-replace(送 [] 清空、null/省略 = 不動)。Request(JobUpdateRequestSchema,Meta.unknown=EXCLUDE):

{
  "name": "訪談紀錄蒐集",
  "description": "…",
  "guide": "…",
  "job_type": "survey",
  "control_uid": null,
  "assignees": [{ "uid": "<user uid>", "is_approver": true }],
  "department_uids": ["<org_unit uid>"],
  "device_uids": ["<device uid>"],
  "surveys": [{ "uid": "<survey uid>", "approval_required": true }],
  "tool": { "uid": "<detection_tool uid>", "params": {"hosts": "<target host>"}, "completion_mode": "manual" },
  "recurrence": {
    "enabled": true, "recur_unit": "monthly", "recur_interval": 1,
    "next_due_at": "2026-11-01T09:00:00+08:00", "auto_execute": true,
    "alert_recipients": [{ "type": "participant", "user_uid": "<user uid>" }, { "type": "party_role", "role_id": "system-owner" }]
  }
}
欄位 型別 必填 說明
name / description / guide string|null 否 任務文案 → job_executions
job_type string|null 否 OneOf GrcJobType(general / survey / detection_tool)
control_uid string|null 否 控制層 workflow_execution.uid;可省略,BE 由 job 的 workflow 階層自動解析
assignees array|null 否 [{uid(必填), is_approver(預設 false;相容 is_admin 別名)}] → task_assignees
department_uids array[string]|null 否 部門 org_unit uid → job_execution_org_units
device_uids array[string]|null 否 設備 uid → job_execution_devices
surveys array|null 否 [{uid(必填), approval_required(預設 null)}] → survey.task_surveys;送 null 保留既有審批旗標(BPMN 編輯器同步不誤蓋,SurveyInputSchema:117-125)
tool object|null 否(detection_tool 型必填才可正常掃描) {uid(必填,detection_tools uid), params(Raw,依 param_schema), completion_mode("auto"|"manual",未帶=partial-update 不動既有值,新建綁定時 app service 補預設 "manual")} → config.job_execution_detection_tools(DetectionToolBindingSchema:138-153)

| recurrence | object|null | 否 | 任務週期(FR-137),整包送;null=不動。enabled(預設 false)/recur_unit(task_frequency 選單 key,啟用時必填)/recur_interval(≥1 整數,預設 1)/next_due_at(帶時區,存 UTC;啟用時必填)/auto_execute/alert_recipients([{type: participant, user_uid}|{type: party_role, role_id}],≤50)。enabled=false 清掉下次啟動、保留頻率。後兩欄限 detection_tool 型 |

週期驗證錯誤碼:GRC_400141 頻率不在系統選單內(或啟用卻沒選)/GRC_400142 間隔不是 ≥1 整數/GRC_400143 啟用但沒填下次啟動/GRC_400144 非檢測工具任務設了自動執行或額外通知對象/GRC_400145 通知對象不是本專案參與者或不在系統責任角色選單內。出處:api/flow_control/serializers/job.py::RecurrenceInputSchema、app/flow_control/service/job_recurrence_validator.py、寫入 job_recurrence_service.py::apply(由 job_service.update_job 呼叫)。

出處:request api/grc/serializers/job.py:128-153(含 DetectionToolBindingSchema:138-153)、route api/grc/routes/job_route.py:100-136、service app/grc/service/job_service.py(update_job + _reconcile_task_surveys:228 + _replace_detection_tool_binding)

Response data(JobResponseSchema,形狀同 control-tree 的 Job 節點):

欄位 型別 說明
id / name / description / guide string 任務基本資料(id=job uid)
job_type / status string 類型 / 狀態
template_job_id string|null 範本任務來源
can_revert_job bool 可否退回
assignees / departments / devices / surveys array 關聯子物件(形狀同 control-tree Job 節點)
recurrence object|null 週期(FR-137):{enabled, recur_unit, recur_interval, next_due_at, auto_execute, alert_recipients[{type, user_uid, user_name, role_id}], run_count, last_run_at};enabled=有頻率且有下次啟動,run_count=排程已啟動過幾次(JobRecurrenceResponseSchema)
tool object|null detection_tool 型才有值:{uid, code, name, params, completion_mode, secret_keys_set}(JobToolResponseSchema,供 FE 讀取端 prefill)。params 已剝除所有加密 envelope 欄位(整個 key 不出現);secret_keys_set 是已設定值的 secret 欄位 key 名清單(只回 key 不回值),供 FE 顯示「已設定,留空=沿用原值」——剝除後 FE 無從分辨「沒設過」與「設過但看不到」,會把已設定的必填欄位標成缺漏(FR-058.0 T-0.3)

出處:api/grc/serializers/job.py:71-98(JobToolResponseSchema,含 secret_keys_set)/:86-(JobResponseSchema);加解密與剝除 util common/util/detection_secret_params.py;寫入端 app/grc/service/job_service.py::_encrypt_tool_secret_params:338-370

6.3 批次動作

快速配置(無專屬 endpoint,FE 逐一 PUT)

FE-only 編排:多選人員(來源 = 專案參與人員)→ 迴圈所有可編輯(canEditCtrl)控制項底下的任務 → 每個 job 把選取人員 additive 併入既有指派(已存在者跳過)→ 逐一 PUT job。⚠️ 每筆 PUT 送 department_uids: [] / device_uids: [] / surveys: []——因 full-replace 語意,會清空該任務既有的部門 / 設備 / 問卷關聯(見 §12 坑)。出處:ProjectPlanningView.vue:executeQuickConfig:1192-1230。

[POST] /grc/project/{project_uid}/start-task-execution

無 body。把該專案「已指派且 TODO」的 prep-job 批次轉 PROCESSING 並發站內通知。限專案 manager(_check_manager,否則 GRC_403002);idempotent 可重按。Response data:{"activated_count": int, "detection_dispatched_count": int}(分別為實際轉 PROCESSING 的任務數、自動派出首次掃描的 detection_tool 任務數)。

FR-056:自動派 detection_tool 首次掃描——批次轉 PROCESSING 的同時,對其中 job_type='detection_tool' 且查無任何 detection_executions 紀錄(NOT EXISTS,冪等防重)的任務逐筆呼叫 DetectionOrchestrationService.start_execution() 派出第一次掃描;單筆派工失敗(如未設定憑證)用 SAVEPOINT 隔離、不擋整批發佈與其餘任務,失敗的任務留給執行者從 任務執行抽屜 手動補派。出處:service app/grc/service/task_execution_service.py(_check_manager:49-56、start_task_execution:58-96、_auto_dispatch_detection_scans:98-134)、query infra/grc/repository/task_execution_query.py::get_detection_jobs_without_execution:134-、serializer api/grc/serializers/task_execution.py。

scan_mode=upload 的任務不進自動派發(FR-058.7,D33):自動路徑不帶檔案,upload 任務必然打中「首次無檔」400(DETECTION_TOOLS_400005),且該錯誤被批次派發的 try/except 吞掉只留 warning log——PM 看到成功 toast、執行者無感,又因失敗不產生執行紀錄、冪等條件永遠成立,每按一次「開始執行任務」就再失敗一次。故在挑選階段排除:get_detection_jobs_without_execution 的查詢排除 tool_params->>'scan_mode'='upload' 的綁定(task_execution_query.py:142/:169),upload 任務一律由執行者在任務抽屜上傳檔案後手動執行(抽屜有無檔不給按執行的前置擋門);pull/scan 自動派發行為不變。FE 配套:批次發佈 confirm 文案加註「上傳型掃描任務需由執行者上傳檔案後手動執行」(i18n start_task_execution_confirm_message)。已執行過的 upload 任務本來就不在冪等清單內,不受影響。

7. 前端檔案地圖(compliance-manager-fe/)

檔案 角色
src/views/project/ProjectPlanningView.vue 任務的建立/刪除/儲存與所有 API 呼叫(saveTask PUT、countTasks、taskMutationBlockedReason、canDeleteTask);檢測工具區(onSelectDetectionTool 帶 default + 清舊 secret 狀態、detectionParamSchemaCache 依工具快取 schema、FR-058 掃描參數必填驗證)
src/components/grc/planning/AoTaskSection.vue 任務區塊的殼:標題列+新增鈕+提示條+TabView 籤列+左右佈局
src/components/grc/planning/TaskRecurrenceSummary.vue / TaskRecurrenceDialog.vue 週期摘要列與設定 Dialog(FR-137);值的持有者仍是父頁 taskMap
src/utils/taskRecurrence.js / src/composables/useTaskFrequencyOptions.js 週期 state ⇄ API 轉換;頻率選項讀系統選單
src/components/grc/planning/AoTaskForm.vue 選中那一筆的完整表單(四關聯 MultiSelect、審核人 / 審批 switch、檢測工具分派列、刪除鈕)
src/utils/detectionAssignmentParams.js 分派列參數組裝與比對的純函式(buildRowParams / 重複列 / 必填可見性)——存檔擋門在父頁、列內渲染在表單,規則只有一份
src/views/workflow/WorkflowSetupEditor.vue 流程圖編輯器:屬性面板只剩名稱、存檔只 PUT 一次 XML、刪方塊存檔前確認
src/components/detection-tools/DetectionConfigField.vue 掃描參數的動態欄位渲染,與檢測工具管理設定表單共用同一元件(hasExistingValue 在本頁綁 toolSecretKeysSet,在該頁綁 config.has_credentials)
src/utils/detectionFieldValidation.js (FR-058)isDetectionFieldVisible() / findMissingRequiredDetectionFields()——condition 可見性與必填檢查的共用實作,本頁 saveTask、DetectionConfigField、檢測工具管理頁三處同源
src/service/BaseService.js put(...) 打 /grc/project/{uid}/job/{jobId}

8. 後端檔案地圖(本頁核心鏈路)

鏈路 Route App Service 底層
新增任務 api/flow_control/routes/job_route.py::ProjectJobCreateResource(POST) JobService.create_job(manager +規劃期守門 → 建任務 → 問卷快照 → 檢測工具綁定) flow_control_job_repo_impl.create_job():沒帶節點編號時查範本 XML → 套件 append_parallel_user_task_to_xml() 插節點 → 建 job_executions(初始狀態依 get_upstream_kind() 判定)→ inject_job_execution_uid() → _write_template_xml() 主表+翻譯表雙寫
刪除任務 同檔 ProjectJobDetailResource(DELETE) JobService.delete_job(manager +規劃期守門 → _delete_job_without_project_guard()) repo.delete_job():先移圖上節點(套件 remove_user_task_from_xml(),會收掉退化閘道、圖上剩最後一個時拋 412)→ 清五張關聯表 → 硬刪 job
流程圖存檔三向 diff api/module_frame/__init__.py:85 → ModuleFrameItemXmlRoute(PUT) WorkflowXmlSyncService.sync()(app/flow_control/service/workflow_xml_sync_service.py,@transaction) 驗證(重複編號/可達性/查不到的編號/一節點多筆任務)→ 刪除走 job_service.delete_job_from_diagram()、新增走 repo.create_job_for_node() → repo.write_template_xml()
任務更新 api/grc/routes/job_route.py(PUT,僅 JWT) JobService.update_job(full-replace reconcile + BPMN 回寫 name,同一 transaction) compliance.job_executions + reconcile task_assignees / job_execution_org_units / job_execution_devices + survey.task_surveys(_reconcile_task_surveys)+ config.job_execution_detection_tools(_replace_detection_tool_binding,FR-056)
週期到期(FR-137) —(排程 core/scheduler.py::task_recurrence_tick) app/flow_control/service/task_recurrence_app_service.py::run_once(停止條件 → 推進 → 重開 → task_auto_execute_service.start → notify_user_todo_job) infra/flow_control/repository/task_recurrence_query.py(list_due / mark_run 條件式推進 / reopen_job);推算 task_recurrence_schedule.advance_due
開始執行任務(含自動派掃描) api/grc/routes/(start-task-execution) app/grc/service/task_execution_service.py::start_task_execution(_check_manager → activate_jobs → 通知 → _auto_dispatch_detection_scans) job_executions(狀態轉換)+ DetectionOrchestrationService.start_execution()(見 檢測工具管理)

⚠️ DDD 提醒:PUT job route 層無角色守門(別比照為權限典範);問卷關聯跨到 survey schema 的 task_surveys(非 compliance.job_execution_surveys),維護留意 schema 邊界;detection_tool 綁定表 config.job_execution_detection_tools 屬 config schema(非 compliance),跨 schema 命名慣例注意。

9. DB

9.1 資料表總清單(本頁讀寫的全部表)

表 讀寫 說明 詳述
compliance.job_executions 讀 / 寫 任務本體(name / description / guide / job_type;週期欄位) §9.3
compliance.task_assignees 讀 / 寫(reconcile) 指派人員(含 is_approver) §9.3
compliance.job_execution_org_units 讀 / 寫(全刪重建) 任務↔部門 §9.3
compliance.job_execution_devices 讀 / 寫(全刪重建) 任務↔設備 §9.3
survey.task_surveys 讀 / 寫(reconcile) 任務↔問卷(含 approval_required) §9.3
config.job_execution_detection_tools 讀 / 寫(FR-056) 任務↔檢測工具綁定(工具 uid / 掃描參數 / 完成模式,is_delete 軟刪) §9.3

9.2 ER 圖

任務鏈:AO 經 BPMN 範本快照長出 prep-job,本頁寫四張關聯表;虛線 = props 參照 / 展開

9.3 核心表欄位(取自 DEV DB dump,含 DB comment)

compliance.job_executions — 任務本體

欄位 型別 說明
uid varchar(50) 任務 UUID(對外 job id)
name varchar(500) 任務名稱 ← name
description / guide text 描述 / 指南
job_type varchar(50) general / survey ← job_type(另有 type jobtype enum 欄)
status jobstatus 任務狀態(TODO / PROCESSING…)
org_unit_id / tenant_id integer 租戶 / 組織(RLS)
recur_unit varchar(32)|null 週期單位(task_frequency 選單 key),空=不重複
recur_interval integer 每幾個單位一次(預設 1)
next_due_at timestamptz|null 下次啟動時間;到了排程在同一筆任務上重開/開掃(部分索引 ix_job_executions_next_due_at)
auto_execute boolean 到了啟動時間是否直接開掃(限檢測工具任務)
alert_recipients jsonb|null 新出現 high 以上時額外通知對象 [{type:participant,user_id}|{type:party_role,role_id}](限檢測工具任務)
run_count / last_run_at integer / timestamptz 週期排程已啟動過幾次、最近一次啟動時間(抽屜顯示第 run_count+1 輪)

compliance.task_assignees — 指派人員

欄位 型別 說明
project_id / control_id / task_id integer 複合定位
task_uid / task_template_id varchar 任務 uid / 範本任務 id
user_id integer 指派人
role varchar(50) 角色
is_approver integer 是否審核者(0/1)← assignees[].is_approver

compliance.job_execution_org_units / job_execution_devices — 部門 / 設備關聯

欄位 型別 說明
job_execution_id integer → job_executions
org_unit_id / device_id integer 部門 / 設備(全刪重建 reconcile)

survey.task_surveys — 問卷關聯(跨 schema)

欄位 型別 說明
task_id / survey_id integer 任務 / 問卷
approval_required boolean 是否需審核 ← surveys[].approval_required
device_id / department_id integer 問卷施測的設備 / 部門
job_evidence_id integer soft ref → job_evidences.id(標記 GRC 建立)
is_delete boolean 軟刪旗標

⚠️ schema 註:問卷關聯在 survey schema 的 task_surveys(job_service._reconcile_task_surveys 管理),不是 compliance.job_execution_surveys(那是另一張簡單映射表,本更新路徑不寫)。欄位一律取自 db_schema.json。

config.job_execution_detection_tools — 任務↔檢測工具綁定(FR-056)

欄位 型別 說明
job_execution_id integer → compliance.job_executions(uq_jedt_job_active 唯一約束:is_delete=false 下每任務僅一筆有效綁定)
detection_tool_id bigint → config.detection_tools(工具目錄)
tenant_config_id bigint|null → tenant_detection_tool_configs.id;建表時預留,寫入端從未寫過此欄(一律 NULL,見 檢測工具管理 §12 已知坑),憑證解析改走 (tenant_id, detection_tool_id) fallback 反查
tool_params jsonb 掃描參數(動態欄位,依 detection_tool_param_schemas 定義)。secret: true 欄位以加密 envelope {"__enc__":"fernet","value":"<密文>"} 存放(FR-058.0),與明文欄位混在同一個 JSONB;金鑰同租戶層憑證的 DETECTION_TOOL_ENCRYPTION_KEY
completion_mode varchar(20) manual(預設)/ auto
is_delete boolean 軟刪(重新綁定其他工具=標舊列刪除 + 建新列,非 update)

10. 頁面邏輯與資料對應

任務隨 control-tree 一次載入(內嵌於 AO 節點)。欄位對應:

畫面元素 FE state payload 欄位 → DB
名稱 / 描述 / 指南 task.name / .description / .guide 同名 → job_executions
類型 task.jobType job_type → job_executions.job_type
指派人員(含審核人) task.assignees[{id,name,is_approver}] assignees[{uid,is_approver}] → task_assignees
部門 / 設備 task.departments / .devices department_uids / device_uids → org_units / devices 關聯
問卷(含審批) task.surveys[{id,name,approval_required}] surveys[{uid,approval_required}] → survey.task_surveys
檢測工具 / 參數 / 完成模式(FR-056) task.tool.{uid,params,completionMode} tool{uid,params,completion_mode} → config.job_execution_detection_tools
secret 參數的「已設定」狀態(FR-058.0) task.toolSecretKeysSet(string[]) response tool.secret_keys_set(唯讀,不回送)

選定檢測工具的預設值帶入(FR-057,FR-058 補清 secret 狀態):切換 task.toolUid 時先把 toolParams 清成 {}、toolSecretKeysSet 清空(換工具=換一套 schema,舊工具的 secret 已設定狀態不再適用),取回該工具的 param_schema 後,遍歷欄位定義找出有 default 值(非 undefined/null)的欄位組成預設物件,一次性寫回 toolParams——避免使用者看到全空表單以為沒有預設值可用(如 timeout_sec 預設 3600、Nmap port_range 預設 1-1024、CINC Auditor transport 預設 ssh)。出處:ProjectPlanningView.vue::onSelectDetectionTool:1011-1023。

儲存流程:saveTask 依序做 client 檢查——① detection_tool 型必須選定工具(warn_tool_required)→ ② 掃描參數必填檢查(FR-058,condition-aware,缺漏列出欄位名,見 §5)→ ③「survey 有審批要求但無審核人」→ 任一不過皆 toast warn 不送 → PUT → 前端 state 更新。錯誤對應:GRC_404005(任務不存在)/ GRC_409033(問卷已有填答無法移除)→ toast。

11. 背景行為與外部依賴

類型 內容
socket 無(儲存後前端 state 更新)
BPMN 同步 update_job 在同一 transaction 回寫 BPMN userTask 的 name 一個欄位(圖上其餘 camunda 屬性不再寫,見 §12 坑 16)
合流閘道等待(FR-112) 完成一個任務後,若下一個任務接在合流閘道後面,必須該閘道所有入線分支都結束才會被啟動(COMPLETED/CANCEL 皆視同結束,否則取消一筆分支會永遠卡住)。拓樸事實問套件、「算不算結束」的業務判斷留主專案。兩個落點都接上:complete_job()(收證據流程,批次完成會連續呼叫)與 complete_main_workflow_job()(輪次推進走這支)
寫圖必須雙寫(FR-112) 範本 XML 是可翻譯欄位——規劃頁走原生 SQL 讀主表、編輯器走倉儲的語系備援讀翻譯表。所有寫圖路徑(插節點/移節點/存檔回寫編號/專案建立時把任務編號寫回範本)都走倉儲的 write_template_xml() 同時寫兩邊(見 §12 坑 19)
Drive 任務名稱變更 → best-effort 觸發 Drive 資料夾改名(無 Drive 整合則靜默跳過)
週期排程(FR-137) core/scheduler.py 的 task_recurrence_tick 每 5 分鐘撈 next_due_at 已到的任務(TaskRecurrenceAppService.run_once,全租戶撈、逐租戶身分執行):①停止條件——所屬輪次已結案或專案已完成/封存 → 清掉 next_due_at、不再啟動;②推進——下次啟動從本次啟動時間往後推一格(停擺多格也只跑一輪、不會往後漂;月以上 31 號遇小月取月底),run_count+1、記 last_run_at,條件式更新防多副本重跑;③任務已完成就重開成進行中(流程實例已完成也一起重開);④開了自動執行的檢測任務以系統身分開掃(任務還在排隊、非進行中則本輪不開掃);⑤不論有無自動執行都寄「待辦」信給指派人。單筆失敗不擋同批。同一個 tick 之後接著跑支援期提醒(見通知紀錄)
依賴 /users/menu(指派)/ /org-units/menu(部門)/ /surveys/menu(問卷)/ POST /devices(設備)

12. 邊界情況與已知坑

  1. PUT job 無 BE 角色守門:route 僅 @jwt_required(),FE 靠三層 canEdit 擋;繞過 FE 直打 API 任一登入者都能改任務指派(與規劃頁 §12 同一 finding)。
  2. 審核人檢查只在 FE:「survey 有審批要求則至少一審核人」只在 saveTask 前 client 檢查(toast 不送),BE 無此驗證——直打 API 可存出「有審批要求卻無審核人」的任務。
  3. 問卷關聯跨 schema、非直覺:任務問卷走 survey.task_surveys(_reconcile_task_surveys),不是 compliance.job_execution_surveys;後者是另一映射表,本路徑不動。
  4. surveys[].approval_required 送 null 保留既有值:SurveyInputSchema 的 approval_required load_default=None——BPMN 編輯器同步問卷時不帶此欄(無此 UI),送 null → _reconcile_task_surveys 保留既有審批旗標,不誤蓋規劃頁設定;規劃頁顯式送 True/False 行為不變。
  5. is_admin → is_approver 別名:FE 早期送 is_admin,serializer @post_load coalesce 進 is_approver;下游只讀 is_approver。
  6. 關聯 full-replace 語意:assignees/department_uids/device_uids/surveys 送 [] 清空、null/省略不動;部門 / 設備關聯是全刪重建(reconcile)。
  7. 問卷已有填答不可移除:移除已有答案的問卷 evidence → GRC_409033(GRC_SURVEY_HAS_ANSWERS)。
  8. ⚠️ 快速配置會清空部門 / 設備 / 問卷關聯:executeQuickConfig 對每個 job 只 additive 合併指派人員,但送 department_uids: [] / device_uids: [] / surveys: []——因 full-replace 語意([]=清空),會把該任務既有的部門 / 設備 / 問卷關聯一併清掉。若已配置這些關聯,用快速配置補人員前要留意會被清空(ProjectPlanningView.vue:1218)。
  9. survey × device 笛卡爾積:任務掛多問卷多設備時 survey.task_surveys 列數會乘開(每問卷 × 每設備一列),指派前想清楚資料量。
  10. date-only 顯示 08:00 陷阱:任務時間相關欄位 BE 慣例吐 YYYY-MM-DDTHH:MM:00 無 tz,前端 date-only 顯示要留意時區偏移(見 _overview 時區慣例)。
  11. job_execution_detection_tools.tenant_config_id 恆為 NULL(FR-056 已知坑):欄位建表時預留、讀取端(start_execution)會讀,但寫入端 _replace_detection_tool_binding 從未寫過此值——實務上憑證解析一律 fallback 走 (tenant_id, detection_tool_id) 反查(該組合有 UNIQUE 約束可唯一定位)。若使用者「先建任務、後設工具憑證」,查詢時解析比綁定時寫死更穩,故此設計非遺漏;該欄位是否退役留待未來討論。
  12. secret 掃描參數留空=沿用、不是清空(FR-058.0):encrypt_secret_params() 對空值會從既有綁定沿用舊密文——因為 FE 對已設定的 secret 欄位承諾「留空=沿用原值」,不沿用則使用者每次改別的參數(如 timeout_sec)都會把密碼清空,且症狀要到掃描時才浮現。代價是「清空單一 secret 參數」沒有 UI 入口;真要清空得換工具(onSelectDetectionTool 會清 toolParams 與 toolSecretKeysSet)或直接改 DB。
  13. 換檢測工具會清空全部掃描參數:onSelectDetectionTool 切換 toolUid 時先把 toolParams 清成 {}、toolSecretKeysSet 清空,再依新工具 param_schema 帶入 default。這是刻意的——不清會讓新工具的同名 secret 欄位誤顯示「已設定」,使用者不填就存 → 存進空密碼。誤按下拉再切回原工具,參數要重填。
  14. tool_params 的加密判定看值不看 schema:is_encrypted() 只認 envelope 的 __enc__ 標記,不查 param_schema 哪些 key 是 secret——因為 param_schema 版本化(is_current 可切換),若靠 schema 判定,改版後舊任務會把明文當密文解(拋例外)或把密文當明文送給 agent(帳密以密文形式送出,掃描失敗且難查)。改動加解密路徑時勿引入 schema 依賴。
  15. 一個節點對到多筆任務=舊資料,存檔會被擋下:舊版逐節點打 API 的編輯器留下的殘留(同一個 template_job_id 掛兩筆 job_executions)。這種圖存檔會回 GRC_400127,必須先決定留哪一筆——刻意不自動選,因為哪一筆才是有證據的那筆只有人知道。這也是為什麼「圖上至少一個 userTask」與「任務至少一筆」兩道守門刻意都留:正常情況兩者一致,髒資料會讓它們對不上。
  16. 圖上的舊 camunda 屬性是殘留,既不寫也不讀(FR-112):workDescription / guide / actionType / actionInfo / toolParams / completionMode 這幾個屬性曾經由 _sync_task_to_template_xml() 寫、也曾被 control-tree 讀回來當真值。現在寫入端只寫 name、讀取端改讀 job_executions 的 description / guide / job_type 欄位。既有 XML 不清理——清除 migration 要改所有租戶的範本 XML,改壞沒有回頭路(XML 無欄位層級版控);沒有人讀它就只是一段被忽略的文字。但寫入端必須收斂,否則舊屬性會被持續刷新、永遠不會自然凋零。
  17. 平行結構下「可不可以退回」要整批看(FR-112):_compute_can_revert_job() 改成「下一批任務沒有任何一筆已完成」才可退。舊寫法只看下一批第 0 筆是否進行中——第 0 筆還沒動、第 1 筆早就做完了也照樣放行,退回會把已完成的那筆打回待辦,使用者做過的事無聲消失。
  18. 新任務初始狀態看拓樸不看數量(FR-112):舊判準「此流程還有沒有未完成的任務,有就開 TODO」在平行下必錯——三條平行分支只有第一條 PROCESSING、其餘排隊,畫面上是「有三個平行任務但只有一個做得動」,與「誰先做都行」完全相反且沒有錯誤訊息。現改問上游節點型別(見 §5)。
  19. 範本 XML 是可翻譯欄位,只寫主表會讓編輯器永遠看到舊圖(FR-112):規劃頁讀主表、編輯器讀翻譯表。最隱晦的一條寫入路徑是專案建立時把任務編號寫回範本——漏了翻譯表的症狀是「新專案一開編輯器、什麼都沒改按儲存就被擋下,訊息還文不對題」(編輯器看到的圖上所有方塊都沒有編號 → 三向 diff 判成「使用者把任務全刪了」)。所有寫圖路徑一律走倉儲的 write_template_xml()。⚠️ 此修正只改寫入路徑、不補舊資料:修正前建立的專案,編輯器仍會被擋。
  20. 覆核輪對多任務 AO 只會建第一筆:prep_job_generation_service._first_user_task_id() 只抓圖上第一個 userTask。開覆核輪時會 re-clone 第一輪已被編輯過的範本再跑一次生成——AO 可多任務後,會出現「圖上三個方塊、資料庫一筆任務」且沒有錯誤訊息。已知缺口,留延伸 FR(本頁只標註現況)。
  21. 檢測工具任務不走 flow-engine complete 觸發掃描:detection_tool 型任務的「開始執行」走獨立的 DetectionOrchestrationService(見 任務執行抽屜 §11.1),與本頁「儲存任務」/「開始執行任務」的批次轉 PROCESSING 是兩件事——後者只是把任務從 TODO 轉 PROCESSING 讓任務「可以」被執行,掃描本身另外觸發(規劃頁批次轉 PROCESSING 時會自動觸發首次掃描,見 §6.3)。
  22. 週期是同一筆任務一直跑(FR-137):到期不複製任務、不掛新流程節點;證據、執行紀錄都累積在同一筆上。已完成的任務會被重開成進行中——稽核上看到「完成過又回到進行中」是週期造成,不是有人退回。輪次結案或專案結束後排程自動停,不必手動關。

13. 開發與驗證

  • 跑起來:起 BE + FE;登入見 .env / 部署文件測試帳號(需該節點 manager;開始執行需專案 manager)
  • 導航:規劃頁 Tab0 → 左樹選 AO → 右面板任務區塊(籤列選一筆、下方表單編輯);快速配置 / 開始執行在 Header
  • 前置資料:需 AP=planning 的專案 + 至少一個控制項有 AO 任務;備 user / org_unit / device / survey master 測四關聯;切 survey 型測問卷 + 審批 + 審核人 switch 連動;測快速配置後檢查部門/設備是否被清空(坑 8)
  • 平行任務怎麼驗:同一 AO 連續新增到三筆 → 三筆全部是進行中(不是兩筆排隊)→ 打開流程圖編輯器看是三條並排不是串成一條龍 → 只完成一筆時該 AO 不可以變已完成、三筆全完成才變。合流等待另驗:在編輯器把圖改成「兩條平行 → 合流 → 主管覆核」,完成第一條時覆核任務不得變進行中。
  • 相關:現況編輯 / 程序書見 現況編輯;批次匯入見 批次維護;任務資料真相=job_executions,流程圖只存拓樸與任務編號;問卷設計見 survey 功能群