Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions