## 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
145 lines
5.5 KiB
Markdown
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. |