gomcp/internal/domain/soc/correlation.go

216 lines
7.5 KiB
Go

package soc
import (
"sort"
"time"
)
// SOCCorrelationRule defines a time-windowed correlation rule for SOC events.
// Unlike oracle.CorrelationRule (pattern-based), SOC rules operate on event
// categories within a sliding time window.
type SOCCorrelationRule struct {
ID string `json:"id"`
Name string `json:"name"`
RequiredCategories []string `json:"required_categories"` // Event categories that must co-occur
MinEvents int `json:"min_events"` // Minimum distinct events to trigger
TimeWindow time.Duration `json:"time_window"` // Sliding window for temporal correlation
Severity EventSeverity `json:"severity"` // Resulting incident severity
KillChainPhase string `json:"kill_chain_phase"`
MITREMapping []string `json:"mitre_mapping"`
Description string `json:"description"`
}
// DefaultSOCCorrelationRules returns built-in SOC correlation rules (§7 from spec).
func DefaultSOCCorrelationRules() []SOCCorrelationRule {
return []SOCCorrelationRule{
{
ID: "SOC-CR-001",
Name: "Multi-stage Jailbreak",
RequiredCategories: []string{"jailbreak", "tool_abuse"},
MinEvents: 2,
TimeWindow: 5 * time.Minute,
Severity: SeverityCritical,
KillChainPhase: "Exploitation",
MITREMapping: []string{"T1059", "T1203"},
Description: "Jailbreak attempt followed by tool abuse indicates a staged attack to bypass guardrails and escalate privileges.",
},
{
ID: "SOC-CR-002",
Name: "Coordinated Attack",
RequiredCategories: []string{}, // Any 3+ distinct categories from same source
MinEvents: 3,
TimeWindow: 10 * time.Minute,
Severity: SeverityCritical,
KillChainPhase: "Exploitation",
MITREMapping: []string{"T1595", "T1190"},
Description: "Three or more distinct threat categories from the same source within 10 minutes indicates a coordinated multi-vector attack.",
},
{
ID: "SOC-CR-003",
Name: "Privilege Escalation Chain",
RequiredCategories: []string{"auth_bypass", "exfiltration"},
MinEvents: 2,
TimeWindow: 15 * time.Minute,
Severity: SeverityCritical,
KillChainPhase: "Exfiltration",
MITREMapping: []string{"T1078", "T1041"},
Description: "Authentication bypass followed by data exfiltration attempt within 15 minutes indicates a credential compromise leading to data theft.",
},
{
ID: "SOC-CR-004",
Name: "Injection Escalation",
RequiredCategories: []string{"prompt_injection", "jailbreak"},
MinEvents: 2,
TimeWindow: 5 * time.Minute,
Severity: SeverityHigh,
KillChainPhase: "Exploitation",
MITREMapping: []string{"T1059.007"},
Description: "Prompt injection followed by jailbreak within 5 minutes indicates progressive guardrail erosion attack.",
},
{
ID: "SOC-CR-005",
Name: "Sensor Manipulation",
RequiredCategories: []string{"sensor_anomaly", "tool_abuse"},
MinEvents: 2,
TimeWindow: 5 * time.Minute,
Severity: SeverityCritical,
KillChainPhase: "Defense Evasion",
MITREMapping: []string{"T1562"},
Description: "Sensor anomaly combined with tool abuse suggests attacker is trying to blind defensing before exploitation.",
},
{
ID: "SOC-CR-006",
Name: "Data Exfiltration Pipeline",
RequiredCategories: []string{"exfiltration", "encoding"},
MinEvents: 2,
TimeWindow: 10 * time.Minute,
Severity: SeverityCritical,
KillChainPhase: "Exfiltration",
MITREMapping: []string{"T1041", "T1132"},
Description: "Data exfiltration combined with encoding/obfuscation indicates staged data theft with cover-up.",
},
{
ID: "SOC-CR-007",
Name: "Stealth Persistence",
RequiredCategories: []string{"jailbreak", "persistence"},
MinEvents: 2,
TimeWindow: 30 * time.Minute,
Severity: SeverityHigh,
KillChainPhase: "Persistence",
MITREMapping: []string{"T1546", "T1053"},
Description: "Jailbreak followed by persistence mechanism indicates attacker establishing long-term foothold.",
},
}
}
// CorrelationMatch represents a triggered correlation rule with matched events.
type CorrelationMatch struct {
Rule SOCCorrelationRule `json:"rule"`
Events []SOCEvent `json:"events"`
MatchedAt time.Time `json:"matched_at"`
}
// CorrelateSOCEvents runs all correlation rules against a set of events.
// Events should be pre-filtered to a reasonable time window (e.g., last hour).
// Returns matches sorted by severity (CRITICAL first).
func CorrelateSOCEvents(events []SOCEvent, rules []SOCCorrelationRule) []CorrelationMatch {
if len(events) == 0 || len(rules) == 0 {
return nil
}
now := time.Now()
var matches []CorrelationMatch
for _, rule := range rules {
match := evaluateRule(rule, events, now)
if match != nil {
matches = append(matches, *match)
}
}
// Sort by severity (CRITICAL first)
sort.Slice(matches, func(i, j int) bool {
return matches[i].Rule.Severity.Rank() > matches[j].Rule.Severity.Rank()
})
return matches
}
// evaluateRule checks if a single rule matches against the event set.
func evaluateRule(rule SOCCorrelationRule, events []SOCEvent, now time.Time) *CorrelationMatch {
windowStart := now.Add(-rule.TimeWindow)
// Filter events within time window.
var inWindow []SOCEvent
for _, e := range events {
if !e.Timestamp.Before(windowStart) {
inWindow = append(inWindow, e)
}
}
if len(inWindow) < rule.MinEvents {
return nil
}
// Special case: SOC-CR-002 (Coordinated Attack) — check distinct category count.
if len(rule.RequiredCategories) == 0 && rule.MinEvents > 0 {
return evaluateCoordinatedAttack(rule, inWindow)
}
// Standard case: check that all required categories are present.
categorySet := make(map[string]bool)
var matchedEvents []SOCEvent
for _, e := range inWindow {
categorySet[e.Category] = true
// Collect events matching required categories.
for _, rc := range rule.RequiredCategories {
if e.Category == rc {
matchedEvents = append(matchedEvents, e)
break
}
}
}
// Check all required categories are present.
for _, rc := range rule.RequiredCategories {
if !categorySet[rc] {
return nil
}
}
if len(matchedEvents) < rule.MinEvents {
return nil
}
return &CorrelationMatch{
Rule: rule,
Events: matchedEvents,
MatchedAt: time.Now(),
}
}
// evaluateCoordinatedAttack checks for N+ distinct categories from same source.
func evaluateCoordinatedAttack(rule SOCCorrelationRule, events []SOCEvent) *CorrelationMatch {
// Group by source, count distinct categories.
sourceCategories := make(map[EventSource]map[string]bool)
sourceEvents := make(map[EventSource][]SOCEvent)
for _, e := range events {
if sourceCategories[e.Source] == nil {
sourceCategories[e.Source] = make(map[string]bool)
}
sourceCategories[e.Source][e.Category] = true
sourceEvents[e.Source] = append(sourceEvents[e.Source], e)
}
for source, cats := range sourceCategories {
if len(cats) >= rule.MinEvents {
return &CorrelationMatch{
Rule: rule,
Events: sourceEvents[source],
MatchedAt: time.Now(),
}
}
}
return nil
}