FR-040 稽核計畫填寫(ap-authoring)優化 — 設計(SD)

Date: 2026-06-20 · Branch: FR-040-AP-improvement 決策依據與被排除選項見 docs/analysis/2026-06-20-ap-task-control-subject-linkage.md OSCAL 依據:v1.2.2 schema + 官方範例 oscal-content/examples/ap/json/ifa_assessment-plan.json

1. 範圍

稽核計畫填寫頁(RoundApAuthoringView.vue,route project-ap-authoring-round)兩件事:

  1. (已完成,先行) 查核方法選項改 system_menu(group AUDIT_METHOD)驅動,後台可擴充。 見 commit 8de251b6(BE seed) / f1b2052(FE)。
  2. (本設計) 「行程與方法」一條行程(task)能多對多鉤稽 控制項 / 受評對象(設備·系統·人員)/ 參與人員

2. 使用情境

受評單位看一條行程即知:幾點 → 稽核員用什麼方法 → 要查哪些控制項 → 要檢查哪些設備/系統 → 哪些人(受評方)要到場 → 稽核方誰會來。據此準備人與物。

3. 一條「行程」的資料模型

欄位 意義 多選 選項母體(同步來源)
title / type 既有
timing 時間(幾點) 既有
method 查核方法 system_menu AUDIT_METHOD
controls 要查哪些控制項 Tab1 受評控制項 勾選集
subjects 受評對象(設備/系統/人員) ✅ 跨類型 Tab2 受評對象 全部項目
participants 稽核人員(稽核方) SSP parties(OSCAL party 池,與受評對象同源;外部稽核員加為 SSP party)

4. OSCAL 對應 + 儲存落點

控制項/方法用現有欄位;受評對象/參與人員各開一張 link 表(2026-06-21 決策:路線圖有 行程通知/行事曆/儀表板,按人/對象反查行程,join 遠勝 JSONB containment)。建表理由與表結構見 analysis 受評對象/參與人員段

ap_tasks(一條行程;1 task = 1 activity)
 ├─ timing (JSONB)                時間
 ├─ associated_activities (JSONB) [{ activity-uuid }]   ← 引用 ap_assessment_activities
 │   (subjects / responsible_roles 兩 JSONB 欄改不寫,SoT 移到下面 link 表)
 ├── ap_task_subjects (新表)      受評對象  (task_id, subject_uuid, subject_type, include)
 └── ap_task_participants (新表)  參與人員  (task_id, party_uuid, role_id)
ap_assessment_activities(一筆活動)
 ├─ related_controls (JSONB)      控制項   {control-selections:[{include-controls:[{control-id}]}]}
 ├─ props (JSONB)                 方法     [{name:"method", value:"EXAMINE"}, …](多選=多筆)
 └─ steps (JSONB)                 查核步驟(選填,本期不做 UI)
要存的 表.欄 OSCAL 路徑
控制項 ap_assessment_activities.related_controls (JSONB) activity.related-controls → control-id
方法(多) ap_assessment_activities.props (JSONB) activity.props name=method
受評對象(多,跨類型) ap_task_subjects(link 表,SoT) task.subjects → subject-uuid + type
參與人員(多) ap_task_participants(link 表,SoT) task.responsible-roles → role-id + party-uuids
行程↔︎活動 ap_tasks.associated_activities (JSONB) task.associated-activities.activity-uuid
時間 ap_tasks.timing (JSONB) task.timing

method 放 props:官方範例就是 props:[{name:method,value:EXAMINE}],props 是 list 天生多選, 是 OSCAL 對 assessment method 的標準表示,不是硬塞。 受評對象/參與人員改 link 表(非 JSONB):單一資料源 = 表;OSCAL 匯出由 mapper 從表組回 task.subjects / task.responsible-roles,匯入時 parse 進表 → 不雙寫、相容性不變。

5. 選項同步規則(前端)

  • 控制項下拉可選集 = Tab1 已勾選受評控制項(取消勾選的控制項,已選進行程者需提示/移除)。
  • 受評對象下拉可選集 = Tab2 已設定受評對象(component/inventory/party 分組顯示)。
  • 稽核人員下拉可選集 = SSP parties(OSCAL responsible-role 引用 party;與受評對象同一 party 池, 選稽核方那些)。專案參與者(那是操作系統的內部使用者)。外部稽核員須先在 SSP「人員/單位」加為 party。

    2026-06-21 修正:原設計誤用專案參與者;user 指出稽核員(含外部)應源自 SSP party。

  • 精神對齊 OSCAL:reviewed-controls / assessment-subjects 是母體,task 只引用其子集。

6. API 合約(沿用既有端點,擴充 payload)

PUT /ap/{apUid}/tasks — body { tasks: [...] },每條行程:

{
  "title": "…", "type": "action", "timing": <既有>, "description": "…",
  "methods":  ["EXAMINE", "INTERVIEW"],                 // → activity.props
  "controls": ["ac-6.1", "ia-2"],                       // → activity.related_controls(control-id)
  "subjects": [ {"subject_uuid":"…","type":"component"},// → task.subjects
                {"subject_uuid":"…","type":"party"} ],
  "participants": [ {"party_uuid":"…","role_id":"assessor"} ]  // → task.responsible_roles
}

讀取(GET …/ap 內 tasks)還原同形狀(join ap_tasks + ap_assessment_activities)。 全量覆寫沿用現有 set_tasks 策略(一併重建 activity)。

7. UI(RoundApAuthoringView.vue Tab3)

每條行程 row 既有(標題/類型/時間/方法)+ 新增兩個 MultiSelect:

  • 控制項(options=Tab1 勾選集,顯示 control-id + 標題)
  • 受評對象(options=Tab2,依 type 分組:設備/系統/人員)
  • 稽核人員(options=SSP parties,與受評對象同一 party 池)

readonly 規則沿用既有(僅 audit_planning 且 auditor/manager 可編)。

8. OSCAL 相容性

完全符合。匯出 OSCAL JSON 時 mapper 組裝:

  • ap_assessment_activitieslocal-definitions.activities[](method 已是 props、controls 已是 related-controls)。
  • ap_tasks.associated_activitiestask.associated-activities[]
  • ap_task_subjects(link 表)→ task.subjects[](依 type 群組 include-subjects)。
  • ap_task_participants(link 表)→ task.responsible-roles[](依 role_id 群組 party-uuids)。 匯入時反向 parse 進 link 表。link 表為單一資料源,不再寫 ap_tasks.subjects/responsible_roles JSONB。

9. 不在本期範圍

  • AO/statement 層精準(statement-ids / control-objective-selections)—— 先做控制項層。
  • activity.steps 的 UI。
  • 行程↔︎活動 1:N(多方法各對不同控制項)。
  • 受評對象/參與人員升級 link 表(B2)。

10. 稽核人員 = AP metadata.parties + 就地新增(2026-06-21 增補,取代「稽核人員=SSP parties」)

為什麼(OSCAL 依據,官方範例證實)

NIST ifa_assessment-plan.jsonmetadata 內:roles=[{id:"assessor"}]parties=[{type:"person", name:"Amy Assessor"}, {type:"organization", name:"IFA"}]responsible-parties=[{role-id:"assessor", party-uuids:[Amy]}]task.responsible-roles=[{role-id:"assessor"}]。 → 稽核團隊放在 AP 自己的 metadata.parties(非 SSP、非專案參與者)。SSP 另由 import-ssp 帶入=受評方。

實務流程支撐:SSP 階段常不知道誰來稽核;AP 常由受評公司代填,稽核窗口此時才確定 → 在 AP 階段「就地新增」稽核人員 = OSCAL 標準做法,不是 workaround。

落地(零新表,重用既有 metadata-scoped party 基礎設施)

  • oscal.assessment_plans.metadata_id(1:1)已存在;oscal.parties / oscal.rolesmetadata_id scope。
  • AP parties = oscal.parties WHERE metadata_id = AP.metadata_id(= OSCAL AP.metadata.parties)。
  • 受評對象仍來自 import 的 SSP parties(受評方);稽核人員來自 AP parties(稽核方)→ 兩池分離、不再混淆。
  • per-task 選擇:沿用 ap_task_participants(party_uuid, role_id="assessor")(party_uuid 改指 AP party uuid)。
  • 角色:確保 AP metadata 的 oscal.rolesassessor(建 AP draft 時 seed,或首次新增 party 時補)。

行為

  • 稽核人員下拉 options = AP parties(list by ap.metadata_id)。
  • 就地新增(inline quick-add):下拉內「+新增」開小表單(姓名 / person·organization / 預設 role=assessor) → 建一筆 oscal.parties(metadata_id=AP.metadata_id)→ 回 uuid → 自動選入。建真 party 拿 uuid,不存自由文字
  • 重用 ssp_party_app_service 的 party CRUD 形態,只是 metadata_id 來源改 AP(mirror,不另造輪子)。

OSCAL 匯出

ap_task_participantstask.responsible-roles[](role-id + party-uuids);AP parties → metadata.parties[]assessor role → metadata.roles[];文件層 metadata.responsible-parties[] 由 mapper 彙整。

取代

本節取代 §3/§5/§7 的「稽核人員 = SSP parties」過渡做法(commit 1e8c231)。過渡版可運作但語意不對,B 落地後切換。

11. 行程說明 → 每方法查核指引(Phase 7,activity.steps)

11.1 目標 / WHY

受評單位常需要的不是一句籠統說明,而是「每個查核方法各自要看什麼、怎麼看」,例:

文件檢查:檢視帳號權限總表、FORM4042640 帳號權限審查紀錄表,確認「一人一帳號」。 電腦設定比對:比對電腦使用者帳號設定,確認每項程序與帳號皆具唯一性。

把現在的單一多行「說明」(自由文字 Textarea,過渡 commit 0103997) 升級成結構化、依方法分段的查核指引:受評方一看就知每個方法要準備/檢視什麼,且 OSCAL-native、可被匯出/報表/通知引用。

11.2 OSCAL 依據

activity.steps[](required uuid;含 title / description / props…)。官方範例每 step = 一個查核程序步驟 ({uuid, title, description})。本期把 step ↔︎ 查核方法 1:1 綁定step.props{name:"method", value:<method value>}

11.3 資料模型 / 儲存(零新表)

ap_assessment_activities.steps JSONB(欄位已存在):

[
  { "uuid": "...", "title": "<method value/label>",
    "description": "<該方法的查核指引>",
    "props": [{ "name": "method", "value": "EXAMINE" }] }
]
  • method 清單activity.props name=method)仍是「這條行程用哪些方法」的權威來源(chip / 統計)。
  • steps 是「每個方法的指引」。規則:steps ⊆ methods(只存「已選方法且有填指引」者;取消勾選的方法不存其 step)。
  • ap_tasks.description(多行 Textarea)保留為「行程整體備註」(選填),與 per-method steps 並存、語意不同。

11.4 UI(RoundApAuthoringView.vue Tab3 行程卡身)

在「查核方法」下方新增一區 「查核指引」:依 tk.methods 動態列出每個已選方法一列(方法 label + 該方法的指引 Textarea)。

  • 未選方法 → 不顯示;勾選新方法 → 自動長出該列;取消 → 該列隱藏。
  • FE 狀態:tk.stepGuidance: Record<methodValue, string>(method value → 指引文字)。
    • 載入:ap.tasks[].steps → map(step.props.methodstep.description)。
    • 顯示:v-for over tk.methods,每方法一個 Textarea 綁 tk.stepGuidance[method]
    • 取消勾選的方法其文字留在記憶體 map(重新勾回還在),存檔時只輸出 methods 內的。
  • readonly 沿用既有。

11.5 API payload(PUT /ap/{apUid}/tasks 每筆 task 增欄)

"steps": [ { "method": "EXAMINE", "description": "檢視帳號權限總表…" }, … ]   // 只帶有指引的方法
  • 讀回(GET …/ap 的 tasks)同形狀:steps: [{method, description}](method 由 step.props 還原;title 由 FE i18n 出 label)。

11.6 BE 改動

  • set_tasks:每筆 task 把 payload stepsactivity.steps,每筆補 uuid(uuid4) + props=[{name:"method",value:method}] + title(= method value,FE 顯示用 i18n) + description
  • _ap_detail:讀 activity.stepssteps:[{method: <props.method>, description}]
  • helpers:_steps_from_payload(steps) / _steps_to_payload(activity_steps)
  • route schema _TaskItemstepsfields.List(Nested(method, description)),load_default None)。
  • 零新表 / 零 migration(用既有 ap_assessment_activities.steps)。

11.7 OSCAL 匯出

activity.steps 即 OSCAL steps(title/description/props);匯出零轉換。

11.8 邊界 / 不做

  • 一方法一指引(1:1);不做多 step / 巢狀 step。
  • step 的 reviewed-controls / responsible-roles(OSCAL 容許)本期不用。
  • 不刪 description(整體備註保留)。

11.9 狀態

實作完成(2026-06-21),BE 12 測試綠 / FE build 過,待 user 手測驗收。零套件改動、零 migration (ap_assessment_activities.steps 欄位早已存在,entity/mapper 已雙向接好)。 commit:主專案 + FE FR-040-AP-improvement(未 push)。changelog docs/changelog/2026-06-21-feat-fr040-phase7-ap-task-step-guidance.md

  • 附帶同頁修補:查核指引方法標籤改 soft pill;受評對象 party 角色改走 lang.oscal_role.ssp_party_role i18n(原顯示 raw role-id)。

12. 查核指引下沉到 AO 層(評估後不做,定案維持 Phase 7 per-method)

定案(2026-06-21):維持 Phase 7「每方法一條 textarea」為最終形態,不下沉到 AO 層、不加控制項維度。 理由:per-method 已是 OSCAL 標準做法(activity.steps + props method,官方範例即如此),對齊報表 「檢查方式/檢查方法」結構;AO 的 [a][b][c] 差異 auditor 可在該方法的指引文字內以文字註明,不值得為它 做 3 維 grid(控制項×AO×方法)的 UI 複雜度 + AO 端點。逐列儲存(Part B)亦暫不做,維持全量覆寫。 下方 12.1~12.8 為當初評估 AO 方案的完整軌跡(含被排除原因),保留供未來若報表要 per-AO 自動拆分時重啟參考。 未來反悔條件:若要「那份 per-AO Excel 報表自動填充、按 AO 拆證據/結果」→ 才需重啟 AO 層(見 §12.7 ③)。

12.1 WHY / 問題(已評估,定案不做,見上方 banner)

Phase 7 把「查核指引」綁在 (行程, 方法) —— 一個方法只能對應一條指引。但實際稽核粒度是 (控制項, AO, 方法):一個控制項有多個 AO(控制目標 [a][b][c]),同一方法用在不同 AO 的 「要看什麼、怎麼看」完全不同(實例:CMMC IA.L1-B.1.V 的 [a] 文件檢查 vs [b] 文件檢查 指引不同)。 現況指引會把同控制項的多個 AO 混成一條。

並行需求:行程現在是全量覆寫、一顆按鈕全存set_tasks delete-all-recreate)。多列各自編輯時要能 逐列儲存(避免一存覆蓋全部、多人/多次編輯互蓋)。

抉擇結論(2026-06-21,user 拍板):

  • 行程粒度:選 A — 行程維持可跨多控制項,指引區改 控制項 → AO → 已選方法 分組(非「一行程=一控制項」)。
  • 指引歸屬:指引內容是「本案/本輪特定的」(引用特定表單/系統),屬 AP/這一輪上移到 catalog (catalog 屬性可重用、指引不可重用)→ 仍存 ap_assessment_activities.steps(AP-scoped)。
  • 本期 scope:①AO 指引 + ②逐列儲存。③帶去 AR + per-AO Excel 報表延後(AR matrix 改走 AO 層 + 報表來源/格式需另釐清,另開 phase)。

12.2 OSCAL 依據(AO scope 放 step)

OSCAL step.reviewed-controlsAssessmentReviewedControls)可帶 control-selections,其 select-control-by-id 支援 statement-ids[] 指定該控制下的特定 statement/objective。 → 每個 step 可精準 scope 到 (control-id, AO),OSCAL-native,無需自創欄位。 AO 識別碼用全系統 canonical _obj.Ncatalog_control_parts.part_idpart_name='assessment-objective'), 與 AR 判定 / 準備期 job 同一套(共用 app/grc/service/ao_derivation.py:derive_ao_pairs,無 AO 走控制層 fallback)。

