mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
218 lines
6.7 KiB
Go
218 lines
6.7 KiB
Go
// Copyright 2026 Syntrex Lab. All rights reserved.
|
|
// Use of this source code is governed by an Apache-2.0 license
|
|
// that can be found in the LICENSE file.
|
|
|
|
// 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-lab/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
|
|
}
|
|
}
|