# Stage 3：POA&M 矯正（Phase 4）設計規格書

> 日期：2026-03-24
> 狀態：Draft

---

## 一、目標

實作 Phase 4 矯正階段：POA&M CRUD、close-round 觸發覆核、與 Stage 2 稽核流程的迴圈銜接。

---

## 二、完整流程

```
confirm_audit 自動產生 POA&M (status=open)，AP → remediation
  → PM 填寫改善計畫（remediation_plan / due_date / assignee）→ in_progress
    → 執行人處理被退回的 Job（補件上傳）
      → PM 確認矯正完成 → POA&M → closed（手動）
        → 全部 POA&M closed → PM/Auditor 觸發 close-round
          → AP → auditing，重置相關 AR control verdict = null
            → 稽核員覆核（復用 Stage 2 verdict/finding CRUD）
              → confirm_audit → 全 pass → AP → closed（結案）
              → confirm_audit → 有 fail → 再次 remediation（迴圈）
```

### 關鍵：close-round 不呼叫 launch_audit

close-round 直接將 AP 從 `remediation` 改回 `auditing`，**不經過 `active`、不呼叫 `launch_audit`**。
同一份 `assessment_result_datas`（run_no=1）和其下的 `assessment_result_controls` 被複用，
只有與 POA&M 相關的 AR control 的 `verdict` 被重置為 `null`。
稽核員在原有的 AR controls 上重新填寫 verdict，然後呼叫 `confirm_audit`。

### 角色分工

| 階段 | 角色 | 動作 |
|------|------|------|
| POA&M 產生 | 系統 | confirm_audit 自動從 findings 建立 |
| 填寫改善計畫 | PM | 填 remediation_plan、指派 assignee、設 due_date |
| 補件執行 | 任務執行人 | 在被 revert 的 Job 中重新上傳證據 |
| 標記矯正完成 | PM | 手動將 POA&M → closed |
| 觸發覆核 | Auditor | close-round，AP 回到 auditing |
| 覆核判定 | 稽核員 | 復用 Stage 2 verdict/finding CRUD |
| 結案 / 再矯正 | 稽核員 | confirm_audit 決定 closed 或再次 remediation |

---

## 三、資料模型變更

### 3.1 compliance.poams 欄位擴充

現有欄位（Stage 2 已建）：

| 欄位 | 類型 | 說明 |
|------|------|------|
| id | SERIAL PK | |
| uid | VARCHAR(36) | |
| assessment_plan_id | INTEGER | FK → oscal.assessment_plans.id |
| ar_finding_id | INTEGER | FK → oscal.assessment_result_findings.id |
| control_identifier | VARCHAR(50) | 控制項編號 snapshot |
| ao_uid | VARCHAR(36) | 對應 AP Task UID |
| status | VARCHAR(30) | open / in_progress / closed |
| closed_at | TIMESTAMP | |
| tenant_id | INTEGER | |
| org_unit_id | INTEGER | |
| created_at / updated_at | TIMESTAMP | |
| created_user / updated_user | VARCHAR(50) | |

**新增欄位：**

| 欄位 | 類型 | Nullable | 說明 |
|------|------|----------|------|
| remediation_plan | TEXT | YES | 矯正方案描述 |
| due_date | DATE | YES | 預計完成日 |
| assignee_uid | VARCHAR(36) | YES | 負責人 user UID |

### 3.2 Migration SQL

```sql
-- Stage 3: POA&M Remediation
ALTER TABLE compliance.poams ADD COLUMN IF NOT EXISTS remediation_plan TEXT;
ALTER TABLE compliance.poams ADD COLUMN IF NOT EXISTS due_date DATE;
ALTER TABLE compliance.poams ADD COLUMN IF NOT EXISTS assignee_uid VARCHAR(36);
```

---

## 四、API 設計

所有 API 在 AP-scoped 路徑下：`/api/1.0/grc/project/<project_uid>/ap/<ap_uid>/...`

### 4.1 POA&M 分頁列表

```
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/poams/list
```

**Request（繼承 RequestMetaSchema）：**
```json
{
  "pager": { "page": 1, "page_size": 25 },
  "sort": [{ "field": "created_at", "order": "desc" }],
  "filters": {
    "status": "open",
    "search": "AC.L2"
  }
}
```

**Response（繼承 EnvelopeSchema）：**
```json
{
  "code": 1,
  "data": [
    {
      "uid": "poam-uuid",
      "control_identifier": "AC.L2-3.1.1",
      "ao_uid": "task-uuid",
      "status": "open",
      "finding_title": "存取控制政策未完整",
      "finding_severity": "high",
      "finding_category": "deficiency",
      "remediation_plan": null,
      "due_date": null,
      "assignee_uid": null,
      "assignee_name": null,
      "created_at": "2026-03-24T10:00:00"
    }
  ],
  "meta": { "paging": true, "page": 1, "page_size": 25, "total": 3 }
}
```

`assignee_name` 透過 LEFT JOIN `users` 表（by `assignee_uid = users.uid`）取得 `nickname`，在 repo 層查詢時一次性 JOIN，避免 N+1。

**Filters：**