12.3 資料模型(零新表,沿用 ap_assessment_activities.steps JSONB,每 step 加 AO scope)

// activity.steps[] 每筆 = (控制項, AO, 方法) → 一條指引
{
  "uuid": "...",
  "description": "<檢查方法/查核指引>",
  "props": [{ "name": "method", "value": "EXAMINE" }],
  "reviewed-controls": { "control-selections": [
    { "include-controls": [ { "control-id": "ia-1", "statement-ids": ["ia-1_obj.a"] } ] } ] }
}
  • method 權威來源仍是 activity.props(行程用哪些方法、chip/統計);step 是「每 (AO,方法) 的指引」。
  • 規則 step ⊆ (行程選的控制項 × 各控制項 AO × 行程選的方法),且只存「有填指引」者。
  • 控制層 fallback:無 AO 的控制項,step 的 statement-ids 省略(scope 退到 control-id),等同 Phase 7 行為。
  • 向後相容:Phase 7 已存的 method-only step(無 reviewed-controls)→ 載入時歸到「控制層」格,不壞;dev 重填即升級到 AO 層。

12.4 API

AO 清單(新增,FE 畫 grid 用) GET /ap/{apUid}/control-objectives{ "<control_id>": [ { "ao_id":"ia-1_obj.a", "label":"[a]", "prose":"..." }, … ] }

  • 只回該 AP reviewed-controls 的控制項(grid 母體 = 行程能選的控制項)。
  • BE 重用 AR 既有 AO 推導鏈:AP→凍結 SSP→專案 profile→catalog→catalog_control_repo.get_by_catalog 組 cc_map → derive_ao_pairs + 取 part title/prose/label。label 來源待 pre-flight 驗(part.props name=label / part_id 尾碼 / prose 首段,擇一)。
  • DI 待定:AP app service 目前無 _part_repo/_catalog_control_repo/_profile_import_repo → 方案(i) 在 AP service 補 wiring;方案(ii) route 借 AR app service 既有方法。pre-flight 決定(傾向 i,read 歸 AP service)。

逐列儲存(Part B,取代全量覆寫為主路徑)

  • POST /ap/{apUid}/tasks 新增一條行程 → 回 { uid, … }
  • PUT /ap/{apUid}/tasks/{taskUid} 更新單條行程(含其 activity + steps + 受評對象/稽核人員 link)
  • DELETE /ap/{apUid}/tasks/{taskUid} 刪一條行程(cascade 其 activity + link)
  • 單條 payload 沿用 Phase 7 _TaskItem + steps 改成 [{control_id, ao_id|null, method, description}]
  • 既有全量 PUT /ap/{apUid}/tasks(set_tasks)保留為相容/批次入口;FE 改走逐列。

12.5 UI(RoundApAuthoringView.vue Tab3)

  • 行程卡「查核指引」區改 grid:v-for 控制項(tk.controls)v-for AO(該控制項 objectives)v-for 已選方法(tk.methods) → 一格 Textarea。
    • AO 清單來自 GET …/control-objectives,只顯示 tk.controls 內控制項;無 AO 控制項退成單層(控制項 → 方法)。
    • 狀態:tk.stepGuidance: Record<"<control_id>|<ao_id>|<method>", string>(三元組 key)。
    • 取消勾選的控制項/方法其文字留記憶體,存檔只輸出在 (controls×AO×methods) 內者。
  • 每張卡一顆「儲存此行程」(呼單列端點)+「刪除」;頂部保留「新增行程」。離開有未存變更提示(防誤丟)。
  • readonly 沿用既有。

12.6 BE 改動

  • _steps_from_payload / _steps_to_payload 改 (control_id, ao_id, method, description) ↔︎ OSCAL step(含 reviewed-controls)。
  • list_control_objectives(ap_uid)(AO 清單)。
  • 新單列 add_task / update_task / delete_task(per-task upsert,需穩定 task uid;repo 單列 add/update/delete 能力 pre-flight 驗套件)。
  • route + serializer:3 個單列端點 + _TaskStepItemcontrol_id/ao_id

