enodia-sentinal/go-agent/internal/events/pipeline.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)))
}