| 參數 | 類型 | 說明 |
|------|------|------|
| status | String | open / in_progress / closed |
| search | String | 模糊搜尋 control_identifier / finding_title |

### 4.2 POA&M 詳情

```
GET /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/poam/<poam_uid>
```

**Response：**
```json
{
  "code": 1,
  "data": {
    "uid": "poam-uuid",
    "control_identifier": "AC.L2-3.1.1",
    "ao_uid": "task-uuid",
    "status": "in_progress",
    "remediation_plan": "補充存取控制政策文件並提交審查",
    "due_date": "2026-04-15",
    "assignee_uid": "user-uuid",
    "assignee_name": "王小明",
    "closed_at": null,
    "finding": {
      "uid": "finding-uuid",
      "title": "存取控制政策未完整",
      "category": "deficiency",
      "severity": "high",
      "description": "未能提供完整的存取控制政策文件...",
      "recommendation": "建議補充政策文件..."
    },
    "job_info": {
      "workflow_execution_id": 123,
      "job_uid": "job-uuid",
      "job_status": "PROCESSING"
    },
    "created_at": "2026-03-24T10:00:00",
    "updated_at": "2026-03-24T10:00:00"
  }
}
```

`job_info` 從 `ao_uid` 反查：`ao_uid → assessment_plan_tasks.uid → ap_task_wf_execution_mapping → workflow_execution_id → job_executions (type=USER)`。取最新一筆 USER job（不限 status），因為被 revert 後 job 可能處於 TODO 或 PROCESSING。若查無結果，`job_info` 回傳 `null`。

Repo 層查詢時需驗證 POA&M 的 `assessment_plan_id` 對應的 AP 的 `uid` 等於 URL 中的 `ap_uid`，防止跨 AP 存取。

### 4.3 POA&M 更新

```
PUT /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/poam/<poam_uid>
```

**Request：**
```json
{
  "status": "in_progress",
  "remediation_plan": "補充存取控制政策文件...",
  "due_date": "2026-04-15",
  "assignee_uid": "user-uuid-123"
}
```

所有欄位皆為 optional，只更新有帶的欄位。

**狀態轉換規則：**

| 從 | 到 | 條件 |
|----|-----|------|
| open | in_progress | 需同時填寫 remediation_plan（本次 request 帶值，或 DB 中已有值） |
| open | closed | 允許（輕微 finding 可直接結案），自動寫入 closed_at |
| in_progress | closed | 允許（PM 手動確認），自動寫入 closed_at |
| closed | in_progress | 允許（重新開啟），清除 closed_at |

驗證邏輯放在 App Service 層（`poam_service.py`），serializer 層只負責格式驗證。

**Response：** 回傳更新後的 POA&M 基本欄位（不含 finding / job_info nested object）：

```json
{
  "code": 1,
  "data": {
    "uid": "poam-uuid",
    "control_identifier": "AC.L2-3.1.1",
    "ao_uid": "task-uuid",
    "status": "in_progress",
    "remediation_plan": "補充存取控制政策文件...",
    "due_date": "2026-04-15",
    "assignee_uid": "user-uuid-123",
    "assignee_name": "王小明",
    "closed_at": null,
    "created_at": "2026-03-24T10:00:00",
    "updated_at": "2026-03-24T11:30:00"
  }
}
```

### 4.4 Close Round

```
POST /api/1.0/grc/project/<project_uid>/ap/<ap_uid>/close-round
```

**無 Request Body。**

**邏輯：**
1. 驗證 AP status = `remediation`（若非 remediation，回 412 GRC_AP_NOT_REMEDIATION；已經是 auditing 代表 close-round 已執行過，確保冪等性）
2. 查詢該 AP 所有 POA&M（by `assessment_plan_id`）
3. 驗證全部 POA&M status = `closed`（若有未 closed 的，回傳 412 + 未完成數量）
4. 找出有 POA&M 的 AR control：從 POA&M 的 `control_identifier` 集合 → 查同一 `assessment_result_data`（run_no=1）下 `control_id` 匹配的 `assessment_result_controls` → 重置 `verdict = null`、清除 `confidence`/`rationale`/`remarks`
5. AP status → `auditing`
6. 稽核員即可復用 Stage 2 的 verdict/finding CRUD 進行覆核

**Response：**
```json
{
  "code": 1,
  "data": {
    "ap_uid": "ap-uuid",
    "status": "auditing",
    "reset_count": 5
  }
}
```

---

## 五、權限設計

| API | 允許角色 | 檢查方式 |
|-----|---------|---------|
| POA&M 列表 | 專案參與者 | JWT 登入即可（RLS 控制 tenant 隔離） |
| POA&M 詳情 | 專案參與者 | 同上 |
| POA&M 更新 | 專案參與者 | 檢查 user 是否為 `project_participants` 成員（任何 role 皆可），不額外限制角色。業務上由 PM 主導，但系統不強制 role 檢查，保持彈性 |
| close-round | auditor | 復用 `_check_auditor_role` |

> **設計考量**：現有 `project_participants.role` 欄位的有效值在各專案間不一致（有些用 `owner`、`member`、`auditor`），若硬編碼 `pm` 角色會造成限制。Stage 3 先以專案參與者為準，未來可透過 capability 機制細化。

