gomcp/internal/domain/soc/rule_loader.go

84 lines
2.4 KiB
Go

package soc
import (
"fmt"
"os"
"time"
"gopkg.in/yaml.v3"
)
// RuleConfig is the YAML format for custom correlation rules (§7.5).
//
// Example rules.yaml:
//
// rules:
// - id: CUSTOM-001
// name: API Key Spray
// required_categories: [auth_bypass, brute_force]
// min_events: 5
// time_window: 2m
// severity: HIGH
// kill_chain_phase: Reconnaissance
// mitre_mapping: [T1110]
// cross_sensor: true
type RuleConfig struct {
Rules []YAMLRule `yaml:"rules"`
}
// YAMLRule is a single custom correlation rule loaded from YAML.
type YAMLRule struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
RequiredCategories []string `yaml:"required_categories"`
MinEvents int `yaml:"min_events"`
TimeWindow string `yaml:"time_window"` // e.g., "5m", "10m", "1h"
Severity string `yaml:"severity"`
KillChainPhase string `yaml:"kill_chain_phase"`
MITREMapping []string `yaml:"mitre_mapping"`
Description string `yaml:"description"`
CrossSensor bool `yaml:"cross_sensor"` // Allow cross-sensor correlation
}
// LoadRulesFromYAML loads custom correlation rules from a YAML file.
// Returns nil and no error if the file doesn't exist (optional config).
func LoadRulesFromYAML(path string) ([]SOCCorrelationRule, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // Optional — no custom rules
}
return nil, fmt.Errorf("read rules file: %w", err)
}
var cfg RuleConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse rules YAML: %w", err)
}
rules := make([]SOCCorrelationRule, 0, len(cfg.Rules))
for _, yr := range cfg.Rules {
dur, err := time.ParseDuration(yr.TimeWindow)
if err != nil {
return nil, fmt.Errorf("rule %s: invalid time_window %q: %w", yr.ID, yr.TimeWindow, err)
}
if yr.MinEvents == 0 {
yr.MinEvents = 2 // Default
}
rules = append(rules, SOCCorrelationRule{
ID: yr.ID,
Name: yr.Name,
RequiredCategories: yr.RequiredCategories,
MinEvents: yr.MinEvents,
TimeWindow: dur,
Severity: EventSeverity(yr.Severity),
KillChainPhase: yr.KillChainPhase,
MITREMapping: yr.MITREMapping,
Description: yr.Description,
CrossSensor: yr.CrossSensor,
})
}
return rules, nil
}