92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// Package correlation adds higher-confidence stories without hiding or
|
|
// replacing their raw alerts. It is transport- and persistence-independent.
|
|
package correlation
|
|
|
|
import "sort"
|
|
|
|
const SIDMultiStageIntrusion = 100080
|
|
|
|
type Incident struct {
|
|
FirstTimestamp float64 `json:"first_ts"`
|
|
LastTimestamp float64 `json:"last_ts"`
|
|
Signatures []string `json:"signatures"`
|
|
}
|
|
|
|
type Record struct {
|
|
SID int `json:"sid"`
|
|
Signature string `json:"signature"`
|
|
Classtype string `json:"classtype"`
|
|
Severity string `json:"severity"`
|
|
Summary string `json:"summary"`
|
|
WindowSeconds int `json:"window_seconds"`
|
|
MatchedSignatures []string `json:"matched_signatures"`
|
|
}
|
|
|
|
type Rule struct {
|
|
SID int
|
|
Signature string
|
|
Classtype string
|
|
Severity string
|
|
Summary string
|
|
RequiredAny []map[string]bool
|
|
MaxWindow int
|
|
}
|
|
|
|
var DefaultRules = []Rule{{
|
|
SID: SIDMultiStageIntrusion, Signature: "correlation.multi_stage_intrusion",
|
|
Classtype: "multi-stage-intrusion", Severity: "CRITICAL",
|
|
Summary: "Web/database service spawned a shell and the same incident showed " +
|
|
"suspicious egress or an unusual listener within the correlation window",
|
|
RequiredAny: []map[string]bool{
|
|
{"exec_rule.web-rce": true},
|
|
{"host_rule.suspicious-egress": true, "host_rule.suspicious-listener": true},
|
|
},
|
|
MaxWindow: 600,
|
|
}}
|
|
|
|
func Correlate(incident Incident, rules []Rule) []Record {
|
|
if rules == nil {
|
|
rules = DefaultRules
|
|
}
|
|
signatures := make(map[string]bool, len(incident.Signatures))
|
|
for _, signature := range incident.Signatures {
|
|
signatures[signature] = true
|
|
}
|
|
duration := incident.LastTimestamp - incident.FirstTimestamp
|
|
result := make([]Record, 0)
|
|
for _, rule := range rules {
|
|
if duration > float64(rule.MaxWindow) {
|
|
continue
|
|
}
|
|
matched := true
|
|
for _, choices := range rule.RequiredAny {
|
|
found := false
|
|
for choice := range choices {
|
|
if signatures[choice] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
matchedSignatures := make([]string, 0, len(signatures))
|
|
for signature := range signatures {
|
|
matchedSignatures = append(matchedSignatures, signature)
|
|
}
|
|
sort.Strings(matchedSignatures)
|
|
result = append(result, Record{
|
|
SID: rule.SID, Signature: rule.Signature, Classtype: rule.Classtype,
|
|
Severity: rule.Severity, Summary: rule.Summary,
|
|
WindowSeconds: rule.MaxWindow, MatchedSignatures: matchedSignatures,
|
|
})
|
|
}
|
|
return result
|
|
}
|