---

## 六、Error Codes

新增至 `common/code/grc_error_code.py`：

| Code | 中文 | Error Code |
|------|------|------------|
| GRC_POAM_NOT_FOUND | POA&M 不存在 | GRC_404019 |
| GRC_POAM_INVALID_STATUS_TRANSITION | POA&M 狀態轉換不合法 | GRC_412006 |
| GRC_POAM_REMEDIATION_PLAN_REQUIRED | 需填寫矯正方案 | GRC_412007 |
| GRC_AP_NOT_REMEDIATION | 稽核計畫不在矯正中狀態 | GRC_412004 |
| GRC_POAM_NOT_ALL_CLOSED | 尚有 POA&M 未結案 | GRC_412005 |

> Error code 統一使用 412（PreconditionFailed）表示前置條件不滿足，與現有 `GRC_412001`~`GRC_412003` 延續同一序列。

---

## 七、DDD 分層架構

### 新增檔案

| Layer | 檔案 | 說明 |
|-------|------|------|
| Migration | `scripts/sql/stage3_migration.sql` | ALTER TABLE 加欄位 |
| Domain Service | `domain/grc/service/grc_poam_domain_service.py` | 委派 repo |
| DTO | `app/grc/dto/poam_dto.py` | POA&M DTO + FindingDto 複用 |
| App Service | `app/grc/service/poam_service.py` | POA&M CRUD + 狀態驗證 |
| Serializer | `api/grc/serializers/poam.py` | Request/Response schemas |
| Route | `api/grc/routes/poam_route.py` | 3 個 Resource（List/Detail/Update） |

### 修改檔案

| 檔案 | 變更 |
|------|------|
| `infra/grc/model/poam_model.py` | 加 remediation_plan / due_date / assignee_uid |
| `domain/grc/entities/poam_entity.py` | 加 3 個欄位 |
| `infra/grc/mapper/poam_mapper.py` | 映射新欄位 |
| `domain/grc/repository/i_poam_repo.py` | 新增 list_poams / get_poam / update_poam 方法 |
| `infra/grc/repository/poam_repo_impl.py` | 實作查詢（JOIN finding + users）+ update |
| `common/code/grc_error_code.py` | 新增 5 個 error codes |
| `app/project/service/oscal_audit_service.py` | 新增 `close_round` 方法 |
| `di_containers/grc/grc_containers.py` | 加入 poam_domain_service / poam_service |
| `di_containers/project/project_containers.py` | oscal_audit_service 加注入 poam_domain_service（如需要） |
| `api/grc/routes/audit_route.py` | 新增 CloseRoundResource |
| `api/grc/__init__.py` | 註冊 4 個新 URL |

### close_round 放置位置

`close_round` 放在 `app/project/service/oscal_audit_service.py`，與 `launch_audit`、`confirm_audit` 並列。理由：三者都是 AP 狀態轉換的編排操作，需要協調 AP service、AR service、POA&M repo 等多個依賴，屬於同一個 orchestration 職責。

---

## 八、與 Stage 2 的銜接

### close-round → auditing 的覆核流程

close-round 將 AP 從 `remediation` 改回 `auditing` 後：

1. 稽核員使用 Stage 2 的 `POST .../ar/controls/list` 查看控制項
2. 有 POA&M 的控制項 verdict 已被重置為 null，需重新填寫
3. 沒有 POA&M 的控制項 verdict 保持原值（pass / na）
4. 稽核員覆核完畢後呼叫 `confirm_audit`
5. `confirm_audit` 邏輯不變：全 pass → closed，有 fail → 再次 remediation + 產生新 POA&M

**注意**：close-round 不呼叫 `launch_audit`，不建立新的 `assessment_result_datas`。同一份 ar_data（run_no=1）被複用，避免 UniqueConstraint 衝突。

### 迴圈控制

每次 remediation → auditing → confirm_audit 失敗時，會產生新的 POA&M（status=open）。舊的 closed POA&M 保留作為歷史紀錄。POA&M 列表 API 支援 status filter，前端可區分：
- 當前待處理：`status != closed`
- 歷史紀錄：`status = closed`

### POA&M 不支援刪除

POA&M 由系統自動產生（`confirm_audit`），不提供 DELETE API。若稽核員刪除了對應的 AR finding，POA&M 會成為孤立紀錄，但不影響流程 — close-round 只檢查 POA&M 狀態是否全部 closed，不回查 finding 是否仍存在。

---

## 九、證據處理

不新建證據表。POA&M 詳情 API 透過 `ao_uid` 反查關聯的 Job 資訊：

```
ao_uid → assessment_plan_tasks.uid
  → ap_task_wf_execution_mapping → workflow_execution_id
    → job_executions (type=USER, 依 id DESC 取最新一筆，不限 status)
```

前端在 POA&M 詳情頁顯示 Job 連結，點擊跳轉到 Job 頁面處理補件上傳。Job 被 revert 後可能處於 `TODO`（待處理）或 `PROCESSING`（進行中），前端依 `job_status` 顯示對應狀態。
