日期:2026-03-24 狀態:Draft — 待確認
實作 GRC 合規管理系統的外部稽核流程(Phase 3),讓稽核員能逐控制項寫入稽核判定(verdict)與發現(findings),並在稽核結束時自動處理矯正流程。
launch_audit → 稽核員寫 verdict/findings → confirm_auditconfirm_audit 自動產生 POA&M 記錄 + revert 失敗 AO 的 workflow| 決策 | 選擇 | 原因 |
|---|---|---|
| 範圍 | Phase 3 + confirm_audit 自動化 | confirm_audit 不產生 POA&M 的話系統沒有落地行動項 |
| 權限控管 | project_participants.role = "auditor" |
用現有機制,不新建表 |
| launch_audit 前置驗證 | 警告但不阻擋 | 稽核時程常由外部決定,不應被內部進度卡住 |
| AR run 模型 | 單一 run(run_no=1) | 簡化 API,多 run 需求到 Stage 3 再擴充 |
| confirm_audit revert 粒度 | 由稽核員在 finding 指定 ao_uid | 稽核員最清楚哪個 AO 有問題 |
| confirm_audit 前置驗證 | 所有控制項必須有 verdict | 稽核結果是正式文件,不允許模糊空間 |
| 架構 | 拆兩個 service(編排 + CRUD) | oscal_project_service 已過大,職責分離 |
| AR controls 頁面 | Flat list + group 資訊,前端建樹 | 數量可控,flat list 支援分頁/篩選更彈性 |
移除 draft、completed、archived,最終 4 個值:
class AssessmentPlanStatus(StrEnum):
ACTIVE = "active" # Phase 1+2:任務指派 + 執行
AUDITING = "auditing" # Phase 3:外部稽核
REMEDIATION = "remediation" # Phase 4:矯正
CLOSED = "closed" # 本輪結案active → auditing → remediation → closed
↘ ↗
──── (全 pass) ──────
| 從 | 到 | 觸發 API | 條件 |
|---|---|---|---|
active |
auditing |
launch_audit |
AP 必須是 active |
auditing |
closed |
confirm_audit |
全部 verdict = pass 或 na |
auditing |
remediation |
confirm_audit |
任一 verdict = fail 或 partial |
remediation |
auditing |
launch_audit(Stage 3+4) |
矯正後重新稽核 |
remediation |
closed |
close_round(Stage 3) |
手動結案 |
每個轉換 API 檢查當前 status 是否在合法的「從」狀態,不合法拋 PreconditionFailedError。
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/launch-audit
權限: auditor 或 project owner
Request body:
{
"force": false
}流程:
activeassessment_plan_tasks.status != "completed")force = false → 回傳 warning,不執行force = true 或無未完成 tasks → 執行:
start_oscal_project 會建立空白 AR)assessment_result_datas(run_no=1)+ 初始化 assessment_result_controls(verdict = null)assessment_results(含 metadata + document)+ assessment_result_datas(run_no=1)+ 初始化 assessment_result_controls(verdict = null)auditing注意:
start_oscal_project已會建立空白 AR(只有 AR 本體,無 run 和 controls),因此launch_audit通常是在現有 AR 下新增 run + controls,而非重建 AR。
Warning response(force=false 且有未完成 tasks):
{
"code": 1,
"data": {
"can_launch": true,
"warning": true,
"incomplete_tasks": 5,
"total_tasks": 20
}
}Success response:
{
"code": 1,
"data": {
"ar_uid": "...",
"status": "auditing",
"incomplete_tasks": 5,
"total_tasks": 20
}
}Service: app/project/service/oscal_audit_service.py → launch_audit(ap_uid, force, curr_user)
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/controls/list
權限: 所有專案參與者
Request body: 標準 RequestMetaSchema(pager + sort + filters)
Filters:
| 欄位 | 類型 | 說明 |
|---|---|---|
search |
string | 控制項代號或名稱 |
verdict |
string | pass / fail / partial / na / null(未填) |
Response item:
{
"ar_control_uid": "...",
"control_id": "AC-1",
"control_title": "Access Control Policy",
"verdict": "fail",
"confidence": 85,
"findings_count": 2,
"group_uid": "...",
"group_name": "Access Control"
}前端依 group_uid 分組建樹狀導航。
Service: app/grc/service/audit_service.py
GET /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/control/<ar_control_uid>
權限: 所有專案參與者
Response:
{
"data": {
"ar_control_uid": "...",
"control_id": "AC-1",
"control_title": "Access Control Policy",
"description": "...",
"guidance": "...",
"verdict": "fail",
"confidence": 85,
"rationale": "...",
"remarks": "...",
"group_uid": "...",
"group_name": "Access Control",
"ssp_implementation": {
"implementation_status": "partial",
"implementation_description": "..."
},
"findings": [
{
"uid": "...",
"category": "deficiency",
"severity": "high",
"title": "...",
"description": "...",
"recommendation": "...",
"ao_uid": "...",
"ao_name": "..."
}
],
"assessment_objects": [
{
"ao_uid": "...",
"ao_name": "...",
"evidences": [
{
"uid": "...",
"evidence_type": "file",
"description": "...",
"file_id": 123,
"reference_url": null
}
]
}
]
}
}PUT /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/control/<ar_control_uid>/verdict
權限: auditor
前置條件: AP status = auditing
Request body:
{
"verdict": "pass | fail | partial | na",
"confidence": 85,
"rationale": "...",
"remarks": "..."
}Response: 更新後的 ar_control 資料
新增 Finding:
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/control/<ar_control_uid>/findings
Request body:
{
"category": "deficiency | observation | recommendation",
"severity": "low | medium | high | critical",
"title": "...",
"description": "...",
"recommendation": "...",
"ao_uid": "..."
}ao_uid 必填 — 指定哪個 AO 需要矯正。
新增 Finding response:
{
"code": 1,
"data": {
"uid": "...",
"category": "deficiency",
"severity": "high",
"title": "...",
"description": "...",
"recommendation": "...",
"ao_uid": "...",
"ao_name": "..."
}
}注意: Stage 2 期間 finding 可自由刪除(AP status =
auditing)。Stage 3 實作 POA&M CRUD 後,應加檢查防止刪除已有 POA&M 關聯的 finding。
Finding 詳情 / 更新 / 刪除:
GET /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/finding/<finding_uid>
PUT /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/finding/<finding_uid>
DELETE /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/finding/<finding_uid>
權限: auditor(寫入),所有參與者(讀取)
前置條件: AP status = auditing(寫入操作)
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/confirm-audit
權限: auditor 或 project owner
前置驗證:
auditing驗證失敗 response(412):
{
"code": 0,
"msg": "以下控制項尚未填寫 verdict",
"data": {
"missing_verdicts": [
{"control_id": "AC-1", "control_title": "Access Control Policy"},
{"control_id": "AC-2", "control_title": "Account Management"}
]
}
}執行流程:
收集所有 ar_controls 的 verdict
判定路徑:
矯正路徑:
assessment_result_datas.completed_at = now()ao_uid(去重)ao_uid → assessment_plan_tasks → assessment_plan_task_workflow_execution_mapping → workflow_execution_idjob_executions,找到最新的 COMPLETED 或 PROCESSING job 作為 revert 起點revert_job(workflow_uid, current_job_id, first_job_id, ...)compliance.poams(status = open)remediation直接結案路徑:
assessment_result_datas.completed_at = now()closedSuccess response:
{
"code": 1,
"data": {
"outcome": "closed | remediation",
"findings_count": 3,
"reverted_ao_count": 2,
"poam_count": 3
}
}Service: app/project/service/oscal_audit_service.py → confirm_audit(ap_uid, curr_user)
jedi-oscal 套件(需更版):
| 檔案 | 變更 |
|---|---|
common/enum/status_enum.py |
AssessmentPlanStatus 改為 4 值(移除 draft/completed/archived,新增 auditing/remediation/closed) |
infra/model/ar/assessment_result_control.py |
verdict 欄位改為 nullable=True(launch_audit 初始化時 verdict 為 null) |
infra/model/ar/assessment_result_finding.py |
新增 ao_uid VARCHAR(36) 欄位(稽核員指定矯正對象) |
domain/entity/ar/assessment_result_finding_entity.py |
新增 ao_uid 欄位 |
infra/mapper/ar/assessment_result_finding_mapper.py |
映射 ao_uid |
jedi-oscal DB migration:
-- verdict 改為 nullable
ALTER TABLE oscal.assessment_result_controls
ALTER COLUMN verdict DROP NOT NULL;
-- finding 新增 ao_uid
ALTER TABLE oscal.assessment_result_findings
ADD COLUMN ao_uid VARCHAR(36);
-- 現有 AP status 資料遷移
UPDATE oscal.assessment_plans SET status = 'active' WHERE status = 'draft';
UPDATE oscal.assessment_plans SET status = 'closed' WHERE status IN ('completed', 'archived');主專案 — 編排層:
| 層 | 檔案 | 說明 |
|---|---|---|
| App Service | app/project/service/oscal_audit_service.py |
launch_audit() / confirm_audit() |
| DI Container | di_containers/project/project_containers.py |
註冊 oscal_audit_service |
主專案 — GRC 稽核 CRUD:
| 層 | 檔案 | 說明 |
|---|---|---|
| Route | api/grc/routes/audit_route.py |
所有稽核相關端點 |
| Serializer | api/grc/serializers/audit.py |
Request / Response schemas |
| App Service | app/grc/service/audit_service.py |
verdict / finding / ar_controls 查詢 |
| DTO | app/grc/dto/audit_dto.py |
ArControlDto、ArControlDetailDto、ArFindingDto |
| Domain Service | domain/grc/service/grc_audit_domain_service.py |
委派 repo |
| Repo Interface | domain/grc/repository/i_grc_audit_repo.py |
抽象介面 |
| Repo Impl | infra/grc/repository/grc_audit_repo_impl.py |
SQLAlchemy 查詢 |
| Mapper | infra/grc/mapper/grc_audit_mapper.py |
ORM ↔︎ Entity |
| Entity | domain/grc/entities/grc_audit_entity.py |
AR control / finding domain entities |
| Blueprint | api/grc/__init__.py |
註冊新 route |
| DI Container | di_containers/grc/grc_containers.py |
註冊 audit service / repo |
| Error Code | common/code/grc_error_code.py |
新增 error codes |
主專案 — POA&M 基礎建設:
| 層 | 檔案 | 說明 |
|---|---|---|
| Model | infra/grc/model/poam_model.py |
ORM model |
| Entity | domain/grc/entities/poam_entity.py |
domain entity |
| Repo Interface | domain/grc/repository/i_poam_repo.py |
抽象介面(Stage 2 只有 batch create) |
| Repo Impl | infra/grc/repository/poam_repo_impl.py |
insert 實作 |
| Mapper | infra/grc/mapper/poam_mapper.py |
ORM ↔︎ Entity |
| Migration | scripts/sql/poam_migration.sql |
DDL |
| DI Container | di_containers/grc/grc_containers.py |
註冊 poam repo |
# common/code/grc_error_code.py
GRC_NOT_AUDITOR = ("使用者不具備稽核員角色", "GRC_403001")
GRC_AP_NOT_ACTIVE = ("稽核計畫狀態不是進行中,無法啟動稽核", "GRC_412001")
GRC_AP_NOT_AUDITING = ("稽核計畫不在稽核中狀態", "GRC_412002")
GRC_AR_VERDICT_INCOMPLETE = ("尚有控制項未填寫稽核判定", "GRC_412003")
GRC_AR_NOT_FOUND = ("稽核結果不存在", "GRC_404016")
GRC_AR_CONTROL_NOT_FOUND = ("稽核控制項不存在", "GRC_404017")
GRC_AR_FINDING_NOT_FOUND = ("稽核發現不存在", "GRC_404018")CREATE TABLE compliance.poams (
id SERIAL PRIMARY KEY,
uid VARCHAR(36) NOT NULL DEFAULT gen_random_uuid(),
assessment_plan_id INTEGER NOT NULL,
ar_finding_id INTEGER NOT NULL,
control_identifier VARCHAR(50) NOT NULL,
ao_uid VARCHAR(36) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'open',
closed_at TIMESTAMP,
tenant_id INTEGER NOT NULL,
org_unit_id INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now(),
created_user VARCHAR(50),
updated_user VARCHAR(50)
);
CREATE UNIQUE INDEX uq_poams_uid ON compliance.poams (uid);
CREATE INDEX ix_poams_ap_id ON compliance.poams (assessment_plan_id);
CREATE INDEX ix_poams_status ON compliance.poams (status);
CREATE INDEX ix_poams_tenant ON compliance.poams (tenant_id);Stage 2 只自動建立記錄,不提供前端 CRUD API(Stage 3 實作)。
所有 URL 前綴:/api/1.0/grc
| 功能 | 方法 | URL | 權限 |
|---|---|---|---|
| 啟動稽核 | POST | /project/<pid>/ap/<ap_uid>/launch-audit |
auditor / owner |
| AR 控制項列表 | POST | /project/<pid>/ap/<ap_uid>/ar/controls/list |
所有參與者 |
| AR 控制項詳情 | GET | /project/<pid>/ap/<ap_uid>/ar/control/<ar_control_uid> |
所有參與者 |
| 寫入 Verdict | PUT | /project/<pid>/ap/<ap_uid>/ar/control/<ar_control_uid>/verdict |
auditor |
| 新增 Finding | POST | /project/<pid>/ap/<ap_uid>/ar/control/<ar_control_uid>/findings |
auditor |
| Finding 詳情 | GET | /project/<pid>/ap/<ap_uid>/ar/finding/<finding_uid> |
所有參與者 |
| 更新 Finding | PUT | /project/<pid>/ap/<ap_uid>/ar/finding/<finding_uid> |
auditor |
| 刪除 Finding | DELETE | /project/<pid>/ap/<ap_uid>/ar/finding/<finding_uid> |
auditor |
| 確認稽核結案 | POST | /project/<pid>/ap/<ap_uid>/confirm-audit |
auditor / owner |
| 舊值 | 新值 | 說明 |
|---|---|---|
draft |
移除 | 合併到 active |
active |
保留 | Phase 1+2 |
completed |
移除 | 改用 closed |
archived |
移除 | 未來再議 |
| — | auditing(新增) |
Phase 3 |
| — | remediation(新增) |
Phase 4 |
| — | closed(新增) |
結案 |
前端需更新 AP status 的 badge / 顯示映射。
進入條件: AP status = auditing(或 remediation / closed 為唯讀)
UI 結構:
group_uid / group_name 前端分組)
唯讀資訊:
job_evidences,透過 AO → workflow → job 鏈路查詢)可編輯區域(AP status = auditing 且 role = auditor):
| AP status | 稽核總覽頁 | Verdict / Finding 操作 | launch-audit 按鈕 | confirm-audit 按鈕 |
|---|---|---|---|---|
active |
隱藏 | 不可用 | 顯示 | 隱藏 |
auditing |
顯示 | 可編輯(auditor) | 隱藏 | 顯示(全填完才啟用) |
remediation |
顯示(唯讀) | 不可用 | 隱藏 | 隱藏 |
closed |
顯示(唯讀) | 不可用 | 隱藏 | 隱藏 |
點擊「啟動稽核」
→ POST launch-audit { force: false }
→ response.warning == true?
→ 是:顯示確認對話框
「尚有 {incomplete_tasks}/{total_tasks} 項任務未完成,確定啟動外部稽核?」
→ 確定:POST launch-audit { force: true } → 成功,重新載入 AP 狀態
→ 取消:不動作
→ 否:直接成功,重新載入 AP 狀態
點擊「確認結案」
→ POST confirm-audit
→ response.code == 1?
→ outcome == "closed":
顯示成功訊息「稽核完成,所有控制項通過」
AP 狀態更新為 closed
→ outcome == "remediation":
顯示訊息「{findings_count} 項發現需要矯正,{reverted_ao_count} 個評估物件已退回重做」
AP 狀態更新為 remediation
→ response.code == 0(412)?
→ 顯示「以下控制項尚未填寫 verdict」+ missing_verdicts 列表
| verdict | 中文 | 英文 | Badge 顏色 |
|---|---|---|---|
pass |
通過 | Pass | 綠色 |
fail |
不通過 | Fail | 紅色 |
partial |
部分通過 | Partial | 橘色 |
na |
不適用 | N/A | 灰色 |
null |
未填寫 | Pending | 淺灰 / 虛線框 |
| category | 中文 | 英文 |
|---|---|---|
deficiency |
缺失 | Deficiency |
observation |
觀察 | Observation |
recommendation |
建議 | Recommendation |
| severity | 中文 | 英文 | Badge 顏色 |
|---|---|---|---|
critical |
嚴重 | Critical | 紅色 |
high |
高 | High | 橘色 |
medium |
中 | Medium | 黃色 |
low |
低 | Low | 灰色 |
以下 API 不受 Stage 2 影響:
Stage 2 上線後,前端需要: