// SPDX-License-Identifier: GPL-3.0-or-later package events import ( "bufio" "encoding/json" "fmt" "io" "strings" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model" ) type EventKind string const ( ExecKind EventKind = "exec" SyscallKind EventKind = "syscall" HostKind EventKind = "host" ) // RuleSet is the transport-independent destination for every live or replayed // kernel event. A cilium/ebpf source only needs to decode into these records; // it does not need to know rule details or alert schemas. type RuleSet struct { Exec ExecRuleEngine Syscall SyscallRuleEngine Host HostRuleEngine } func NewRuleSet(exec ExecRuleEngine) RuleSet { return RuleSet{ Exec: exec, Syscall: DefaultSyscallRuleEngine(), Host: DefaultHostRuleEngine(), } } func (r RuleSet) MatchJSON(raw []byte) ([]model.Alert, error) { kind, err := eventKind(raw) if err != nil { return nil, err } switch kind { case ExecKind: var event ExecEvent if err := json.Unmarshal(raw, &event); err != nil { return nil, err } return r.Exec.Match(event), nil case SyscallKind: var event SyscallEvent if err := json.Unmarshal(raw, &event); err != nil { return nil, err } return r.Syscall.Match(event), nil case HostKind: var event HostEvent if err := json.Unmarshal(raw, &event); err != nil { return nil, err } return r.Host.Match(event), nil default: return nil, fmt.Errorf("event JSON must be an exec, syscall, or host event") } } func eventKind(raw []byte) (EventKind, error) { var values map[string]any if err := json.Unmarshal(raw, &values); err != nil { return "", err } explicit := strings.ToLower(stringValue(firstValue(values, "event", "type"))) switch explicit { case "exec", "execve": return ExecKind, nil case "syscall", "sys": return SyscallKind, nil case "tcp_connect", "bind", "listen", "accept", "file_write", "chmod", "chown", "setuid", "setgid", "capset", "module_load": return HostKind, nil } if _, ok := values["filename"]; ok { return ExecKind, nil } if _, ok := values["argv"]; ok { return ExecKind, nil } if _, ok := values["parent_comm"]; ok { return ExecKind, nil } if _, ok := values["syscall"]; ok { return SyscallKind, nil } if _, ok := values["args"]; ok { return SyscallKind, nil } return "", fmt.Errorf("event JSON must be an exec, syscall, or host event") } // ScanJSONL feeds non-empty lines to the matcher callback with line-numbered // errors. Scanner capacity is raised for long argv or future module metadata. func ScanJSONL(reader io.Reader, consume func([]byte) error) error { scanner := bufio.NewScanner(reader) scanner.Buffer(make([]byte, 64*1024), 1024*1024) line := 0 for scanner.Scan() { line++ raw := bytesTrimSpace(scanner.Bytes()) if len(raw) == 0 { continue } if err := consume(raw); err != nil { return fmt.Errorf("event stream line %d: %w", line, err) } } return scanner.Err() } func bytesTrimSpace(value []byte) []byte { return []byte(strings.TrimSpace(string(value))) }