package soc import ( "fmt" "sync/atomic" "time" ) // IncidentStatus tracks the lifecycle of a SOC incident. type IncidentStatus string const ( StatusOpen IncidentStatus = "OPEN" StatusInvestigating IncidentStatus = "INVESTIGATING" StatusResolved IncidentStatus = "RESOLVED" StatusFalsePositive IncidentStatus = "FALSE_POSITIVE" ) // IncidentNote represents an analyst investigation note. type IncidentNote struct { ID string `json:"id"` Author string `json:"author"` Content string `json:"content"` CreatedAt time.Time `json:"created_at"` } // TimelineEntry represents a single event in the incident timeline. type TimelineEntry struct { Timestamp time.Time `json:"timestamp"` Type string `json:"type"` // event, playbook, status_change, note, assign Actor string `json:"actor"` // system, analyst name, playbook ID Description string `json:"description"` Metadata map[string]any `json:"metadata,omitempty"` } // Incident represents a correlated security incident aggregated from multiple SOCEvents. // Each incident maintains a cryptographic anchor to the Decision Logger hash chain. type Incident struct { ID string `json:"id"` // INC-YYYY-NNNN TenantID string `json:"tenant_id,omitempty"` Status IncidentStatus `json:"status"` Severity EventSeverity `json:"severity"` // Max severity of constituent events Title string `json:"title"` Description string `json:"description"` Events []string `json:"events"` // Event IDs EventCount int `json:"event_count"` DecisionChainAnchor string `json:"decision_chain_anchor"` // SHA-256 hash (§5.6) ChainLength int `json:"chain_length"` CorrelationRule string `json:"correlation_rule"` // Rule that triggered this incident KillChainPhase string `json:"kill_chain_phase"` // Reconnaissance/Exploitation/Exfiltration MITREMapping []string `json:"mitre_mapping"` // T-codes PlaybookApplied string `json:"playbook_applied,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` ResolvedAt *time.Time `json:"resolved_at,omitempty"` AssignedTo string `json:"assigned_to,omitempty"` Notes []IncidentNote `json:"notes,omitempty"` Timeline []TimelineEntry `json:"timeline,omitempty"` } // incidentCounter is an atomic counter for concurrent-safe incident ID generation. var incidentCounter atomic.Int64 // noteCounter for unique note IDs. var noteCounter atomic.Int64 // NewIncident creates a new incident from a correlation match. // Thread-safe: uses atomic increment for unique ID generation. func NewIncident(title string, severity EventSeverity, correlationRule string) Incident { seq := incidentCounter.Add(1) now := time.Now() inc := Incident{ ID: fmt.Sprintf("INC-%d-%04d", now.Year(), seq), Status: StatusOpen, Severity: severity, Title: title, CorrelationRule: correlationRule, CreatedAt: now, UpdatedAt: now, } inc.Timeline = append(inc.Timeline, TimelineEntry{ Timestamp: now, Type: "created", Actor: "system", Description: fmt.Sprintf("Incident created by rule: %s", correlationRule), }) return inc } // AddEvent adds an event ID to the incident and updates severity if needed. func (inc *Incident) AddEvent(eventID string, severity EventSeverity) { inc.Events = append(inc.Events, eventID) inc.EventCount = len(inc.Events) if severity.Rank() > inc.Severity.Rank() { inc.Severity = severity } inc.UpdatedAt = time.Now() inc.Timeline = append(inc.Timeline, TimelineEntry{ Timestamp: inc.UpdatedAt, Type: "event", Actor: "system", Description: fmt.Sprintf("Event %s correlated (severity: %s)", eventID, severity), }) } // SetAnchor sets the Decision Logger chain anchor for forensics (§5.6). func (inc *Incident) SetAnchor(hash string, chainLength int) { inc.DecisionChainAnchor = hash inc.ChainLength = chainLength inc.UpdatedAt = time.Now() } // Resolve marks the incident as resolved. func (inc *Incident) Resolve(status IncidentStatus, actor string) { now := time.Now() oldStatus := inc.Status inc.Status = status inc.ResolvedAt = &now inc.UpdatedAt = now inc.Timeline = append(inc.Timeline, TimelineEntry{ Timestamp: now, Type: "status_change", Actor: actor, Description: fmt.Sprintf("Status changed: %s → %s", oldStatus, status), }) } // Assign assigns an analyst to the incident. func (inc *Incident) Assign(analyst string) { prev := inc.AssignedTo inc.AssignedTo = analyst inc.UpdatedAt = time.Now() desc := fmt.Sprintf("Assigned to %s", analyst) if prev != "" { desc = fmt.Sprintf("Reassigned: %s → %s", prev, analyst) } inc.Timeline = append(inc.Timeline, TimelineEntry{ Timestamp: inc.UpdatedAt, Type: "assign", Actor: analyst, Description: desc, }) } // ChangeStatus updates incident status without resolving. func (inc *Incident) ChangeStatus(status IncidentStatus, actor string) { old := inc.Status inc.Status = status inc.UpdatedAt = time.Now() if status == StatusResolved || status == StatusFalsePositive { now := time.Now() inc.ResolvedAt = &now } inc.Timeline = append(inc.Timeline, TimelineEntry{ Timestamp: inc.UpdatedAt, Type: "status_change", Actor: actor, Description: fmt.Sprintf("Status: %s → %s", old, status), }) } // AddNote adds an investigation note from an analyst. func (inc *Incident) AddNote(author, content string) IncidentNote { seq := noteCounter.Add(1) note := IncidentNote{ ID: fmt.Sprintf("note-%d", seq), Author: author, Content: content, CreatedAt: time.Now(), } inc.Notes = append(inc.Notes, note) inc.UpdatedAt = note.CreatedAt inc.Timeline = append(inc.Timeline, TimelineEntry{ Timestamp: note.CreatedAt, Type: "note", Actor: author, Description: content, }) return note } // IsOpen returns true if the incident is not resolved. func (inc *Incident) IsOpen() bool { return inc.Status == StatusOpen || inc.Status == StatusInvestigating } // MTTD returns Mean Time To Detect (time from first event to incident creation). // Requires the timestamp of the first correlated event. func (inc *Incident) MTTD(firstEventTime time.Time) time.Duration { return inc.CreatedAt.Sub(firstEventTime) } // MTTR returns Mean Time To Resolve (time from creation to resolution). // Returns 0 if not yet resolved. func (inc *Incident) MTTR() time.Duration { if inc.ResolvedAt == nil { return 0 } return inc.ResolvedAt.Sub(inc.CreatedAt) }