TenantAtlas/specs/154-finding-risk-acceptance/data-model.md
ahmido b1e1e06861 feat: implement finding risk acceptance lifecycle (#184)
## Summary
- add a first-class finding exception domain with request, approval, rejection, renewal, and revocation lifecycle support
- add tenant-scoped exception register, finding governance surfaces, and a canonical workspace approval queue in Filament
- add audit, badge, evidence, and review-pack integrations plus focused Pest coverage for workflow, authorization, and governance validity

## Validation
- vendor/bin/sail bin pint --dirty --format agent
- CI=1 vendor/bin/sail artisan test --compact
- manual integrated-browser smoke test for the request-exception happy path, tenant register visibility, and canonical queue visibility

## Notes
- Filament implementation remains on v5 with Livewire v4-compatible surfaces
- canonical queue lives in the admin panel; provider registration stays in bootstrap/providers.php
- finding exceptions stay out of global search in this rollout

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #184
2026-03-20 01:07:55 +00:00

145 lines
5.5 KiB
Markdown

# Data Model: Finding Risk Acceptance Lifecycle
## 1. FindingException
- **Purpose**: Tenant-owned governance aggregate that represents the current accepted-risk exception state for one finding.
- **Ownership**: Tenant-owned (`workspace_id` + `tenant_id` NOT NULL).
- **Fields**:
- `id`
- `workspace_id`
- `tenant_id`
- `finding_id`
- `status` enum: `pending`, `active`, `expiring`, `expired`, `rejected`, `revoked`, `superseded`
- `requested_by_user_id`
- `owner_user_id`
- `approved_by_user_id` nullable
- `current_decision_id` nullable
- `request_reason` text
- `approval_reason` text nullable
- `rejection_reason` text nullable
- `revocation_reason` text nullable
- `requested_at`
- `approved_at` nullable
- `rejected_at` nullable
- `revoked_at` nullable
- `effective_from` nullable
- `expires_at` nullable
- `review_due_at` nullable
- `evidence_summary` JSONB nullable
- `current_validity_state` enum: `valid`, `expiring`, `expired`, `revoked`, `rejected`, `missing_support`
- `created_at`, `updated_at`
- **Relationships**:
- belongs to `Finding`
- belongs to `Tenant`
- belongs to `Workspace`
- belongs to requester `User`
- belongs to owner `User`
- belongs to approver `User`
- has many `FindingExceptionDecision`
- has many `FindingExceptionEvidenceReference`
- **Validation / invariants**:
- `workspace_id`, `tenant_id`, and `finding_id` are always required.
- `finding_id` must reference a finding in the same workspace and tenant.
- At most one current valid active exception may govern one finding at a time.
- `approved_by_user_id` must differ from `requested_by_user_id` in v1.
- `expires_at` must be after `effective_from` when both are present.
## 2. FindingExceptionDecision
- **Purpose**: Append-only historical record of every exception lifecycle decision.
- **Ownership**: Tenant-owned (`workspace_id` + `tenant_id` NOT NULL).
- **Fields**:
- `id`
- `workspace_id`
- `tenant_id`
- `finding_exception_id`
- `decision_type` enum: `requested`, `approved`, `rejected`, `renewal_requested`, `renewed`, `revoked`
- `actor_user_id`
- `reason` text nullable
- `effective_from` nullable
- `expires_at` nullable
- `metadata` JSONB nullable
- `decided_at`
- `created_at`, `updated_at`
- **Relationships**:
- belongs to `FindingException`
- belongs to actor `User`
- **Validation / invariants**:
- Decision rows are append-only after creation.
- Decision type must be compatible with the parent exception's lifecycle state.
- Renewal decisions must not erase prior approval or rejection records.
## 3. FindingExceptionEvidenceReference
- **Purpose**: Structured pointer to evidence used to justify or review the exception.
- **Ownership**: Tenant-owned (`workspace_id` + `tenant_id` NOT NULL).
- **Fields**:
- `id`
- `workspace_id`
- `tenant_id`
- `finding_exception_id`
- `source_type` string
- `source_id` string nullable
- `source_fingerprint` string nullable
- `label` string
- `summary_payload` JSONB nullable
- `measured_at` nullable
- `created_at`, `updated_at`
- **Relationships**:
- belongs to `FindingException`
- **Validation / invariants**:
- References must stay intelligible even if the live source artifact later expires or is removed from active views.
- `summary_payload` is bounded, sanitized, and not a raw payload dump.
## 4. Finding Risk Governance Projection
- **Purpose**: Derived truth used by finding detail, tenant exception lists, canonical queues, and downstream evidence/reporting consumers.
- **Derived from**:
- `Finding.status`
- `FindingException.status`
- exception validity window (`effective_from`, `expires_at`)
- current exception evidence support state
- **Values**:
- `ungoverned`
- `pending_exception`
- `valid_exception`
- `expiring_exception`
- `expired_exception`
- `revoked_exception`
- `rejected_exception`
- `risk_accepted_without_valid_exception`
- **Invariant**:
- Downstream consumers must use this projection, not finding status alone, when determining whether accepted risk is currently governed.
## State Transitions
### FindingException
- `pending` -> `active` on approval
- `pending` -> `rejected` on rejection
- `active` -> `expiring` when within reminder threshold
- `active|expiring` -> `expired` when `expires_at` passes
- `active|expiring` -> `revoked` on explicit revoke
- `active|expiring|expired` -> `superseded` when a renewal produces a newer governing decision under the same aggregate semantics
### FindingExceptionDecision
- `requested` always occurs first
- `approved` or `rejected` resolves a pending request
- `renewal_requested` may occur from `active`, `expiring`, or `expired`
- `renewed` extends a current or lapsed exception through a new decision
- `revoked` ends current validity explicitly
## Indexing and Query Needs
- Composite indexes on `(workspace_id, tenant_id, status)` for tenant register filtering.
- Composite indexes on `(workspace_id, status, review_due_at)` for canonical queue and expiring views.
- Unique partial index to prevent more than one current valid active exception per finding.
- Composite index on `(finding_id, tenant_id)` for finding-detail resolution.
## Relationship to Existing Domain Records
- `Finding` remains the system of record for the detected issue and status workflow.
- `FindingWorkflowService` remains the only allowed path for changing finding status.
- `AuditLog` remains the immutable historical event stream for every lifecycle mutation.
- `EvidenceSnapshot` and related artifacts remain separate systems of record referenced by exception evidence links.