稽核執行 功能群總覽¶
本檔放「本功能群多個頁面共用的知識」:頁面地圖、稽核階段與狀態機、角色權限基調、AP→AR→POA&M 核心資料模型。
各頁面 spec 引用本檔,只寫自己頁面的差異——改共用邏輯時改這裡一份。稽核輪次狀態機本體與轉換圖在 專案管理群 _overview §2(同一個
compliance.project_audit_rounds實體),本檔不重畫,只補本群相關的階段轉換與資料流。變更紀錄
日期 內容 2026-08-02 §4 engagement 模型補「覆核輪 clone AP 時就地收斂成 narrowed scope(母輪 not_met)」——含稽核行程( activity.related-controls)同步收斂、空集行程整條不 clone;§5 補坑 9「覆核輪 AP 範圍有兩個寫入者」。commits167f578f/1182034d/9b2eae04
1. 頁面地圖¶
稽核執行群承接啟動稽核之後的三個階段主場頁面,加上兩個跨階段的個人任務入口。
| 頁面 | FE route | 用途一句話 | spec |
|---|---|---|---|
| 稽核計畫填寫頁 | /project/projects/:id/round/:roundUid/ap-authoring |
audit_planning 階段:定義 AP(受評控制項 / 行程與查核方法 / 受評對象 / 稽核人員) | ✅ 稽核計畫填寫頁 |
| 稽核執行頁(AR 判定) | /project/projects/:id/round/:roundUid/audit |
auditing 階段:逐控制項判定 met/not_met/pending、記觀察、建系統風險、finalize | ✅ 稽核執行頁 |
| POA&M 矯正頁 | /project/projects/:id/round/:roundUid/poam |
remediation 階段:整改計畫與里程碑追蹤 | ⬜ poam.md |
| 我的任務(受稽方執行) | /project/task-manage |
跨專案的證據蒐集任務收件匣 | ⬜ my-tasks.md |
| 我的稽核 | /project/audit-manage |
稽核員視角的待辦稽核清單 | ⬜ my-audits.md |
頁面 × 階段對應(每個稽核輪次階段有一個「主場頁面」):
planning → 專案規劃頁(專案管理群) living SSP 現況 + 證據蒐集任務準備
audit_planning → 稽核計畫填寫頁(本群) 凍結 SSP 快照 → 定義 AP
auditing → 稽核執行頁 / AR 判定(本群) 逐控制項判定 → 產出 AR
remediation → POA&M 矯正頁(本群) not_met 缺失 → 整改追蹤
closed → 各頁唯讀
進入本群頁面前,輪次已
launch-audit過(living SSP 已凍結成project_audit_rounds.ssp_id快照)。本群所有頁面讀寫的控制項母體都來自凍結快照,不是 living SSP——這是與專案規劃頁最大的差異。
2. 稽核階段與狀態機(本群關心的轉換)¶
輪次 7 態狀態機與完整轉換圖見 專案管理群 _overview §2。本群關心的是「啟動稽核之後」的三段推進,enforcement 在 app/grc/service/audit_round_app_service.py:
| 轉換 | API | 誰能按 | 前置條件 | 失敗 error code |
|---|---|---|---|---|
| 啟動稽核 launch-audit → audit_planning | POST /audit-round/{uid}/launch-audit |
manager | status=planning、living SSP 非空(凍結快照) | — |
| 開始稽核 start-auditing → auditing | FE 實走 POST /project/{uid}/audit-round/{uid}/stage/advance(precondition registry);底層直接 API POST /audit-round/{uid}/start-auditing |
auditor(manager 可代行) | stage/advance 路徑:ApReviewedControlsSetCheck 查 reviewed-controls 非空(oscal_stage_preconditions.py:40,precondition 失敗回 i18n reason key flow_engine.precondition.ap_reviewed_controls_required,非 error code);直接 API start_auditing() 只驗 AP 存在(audit_round_app_service.py,可繞過 reviewed-controls 檢查) |
直接 API 無 AP → GRC_AP_NOT_FOUND。(GRC_AP_REVIEWED_CONTROLS_REQUIRED(412)實為 AP 編輯期 set_reviewed_controls 擋空清單,assessment_plan_app_service.py:470,非本轉換的關卡) |
| 完成判定 finalize → remediation | closed | POST /audit-round/{uid}/ar/finalize |
auditor | 無 pending 判定 + 每條 risk 至少關聯 1 finding | GRC_VERDICT_INCOMPLETE / GRC_RISK_NO_FINDING_LINKED(412) |
| 結束本輪 close-round → closed | POST /audit-round/{uid}/close-round |
manager | POA&M 全部 closed | GRC_POAM_NOT_ALL_CLOSED(412) |
| 啟動複驗 launch-reverify(開覆核輪) | POST /audit-round/{uid}/launch-reverify |
auditor(manager 可代行) | status=pending_reverify | — |
finalize 的分歧(本群最重要的轉換):ar/finalize 依判定結果決定去向——
- not_met = 0 → 輪次直接 closed(無缺失,不進整改)
- not_met > 0 → 自動從 not_met findings 生成 POA&M(PoamService.generate_from_findings),輪次轉 remediation、回填 project_audit_rounds.poam_id
通用 phase guard(本群與專案規劃頁的關鍵差異):本群每個寫入 API 都用 common/util/round_guard.py 的 assert_round_phase(round, {允許階段}) 硬擋——AP 寫入只在 audit_planning、AR 寫入只在 auditing,過了階段回 GRC_ROUND_PHASE_READONLY(412)。這與專案規劃頁「唯讀只是 FE 鎖、BE upsert 照寫」不同(見 project-planning §12 坑 3);本群的唯讀是 BE 強制的。
3. 角色權限基調¶
角色是誰:
- auditor(稽核員,本群主角)——填 AP 行程與查核方法、逐控制項判定 met/not_met、記觀察與風險、finalize。「身為 auditor,我要依稽核計畫逐控制項判定合規狀態,記錄觀察與風險,完成後送出這一輪的結果。」
- manager(專案負責人)——可代行 auditor 的所有判定操作,且獨掌輪次收尾。「身為 manager,我要在 auditor 判定完成、整改都結束後,把這一輪正式結案。」
- viewer(一般參與者)——檢視稽核進度,無編輯權。
專案層角色(compliance.project_participants.role):manager / auditor / viewer。角色定義與 manager 全域代行慣例見 專案管理群 _overview §3。本群的基調轉向 auditor 為主角(專案規劃頁以 manager 為主角):
| 動作類型 | manager | auditor | viewer |
|---|---|---|---|
| AP 編輯(受評控制項 / 行程 / 對象 / 稽核人員) | ✓(代行) | ✓ | ✗ |
| AR 判定(verdict / 觀察 / 風險 / finalize) | ✓(代行) | ✓ | ✗ |
| 輪次生命週期(launch / close-round) | ✓ | ✗ | ✗ |
| 檢視 | ✓ | ✓ | ✓ |
BE 檢查點:AP / AR 寫入走 _check_auditor(project_id, user_id)(role in ("auditor","manager") 才過,否則 GRC_NOT_AUDITOR);close-round 走 _check_role(..., "manager")。FE 判定:兩頁都以 roundStatus === '<本頁階段>' && (isAuditor || isProjectManager) 決定可編輯(RoundApAuthoringView.vue / RoundAuditReviewView.vue),與 BE 對齊。
4. 核心資料模型(AP → AR → POA&M)¶
本群走的是 OSCAL 資訊流的後半段:Catalog → Profile → SSP → AP → AR → POA&M。三份文件是獨立的 OSCAL root,靠 import-* FK 串接。
三份文件的職責:
| 文件 | root 表 | 承接自 | 內容 | 本群哪頁編輯 |
|---|---|---|---|---|
| AP(評估計畫) | oscal.assessment_plans |
import_ssp_id → 凍結 SSP |
reviewed-controls(查核範圍)/ tasks(行程+方法)/ assessment-subjects(抽查對象)/ parties(稽核人員) | 稽核計畫填寫頁 |
| AR(評估結果) | oscal.assessment_results → oscal.ar_results(每輪一筆 result) |
import_ap_id → AP |
findings(逐 AO 判定)/ observations(觀察)/ risks(系統風險) | 稽核執行頁 |
| POA&M(矯正計畫) | oscal.poams → oscal.poam_items |
poam_id(round 回填),內容源自 AR 的 not_met findings |
poam-items / remediations(response)/ milestones | POA&M 矯正頁 |
engagement 模型(本群最重要的概念):一個 engagement = 1 輪主稽核(initial/surveillance)+ N 輪覆核(close-out),共用同一份 AP(OSCAL import_ssp_id 單一)與同一份 AR root(import_ap_id 單一),但每輪各 append 一筆 oscal.ar_results;覆核輪 launch-reverify 時 clone 母輪 AP(clone_ap_for_round,party uuid 依 name remap)成獨立副本。
覆核輪 clone 是「收斂的 clone」,不是全量複製(2026-08-02 起)——覆核輪=OSCAL partial reassessment,本輪範圍收斂成母輪未通過控制(narrowed scope):
| clone 對象 | 覆核輪處理 |
|---|---|
ap_reviewed_controls.control_selections(受評控制項) |
只留 narrowed(母輪 assessment_findings 取 not-satisfied 對回控制項) |
ap_assessment_activities.related_controls(稽核行程鉤稽的控制項) |
同步過濾成 narrowed;過濾後空集的行程整條不 clone,其 ap_tasks 一併不 clone(不留孤兒行程) |
| metadata / parties / assessment-subjects | 全量 clone(不收斂) |
narrowed 由 AssessmentResultAppService.narrowed_control_ids_from_parent(parent) 在 clone 之前算出(只需母輪、不需新輪 entity)。範圍規則全文見 專案管理群 _overview §4。
AO(assessment objective)判定粒度:
- 判定矩陣是 AO 層:每個在範圍控制項的每個 AO = 一筆 finding(target_id = ao_id,_obj.N 格式);控制項無 AO part 時 fallback 成 control 層(target_id = control_id)
- UI 與 POA&M 是控制項層:稽核執行頁面板以控制項為單位,POA&M 一控制項一 item
- 控制項 verdict roll-up:控制項下所有 AO 都 met → 控制項 met;任一 AO not_met → 控制項 not_met
- AO 衍生邏輯 app/grc/service/ao_derivation.py::derive_ao_pairs(B5 AR 與規劃期 prep-job 共用),AO 識別碼 canonical = _obj.N(見 專案管理群 _overview §5)
5. 本群共用的已知坑¶
- 凍結快照,不是 living SSP:本群讀控制項 / 資產一律走
project_audit_rounds.ssp_id(launch-audit 當下拍的快照)。專案規劃頁的 living SSP 之後繼續演進,不影響本輪稽核 - phase guard 是 BE 硬擋:AP 只能在 audit_planning 改、AR 只能在 auditing 改,過階段 BE 回 412(不是只有 FE 鎖)——與專案規劃頁相反,別把規劃頁「BE 照寫」的心智模型套過來
- finding 判定 AO 層 / 顯示與 POA&M 控制項層:改判定相關邏輯先確認你在哪一層;
stats有met/not_met/pending(AO 計數)與controls_with_not_met(控制項計數)兩套 - verdict 值 API ≠ DB,但
target_status_state直存 4 種 token:API 用FindingStateenum(met/not_met/pending,另有第 4 值not_applicable);DBoscal.assessment_findings.target_status_state直接存 4 種 product token——satisfied(met)/not-satisfied(not_met)/not-applicable(na,OSCAL 規格無此值、比照 pending 直存 product token)/pending(未判定)。target_statusJSONB 是全狀態的冗餘鏡射(非pending專屬),比對以target_status_state為準(jedi-oscal-v2 .../domain/service/ar/ar_finding_matrix_service.py) - risk severity 存 JSONB:
oscal.assessment_risks無 severity 欄,等級存在characterizationsJSONB(稽核員判定),不是列舉欄位 - finding ↔ risk 多對多:
oscal.assessment_finding_risks關聯表 + 雙寫finding.related_risks[](OSCAL 匯出保真);finalize 前每條 risk 必須至少關聯一條 finding - 一 engagement 一 AP/AR:不要為覆核輪另開全新 AP/AR root;覆核走 clone_ap_for_round + append ar_results。輪次反查走
task_assignees.project_id,不要比對 workflow_execution_uid(那是死路) - 輪次狀態 BPMN 驅動:直接 UPDATE
project_audit_rounds.status會跟 workflow 脫鉤,一律走 API(start-auditing/ar/finalize/close-round) - 覆核輪 AP 受評範圍有兩個寫入者(2026-08-02):
ap_reviewed_controls除了launch-reverify建輪時寫,每次PUT /ap/{apUid}/tasks(存行程)也會被_backfill_scope_from_tasks用「行程控制項聯集」整個回寫(FR-040 Phase9 行程驅動)。因此覆核輪的收斂必須行程與 reviewed-controls 一起做——只收斂 reviewed-controls 而行程仍帶母輪全集,下一次存行程就打回全集(STG 實證:建輪後 11 秒被打回)。改既有髒資料時同理,兩張表都要改,且兩張表 JSONB 形狀不同:ap_reviewed_controls.control_selections是陣列[{"include-controls":[...]}]、ap_assessment_activities.related_controls是物件多包一層{"control-selections":[...]},共用同一個賦值會拿到 NULL 而清空範圍