mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-08 11:02:37 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
|
|
@ -1,115 +1,277 @@
|
|||
package soc
|
||||
|
||||
// PlaybookAction defines automated responses triggered by playbook rules.
|
||||
type PlaybookAction string
|
||||
|
||||
const (
|
||||
ActionAutoBlock PlaybookAction = "auto_block" // Block source via shield
|
||||
ActionAutoReview PlaybookAction = "auto_review" // Flag for human review
|
||||
ActionNotify PlaybookAction = "notify" // Send notification
|
||||
ActionIsolate PlaybookAction = "isolate" // Isolate affected session
|
||||
ActionEscalate PlaybookAction = "escalate" // Escalate to senior analyst
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PlaybookCondition defines when a playbook fires.
|
||||
type PlaybookCondition struct {
|
||||
MinSeverity EventSeverity `json:"min_severity" yaml:"min_severity"` // Minimum severity to trigger
|
||||
Categories []string `json:"categories" yaml:"categories"` // Matching categories
|
||||
Sources []EventSource `json:"sources,omitempty" yaml:"sources"` // Restrict to specific sources
|
||||
MinEvents int `json:"min_events" yaml:"min_events"` // Minimum events before trigger
|
||||
// PlaybookEngine implements §10 — automated incident response.
|
||||
// Executes predefined response actions when incidents match playbook triggers.
|
||||
type PlaybookEngine struct {
|
||||
mu sync.RWMutex
|
||||
playbooks map[string]*Playbook
|
||||
execLog []PlaybookExecution
|
||||
maxLog int
|
||||
handler ActionHandler
|
||||
}
|
||||
|
||||
// Playbook is a YAML-defined automated response rule (§10).
|
||||
// ActionHandler executes playbook actions. Implement for real integrations.
|
||||
type ActionHandler interface {
|
||||
Handle(action PlaybookAction, incidentID string) error
|
||||
}
|
||||
|
||||
// LogHandler is the default action handler — logs what would be executed.
|
||||
type LogHandler struct{}
|
||||
|
||||
func (h LogHandler) Handle(action PlaybookAction, incidentID string) error {
|
||||
slog.Info("playbook action", "action", action.Type, "incident", incidentID, "params", action.Params)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Playbook defines an automated response procedure.
|
||||
type Playbook struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
Condition PlaybookCondition `json:"condition" yaml:"condition"`
|
||||
Actions []PlaybookAction `json:"actions" yaml:"actions"`
|
||||
Priority int `json:"priority" yaml:"priority"` // Higher = runs first
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Trigger PlaybookTrigger `json:"trigger"`
|
||||
Actions []PlaybookAction `json:"actions"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Priority int `json:"priority"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Matches checks if a SOC event matches this playbook's conditions.
|
||||
func (p *Playbook) Matches(event SOCEvent) bool {
|
||||
if !p.Enabled {
|
||||
return false
|
||||
}
|
||||
// PlaybookTrigger defines when a playbook activates.
|
||||
type PlaybookTrigger struct {
|
||||
Severity string `json:"severity,omitempty"`
|
||||
Categories []string `json:"categories,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
KillChainPhase string `json:"kill_chain_phase,omitempty"`
|
||||
}
|
||||
|
||||
// Check severity threshold.
|
||||
if event.Severity.Rank() < p.Condition.MinSeverity.Rank() {
|
||||
return false
|
||||
}
|
||||
// PlaybookAction is a single response step.
|
||||
type PlaybookAction struct {
|
||||
Type string `json:"type"`
|
||||
Params map[string]string `json:"params"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
// Check category if specified.
|
||||
if len(p.Condition.Categories) > 0 {
|
||||
matched := false
|
||||
for _, cat := range p.Condition.Categories {
|
||||
if cat == event.Category {
|
||||
matched = true
|
||||
// PlaybookExecution records a playbook run.
|
||||
type PlaybookExecution struct {
|
||||
ID string `json:"id"`
|
||||
PlaybookID string `json:"playbook_id"`
|
||||
IncidentID string `json:"incident_id"`
|
||||
Status string `json:"status"`
|
||||
ActionsRun int `json:"actions_run"`
|
||||
Duration string `json:"duration"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// NewPlaybookEngine creates the automated response engine with built-in playbooks.
|
||||
func NewPlaybookEngine() *PlaybookEngine {
|
||||
pe := &PlaybookEngine{
|
||||
playbooks: make(map[string]*Playbook),
|
||||
maxLog: 200,
|
||||
handler: LogHandler{},
|
||||
}
|
||||
pe.loadDefaults()
|
||||
return pe
|
||||
}
|
||||
|
||||
// SetHandler replaces the action handler (for real integrations: webhook, SOAR, etc.).
|
||||
func (pe *PlaybookEngine) SetHandler(h ActionHandler) {
|
||||
pe.mu.Lock()
|
||||
defer pe.mu.Unlock()
|
||||
pe.handler = h
|
||||
}
|
||||
|
||||
func (pe *PlaybookEngine) loadDefaults() {
|
||||
defaults := []Playbook{
|
||||
{
|
||||
ID: "pb-block-jailbreak", Name: "Auto-Block Jailbreak Source",
|
||||
Description: "Blocks source IP on confirmed jailbreak attempts",
|
||||
Trigger: PlaybookTrigger{Severity: "CRITICAL", Categories: []string{"jailbreak"}},
|
||||
Actions: []PlaybookAction{
|
||||
{Type: "log", Params: map[string]string{"message": "Jailbreak detected"}, Order: 1},
|
||||
{Type: "block_ip", Params: map[string]string{"duration": "3600"}, Order: 2},
|
||||
{Type: "notify", Params: map[string]string{"channel": "soc-alerts"}, Order: 3},
|
||||
},
|
||||
Enabled: true, Priority: 1,
|
||||
},
|
||||
{
|
||||
ID: "pb-quarantine-exfil", Name: "Quarantine Data Exfiltration",
|
||||
Description: "Isolates sessions on data exfiltration detection",
|
||||
Trigger: PlaybookTrigger{Severity: "HIGH", Categories: []string{"exfiltration"}},
|
||||
Actions: []PlaybookAction{
|
||||
{Type: "quarantine", Params: map[string]string{"scope": "session"}, Order: 1},
|
||||
{Type: "escalate", Params: map[string]string{"team": "ir-team"}, Order: 2},
|
||||
},
|
||||
Enabled: true, Priority: 2,
|
||||
},
|
||||
{
|
||||
ID: "pb-notify-injection", Name: "Alert on Prompt Injection",
|
||||
Description: "Sends notification on prompt injection detection",
|
||||
Trigger: PlaybookTrigger{Severity: "MEDIUM", Categories: []string{"injection"}},
|
||||
Actions: []PlaybookAction{
|
||||
{Type: "log", Params: map[string]string{"message": "Prompt injection detected"}, Order: 1},
|
||||
{Type: "notify", Params: map[string]string{"channel": "soc-alerts"}, Order: 2},
|
||||
},
|
||||
Enabled: true, Priority: 3,
|
||||
},
|
||||
{
|
||||
ID: "pb-c2-killchain", Name: "Kill Chain C2 Response",
|
||||
Description: "Immediate response to C2 communication detection",
|
||||
Trigger: PlaybookTrigger{KillChainPhase: "command_control"},
|
||||
Actions: []PlaybookAction{
|
||||
{Type: "block_ip", Params: map[string]string{"duration": "86400"}, Order: 1},
|
||||
{Type: "quarantine", Params: map[string]string{"scope": "host"}, Order: 2},
|
||||
{Type: "webhook", Params: map[string]string{"event": "kill_chain_alert"}, Order: 3},
|
||||
{Type: "escalate", Params: map[string]string{"team": "threat-hunters"}, Order: 4},
|
||||
},
|
||||
Enabled: true, Priority: 1,
|
||||
},
|
||||
}
|
||||
for i := range defaults {
|
||||
defaults[i].CreatedAt = time.Now()
|
||||
pe.playbooks[defaults[i].ID] = &defaults[i]
|
||||
}
|
||||
}
|
||||
|
||||
// AddPlaybook registers a custom playbook.
|
||||
func (pe *PlaybookEngine) AddPlaybook(pb Playbook) {
|
||||
pe.mu.Lock()
|
||||
defer pe.mu.Unlock()
|
||||
if pb.ID == "" {
|
||||
pb.ID = fmt.Sprintf("pb-%d", time.Now().UnixNano())
|
||||
}
|
||||
pb.CreatedAt = time.Now()
|
||||
pe.playbooks[pb.ID] = &pb
|
||||
}
|
||||
|
||||
// RemovePlaybook deactivates a playbook.
|
||||
func (pe *PlaybookEngine) RemovePlaybook(id string) {
|
||||
pe.mu.Lock()
|
||||
defer pe.mu.Unlock()
|
||||
if pb, ok := pe.playbooks[id]; ok {
|
||||
pb.Enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs matching playbooks for an incident.
|
||||
func (pe *PlaybookEngine) Execute(incidentID, severity, category, killChainPhase string) []PlaybookExecution {
|
||||
pe.mu.Lock()
|
||||
defer pe.mu.Unlock()
|
||||
|
||||
var results []PlaybookExecution
|
||||
for _, pb := range pe.playbooks {
|
||||
if !pb.Enabled || !pe.matches(pb, severity, category, killChainPhase) {
|
||||
continue
|
||||
}
|
||||
start := time.Now()
|
||||
exec := PlaybookExecution{
|
||||
ID: genID("exec"),
|
||||
PlaybookID: pb.ID,
|
||||
IncidentID: incidentID,
|
||||
Status: "success",
|
||||
ActionsRun: len(pb.Actions),
|
||||
Timestamp: start,
|
||||
}
|
||||
for _, action := range pb.Actions {
|
||||
if err := pe.handler.Handle(action, incidentID); err != nil {
|
||||
exec.Status = "partial_failure"
|
||||
exec.Error = err.Error()
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
exec.Duration = time.Since(start).String()
|
||||
if len(pe.execLog) >= pe.maxLog {
|
||||
copy(pe.execLog, pe.execLog[1:])
|
||||
pe.execLog[len(pe.execLog)-1] = exec
|
||||
} else {
|
||||
pe.execLog = append(pe.execLog, exec)
|
||||
}
|
||||
results = append(results, exec)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Check source restriction if specified.
|
||||
if len(p.Condition.Sources) > 0 {
|
||||
matched := false
|
||||
for _, src := range p.Condition.Sources {
|
||||
if src == event.Source {
|
||||
matched = true
|
||||
func (pe *PlaybookEngine) matches(pb *Playbook, severity, category, killChainPhase string) bool {
|
||||
t := pb.Trigger
|
||||
if t.Severity != "" && severityRank(severity) < severityRank(t.Severity) {
|
||||
return false
|
||||
}
|
||||
if len(t.Categories) > 0 {
|
||||
found := false
|
||||
for _, c := range t.Categories {
|
||||
if c == category {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if t.KillChainPhase != "" && t.KillChainPhase != killChainPhase {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DefaultPlaybooks returns the built-in playbook set (§10 from spec).
|
||||
func DefaultPlaybooks() []Playbook {
|
||||
return []Playbook{
|
||||
{
|
||||
ID: "pb-auto-block-jailbreak",
|
||||
Name: "Auto-Block Jailbreak",
|
||||
Description: "Automatically block confirmed jailbreak attempts",
|
||||
Enabled: true,
|
||||
Condition: PlaybookCondition{
|
||||
MinSeverity: SeverityHigh,
|
||||
Categories: []string{"jailbreak", "prompt_injection"},
|
||||
},
|
||||
Actions: []PlaybookAction{ActionAutoBlock, ActionNotify},
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
ID: "pb-escalate-exfiltration",
|
||||
Name: "Escalate Exfiltration",
|
||||
Description: "Escalate data exfiltration attempts to senior analyst",
|
||||
Enabled: true,
|
||||
Condition: PlaybookCondition{
|
||||
MinSeverity: SeverityCritical,
|
||||
Categories: []string{"exfiltration", "data_leak"},
|
||||
},
|
||||
Actions: []PlaybookAction{ActionIsolate, ActionEscalate, ActionNotify},
|
||||
Priority: 200,
|
||||
},
|
||||
{
|
||||
ID: "pb-review-tool-abuse",
|
||||
Name: "Review Tool Abuse",
|
||||
Description: "Flag tool abuse attempts for human review",
|
||||
Enabled: true,
|
||||
Condition: PlaybookCondition{
|
||||
MinSeverity: SeverityMedium,
|
||||
Categories: []string{"tool_abuse", "unauthorized_tool_use"},
|
||||
},
|
||||
Actions: []PlaybookAction{ActionAutoReview},
|
||||
Priority: 50,
|
||||
},
|
||||
func severityRank(s string) int {
|
||||
switch s {
|
||||
case "CRITICAL":
|
||||
return 4
|
||||
case "HIGH":
|
||||
return 3
|
||||
case "MEDIUM":
|
||||
return 2
|
||||
case "LOW":
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ListPlaybooks returns all playbooks.
|
||||
func (pe *PlaybookEngine) ListPlaybooks() []Playbook {
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
result := make([]Playbook, 0, len(pe.playbooks))
|
||||
for _, pb := range pe.playbooks {
|
||||
result = append(result, *pb)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExecutionLog returns recent playbook executions.
|
||||
func (pe *PlaybookEngine) ExecutionLog(limit int) []PlaybookExecution {
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
if limit <= 0 || limit > len(pe.execLog) {
|
||||
limit = len(pe.execLog)
|
||||
}
|
||||
start := len(pe.execLog) - limit
|
||||
result := make([]PlaybookExecution, limit)
|
||||
copy(result, pe.execLog[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
// PlaybookStats returns engine statistics.
|
||||
func (pe *PlaybookEngine) PlaybookStats() map[string]any {
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
enabled := 0
|
||||
for _, pb := range pe.playbooks {
|
||||
if pb.Enabled {
|
||||
enabled++
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"total_playbooks": len(pe.playbooks),
|
||||
"enabled": enabled,
|
||||
"total_executions": len(pe.execLog),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue