enodia-sentinal/go-agent/internal/events/host_event.go

139 lines
4.1 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
package events
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
// HostEvent is the common typed record for network, filesystem, identity,
// capability, and module activity that is neither exec nor raw syscall data.
type HostEvent struct {
Event string `json:"event"`
PID int `json:"pid"`
PPID int `json:"ppid"`
UID int `json:"uid"`
Comm string `json:"comm"`
ParentComm string `json:"parent_comm"`
Path string `json:"path"`
Argv []string `json:"argv"`
PeerIP string `json:"peer_ip"`
PeerPort int `json:"peer_port"`
LocalIP string `json:"local_ip"`
LocalPort int `json:"local_port"`
TargetUID int `json:"target_uid"`
TargetGID int `json:"target_gid"`
Mode int `json:"mode"`
Capabilities []string `json:"capabilities"`
ModuleName string `json:"module_name"`
}
func (e HostEvent) ArgvString() string {
parts := make([]string, 0, len(e.Argv)+1)
parts = append(parts, e.Path)
parts = append(parts, e.Argv...)
return strings.TrimSpace(strings.Join(parts, " "))
}
// UnmarshalJSON accepts the same compatibility aliases as Python ruleops:
// type/event, remote/bind endpoint names, numeric strings, singular capability,
// and module name. Defaults of -1 for absent target IDs are semantically
// significant because zero means a requested root transition.
func (e *HostEvent) UnmarshalJSON(raw []byte) error {
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
return err
}
e.TargetUID, e.TargetGID = -1, -1
e.Event = stringValue(firstValue(values, "event", "type"))
e.PID = integerValue(values["pid"], 0)
e.PPID = integerValue(values["ppid"], 0)
e.UID = integerValue(values["uid"], 0)
e.Comm = stringValue(values["comm"])
e.ParentComm = stringValue(values["parent_comm"])
e.Path = stringValue(values["path"])
e.Argv = stringSlice(values["argv"])
e.PeerIP = stringValue(firstValue(values, "peer_ip", "remote_ip"))
e.PeerPort = integerValue(firstValue(values, "peer_port", "remote_port"), 0)
e.LocalIP = stringValue(firstValue(values, "local_ip", "bind_ip", "listen_ip"))
e.LocalPort = integerValue(firstValue(values, "local_port", "bind_port", "listen_port"), 0)
e.TargetUID = integerValue(firstValue(values, "target_uid", "uid_target"), -1)
e.TargetGID = integerValue(firstValue(values, "target_gid", "gid_target"), -1)
e.Mode = integerValue(values["mode"], 0)
e.Capabilities = stringSlice(firstValue(values, "capabilities", "caps", "cap_effective"))
if len(e.Capabilities) == 0 {
if capability := stringValue(values["capability"]); capability != "" {
e.Capabilities = []string{capability}
}
}
for index := range e.Capabilities {
e.Capabilities[index] = strings.ToUpper(e.Capabilities[index])
}
e.ModuleName = stringValue(firstValue(values, "module_name", "name"))
return nil
}
func firstValue(values map[string]any, keys ...string) any {
for _, key := range keys {
if value, ok := values[key]; ok && value != nil && stringValue(value) != "" {
return value
}
}
return nil
}
func stringValue(value any) string {
if value == nil {
return ""
}
if text, ok := value.(string); ok {
return text
}
return fmt.Sprint(value)
}
func integerValue(value any, fallback int) int {
if value == nil {
return fallback
}
switch typed := value.(type) {
case float64:
return int(typed)
case int:
return typed
case string:
text := strings.TrimSpace(strings.ToLower(typed))
if strings.HasPrefix(text, "0o") {
parsed, err := strconv.ParseInt(text[2:], 8, 64)
if err == nil {
return int(parsed)
}
}
parsed, err := strconv.ParseInt(text, 0, 64)
if err == nil {
return int(parsed)
}
}
return fallback
}
func stringSlice(value any) []string {
if text, ok := value.(string); ok {
if text == "" {
return []string{}
}
return []string{text}
}
items, ok := value.([]any)
if !ok {
return []string{}
}
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, stringValue(item))
}
return result
}