feat(go): advance validation sidecar toward production
This commit is contained in:
parent
6a06eba255
commit
f85c2e831a
92 changed files with 8881 additions and 91 deletions
201
go-agent/internal/events/host_rules.go
Normal file
201
go-agent/internal/events/host_rules.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/netutil"
|
||||
)
|
||||
|
||||
type HostRule struct {
|
||||
SID int
|
||||
Message string
|
||||
Severity string
|
||||
Classtype string
|
||||
Events map[string]bool
|
||||
Comms map[string]bool
|
||||
ParentComms map[string]bool
|
||||
PeerPublic *bool
|
||||
PeerPorts map[int]bool
|
||||
PeerPortExclude map[int]bool
|
||||
LocalPorts map[int]bool
|
||||
LocalPortExclude map[int]bool
|
||||
PathPrefixes []string
|
||||
TargetUIDs map[int]bool
|
||||
TargetGIDs map[int]bool
|
||||
CapabilitiesAny map[string]bool
|
||||
ModuleNames map[string]bool
|
||||
ArgvRegex string
|
||||
compiled *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewHostRule(rule HostRule) (HostRule, error) {
|
||||
if len(rule.Events) == 0 {
|
||||
return HostRule{}, fmt.Errorf("rule sid=%d has no event types", rule.SID)
|
||||
}
|
||||
if len(rule.Comms) == 0 && len(rule.ParentComms) == 0 && rule.PeerPublic == nil &&
|
||||
len(rule.PeerPorts) == 0 && len(rule.PeerPortExclude) == 0 &&
|
||||
len(rule.LocalPorts) == 0 && len(rule.LocalPortExclude) == 0 &&
|
||||
len(rule.PathPrefixes) == 0 && len(rule.TargetUIDs) == 0 &&
|
||||
len(rule.TargetGIDs) == 0 && len(rule.CapabilitiesAny) == 0 &&
|
||||
len(rule.ModuleNames) == 0 && rule.ArgvRegex == "" {
|
||||
return HostRule{}, fmt.Errorf("rule sid=%d has no match conditions", rule.SID)
|
||||
}
|
||||
if rule.ArgvRegex != "" {
|
||||
compiled, err := regexp.Compile("(?i)" + rule.ArgvRegex)
|
||||
if err != nil {
|
||||
return HostRule{}, fmt.Errorf("rule sid=%d argv_regex: %w", rule.SID, err)
|
||||
}
|
||||
rule.compiled = compiled
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (r HostRule) Matches(event HostEvent) bool {
|
||||
if !r.Events[event.Event] || len(r.Comms) > 0 && !r.Comms[event.Comm] ||
|
||||
len(r.ParentComms) > 0 && !r.ParentComms[event.ParentComm] {
|
||||
return false
|
||||
}
|
||||
if r.PeerPublic != nil && netutil.IsPublicIP(event.PeerIP) != *r.PeerPublic {
|
||||
return false
|
||||
}
|
||||
if len(r.PeerPorts) > 0 && !r.PeerPorts[event.PeerPort] ||
|
||||
r.PeerPortExclude[event.PeerPort] ||
|
||||
len(r.LocalPorts) > 0 && !r.LocalPorts[event.LocalPort] ||
|
||||
r.LocalPortExclude[event.LocalPort] {
|
||||
return false
|
||||
}
|
||||
if len(r.PathPrefixes) > 0 && !pathMatches(event.Path, r.PathPrefixes) {
|
||||
return false
|
||||
}
|
||||
if len(r.TargetUIDs) > 0 && !r.TargetUIDs[event.TargetUID] ||
|
||||
len(r.TargetGIDs) > 0 && !r.TargetGIDs[event.TargetGID] ||
|
||||
len(r.ModuleNames) > 0 && !r.ModuleNames[event.ModuleName] {
|
||||
return false
|
||||
}
|
||||
if len(r.CapabilitiesAny) > 0 {
|
||||
matched := false
|
||||
for _, capability := range event.Capabilities {
|
||||
if r.CapabilitiesAny[strings.ToUpper(capability)] {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return r.compiled == nil || r.compiled.MatchString(event.ArgvString())
|
||||
}
|
||||
|
||||
func (r HostRule) Alert(event HostEvent) model.Alert {
|
||||
capabilities := strings.Join(event.Capabilities, ",")
|
||||
mode := "-"
|
||||
if event.Mode != 0 {
|
||||
mode = fmt.Sprintf("0o%o", event.Mode)
|
||||
}
|
||||
module := event.ModuleName
|
||||
if module == "" {
|
||||
module = "-"
|
||||
}
|
||||
displayCaps := capabilities
|
||||
if displayCaps == "" {
|
||||
displayCaps = "-"
|
||||
}
|
||||
return model.Alert{
|
||||
SID: r.SID, Severity: r.Severity,
|
||||
Signature: "host_rule." + r.Classtype, Classtype: r.Classtype,
|
||||
Key: fmt.Sprintf(
|
||||
"host:%d:%s:%d:%s:%d:%s:%d:%s:%d:%d:%d:%s:%s",
|
||||
r.SID, event.Event, event.PID, event.PeerIP, event.PeerPort,
|
||||
event.LocalIP, event.LocalPort, event.Path, event.TargetUID,
|
||||
event.TargetGID, event.Mode, capabilities, event.ModuleName),
|
||||
Detail: fmt.Sprintf(
|
||||
"sid=%d %s - pid=%d ppid=%d comm=%s event=%s peer=%s:%d local=%s:%d path=%s target_uid=%d target_gid=%d mode=%s capabilities=%s module=%s",
|
||||
r.SID, r.Message, event.PID, event.PPID, event.Comm, event.Event,
|
||||
event.PeerIP, event.PeerPort, event.LocalIP, event.LocalPort,
|
||||
event.Path, event.TargetUID, event.TargetGID, mode, displayCaps, module),
|
||||
PIDs: []int{event.PID},
|
||||
}
|
||||
}
|
||||
|
||||
type HostRuleEngine struct{ Rules []HostRule }
|
||||
|
||||
func DefaultHostRuleEngine() HostRuleEngine {
|
||||
public := true
|
||||
specifications := []HostRule{
|
||||
{SID: 100067, Message: "Interpreter connected to an unusual public port", Severity: "HIGH", Classtype: "suspicious-egress", Events: stringSet([]string{"tcp_connect"}), Comms: hostInterpreters, PeerPublic: &public, PeerPortExclude: commonPublicPorts},
|
||||
{SID: 100068, Message: "Interpreter opened a listener on an unusual local port", Severity: "HIGH", Classtype: "suspicious-listener", Events: stringSet([]string{"listen"}), Comms: hostInterpreters, LocalPortExclude: commonPublicPorts},
|
||||
{SID: 100073, Message: "Interpreter bound an unusual local port", Severity: "HIGH", Classtype: "suspicious-bind", Events: stringSet([]string{"bind"}), Comms: hostInterpreters, LocalPortExclude: commonPublicPorts},
|
||||
{SID: 100069, Message: "Interpreter wrote to a persistence path", Severity: "HIGH", Classtype: "persistence-write", Events: stringSet([]string{"file_write"}), Comms: hostInterpreters, PathPrefixes: persistencePrefixes},
|
||||
{SID: 100070, Message: "Permissions or ownership changed on a persistence path", Severity: "HIGH", Classtype: "persistence-permission-change", Events: stringSet([]string{"chmod", "chown"}), PathPrefixes: persistencePrefixes},
|
||||
{SID: 100071, Message: "Interpreter requested a root UID transition", Severity: "HIGH", Classtype: "privilege-transition", Events: stringSet([]string{"setuid"}), Comms: hostInterpreters, TargetUIDs: intSet([]int{0})},
|
||||
{SID: 100072, Message: "Interpreter requested a root GID transition", Severity: "HIGH", Classtype: "privilege-transition", Events: stringSet([]string{"setgid"}), Comms: hostInterpreters, TargetGIDs: intSet([]int{0})},
|
||||
{SID: 100076, Message: "Interpreter accepted inbound traffic on an unusual local port", Severity: "HIGH", Classtype: "suspicious-accept", Events: stringSet([]string{"accept"}), Comms: hostInterpreters, PeerPublic: &public, LocalPortExclude: commonPublicPorts},
|
||||
{SID: 100077, Message: "Interpreter requested sensitive Linux capabilities", Severity: "HIGH", Classtype: "capability-escalation", Events: stringSet([]string{"capset"}), Comms: hostInterpreters, CapabilitiesAny: sensitiveCapabilities},
|
||||
{SID: 100078, Message: "Kernel module load requested from a writable runtime path", Severity: "CRITICAL", Classtype: "suspicious-module-load", Events: stringSet([]string{"module_load"}), PathPrefixes: writableRuntimePrefixes},
|
||||
}
|
||||
rules := make([]HostRule, 0, len(specifications))
|
||||
for _, specification := range specifications {
|
||||
rule, err := NewHostRule(specification)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return HostRuleEngine{Rules: rules}
|
||||
}
|
||||
|
||||
func (e HostRuleEngine) Match(event HostEvent) []model.Alert {
|
||||
alerts := make([]model.Alert, 0)
|
||||
for _, rule := range e.Rules {
|
||||
if rule.Matches(event) {
|
||||
alerts = append(alerts, rule.Alert(event))
|
||||
}
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
var hostInterpreterNames = strings.Fields(
|
||||
"sh bash dash zsh ksh ash python python2 python3 perl ruby php lua " +
|
||||
"node nodejs nc ncat netcat socat curl wget fetch")
|
||||
var hostInterpreters = stringSet(hostInterpreterNames)
|
||||
|
||||
// HostInterpreterComms returns the process names used by typed host rules.
|
||||
// Kernel sources use the same list for in-BPF prefiltering.
|
||||
func HostInterpreterComms() []string {
|
||||
return append([]string(nil), hostInterpreterNames...)
|
||||
}
|
||||
|
||||
var commonPublicPorts = intSet([]int{22, 53, 80, 123, 443, 853})
|
||||
var persistencePrefixes = []string{
|
||||
"/etc/cron.d/", "/etc/crontab", "/etc/systemd/system/",
|
||||
"/etc/ld.so.preload", "/root/.ssh/authorized_keys",
|
||||
"/etc/sudoers", "/etc/sudoers.d/",
|
||||
}
|
||||
var writableRuntimePrefixes = []string{"/tmp/", "/var/tmp/", "/dev/shm/", "/run/user/"}
|
||||
var sensitiveCapabilities = stringSet([]string{
|
||||
"CAP_SYS_ADMIN", "CAP_SYS_MODULE", "CAP_SYS_PTRACE",
|
||||
"CAP_DAC_READ_SEARCH", "CAP_NET_ADMIN", "CAP_NET_RAW",
|
||||
})
|
||||
|
||||
func intSet(values []int) map[int]bool {
|
||||
result := make(map[int]bool, len(values))
|
||||
for _, value := range values {
|
||||
result[value] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func pathMatches(path string, prefixes []string) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if path == prefix || strings.HasPrefix(path, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue