121 lines
3 KiB
Go
121 lines
3 KiB
Go
// 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)))
|
|
}
|