// 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 }