mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-25 04:16:22 +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
214
internal/application/sidecar/parser.go
Normal file
214
internal/application/sidecar/parser.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// Package sidecar implements the Universal Sidecar (§5.5) — a zero-dependency
|
||||
// Go binary that runs alongside SENTINEL sensors, tails their STDOUT/logs,
|
||||
// and pushes parsed security events to the SOC Event Bus.
|
||||
package sidecar
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
domsoc "github.com/syntrex/gomcp/internal/domain/soc"
|
||||
)
|
||||
|
||||
// Parser converts a raw log line into a SOCEvent.
|
||||
// Returns nil, false if the line is not a security event.
|
||||
type Parser interface {
|
||||
Parse(line string) (*domsoc.SOCEvent, bool)
|
||||
}
|
||||
|
||||
// ── sentinel-core Parser ─────────────────────────────────────────────────────
|
||||
|
||||
// SentinelCoreParser parses sentinel-core detection output.
|
||||
// Expected format: [DETECT] engine=<name> confidence=<float> pattern=<desc> [severity=<sev>]
|
||||
type SentinelCoreParser struct{}
|
||||
|
||||
var coreDetectRe = regexp.MustCompile(
|
||||
`\[DETECT\]\s+engine=(\S+)\s+confidence=([0-9.]+)\s+pattern=(.+?)(?:\s+severity=(\S+))?$`)
|
||||
|
||||
func (p *SentinelCoreParser) Parse(line string) (*domsoc.SOCEvent, bool) {
|
||||
m := coreDetectRe.FindStringSubmatch(strings.TrimSpace(line))
|
||||
if m == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
engine := m[1]
|
||||
conf, _ := strconv.ParseFloat(m[2], 64)
|
||||
pattern := m[3]
|
||||
severity := mapConfidenceToSeverity(conf)
|
||||
if m[4] != "" {
|
||||
severity = domsoc.EventSeverity(strings.ToUpper(m[4]))
|
||||
}
|
||||
|
||||
evt := domsoc.NewSOCEvent(domsoc.SourceSentinelCore, severity, engine,
|
||||
engine+": "+pattern)
|
||||
evt.Confidence = conf
|
||||
evt.Subcategory = pattern
|
||||
return &evt, true
|
||||
}
|
||||
|
||||
// ── shield Parser ────────────────────────────────────────────────────────────
|
||||
|
||||
// ShieldParser parses shield network block logs.
|
||||
// Expected format: BLOCKED protocol=<proto> reason=<reason> source_ip=<ip>
|
||||
type ShieldParser struct{}
|
||||
|
||||
var shieldBlockRe = regexp.MustCompile(
|
||||
`BLOCKED\s+protocol=(\S+)\s+reason=(.+?)\s+source_ip=(\S+)`)
|
||||
|
||||
func (p *ShieldParser) Parse(line string) (*domsoc.SOCEvent, bool) {
|
||||
m := shieldBlockRe.FindStringSubmatch(strings.TrimSpace(line))
|
||||
if m == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
protocol := m[1]
|
||||
reason := m[2]
|
||||
sourceIP := m[3]
|
||||
|
||||
evt := domsoc.NewSOCEvent(domsoc.SourceShield, domsoc.SeverityMedium, "network_block",
|
||||
"Shield blocked "+protocol+" from "+sourceIP+": "+reason)
|
||||
evt.Subcategory = protocol
|
||||
evt.Metadata = map[string]string{
|
||||
"source_ip": sourceIP,
|
||||
"protocol": protocol,
|
||||
"reason": reason,
|
||||
}
|
||||
return &evt, true
|
||||
}
|
||||
|
||||
// ── immune Parser ────────────────────────────────────────────────────────────
|
||||
|
||||
// ImmuneParser parses immune system anomaly/response logs.
|
||||
// Expected format: [ANOMALY] type=<type> score=<float> detail=<text>
|
||||
//
|
||||
// or: [RESPONSE] action=<action> target=<target> reason=<text>
|
||||
type ImmuneParser struct{}
|
||||
|
||||
var immuneAnomalyRe = regexp.MustCompile(
|
||||
`\[ANOMALY\]\s+type=(\S+)\s+score=([0-9.]+)\s+detail=(.+)`)
|
||||
var immuneResponseRe = regexp.MustCompile(
|
||||
`\[RESPONSE\]\s+action=(\S+)\s+target=(\S+)\s+reason=(.+)`)
|
||||
|
||||
func (p *ImmuneParser) Parse(line string) (*domsoc.SOCEvent, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if m := immuneAnomalyRe.FindStringSubmatch(trimmed); m != nil {
|
||||
anomalyType := m[1]
|
||||
score, _ := strconv.ParseFloat(m[2], 64)
|
||||
detail := m[3]
|
||||
|
||||
evt := domsoc.NewSOCEvent(domsoc.SourceImmune, mapConfidenceToSeverity(score),
|
||||
"anomaly", "Immune anomaly: "+anomalyType+": "+detail)
|
||||
evt.Confidence = score
|
||||
evt.Subcategory = anomalyType
|
||||
return &evt, true
|
||||
}
|
||||
|
||||
if m := immuneResponseRe.FindStringSubmatch(trimmed); m != nil {
|
||||
action := m[1]
|
||||
target := m[2]
|
||||
reason := m[3]
|
||||
|
||||
evt := domsoc.NewSOCEvent(domsoc.SourceImmune, domsoc.SeverityHigh,
|
||||
"immune_response", "Immune response: "+action+" on "+target+": "+reason)
|
||||
evt.Subcategory = action
|
||||
evt.Metadata = map[string]string{
|
||||
"action": action,
|
||||
"target": target,
|
||||
"reason": reason,
|
||||
}
|
||||
return &evt, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ── Generic Parser ───────────────────────────────────────────────────────────
|
||||
|
||||
// GenericParser uses a configurable regex with named groups.
|
||||
// Named groups: "category", "severity", "description", "confidence".
|
||||
type GenericParser struct {
|
||||
Pattern *regexp.Regexp
|
||||
Source domsoc.EventSource
|
||||
}
|
||||
|
||||
func NewGenericParser(pattern string, source domsoc.EventSource) (*GenericParser, error) {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GenericParser{Pattern: re, Source: source}, nil
|
||||
}
|
||||
|
||||
func (p *GenericParser) Parse(line string) (*domsoc.SOCEvent, bool) {
|
||||
m := p.Pattern.FindStringSubmatch(strings.TrimSpace(line))
|
||||
if m == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
names := p.Pattern.SubexpNames()
|
||||
groups := map[string]string{}
|
||||
for i, name := range names {
|
||||
if i > 0 && name != "" {
|
||||
groups[name] = m[i]
|
||||
}
|
||||
}
|
||||
|
||||
category := groups["category"]
|
||||
if category == "" {
|
||||
category = "generic"
|
||||
}
|
||||
description := groups["description"]
|
||||
if description == "" {
|
||||
description = line
|
||||
}
|
||||
severity := domsoc.SeverityMedium
|
||||
if s, ok := groups["severity"]; ok && s != "" {
|
||||
severity = domsoc.EventSeverity(strings.ToUpper(s))
|
||||
}
|
||||
confidence := 0.5
|
||||
if c, ok := groups["confidence"]; ok {
|
||||
if f, err := strconv.ParseFloat(c, 64); err == nil {
|
||||
confidence = f
|
||||
}
|
||||
}
|
||||
|
||||
evt := domsoc.NewSOCEvent(p.Source, severity, category, description)
|
||||
evt.Confidence = confidence
|
||||
return &evt, true
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// ParserForSensor returns the appropriate parser for a sensor type.
|
||||
func ParserForSensor(sensorType string) Parser {
|
||||
switch strings.ToLower(sensorType) {
|
||||
case "sentinel-core":
|
||||
return &SentinelCoreParser{}
|
||||
case "shield":
|
||||
return &ShieldParser{}
|
||||
case "immune":
|
||||
return &ImmuneParser{}
|
||||
default:
|
||||
slog.Warn("sidecar: unknown sensor type, using sentinel-core parser as fallback",
|
||||
"sensor_type", sensorType)
|
||||
return &SentinelCoreParser{} // fallback
|
||||
}
|
||||
}
|
||||
|
||||
func mapConfidenceToSeverity(conf float64) domsoc.EventSeverity {
|
||||
switch {
|
||||
case conf >= 0.9:
|
||||
return domsoc.SeverityCritical
|
||||
case conf >= 0.7:
|
||||
return domsoc.SeverityHigh
|
||||
case conf >= 0.5:
|
||||
return domsoc.SeverityMedium
|
||||
case conf >= 0.3:
|
||||
return domsoc.SeverityLow
|
||||
default:
|
||||
return domsoc.SeverityInfo
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue