# Stage 2：外部稽核（Phase 3）設計規格

> 日期：2026-03-24
> 狀態：Draft — 待確認

---

## 一、目標與範圍

### 目標

實作 GRC 合規管理系統的外部稽核流程（Phase 3），讓稽核員能逐控制項寫入稽核判定（verdict）與發現（findings），並在稽核結束時自動處理矯正流程。

### 範圍

- Phase 3 完整流程：`launch_audit` → 稽核員寫 verdict/findings → `confirm_audit`
- `confirm_audit` 自動產生 POA&M 記錄 + revert 失敗 AO 的 workflow
- AP status 擴充至 4 值生命週期
- **不包含**：POA&M 前端追蹤頁面 / CRUD API（Stage 3）

### 設計決策摘要

| 決策 | 選擇 | 原因 |
|------|------|------|
| 範圍 | 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 支援分頁/篩選更彈性 |

---

## 二、AP Status 生命週期

### Enum 變更（jedi-oscal）

移除 `draft`、`completed`、`archived`，最終 4 個值：

```python
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`。

---

## 三、API 規格

### 3.1 launch_audit — 啟動外部稽核

```
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/launch-audit
```

**權限：** auditor 或 project owner

**Request body：**

```json
{
  "force": false
}
```

**流程：**

1. 驗證 AP status = `active`
2. 驗證呼叫者權限
3. 統計未完成 tasks（`assessment_plan_tasks.status != "completed"`）
4. 若有未完成 tasks 且 `force = false` → 回傳 warning，不執行
5. 若 `force = true` 或無未完成 tasks → 執行：
   - 查詢該 AP 是否已有 AR（`start_oscal_project` 會建立空白 AR）
   - **有現有 AR**：在該 AR 下建立 `assessment_result_datas`（run_no=1）+ 初始化 `assessment_result_controls`（verdict = null）
   - **無現有 AR**：建立 `assessment_results`（含 metadata + document）+ `assessment_result_datas`（run_no=1）+ 初始化 `assessment_result_controls`（verdict = null）
   - AP status → `auditing`

> **注意：** `start_oscal_project` 已會建立空白 AR（只有 AR 本體，無 run 和 controls），因此 `launch_audit` 通常是在現有 AR 下新增 run + controls，而非重建 AR。

**Warning response（force=false 且有未完成 tasks）：**

```json
{
  "code": 1,
  "data": {
    "can_launch": true,
    "warning": true,
    "incomplete_tasks": 5,
    "total_tasks": 20
  }
}
```

**Success response：**

```json
{
  "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)`

---

### 3.2 AR Controls 列表

