Compliance Incidents
Report, investigate, and resolve compliance incidents with INC numbering, severity-tiered SLA banners, a 72-hour GDPR breach gate, and AI post-mortems.
The Incidents module handles the compliance side of incident response — security events, data-handling breaches, control failures, and anything else that needs a post-mortem and an audit record. It’s not a replacement for your engineering on-call pager; it’s where the compliance-relevant facts, timelines, communications, and corrective actions are captured once the technical fire is out.
Every incident gets a tenant-scoped INC-YYYY-NNN number, an SLA banner keyed to severity, a separate 72-hour banner for GDPR breach-notification when the category and severity warrant it, and a workflow that walks it through investigation to closure.
The Incident Lifecycle
Reported → Investigating → Contained → Resolved → Closed
↓ ↓
False Positive Post-mortem fanout
State transitions are restricted by ALLOWED_TRANSITIONS in IncidentService — you can’t skip from reported straight to resolved, and closed + false_positive are terminal. Attempting an invalid transition returns 409 InvalidStateError with the allowed next states in the response body.
Automatic Numbering
Incident numbers follow INC-YYYY-NNN where YYYY is the reporting year and NNN is a 3-digit sequence. The sequence is tenant-scoped per year (each tenant starts at 001 on Jan 1) and is assigned atomically using a dedicated counter table (incident_number_sequence) via INSERT ... ON CONFLICT DO UPDATE RETURNING. Fifty concurrent POSTs all get unique numbers — no race conditions, no duplicates.
Known issue (DI-003): The database-level uniqueness on
incident_numberis currently global (not tenant-scoped), which means tenant B’s first report can occasionally collide with tenant A’sINC-YYYY-001if the sequence generator runs before the unique constraint is dropped. This is tracked for a standalone migration plan. End users won’t hit it in normal practice; dev-test fixtures may.
Reporting an Incident
On /compliance/incidents, click Report Incident. Fill in:
| Field | Purpose |
|---|---|
| Title | Short descriptor |
| Description | Full narrative — what happened, what was noticed first |
| Severity | critical / high / medium / low — drives SLA |
| Category | security_breach, data_loss, privacy_violation, control_failure, policy_violation, regulatory, other |
| Detected at | When you first noticed it (not when you file this form) |
| Affected systems | List of system names — these become the allowed-citation list for the AI post-mortem later |
On save, Muin:
- Generates
incident_numberatomically - Records
reported_at= now() - Computes and stores
sla_due_datebased on severity (see table below) - Creates a
HumanTaskfor the incident owner - Fires a notification to the compliance owner
SLA by Severity (IS15 banner)
| Severity | Internal response SLA |
|---|---|
| Critical | 4 hours |
| High | 24 hours |
| Medium | 72 hours |
| Low | 168 hours (7 days) |
Definitions: SLA_HOURS_BY_SEVERITY in incident_service.py. The SLA deadline is stored on the incident at create time, so the frontend banner and the worker escalation both read the same field.
The banner shows a countdown. When it crosses into the last 25% of the window, it turns amber. When overdue, it turns red and the compliance owner gets a notification.
The GDPR 72h Breach-Notification Gate (separate)
The internal SLA above is not the GDPR 72-hour breach-notification window. For incidents with category=security_breach or category=privacy_violation involving personal data, GDPR Article 33 requires notifying the supervisory authority within 72 hours of awareness.
A separate banner appears for incidents that match the breach criteria. This one counts from reported_at regardless of severity — the 72-hour clock is statutory, not tuned per-tenant. If the incident genuinely involves a personal-data breach, the compliance owner must either:
- File the breach notification (and record the filing date on the incident)
- Explicitly dismiss the banner with a justification (e.g. “no personal data involved after investigation”) — stored for audit
Dismissal requires the compliance owner’s approval and is logged with timestamp, user, and justification text.
Incident Detail (IS14 — Tabbed View)
Click an incident row to open its detail drawer. The detail view is organized into tabs:
| Tab | Contents |
|---|---|
| Overview | Title, description, status, severity, assigned owner, reported_at, sla_due_date, linked controls |
| Timeline | Chronological event log — status changes, comms sent, actions taken, evidence attached |
| People | Reporter, current owner, escalation chain, affected users (when applicable) |
| Communications | Emails / Slack / notifications sent about this incident, fetched via the IncidentCommunicationsLog component (Plan 272 JG-7) |
| Actions | Corrective and preventive actions — who owns each, target date, status |
| Post-Mortem | Root cause, lessons learned, recommended updates — authored directly or via AI draft |
Status Transitions and Who Does What
| Transition | Who fires it | What happens |
|---|---|---|
reported → investigating | Incident owner / compliance owner | Investigation tab becomes active |
reported → false_positive | Compliance owner | Terminal — logged but closed with no post-mortem |
investigating → contained | Incident owner | ”When was it contained” date captured |
investigating → false_positive | Compliance owner | Same as above |
contained → resolved | Incident owner | Triggers post-mortem fanout |
resolved → closed | Compliance owner | Terminal — audit-archived |
When an incident transitions to resolved, Muin automatically:
- Creates a
HumanTask(category=INCIDENT_POSTMORTEM)for the incident owner - Starts a
compliance.incident.post_mortem_reviewworkflow instance - Fires notifications to the compliance owner + linked framework control owners
AI Post-Mortem Draft (Section 8.7)
On a resolved or closed incident, the Draft Post-Mortem button invokes Bedrock Claude with:
- Incident description, status, severity, category
- The affected systems list (these become the only allowed citation list)
- The timeline entries
- Any linked controls’ names and descriptions
The AI returns a structured draft with four prose fields:
- Root cause hypothesis
- Lessons learned
- Recommended updates
- Timeline summary
Plus a cited_affected_systems array and a summary_source badge (llm or fallback).
The Citation Fence
The backend enforces an exact-match citation fence on affected_systems. If the LLM cites a system name that isn’t in the incident’s affected_systems array — case-sensitive exact match — the whole draft is rejected and the rule-based fallback fires instead. This prevents hallucinated “we believe the outage impacted the Foo service” when Foo was never part of the incident.
The fence also rejects:
- Empty prose fields (must have meaningful content)
- Prose fields over 2,000 characters (to prevent runaway costs)
- Missing required keys (if the LLM returns an incomplete schema)
Rate Limits and Caching
- Per-tenant limit: 10 drafts per hour
- 24-hour Redis cache keyed on
(tenant, incident, hash(description + status + systems + timeline))— so edits to any of those fields bust cache and cost a fresh LLM call, but repeated clicks on the same state serve cached output
The Apply Flow
After review, click Apply. The four prose fields map to the incident’s editable fields via the existing drawer onUpdate mutation:
root_cause_hypothesis→root_causelessons_learned→lessons_learnedrecommended_updates→corrective_actions
The incident owner can edit any of these before or after Apply — the draft is a starting point, not a committed record.
SOC 2 CC7.3–CC7.5 Mapping
The Incidents module maps to the SOC 2 CC7 Security Incident criteria:
- CC7.3 — The entity evaluates security events to determine whether they could or have resulted in a failure. Muin’s incident creation + severity assessment covers this.
- CC7.4 — The entity responds to identified security incidents by executing a defined response program. The status lifecycle + SLA + response workflow covers this.
- CC7.5 — The entity identifies, develops, and implements activities to recover from incidents. The
contained→resolvedtransition + corrective actions + post-mortem workflow covers this.
Evidence requirements for these controls include the incident log, the SLA compliance percentage, and the post-mortem documents. Muin’s incident exports are built to serve these audit evidence asks directly.
Communications Log (JG-7)
The Communications tab aggregates every outbound communication about this incident — emails to the compliance owner, Slack notifications to security owners, customer breach notices, regulator filings. Each entry shows:
- Channel (email, Slack, phone — logged by the team)
- Timestamp
- Sender + recipient
- Subject / summary
- Attached documents (if any)
This log is a critical audit artifact — when the regulator asks “when did you notify affected customers,” the timestamp on the customer-notice row is the answer.
FAQs
How is SLA computed?
At create time, the service reads the severity and looks up the tier in SLA_HOURS_BY_SEVERITY (critical=4h, high=24h, medium=72h, low=168h). sla_due_date = reported_at + hours(tier) is stored on the incident row. The banner countdown reads that field — the computation isn’t redone every page load. If the severity is later edited, the SLA updates accordingly.
When do I need to notify the DPA?
Under GDPR Article 33, a controller must notify the supervisory authority within 72 hours of becoming aware of a personal-data breach, unless the breach is unlikely to result in risk. The 72h banner surfaces on incidents flagged as security_breach or privacy_violation involving personal data. Muin doesn’t auto-file the notification — filing is a deliberate compliance-owner action — but the banner keeps the clock visible and requires an explicit dismiss-with-justification for incidents that don’t actually meet the notification criteria.
What if the post-mortem draft is wrong?
The draft is a starting point. The four prose fields are fully editable before and after Apply. If the AI output is hallucinated or off-base, discard it — the rule-based fallback can be invoked instead, or the incident owner can author from scratch. The citation fence should prevent the worst hallucinations (cited systems not in affected_systems), but the owner is always the editorial authority.
Can I transition an incident back to an earlier state?
Not via the normal status dropdown — the state machine forbids backwards transitions. If you truly need to, a compliance admin can edit the incident status directly (bypassing the transition validator), and the change is logged in the audit trail. Use this sparingly; it’s meant for correction of genuine mis-triage, not regular workflow.
What happens to incidents marked false-positive?
They’re terminal — moved out of the active investigation queue, excluded from SLA banners, excluded from post-mortem fanout. They stay in the incident list (with a grey badge) so the audit trail is complete and you can see that the triage decision was made. Moving an incident to false_positive requires compliance owner approval and a justification.
Related
- Compliance Frameworks — link incidents to the controls that failed
- Risk register — an incident may surface a new risk
- Policy hub — incidents can drive policy updates
- SOC 2 audit — incident logs are evidence for CC7.3–CC7.5