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.2recurrence、§9.3、§11 - 2026-10-05(FR-129):分派列的掃描目標不再收自由文字——逐台型欄位(CINC Auditor/GCB/OpenSCAP 的hosts、Nmap 的執行主機hosts)改設備多選、存設備 uid 陣列,派工時才換成 IP;網段型欄位(Nmaptargets、OpenVAShosts)仍填 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承載(migrationf10e45eb,措辭依上述分流分三段) - 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掃描參數型態(下拉常用選項 + 自由輸入,OpenSCAPprofile欄位用);選定工具後有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-PP-02 快速配置與開始執行¶
| 項目 | 內容 |
|---|---|
| 角色 | 快速配置:有編輯權者;開始執行:專案 manager |
| 前置條件 | 輪次 = planning;AO 下已有任務(BPMN 範本衍生的 prep-job) |
| 產出 / 後置條件 | 快速配置:對每個可編輯任務 additive 併入指派人員後逐一 PUT job;開始執行:compliance.task_assignees 等已備妥後 job_executions.status TODO→PROCESSING、站內通知送出 |
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 jobroute 層只有 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 面板 + 任務卡展開(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(措辭依上述分流分三段給:展開型講上限與拆列的 出路/Nmaphosts強調是執行主機/透傳型刻意不提上限),與分派列的說明措辭一致。
逐列驗證(FR-067.8):
- 必填 condition-aware 且逐列判:
GRC_400113,訊息帶「第 N 列(缺哪些欄位)」——SonarQube 的repo_url只在scan模式必填,對 upload 列驗它會讓合法設定存不進去。FE 與 BE 用同一套 判定規則(BEcommon/util/detection_assignment_params.field_visible/FEsrc/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-059options_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 剝除、FEsecret_keys_setper 列顯示「已設定」)。下表機制逐項不變,只是作用範圍從「一個任務一份」變成「一列一份」。另複製此列/新列帶預設值刻意不抄 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,serializerAssigneeInputSchema以@post_loadcoalesce 進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 文案加註「上傳型掃描任務需由執行者上傳檔案後手動執行」(i18nstart_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 jobroute 層無角色守門(別比照為權限典範);問卷關聯跨到surveyschema 的task_surveys(非compliance.job_execution_surveys),維護留意 schema 邊界;detection_tool 綁定表config.job_execution_detection_tools屬configschema(非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 圖¶
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 註:問卷關聯在
surveyschema 的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. 邊界情況與已知坑¶
PUT job無 BE 角色守門:route 僅@jwt_required(),FE 靠三層canEdit擋;繞過 FE 直打 API 任一登入者都能改任務指派(與規劃頁 §12 同一 finding)。- 審核人檢查只在 FE:「survey 有審批要求則至少一審核人」只在
saveTask前 client 檢查(toast 不送),BE 無此驗證——直打 API 可存出「有審批要求卻無審核人」的任務。 - 問卷關聯跨 schema、非直覺:任務問卷走
survey.task_surveys(_reconcile_task_surveys),不是compliance.job_execution_surveys;後者是另一映射表,本路徑不動。 surveys[].approval_required送 null 保留既有值:SurveyInputSchema的approval_requiredload_default=None——BPMN 編輯器同步問卷時不帶此欄(無此 UI),送 null →_reconcile_task_surveys保留既有審批旗標,不誤蓋規劃頁設定;規劃頁顯式送 True/False 行為不變。is_admin→is_approver別名:FE 早期送is_admin,serializer@post_loadcoalesce 進is_approver;下游只讀is_approver。- 關聯 full-replace 語意:
assignees/department_uids/device_uids/surveys送[]清空、null/省略不動;部門 / 設備關聯是全刪重建(reconcile)。 - 問卷已有填答不可移除:移除已有答案的問卷 evidence →
GRC_409033(GRC_SURVEY_HAS_ANSWERS)。 - ⚠️ 快速配置會清空部門 / 設備 / 問卷關聯:
executeQuickConfig對每個 job 只 additive 合併指派人員,但送department_uids: []/device_uids: []/surveys: []——因 full-replace 語意([]=清空),會把該任務既有的部門 / 設備 / 問卷關聯一併清掉。若已配置這些關聯,用快速配置補人員前要留意會被清空(ProjectPlanningView.vue:1218)。 - survey × device 笛卡爾積:任務掛多問卷多設備時
survey.task_surveys列數會乘開(每問卷 × 每設備一列),指派前想清楚資料量。 - date-only 顯示 08:00 陷阱:任務時間相關欄位 BE 慣例吐
YYYY-MM-DDTHH:MM:00無 tz,前端 date-only 顯示要留意時區偏移(見 _overview 時區慣例)。 job_execution_detection_tools.tenant_config_id恆為 NULL(FR-056 已知坑):欄位建表時預留、讀取端(start_execution)會讀,但寫入端_replace_detection_tool_binding從未寫過此值——實務上憑證解析一律 fallback 走(tenant_id, detection_tool_id)反查(該組合有 UNIQUE 約束可唯一定位)。若使用者「先建任務、後設工具憑證」,查詢時解析比綁定時寫死更穩,故此設計非遺漏;該欄位是否退役留待未來討論。- secret 掃描參數留空=沿用、不是清空(FR-058.0):
encrypt_secret_params()對空值會從既有綁定沿用舊密文——因為 FE 對已設定的 secret 欄位承諾「留空=沿用原值」,不沿用則使用者每次改別的參數(如timeout_sec)都會把密碼清空,且症狀要到掃描時才浮現。代價是「清空單一 secret 參數」沒有 UI 入口;真要清空得換工具(onSelectDetectionTool會清toolParams與toolSecretKeysSet)或直接改 DB。 - 換檢測工具會清空全部掃描參數:
onSelectDetectionTool切換toolUid時先把toolParams清成{}、toolSecretKeysSet清空,再依新工具param_schema帶入default。這是刻意的——不清會讓新工具的同名 secret 欄位誤顯示「已設定」,使用者不填就存 → 存進空密碼。誤按下拉再切回原工具,參數要重填。 tool_params的加密判定看值不看 schema:is_encrypted()只認 envelope 的__enc__標記,不查param_schema哪些 key 是 secret——因為param_schema版本化(is_current可切換),若靠 schema 判定,改版後舊任務會把明文當密文解(拋例外)或把密文當明文送給 agent(帳密以密文形式送出,掃描失敗且難查)。改動加解密路徑時勿引入 schema 依賴。- 一個節點對到多筆任務=舊資料,存檔會被擋下:舊版逐節點打 API 的編輯器留下的殘留(同一個
template_job_id掛兩筆job_executions)。這種圖存檔會回GRC_400127,必須先決定留哪一筆——刻意不自動選,因為哪一筆才是有證據的那筆只有人知道。這也是為什麼「圖上至少一個userTask」與「任務至少一筆」兩道守門刻意都留:正常情況兩者一致,髒資料會讓它們對不上。 - 圖上的舊 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 無欄位層級版控);沒有人讀它就只是一段被忽略的文字。但寫入端必須收斂,否則舊屬性會被持續刷新、永遠不會自然凋零。 - 平行結構下「可不可以退回」要整批看(FR-112):
_compute_can_revert_job()改成「下一批任務沒有任何一筆已完成」才可退。舊寫法只看下一批第 0 筆是否進行中——第 0 筆還沒動、第 1 筆早就做完了也照樣放行,退回會把已完成的那筆打回待辦,使用者做過的事無聲消失。 - 新任務初始狀態看拓樸不看數量(FR-112):舊判準「此流程還有沒有未完成的任務,有就開
TODO」在平行下必錯——三條平行分支只有第一條PROCESSING、其餘排隊,畫面上是「有三個平行任務但只有一個做得動」,與「誰先做都行」完全相反且沒有錯誤訊息。現改問上游節點型別(見 §5)。 - 範本 XML 是可翻譯欄位,只寫主表會讓編輯器永遠看到舊圖(FR-112):規劃頁讀主表、編輯器讀翻譯表。最隱晦的一條寫入路徑是專案建立時把任務編號寫回範本——漏了翻譯表的症狀是「新專案一開編輯器、什麼都沒改按儲存就被擋下,訊息還文不對題」(編輯器看到的圖上所有方塊都沒有編號 → 三向 diff 判成「使用者把任務全刪了」)。所有寫圖路徑一律走倉儲的
write_template_xml()。⚠️ 此修正只改寫入路徑、不補舊資料:修正前建立的專案,編輯器仍會被擋。 - 覆核輪對多任務 AO 只會建第一筆:
prep_job_generation_service._first_user_task_id()只抓圖上第一個userTask。開覆核輪時會 re-clone 第一輪已被編輯過的範本再跑一次生成——AO 可多任務後,會出現「圖上三個方塊、資料庫一筆任務」且沒有錯誤訊息。已知缺口,留延伸 FR(本頁只標註現況)。 - 檢測工具任務不走 flow-engine complete 觸發掃描:
detection_tool型任務的「開始執行」走獨立的DetectionOrchestrationService(見 任務執行抽屜 §11.1),與本頁「儲存任務」/「開始執行任務」的批次轉 PROCESSING 是兩件事——後者只是把任務從 TODO 轉 PROCESSING 讓任務「可以」被執行,掃描本身另外觸發(規劃頁批次轉 PROCESSING 時會自動觸發首次掃描,見 §6.3)。 - 週期是同一筆任務一直跑(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 功能群