```
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：**

```json
{
  "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`

---

### 3.3 AR Control 詳情

```
GET /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/control/<ar_control_uid>
```

**權限：** 所有專案參與者

**Response：**

```json
{
  "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
          }
        ]
      }
    ]
  }
}
```

---

### 3.4 寫入 Verdict

```
PUT /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/control/<ar_control_uid>/verdict
```

**權限：** auditor

**前置條件：** AP status = `auditing`

**Request body：**

```json
{
  "verdict": "pass | fail | partial | na",
  "confidence": 85,
  "rationale": "...",
  "remarks": "..."
}
```

**Response：** 更新後的 ar_control 資料

---

### 3.5 Finding CRUD

**新增 Finding：**

```
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/ar/control/<ar_control_uid>/findings
```

**Request body：**

```json
{
  "category": "deficiency | observation | recommendation",
  "severity": "low | medium | high | critical",
  "title": "...",
  "description": "...",
  "recommendation": "...",
  "ao_uid": "..."
}
```

`ao_uid` 必填 — 指定哪個 AO 需要矯正。

**新增 Finding response：**

```json
{
  "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`（寫入操作）

---

### 3.6 confirm_audit — 確認稽核結案

```
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/confirm-audit
```

**權限：** auditor 或 project owner

**前置驗證：**

1. AP status = `auditing`
2. 所有 ar_controls 必須有 verdict（非 null）

**驗證失敗 response（412）：**

```json
{
  "code": 0,
  "msg": "以下控制項尚未填寫 verdict",
  "data": {
    "missing_verdicts": [
      {"control_id": "AC-1", "control_title": "Access Control Policy"},
      {"control_id": "AC-2", "control_title": "Account Management"}
    ]
  }
}
```

**執行流程：**

1. 收集所有 ar_controls 的 verdict
2. 判定路徑：
   - 全部 pass 或 na → 直接結案路徑（步驟 4）
   - 任一 fail 或 partial → 矯正路徑（步驟 3）

3. 矯正路徑：
   - 設定 `assessment_result_datas.completed_at = now()`
   - 收集所有 findings 的 `ao_uid`（去重）
   - 對每個被點名的 AO：
     - `ao_uid → assessment_plan_tasks → assessment_plan_task_workflow_execution_mapping → workflow_execution_id`
     - 查詢該 workflow 的 `job_executions`，找到最新的 COMPLETED 或 PROCESSING job 作為 revert 起點
     - 查詢該 workflow BPMN 的第一個 USER job 作為 revert 目標
     - 呼叫 flow-engine `revert_job(workflow_uid, current_job_id, first_job_id, ...)`
     - 若 workflow 所有 jobs 皆為 TODO，跳過 revert（無需回退）
   - 為每個 finding 建立一筆 `compliance.poams`（status = `open`）
   - AP status → `remediation`

4. 直接結案路徑：
   - 設定 `assessment_result_datas.completed_at = now()`
   - AP status → `closed`

**Success response：**

```json
{
  "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)`

---

## 四、DDD 分層架構

### 新增 / 修改檔案

**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：**

```sql
-- 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 |

### 新增 Error Codes

```python
# 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")
```

---

## 五、POA&M 資料表

```sql
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 實作）。

---

## 六、前端整合規格

### 新增 API 總覽

所有 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 |

### AP Status 變更

| 舊值 | 新值 | 說明 |
|------|------|------|
| `draft` | **移除** | 合併到 active |
| `active` | 保留 | Phase 1+2 |
| `completed` | **移除** | 改用 closed |
| `archived` | **移除** | 未來再議 |
| — | `auditing`（新增）| Phase 3 |
| — | `remediation`（新增）| Phase 4 |
| — | `closed`（新增）| 結案 |

前端需更新 AP status 的 badge / 顯示映射。

### 前端頁面建議

#### 1. 稽核總覽頁

**進入條件：** AP status = `auditing`（或 `remediation` / `closed` 為唯讀）

**UI 結構：**
- 上方：稽核進度統計（已填 verdict / 總數、pass / fail / partial / na 各幾項）
- 左側導航：依 control group 分組的樹狀結構（從 AR controls list 的 `group_uid` / `group_name` 前端分組）
  - 每個 group 可展開，顯示該 group 下的 controls
  - 每個 control 旁顯示 verdict badge（顏色區分）
- 右側內容區：選中某 control 後顯示審查表單（見下方）

#### 2. 控制項審查頁（右側內容區）

**唯讀資訊：**
- 控制項基本資料（code、title、description、guidance）
- SSP implementation description
- 該控制項下所有 AO 的 job evidences 列表（來源為 `job_evidences`，透過 AO → workflow → job 鏈路查詢）

**可編輯區域（AP status = `auditing` 且 role = auditor）：**
- Verdict 表單：verdict（下拉）+ confidence（數字）+ rationale + remarks
- Findings 列表 + 新增按鈕
- Finding 表單：category（下拉）+ severity（下拉）+ title + description + recommendation + ao_uid（下拉，從該 control 的 AO 列表選）

#### 3. AP status 對 UI 的影響

| AP status | 稽核總覽頁 | Verdict / Finding 操作 | launch-audit 按鈕 | confirm-audit 按鈕 |
|-----------|-----------|----------------------|-------------------|-------------------|
| `active` | 隱藏 | 不可用 | 顯示 | 隱藏 |
| `auditing` | 顯示 | 可編輯（auditor） | 隱藏 | 顯示（全填完才啟用） |
| `remediation` | 顯示（唯讀） | 不可用 | 隱藏 | 隱藏 |
| `closed` | 顯示（唯讀） | 不可用 | 隱藏 | 隱藏 |

### launch-audit 前端互動流程

```
點擊「啟動稽核」
  → POST launch-audit { force: false }
  → response.warning == true?
     → 是：顯示確認對話框
           「尚有 {incomplete_tasks}/{total_tasks} 項任務未完成，確定啟動外部稽核？」
           → 確定：POST launch-audit { force: true } → 成功，重新載入 AP 狀態
           → 取消：不動作
     → 否：直接成功，重新載入 AP 狀態
```

### confirm-audit 前端互動流程

```
點擊「確認結案」
  → 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 顯示對照

| verdict | 中文 | 英文 | Badge 顏色 |
|---------|------|------|-----------|
| `pass` | 通過 | Pass | 綠色 |
| `fail` | 不通過 | Fail | 紅色 |
| `partial` | 部分通過 | Partial | 橘色 |
| `na` | 不適用 | N/A | 灰色 |
| `null` | 未填寫 | Pending | 淺灰 / 虛線框 |

### Finding Category 顯示對照

| category | 中文 | 英文 |
|----------|------|------|
| `deficiency` | 缺失 | Deficiency |
| `observation` | 觀察 | Observation |
| `recommendation` | 建議 | Recommendation |

### Finding Severity 顯示對照

| severity | 中文 | 英文 | Badge 顏色 |
|----------|------|------|-----------|
| `critical` | 嚴重 | Critical | 紅色 |
| `high` | 高 | High | 橘色 |
| `medium` | 中 | Medium | 黃色 |
| `low` | 低 | Low | 灰色 |

---

## 七、不變的 API

以下 API 不受 Stage 2 影響：

- Stage 1 所有已遷移的 API（control group / control / AO / job / task-setup / review）
- 專案列表 / 詳情 / 選單
- My Jobs / Job 詳情 / Job CRUD
- Job Comment / Job Evidence
- SSP 編輯
- Dashboard
- 參與者 CRUD

---

## 八、前端遷移 Checklist

Stage 2 上線後，前端需要：

- [ ] 更新 AP status badge 映射（新增 auditing / remediation / closed，移除 draft / completed / archived）
- [ ] 在 AP status = `active` 時顯示「啟動稽核」按鈕
- [ ] 實作 launch-audit 二次確認流程（warning 對話框）
- [ ] 新增稽核總覽頁：
  - [ ] 左側樹狀導航（control group → controls，帶 verdict badge）
  - [ ] 上方進度統計
- [ ] 新增控制項審查頁（右側內容區）：
  - [ ] 唯讀區：控制項資訊 + SSP + evidences
  - [ ] Verdict 表單
  - [ ] Findings 列表 + CRUD
  - [ ] Finding 表單（含 ao_uid 下拉選擇）
- [ ] 在 AP status = `auditing` 且全部 verdict 已填時顯示「確認結案」按鈕
- [ ] 實作 confirm-audit 結果處理（closed vs remediation 兩種路徑）
- [ ] AP status = `remediation` / `closed` 時稽核頁面為唯讀模式
