Implements spec 111 (Findings workflow + SLA) and fixes Workspace findings SLA settings UX/validation. Key changes: - Findings workflow service + SLA policy and alerting. - Workspace settings: allow partial SLA overrides without auto-filling unset severities in the UI; effective values still resolve via defaults. - New migrations, jobs, command, UI/resource updates, and comprehensive test coverage. Tests: - `vendor/bin/sail artisan test --compact` (1779 passed, 8 skipped). Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #135
123 lines
5.3 KiB
Markdown
123 lines
5.3 KiB
Markdown
# 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.
|
|
|