Skip to content

[Tier 6] Investigation case management: multi-finding cases, comments, postmortems #61

@dcoln25-writer

Description

@dcoln25-writer

Problem

Aperio surfaces findings, but operators can only triage one finding at a time. Once UEBA (#12), correlation rules (#11), cloud IAM (#13), and DSPM (#15) ship, finding volume goes up and operators need to:

  • Group related findings into a single investigation case.
  • Assign cases to specific operators or teams.
  • Add status, comments, handoff notes to the case (not the individual finding).
  • Maintain a postmortem record for closed cases (what happened, how we fixed it, what we'll do differently).
  • Hand off cases between shifts / on-call rotations without losing context.

AgentTask (already in the schema) is for agent-driven work and doesn't model the human investigation surface. We need a sibling concept for humans.

Goals

  1. InvestigationCase model spanning multiple SecurityFinding rows.
  2. Assignment + status workflow (OPEN → INVESTIGATING → CONTAINED → REMEDIATED → CLOSED → REOPENED).
  3. Threaded comments + activity timeline for case collaboration.
  4. Auto-correlation suggestions — when a new finding lands, suggest joining it to an existing open case (heuristic + LLM via AI-native investigation, triage & posture narratives #46).
  5. Postmortem template captured on case close; exportable as evidence (Handle disallowed CORS origins explicitly #5).
  6. Shift handoff view — on-call dashboard of open cases with last activity + summary.

Non-goals

Proposed design

Schema

enum InvestigationCaseStatus {
  OPEN
  INVESTIGATING
  CONTAINED
  REMEDIATED
  CLOSED
  REOPENED
}

enum InvestigationCasePriority {
  P1
  P2
  P3
  P4
}

model InvestigationCase {
  id              String   @id @default(cuid())
  organizationId  String   @map("organization_id")
  shortId         String   @map("short_id") @db.VarChar(20)   // "CASE-1247" (org-sequenced)
  title           String   @db.VarChar(220)
  summary         String?  @db.Text
  status          InvestigationCaseStatus @default(OPEN)
  priority        InvestigationCasePriority @default(P3)
  assignedUserId  String?  @map("assigned_user_id")
  assignedTeam    String?  @map("assigned_team") @db.VarChar(120)
  openedByUserId  String?  @map("opened_by_user_id")
  closedAt        DateTime? @map("closed_at")
  closedByUserId  String?  @map("closed_by_user_id")
  postmortem      Json?    // structured postmortem template
  tags            String[] @default([])
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")
  organization    Organization @relation(...)
  findings        CaseFindingLink[]
  comments        CaseComment[]
  activity        CaseActivity[]
  @@unique([organizationId, shortId])
  @@index([organizationId, status, priority])
  @@index([assignedUserId, status])
  @@map("investigation_cases")
}

model CaseFindingLink {
  caseId          String   @map("case_id")
  findingId       String   @map("finding_id")
  linkedAt        DateTime @default(now()) @map("linked_at")
  linkedByUserId  String?  @map("linked_by_user_id")
  case            InvestigationCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
  finding         SecurityFinding @relation(fields: [findingId], references: [id], onDelete: Cascade)
  @@id([caseId, findingId])
  @@index([findingId])
  @@map("case_finding_links")
}

model CaseComment {
  id              String   @id @default(cuid())
  caseId          String   @map("case_id")
  authorUserId    String?  @map("author_user_id")
  body            String   @db.Text
  bodyKind        String   @default("markdown") @map("body_kind") @db.VarChar(20)
  createdAt       DateTime @default(now()) @map("created_at")
  case            InvestigationCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
  @@index([caseId, createdAt])
  @@map("case_comments")
}

model CaseActivity {
  id              String   @id @default(cuid())
  caseId          String   @map("case_id")
  actorUserId     String?  @map("actor_user_id")
  action          String   @db.VarChar(120)  // "status_changed", "finding_linked", "assigned", "commented", ...
  metadata        Json?
  createdAt       DateTime @default(now()) @map("created_at")
  case            InvestigationCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
  @@index([caseId, createdAt])
  @@map("case_activity")
}

Auto-correlation suggestions

When a new finding is created, run heuristics:

Surface as a "Suggested cases" panel on the finding-detail page with one-click "Link to CASE-1247".

Postmortem template

Schema-driven prompts on case close:

{
  "what_happened": "...",
  "impact": "...",
  "root_cause": "...",
  "containment": "...",
  "remediation_actions": ["..."],
  "lessons_learned": "...",
  "follow_up_tasks": [{"title":"...", "owner":"..."}]
}

Once captured, the postmortem JSON exports cleanly into the compliance evidence pack (#5) and the executive report (#19).

UI surface

  • /cases — list of open cases with priority, status, assignee, age, last activity.
  • /cases/<shortId> — case detail: linked findings, comments, activity timeline, assignee, postmortem (when present).
  • /cases/queue — on-call shift view: P1+P2 open cases sorted by age; "your queue" filter.
  • Case action bar on every finding detail page: "Link to case", "Create new case from finding", "Mute via case suppression".

Phasing

Phase Scope
P1 InvestigationCase + CaseFindingLink + CaseComment + CaseActivity schema; case CRUD RPCs; /cases list + detail pages; manual link/unlink
P2 Auto-correlation suggestions (heuristic only); on-call queue view; postmortem template
P3 LLM-driven case suggestion + auto-generated case summary on creation (depends on #46); postmortem auto-export to compliance evidence (#5)
P4 Cross-tenant anonymized case-pattern signaling (community signal of "this attack pattern was seen across 14 customers this week")

Open questions

  • How to handle the same finding linked to multiple cases (allowed? — probably yes, but UI must show all links).
  • Should muting / risk-exception flow at the case level or stay at the finding level? Recommendation: both, with case-level being the more common shortcut.
  • Cross-case dependencies (CASE-X blocks CASE-Y) — defer to a later phase.
  • SLA tracking at the case level vs. per-finding (Workflow & ticketing integration: JIRA, Linear, Slack, Teams, PagerDuty, SLA tracking #50): cases get their own SLA driven by priority; severity-driven finding SLAs still apply but the case "rolls up".

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    case-managementInvestigation case managementtier-6-operational-depthTier 6: operational + analytical depthworkflowTicketing, chatops, paging, SLA

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions