Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SHELL := /bin/bash

GO ?= go
VERSION ?= 0.1.4
VERSION ?= 0.2.0
GOOS ?= $(shell $(GO) env GOOS)
GOARCH ?= $(shell $(GO) env GOARCH)
HOST_OS ?= $(shell uname -s | tr '[:upper:]' '[:lower:]')
Expand Down
2 changes: 1 addition & 1 deletion cmd/aurora/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/spf13/pflag"
)

var version = "0.1.4"
var version = "0.2.0"

func main() {
params := agent.DefaultParameters()
Expand Down
51 changes: 41 additions & 10 deletions lib/consumer/ioc/c2.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,26 @@ import (
"bufio"
"errors"
"os"
"strconv"
"strings"

log "github.com/sirupsen/logrus"
)

func loadC2IOCs(path string, required bool) (map[string]struct{}, map[string]struct{}, error) {
// c2IOCEntry holds a single C2 indicator with its optional score.
type c2IOCEntry struct {
indicator string
score int
}

// defaultC2Score is the score assigned to C2 IOCs without an explicit score.
// C2 indicators are inherently high-severity, so they default to a high score.
const defaultC2Score = 80

func loadC2IOCs(path string, required bool) (map[string]c2IOCEntry, map[string]c2IOCEntry, error) {
path = strings.TrimSpace(path)
if path == "" {
return map[string]struct{}{}, map[string]struct{}{}, nil
return map[string]c2IOCEntry{}, map[string]c2IOCEntry{}, nil
}

f, err := os.Open(path)
Expand All @@ -25,12 +36,12 @@ func loadC2IOCs(path string, required bool) (map[string]struct{}, map[string]str
} else {
log.WithError(err).WithField("path", path).Warn("Failed to open C2 IOC file; C2 IOC matching disabled")
}
return map[string]struct{}{}, map[string]struct{}{}, nil
return map[string]c2IOCEntry{}, map[string]c2IOCEntry{}, nil
}
defer f.Close()

domains := make(map[string]struct{})
ips := make(map[string]struct{})
domains := make(map[string]c2IOCEntry)
ips := make(map[string]c2IOCEntry)

scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
Expand All @@ -48,17 +59,37 @@ func loadC2IOCs(path string, required bool) (map[string]struct{}, map[string]str
continue
}

if ip := normalizeIP(raw); ip != "" {
ips[ip] = struct{}{}
// Parse optional score suffix: INDICATOR;SCORE
indicator := raw
score := defaultC2Score
if idx := strings.LastIndex(raw, ";"); idx >= 0 {
candidateScore := strings.TrimSpace(raw[idx+1:])
if s, err := strconv.Atoi(candidateScore); err == nil {
indicator = strings.TrimSpace(raw[:idx])
score = s
}
// If the part after ; is not a number, treat the whole
// line as the indicator (preserves backwards compatibility
// for entries that might legitimately contain a semicolon).
}

if ip := normalizeIP(indicator); ip != "" {
ips[ip] = c2IOCEntry{indicator: ip, score: score}
continue
}

host := normalizeDomain(raw)
host := normalizeDomain(indicator)
if !isLikelyDomain(host) {
warnSkipIOCLine(path, lineNo, "invalid domain or IP")
// Give a specific hint when ':' is present — likely a
// mistyped score separator (should be ';' not ':').
if strings.Contains(indicator, ":") {
warnSkipIOCLine(path, lineNo, "indicator contains ':' (not valid in FQDN — use ';' as score separator)")
} else {
warnSkipIOCLine(path, lineNo, "invalid domain or IP")
}
continue
}
domains[host] = struct{}{}
domains[host] = c2IOCEntry{indicator: host, score: score}
}

if err := scanner.Err(); err != nil {
Expand Down
61 changes: 46 additions & 15 deletions lib/consumer/ioc/iocconsumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ type Consumer struct {
cfg Config

filenameEntries []filenameIOCEntry
c2Domains map[string]struct{}
c2IPs map[string]struct{}
c2Domains map[string]c2IOCEntry
c2IPs map[string]c2IOCEntry

logger *log.Logger

Expand Down Expand Up @@ -117,19 +117,19 @@ func (c *Consumer) HandleEvent(event provider.Event) error {
switch key {
case "DestinationIp":
if ip := normalizeIP(value); ip != "" {
if _, ok := c.c2IPs[ip]; ok {
if entry, ok := c.c2IPs[ip]; ok {
c.matches.Add(1)
c.emitC2Match(event, key, value, ip)
c.emitC2Match(event, key, value, entry)
}
}
case "DestinationHostname":
host := normalizeDomain(value)
if host == "" {
continue
}
if _, ok := c.c2Domains[host]; ok {
if entry, ok := c.c2Domains[host]; ok {
c.matches.Add(1)
c.emitC2Match(event, key, value, host)
c.emitC2Match(event, key, value, entry)
}
}
}
Expand All @@ -138,12 +138,15 @@ func (c *Consumer) HandleEvent(event provider.Event) error {
}

func (c *Consumer) emitFilenameMatch(event provider.Event, field, value string, entry filenameIOCEntry) {
level, levelName := scoreToLevel(entry.score)

fields := log.Fields{
"ioc_type": "filename",
"ioc_field": field,
"ioc_value": sanitizeFieldForLogging(field, value),
"ioc_regex": entry.rawPattern,
"ioc_score": entry.score,
"ioc_level": levelName,
"ioc_line": entry.line,
"ioc_source": filepath.Base(strings.TrimSpace(c.cfg.FilenameIOCPath)),
"event_provider": event.ID().ProviderName,
Expand All @@ -158,15 +161,19 @@ func (c *Consumer) emitFilenameMatch(event provider.Event, field, value string,
addEventFields(fields, event)

entryLog := log.Entry{Logger: effectiveLogger(c.logger), Data: fields}
entryLog.Log(logLevelForFilenameScore(entry.score), "IOC match")
entryLog.Log(level, "IOC match")
}

func (c *Consumer) emitC2Match(event provider.Event, field, value, indicator string) {
func (c *Consumer) emitC2Match(event provider.Event, field, value string, entry c2IOCEntry) {
level, levelName := scoreToLevel(entry.score)

fields := log.Fields{
"ioc_type": "c2",
"ioc_field": field,
"ioc_value": sanitizeFieldForLogging(field, value),
"ioc_indicator": indicator,
"ioc_indicator": entry.indicator,
"ioc_score": entry.score,
"ioc_level": levelName,
"ioc_source": filepath.Base(strings.TrimSpace(c.cfg.C2IOCPath)),
"event_provider": event.ID().ProviderName,
"event_id": event.ID().EventID,
Expand All @@ -177,7 +184,7 @@ func (c *Consumer) emitC2Match(event provider.Event, field, value, indicator str
addEventFields(fields, event)

entryLog := log.Entry{Logger: effectiveLogger(c.logger), Data: fields}
entryLog.Log(log.ErrorLevel, "IOC match")
entryLog.Log(level, "IOC match")
}

func addEventFields(fields log.Fields, event provider.Event) {
Expand All @@ -200,17 +207,41 @@ func effectiveLogger(logger *log.Logger) *log.Logger {
return log.StandardLogger()
}

func logLevelForFilenameScore(score int) log.Level {
// scoreToLevel maps a numeric IOC score (0–100) to a log level and a
// human-readable level name aligned with the Sigma severity scale.
//
// The mapping mirrors how Sigma rule levels translate to log output:
//
// Score 0–39 → info → log.InfoLevel
// Score 40–59 → low → log.InfoLevel
// Score 60–74 → medium → log.WarnLevel
// Score 75–89 → high → log.ErrorLevel
// Score 90–100 → critical → log.ErrorLevel
//
// This keeps IOC-based alerts consistent with Sigma-based alerts so
// that downstream consumers (SIEM, dashboards) can filter uniformly.
func scoreToLevel(score int) (log.Level, string) {
switch {
case score >= 80:
return log.ErrorLevel
case score >= 90:
return log.ErrorLevel, "critical"
case score >= 75:
return log.ErrorLevel, "high"
case score >= 60:
return log.WarnLevel
return log.WarnLevel, "medium"
case score >= 40:
return log.InfoLevel, "low"
default:
return log.InfoLevel
return log.InfoLevel, "info"
}
}

// logLevelForFilenameScore is kept for backwards compatibility in tests
// that reference the old function name. New code should use scoreToLevel.
func logLevelForFilenameScore(score int) log.Level {
level, _ := scoreToLevel(score)
return level
}

func sanitizeFieldForLogging(key, value string) string {
keyLower := strings.ToLower(key)
for _, marker := range sensitiveValueFieldNames {
Expand Down
Loading
Loading