# API Contracts: 111 — Findings Workflow V2 + SLA **Date**: 2026-02-24 **Branch**: `111-findings-workflow-sla` --- ## Overview This feature does not introduce new external REST endpoints. User interaction is through Filament/Livewire actions on the Findings Resource and Alert Rules configuration. The only new cross-module “contract” is the `sla_due` alert event produced during scheduled alert evaluation and consumed by the Alerts dispatch pipeline. --- ## 1. Alert Event Contract: `sla_due` **Producer:** `App\Jobs\Alerts\EvaluateAlertsJob` **Event Type:** `AlertRule::EVENT_SLA_DUE` (`sla_due`) **Event Cardinality:** At most 1 event per tenant per evaluation window (when newly-overdue findings exist) ### Eligibility (Per Tenant) An event is produced when a tenant has one or more **newly-overdue** open findings since the previous evaluation window: - `status IN (new, triaged, in_progress, reopened)` - `due_at <= now()` - `due_at > windowStart` Terminal statuses (`resolved`, `closed`, `risk_accepted`) never contribute to overdue evaluation. ### Event Payload Shape ```json { "event_type": "sla_due", "tenant_id": 123, "severity": "high", "fingerprint_key": "sla_due:tenant:123", "title": "SLA overdue findings detected", "body": "Tenant Contoso has 5 overdue open findings (critical: 1, high: 2, medium: 2, low: 0).", "metadata": { "overdue_total": 5, "overdue_by_severity": { "critical": 1, "high": 2, "medium": 2, "low": 0 } } } ``` ### Severity Semantics `severity` is the maximum severity among overdue open findings for that tenant at evaluation time. Rationale: alert rules can use `minimum_severity`; a critical-overdue case can bypass a “high-only” rule. --- ## 2. Filament Findings Resource: Workflow Actions Contract All workflow actions: - enforce tenant membership as deny-as-not-found (404) for non-members - enforce capability checks (403 for members lacking capability) - write an audit log entry with before/after and any reason fields ### List Defaults Default list shows: - all finding types (no drift-only default) - open statuses only: `new`, `triaged`, `in_progress`, `reopened` Quick filters: - Open - Overdue (`due_at < now()` and open statuses) - High severity (high + critical) - My assigned (`assignee_user_id = current user`) ### Row Actions (More menu) | Action | Allowed From Status | To Status | Capability | Confirmation | Notes | |--------|---------------------|----------|------------|--------------|------| | Triage | `new`, `reopened` | `triaged` | `TENANT_FINDINGS_TRIAGE` (or legacy `TENANT_FINDINGS_ACKNOWLEDGE` alias) | No | Sets `triaged_at` | | Start progress | `triaged` | `in_progress` | `TENANT_FINDINGS_TRIAGE` (or legacy alias) | No | Sets `in_progress_at` | | Assign | open statuses | unchanged | `TENANT_FINDINGS_ASSIGN` | No | Sets `assignee_user_id` and optional `owner_user_id` (picker limited to tenant members) | | Resolve | open statuses | `resolved` | `TENANT_FINDINGS_RESOLVE` | Yes | Requires `resolved_reason`; sets `resolved_at` | | Close | any status | `closed` | `TENANT_FINDINGS_CLOSE` | Yes | Requires `closed_reason`; sets `closed_at` + `closed_by_user_id` | | Risk accept | any status | `risk_accepted` | `TENANT_FINDINGS_RISK_ACCEPT` | Yes | Requires reason (stored as `closed_reason`); sets `closed_at` + `closed_by_user_id` | | Reopen | `resolved`, `closed`, `risk_accepted` | `reopened` | `TENANT_FINDINGS_TRIAGE` (or legacy alias) | Yes | Manual reopen only. Automatic reopen is allowed only from `resolved` during detection. | ### Bulk Actions Bulk actions are all-or-nothing (if any record is unauthorized for the current tenant context, the action is disabled): | Action | Capability | Confirmation | Notes | |--------|------------|--------------|------| | Bulk triage | `TENANT_FINDINGS_TRIAGE` (or legacy alias) | Yes (typed confirm for large selections) | Moves `new|reopened → triaged` | | Bulk assign | `TENANT_FINDINGS_ASSIGN` | Yes (typed confirm for large selections) | Assignee/owner pickers limited to tenant members | | Bulk resolve | `TENANT_FINDINGS_RESOLVE` | Yes | Reason required | | Bulk close | `TENANT_FINDINGS_CLOSE` | Yes | Reason required | | Bulk risk accept | `TENANT_FINDINGS_RISK_ACCEPT` | Yes | Reason required | --- ## 3. Backfill/Consolidation Operation Contract Backfill is a tenant-context operation that upgrades legacy findings to v2 lifecycle fields and consolidates drift duplicates. It MUST be OperationRun-backed and use OPS-UX feedback surfaces (queued toast, progress surfaces, initiator-only completion notification). **OperationRun type:** `findings.lifecycle.backfill` (label registered in OperationCatalog) **Idempotency:** One active run per tenant (deduped by OperationRun identity) Summary counts use canonical numeric keys only (e.g., `total`, `processed`, `updated`, `failed`, `skipped`). --- ## 4. Audit Contract (Tenant Scope) Every workflow mutation writes a tenant audit record (via `App\Services\Intune\AuditLogger`) with: - `action` string (e.g., `finding.triaged`, `finding.resolved`, `finding.closed`, `finding.risk_accepted`, `finding.reopened`, `findings.lifecycle.backfill.started`) - `metadata` including: `finding_id`, `before_status`, `after_status`, and any reason fields or assignment deltas Audit payloads must remain sanitized and must not include secrets/tokens.