For agentic workers / runner: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. Steps use
- [ ]checkbox syntax.依賴:FR-056.1(需 detection_tools 目錄 API)。可與 FR-056.3 並行。 母案設計見
design.md,本階段設計見 design §8 的 FR-056.2 段。
Goal: 任務設置頁新增第三種任務類型「檢測工具執行」,選定後可綁定「用哪個檢測工具 + 掃描參數 + 完成模式」,且同步寫回範本 BPMN userTask。此階段只做設定期(存得下、讀得出、重開還在),實際「執行」在 FR-056.4 才接。
Architecture: 沿用既有問卷(survey)任務的三段式:(1) GrcJobType 加 detection_tool 值;(2) BPMN userTask 同步——job_type 寫進 actionType、工具選擇寫進 actionInfo、參數與完成模式各寫一個新 camunda:property;(3) 綁定資料落一張簡單的單列 side-table config.job_execution_detection_tools(mirror TaskSurvey 的形狀,但不做 survey 的 snapshot + device×department 笛卡兒積 fan-out——一個任務一個工具一組參數,過度工程無必要)。FE 任務設置頁加第三個 job type 選項 + 工具選擇 + 動態參數表單 + 完成模式選擇。
Tech Stack: 同 FR-056.1(BE DDD / marshmallow / pytest;FE Vue3 / PrimeVue)。
| 項目 | 定案 | 來源/理由 |
|---|---|---|
| GrcJobType 新值 | DETECTION_TOOL = "detection_tool" |
明確語意;不複用 ActionType.TOOL_EXECUTION(見下) |
ActionType.TOOL_EXECUTION |
不直接複用其值 "tool_execution"——它是 dormant placeholder(零使用),但 GrcJobType 與 ActionType 是兩個獨立 enum、不自動同步 |
Explore:兩 enum 非同步;用 detection_tool 語意更準 |
| 綁定資料表 | 新建 config.job_execution_detection_tools(單列/任務),不走 survey 的 snapshot + 笛卡兒積 |
survey 機制對「單工具+參數+flag」過重,比例不當 |
| BPMN property | actionType=job_type、actionInfo=tool_uid、新增 toolParams(JSON-encoded)、completionMode |
mirror survey 的 actionInfo + sp_properties JSON-property pattern |
| 完成模式預設 | manual(系統掃完不自動按完成,任務留 PROCESSING 等人工),另一值 auto(自動 complete_job) |
design D8 |
| 完成模式不是狀態 | completion_mode 只是存在綁定表的 flag,供 FR-056.4 決定掃完要不要自動呼叫 complete_job。不新增 JobStatus、不動 jedi_flow_engine。本階段只存這個值,不做任何狀態邏輯 |
user 澄清 2026-07-26 |
| org_unit | 不涉及(此階段純任務綁定) | — |
兩陷阱同 FR-056.1: QueryEntity 全 Optional=None(幽靈 WHERE)、Entity 可更新欄位預設 None(幽靈覆寫)。
BPMN 同步的既有紀律(務必守): _sync_task_to_template_xml() 的 actionInfo 是 overloaded 欄位——survey 任務存 survey uid,且程式碼註解明言「非 survey 任務的 actionInfo 保留給工具選擇」。detection_tool 正是那個「工具選擇」用途。傳值時只在 job_type == "detection_tool" 時帶 tool_action_info,避免誤蓋別型任務的 actionInfo。
| Task | 對應 | 產出 | 依賴 |
|---|---|---|---|
| Task 1 | T-2.1 | GrcJobType 加值 + BPMN userTask 同步線擴充 | FR-056.1 完成 |
| Task 2 | T-2.2 | job_execution_detection_tools 綁定表 + param_schemas 讀取 + 完成模式,全 DDD 層 | Task 1 |
| Task 3 | T-2.3 | FE 任務設置頁加 detection_tool 類型 + 工具/參數/完成模式 UI | Task 2 |
Files:
common/enum/grc_job_type_enum.pyinfra/grc/repository/grc_job_repo_impl.py(_sync_task_to_template_xml + caller update_job)api/grc/serializers/job.py(兩個 validate.OneOf 自動涵蓋,但要加 detection 專屬欄位)test/test_grc_job_detection_type.pycommon/enum/grc_job_type_enum.py:
class GrcJobType(StrEnum):
GENERAL = "general"
SURVEY = "survey"
DETECTION_TOOL = "detection_tool" # FR-056.2加值後這些 call site 自動涵蓋(都用
[e.value for e in GrcJobType]或_value2member_map_,非硬編碼二值清單):api/grc/serializers/job.py:142,177(OneOf)、app/grc/dto/job_dto.py:176,207(from_view_row)、infra/grc/repository/grc_job_repo_impl.py:200,236。唯一要人工處理:infra/grc/mapper/grc_job_mapper.py:24的to_entity用SURVEY if has_surveys else GENERAL啟發式——這是「從資料反推 job_type」的舊邏輯,detection_tool 的反推放到 Task 2(有綁定表後才能判斷),本 Task 先不動 mapper 啟發式。
test/test_grc_job_detection_type.py:驗證 _sync_task_to_template_xml 在 job_type=detection_tool 時,把 tool_uid 寫進 actionInfo、params 寫進 toolParams property。用既有 grc job repo test 的 XML 斷言慣例(runner 先看 test/ 下既有 _sync_task_to_template_xml 相關 test 怎麼組 template xml fixture)。
在 infra/grc/repository/grc_job_repo_impl.py 的 field_map 建構處,加 detection tool 專屬 property(mirror survey_action_info 的條件寫入):
def _sync_task_to_template_xml(self, job, name, description, guide, job_type,
survey_action_info=None,
tool_action_info=None, # detection tool_uid
tool_params=None, # dict → JSON-encode
completion_mode=None):
...
if job_type is not None: field_map["actionType"] = job_type or "general"
if survey_action_info is not None: field_map["actionInfo"] = survey_action_info
if tool_action_info is not None: field_map["actionInfo"] = tool_action_info # 工具型任務借用 actionInfo
if tool_params is not None: field_map["toolParams"] = json.dumps(tool_params, ensure_ascii=False)
if completion_mode is not None: field_map["completionMode"] = completion_mode注意:
actionInfo被 survey 與 tool 共用,但同一任務只會是其中一型,不衝突。caller 必須確保只在對應 job_type 時帶對應參數。
在 update_job(~line 487)計算 survey_action_info 的相鄰處,加 detection 分支:
tool_action_info = None
tool_params = None
completion_mode = None
if job_type == GrcJobType.DETECTION_TOOL.value:
tool_action_info = (tool or {}).get("uid", "") # tool 為新傳入參數
tool_params = (tool or {}).get("params")
completion_mode = (tool or {}).get("completion_mode")
self._sync_task_to_template_xml(job, name, description, guide, job_type,
survey_action_info=survey_action_info,
tool_action_info=tool_action_info,
tool_params=tool_params,
completion_mode=completion_mode)git add common/enum/grc_job_type_enum.py infra/grc/repository/grc_job_repo_impl.py test/test_grc_job_detection_type.py
git commit -m "feat(fr056): add detection_tool job type + BPMN sync (T-2.1)
GrcJobType 加 detection_tool;_sync_task_to_template_xml 支援 tool_uid(actionInfo)/toolParams/completionMode 寫回 BPMN userTask。"說明: 建 config.job_execution_detection_tools 單列綁定表(DDD 九層,複製 remote_agent 骨架)+ migration。app service 在 job create/update 時,若 job_type=detection_tool,reconcile 這張綁定表(mirror _reconcile_task_surveys 但簡化成單列 upsert)。另加 param_schemas 唯讀讀取(供 FE 拉某工具的參數欄位定義)。
Files:
scripts/sql/2026-07-26-fr056-2-job-execution-detection-tools.sqljob_execution_detection_tool 模組檔(同 FR-056.1 附錄 A 骨架)domain/detection_tools/service/detection_tool_param_schema_domain_service.py(param_schemas 唯讀)app/grc/service/job_service.py(加 _replace_detection_tool_binding)api/grc/serializers/job.py(加 tool 欄位)api/detection_tools/routes/detection_tool_route.py(加 GET param-schema endpoint)test/test_job_detection_tool_binding.pyscripts/sql/2026-07-26-fr056-2-job-execution-detection-tools.sql(照 sql-migration 慣例,GRANT/sequence/schema_migrations 齊):
-- Date: 2026-07-26
-- FR-056.2 任務↔檢測工具綁定(單列/任務,非 survey 的笛卡兒積)
CREATE TABLE config.job_execution_detection_tools (
id BIGSERIAL PRIMARY KEY,
uid VARCHAR(36) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
job_execution_id BIGINT NOT NULL, -- soft-ref → compliance.job_executions.id
detection_tool_id BIGINT NOT NULL, -- FK → config.detection_tools.id(同 schema)
tenant_config_id BIGINT, -- soft-ref → config.tenant_detection_tool_configs.id(用哪份租戶設定)
tool_params JSONB NOT NULL DEFAULT '{}'::jsonb,
completion_mode VARCHAR(20) NOT NULL DEFAULT 'manual', -- auto / manual
is_delete BOOLEAN NOT NULL DEFAULT FALSE,
created_user VARCHAR(255), updated_user VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fk_jedt_detection_tool FOREIGN KEY (detection_tool_id) REFERENCES config.detection_tools (id),
CONSTRAINT uq_jedt_job UNIQUE (job_execution_id) -- 一任務一綁定
);
COMMENT ON TABLE config.job_execution_detection_tools IS 'FR-056.2 任務↔檢測工具綁定(工具+參數快照+完成模式,單列/任務)';
GRANT SELECT, INSERT, UPDATE, DELETE ON config.job_execution_detection_tools TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE config.job_execution_detection_tools_id_seq TO cm_app;
ALTER TABLE config.job_execution_detection_tools ENABLE ROW LEVEL SECURITY;
CREATE POLICY jedt_tenant_isolation ON config.job_execution_detection_tools
USING (current_setting('app.is_super_admin', TRUE) = 'true'
OR tenant_id = ANY (string_to_array(current_setting('app.allowed_tenant_paths', TRUE), ',')::BIGINT[]));
INSERT INTO public.schema_migrations(filename, note) VALUES
('2026-07-26-fr056-2-job-execution-detection-tools.sql', 'FR-056.2 任務檢測工具綁定表') ON CONFLICT (filename) DO NOTHING;套進 DEV + 驗證(同 FR-056.1 Task 1 的 psql 驗證方式)。
加 GET /detection-tools/<tool_uid>/param-schema 回該工具當前生效版本(is_current=True)的 param_schema,供 FR-056.2 FE 拉參數欄位定義。
驗證:(a) job_type=detection_tool + tool payload → 綁定表落一列;(b) 只改別欄位的 partial update 不清空綁定;(c) job_type 改回 general → 綁定標 is_delete。
在 app/grc/service/job_service.py,mirror _reconcile_task_surveys 但簡化:
def _replace_detection_tool_binding(self, job_uid, tool, curr_user, tenant_id):
"""單列 upsert(非笛卡兒積)。tool=None 且非 detection 型 → 不動;job_type 改離 detection → 標 is_delete。"""
existing = self._jedt_domain.get_one(job_execution_uid=job_uid)
if not tool:
if existing: self._jedt_domain.soft_delete(existing.uid, curr_user)
return
if existing:
self._jedt_domain.update_binding(JobExecutionDetectionToolEntity(
uid=existing.uid, tool_params=tool.get("params"),
completion_mode=tool.get("completion_mode"), updated_user=curr_user))
else:
self._jedt_domain.create_binding(JobExecutionDetectionToolEntity(
uid=str(uuid.uuid4()), job_execution_id=..., detection_tool_id=...,
tool_params=tool.get("params") or {}, completion_mode=tool.get("completion_mode") or "manual",
tenant_id=tenant_id, created_user=curr_user, updated_user=curr_user))在 create_job / update_job 於處理 survey 綁定的相鄰處,加 if tool is not None or job_type == "detection_tool": → 呼叫此方法。
JobCreateRequestSchema / JobUpdateRequestSchema 加:
tool = fields.Nested(DetectionToolBindingSchema, allow_none=True, load_default=None)
# DetectionToolBindingSchema: uid(工具 uid), params(Dict), completion_mode(OneOf['auto','manual'])route 的 **data splat 自動把 tool 傳到 app service,app service 簽章加 tool=None kwarg。
infra/grc/mapper/grc_job_mapper.py 的 to_entity:has_surveys 啟發式旁加「有 detection binding → DETECTION_TOOL」判斷(若 mapper 拿得到綁定資訊;拿不到則靠 BPMN actionType 反推,讀路徑見 ssp_control_implementation_service.py 的 props.get("actionType"))。
git commit -m "feat(fr056): job-detection-tool binding + param schema read (T-2.2)"⚠️ 2026-07-26 落點修正(返工):本 Task 原指定
TaskSetupView.vue,經查證該頁是斷頭半成品頁——選單(DBui_routes)無條目、程式內無任何導覽點、fetchTree()打的GET /project/<uid>/task-setup/tree在 BE 根本沒註冊(只有 AP-scoped 版本)。使用者實際設定任務的頁面是ProjectPlanningView.vue(規劃頁「控制項實作」Tab 點 AO 展開的任務面板,路由/project/projects/:id/round/:roundUid/planning),既有jobType: 'general' | 'survey'就在此。首次實作(commitf7d4968)落在錯頁,需 revert 後在正確頁重做。兩頁讀寫任務用同一組 API(jobs/list+PUT /grc/project/<uid>/job/<id>),BE(T-2.2)不受影響。
⚠️ 2026-07-26 追加(plan 遺漏補洞):DEV DB 實查發現
config.detection_tool_param_schemas零 seed——56.1 只 seed 了連線欄位(config_field_schema),漏了掃描參數 schema。而 Agent connector 端params.hosts是必填(缺了 raise「缺少掃描目標」),T-2.3 的參數 UI 又是動態拉 param-schema 渲染——schema 空的會渲染出零欄位,使用者沒地方填掃描目標,整條鏈每次掃描必炸。故本 Task 增加 Step 0.5(BE repo)補 seed migration。
FE repo 為主,Step 0.5 在 BE repo。 檔案 src/views/project/ProjectPlanningView.vue(規劃頁,任務面板在「控制項實作」Tab)。
[
{"key":"hosts","label":"掃描目標(IP/主機,逗號分隔)","type":"text","required":true},
{"key":"timeout_sec","label":"逾時秒數","type":"number","required":false},
{"key":"scan_config_id","label":"Scan Config UUID(進階,留空用預設)","type":"text","required":false},
{"key":"scanner_id","label":"Scanner UUID(進階,留空用預設)","type":"text","required":false}
]key 命名必須與 evidence-agent connector 消費端一致(openvas.py:params.hosts / params.timeout_sec / params.scan_config_id / params.scanner_id)。套 DEV 後實查有進。獨立 commit(BE repo)。
{ label: t('lang.task_setup.job_type_detection'), value: 'detection_tool', icon: 'pi pi-shield' }任務型別(jobType: 'general' | 'survey',約 L46)加 'detection_tool',並加 toolUid: string|null、toolParams: Record<string,any>、completionMode: string。
GET /detection-tools/<uid>/param-schema 拉參數欄位定義 → 用 FR-056.1 建的 DetectionConfigField.vue(同一個動態欄位 renderer 複用)渲染參數表單,v-model 綁 task.toolParams。tool: task.jobType === 'detection_tool' ? {
uid: task.toolUid, params: task.toolParams, completion_mode: task.completionMode
} : null,注意規劃頁 L1271 的「刻意不送某些欄位以保留既有關聯」慣例——tool 欄位的送/不送要與該慣例一致(非 detection_tool 任務送 null 不可誤清 binding;比照 surveys 欄位的處理方式)。
git commit -m "revert(fr056): back out detection_tool UI from dead-end TaskSetupView (T-2.3 rework)"
git commit -m "feat(fr056): add detection_tool job type to planning page task panel (T-2.3)"同 FR-056.1 implementation-plan.md 附錄 A(來源 remote_agent 模組)。綁定表為 tenant-scoped,model 繼承 BaseModel, TenantScopedMixinModel。