12.7 不在本期

  • ③ AR per-AO matrix + 那份 per-AO Excel 報表(輸出端,另開 phase;本期讓資料變 AO 形狀備好源頭)。
  • step 多巢狀、AO 多選方法各對不同子項。

12.8 狀態

評估後定案不做(2026-06-21),維持 Phase 7 per-method。本節作為決策軌跡保留(含被排除原因 + 未來反悔條件)。 implementation-plan「Phase 8」同步標記暫不做。

13. AP 規劃頁改「行程驅動」單頁 + 範圍由行程回填(Phase 9,2026-06-21 定案)

13.1 WHY / 決策

原三頁籤(①受評控制項 ②受評對象 ③行程與方法)有重複輸入:先在 ①②勾母體,再在 ③從母體取子集。 洞察:行程裡每條 task 已各自鉤稽控制項/受評對象(FR-040 落 ap_assessment_activities.related_controls / ap_task_subjects),所有 task 的聯集即等於受評控制項/對象母體 → ①②可由 ③推導,不必手動先選。

定案(user 2026-06-21)

  • 頁籤改序:① 行程與方法(移到最前、唯一編輯面)→ ② 受評控制項(唯讀)→ ③ 受評對象(唯讀)。
  • 後兩頁籤改唯讀「範圍摘要」:顯示「目前行程湊出的受評控制項/對象」,不可編輯(保留「一眼看全範圍」,去除雙重輸入)。
  • 範圍回填set_tasks 存檔時,把所有 task 控制項聯集回填 ap_reviewed_controls、受評對象聯集回填 ap_assessment_subjects
  • 採方案 1(唯讀保留),非完全移除。

13.2 必守 / 相依

  • ap_reviewed_controls 是 AR 驅動源assessment_result_app_service._inscope_control_ids → AR 矩陣初始 / close-round 覆核 / 判定頁)。 → set_tasks 必須回填,否則 AR 拿不到 scope。回填後 downstream 全部零改動。
  • 前置條件位移:原「AP 必須先設 reviewed-controls(空擋 GRC_AP_REVIEWED_CONTROLS_REQUIRED)」改由 set_tasks 回填滿足; set_tasks 內部回填走 domain self._ap.set_reviewed_controls不套空值擋,允許 WIP 存無控制項的行程), stage-advance 既有 gate 仍在真正進階時擋空 scope。
  • 語意改變:在 scope = 一定在某條行程裡(無「在範圍但未排程」狀態)。符合「範圍=排定要查的」流程。

13.3 落地

  • BEassessment_plan_app_service.set_tasks):迴圈收集 task 的 controls/subjects 聯集(保序去重)→ self._ap.set_reviewed_controls(ap_uid, build_control_selections(union), …) + self._ap.set_assessment_subjects(ap_uid, [ApAssessmentSubjectsEntity(...)], …)。同 @transaction
  • FERoundApAuthoringView.vue):
    • TabPanel 改序(行程→控制項→對象)。
    • 任務控制項下拉母體 = 完整控制樹groups,去除 selected 過濾);受評對象下拉母體 = 完整 SSP 對象subjectRegistry 全集,去除 subjectSel 過濾)。
    • ②③改唯讀:顯示由 tasks[] 即時推導的控制項/對象聯集(移除勾選 UI + save 按鈕)。
  • 零新表、零 migration(重用既有 reviewed-controls/subjects 表,只是改成由行程回填)。

13.4 狀態

實作完成並上線(2026-06-21)。commit:BE f39019ed、FE b7b89b6(頁籤改序 + 後兩頁唯讀範圍摘要 + set_tasks 回填 reviewed-controls/subjects)。

延伸(2026-06-21 收尾):AP 進入唯讀後,「行程與方法」原為整片 :disabled 灰掉表單、回頭 review 體感差 → 改成乾淨標籤化唯讀呈現(純文字 + Tag,只顯示有值欄位),編輯階段維持原表單。commit:FE e4e8dc3;changelog docs/changelog/2026-06-21-tweak-ap-readonly-display.md