Documentation
¶
Overview ¶
Package groundtruth maps kernel-level observations (from an eBPF sensor — specifically Tetragon) into Wardyn's append-only audit vocabulary (types.AuditEvent). It is the SECOND of Wardyn's three advertised audit streams: the tamper-proof "ground-truth" counterpart to the agent's own self-report (the Postgres event log) and the human-watchable PTY replay.
DESIGN (mirrors the proxy -> /internal/decisions -> recordAudit pattern): a host-scoped sidecar (cmd/wardyn-tetragon-ingest) consumes Tetragon kernel events, correlates each to a Wardyn run via the container label `wardyn.run-id`, maps a bounded subset to types.AuditEvent here, and POSTs batches to a new internal endpoint that records them append-only — so they land in Postgres AND fan to every SIEM sink with ZERO new fanout code. Every mapped event is keyed on run_id and discriminated by a `kernel.*` action prefix plus data.stream="ebpf".
DEPENDENCY CHOICE: this package consumes Tetragon's JSON EXPORT stream (line-delimited JSON written to a file/stdout via Tetragon's export feature), NOT the Tetragon gRPC client. Defining the minimal structs we need (below) keeps go.mod light — no github.com/cilium/tetragon dependency — and is honest about exactly which fields we read.
HONESTY: this stream is DETECTION, not prevention. The ld-linux/mmap dynamic-linker bypass of execve hooks is real (the documented egress-veto overclaim lesson) and is surfaced (not hidden) via data.loader=true rather than suppressed; we never claim exec-blocking. Host eBPF is also blind inside CC3/Kata microVM guests — callers must emit a one-time kernel.sensor.blind event for such runs.
This package is target-agnostic: it has zero knowledge of Docker, HTTP, the store, or how runs are correlated beyond the small Correlator interface.
Index ¶
- Constants
- func BlindEvent(runID uuid.UUID, reason string) types.AuditEvent
- func HeartbeatEventWithDropped(droppedTotal, observedTotal uint64) types.AuditEvent
- func IsDynamicLinker(binary string) bool
- func IsSensitivePath(path string) bool
- type Correlation
- type Correlator
- type EventData
- type Mapper
- type Subtype
- type TetragonArg
- type TetragonEvent
- type TetragonFileArg
- type TetragonProcess
- type TetragonProcessExec
- type TetragonProcessKprobe
- type TetragonSockArg
Constants ¶
const ( // ActionProcessExec is a process execve observed by the kernel sensor. ActionProcessExec = "kernel.process.exec" // ActionNetworkConnect is an outbound TCP connect observed by the sensor. ActionNetworkConnect = "kernel.network.connect" // ActionFileWrite is a write to a sensitive path observed by the sensor. ActionFileWrite = "kernel.file.write" // ActionSensorHeartbeat is the periodic liveness beat the ingest sidecar // emits (run_id NULL). /healthz keys ebpf_groundtruth state off the most // recent one within a TTL — so the stream is only "healthy" when events // are actually arriving (the overclaim is structurally impossible). ActionSensorHeartbeat = "kernel.sensor.heartbeat" // ActionSensorBlind is the one-time event emitted for a run the host eBPF // sensor cannot see into (CC3/Kata microVM guest). Blindness is made // VISIBLE rather than silent. ActionSensorBlind = "kernel.sensor.blind" )
Kernel-event action namespace. Every event this stream emits carries one of these as types.AuditEvent.Action. The `kernel.` prefix is the discriminator the control plane enforces on the ground-truth ingest endpoint (any action without it is rejected) and the prefix SIEM rules key on alongside data.stream="ebpf".
const KernelActionPrefix = "kernel."
KernelActionPrefix is the required prefix for every ground-truth action. The control-plane endpoint rejects any submitted action lacking it (fail closed).
const SensorActor = "wardyn-tetragon-ingest"
SensorActor is the fixed audit actor for sensor-originated events. The control plane FORCES actor=SensorActor + actor_type=system on ingest, so a compromised sensor cannot impersonate a human or an agent run.
const Stream = "ebpf"
Stream is the value of data.stream on every event from this stream. SIEM rules and the UI discriminate ground-truth events from agent self-report on (data.stream == StreamEBPF) without parsing the action.
Variables ¶
This section is empty.
Functions ¶
func BlindEvent ¶
func BlindEvent(runID uuid.UUID, reason string) types.AuditEvent
BlindEvent builds the one-time event emitted for a run the host eBPF sensor cannot see into (CC3/Kata microVM guest). It is bound to the run (so the gap is attributable) with a fixed reason. This makes the published host-eBPF-vs- Kata blindness gap VISIBLE in the audit stream rather than a silent absence.
func HeartbeatEventWithDropped ¶
func HeartbeatEventWithDropped(droppedTotal, observedTotal uint64) types.AuditEvent
HeartbeatEventWithDropped builds the periodic sensor liveness beat. run_id is NULL (the sensor is host-scoped, not per-run); actor_type=system, actor=sensor. The control plane records these append-only and /healthz keys the ebpf_groundtruth state off the most recent one within a TTL. droppedTotal is the sensor's cumulative backpressure-drop count and observedTotal the cumulative count of real kernel events mapped off the tail; both are surfaced on /healthz so it can tell "sensor alive but observing nothing" (idle) apart from "events flowing" (healthy) and show the drop-gap size. A live heartbeat ALONE never proves kernel ground truth is arriving.
func IsDynamicLinker ¶
IsDynamicLinker reports whether binary is a dynamic linker / loader. Exec of a loader is the ld-linux/mmap bypass surface: it can run an ELF the execve hook never named. We flag it (data.loader=true) so it is visible; we do NOT claim to prevent it. The check also matches the loader appearing as the FIRST argv token (the common `ld-linux.so ./payload` invocation form).
func IsSensitivePath ¶
IsSensitivePath reports whether a written file path is in the allowlist of paths we record on the file_write subtype. Empty paths are not sensitive.
Types ¶
type Correlation ¶
type Correlation string
Correlation records whether an event was bound to a known Wardyn run.
const ( // CorrelationMapped means the event's container/cgroup resolved to a run. CorrelationMapped Correlation = "mapped" // CorrelationUnmapped means it did not. Such events are NEVER silently // dropped — they are emitted with run_id NULL so the blindness is visible // in the audit stream (an unmapped kernel event near a run is a signal). CorrelationUnmapped Correlation = "unmapped" )
type Correlator ¶
type Correlator interface {
// RunForContainer returns the run id for a container id (any prefix length
// Tetragon emits) and whether it is a Wardyn-managed agent container. ok
// is false for unknown / non-Wardyn containers.
RunForContainer(containerID string) (runID uuid.UUID, ok bool)
}
Correlator resolves a container id and/or kernel cgroup id to a Wardyn run. The ingest sidecar implements this by listing docker containers labelled wardyn.managed=true and indexing them by container id (and, where available, cgroup id). It is the only knowledge this package has about how correlation happens — keeping the mapper target-agnostic and unit-testable.
type EventData ¶
type EventData struct {
// Stream is always Stream ("ebpf").
Stream string `json:"stream"`
// Subtype is the fine-grained kernel-event kind.
Subtype Subtype `json:"subtype"`
// CgroupID is the kernel cgroup id of the observed process (0 if unknown).
CgroupID uint64 `json:"cgroup_id,omitempty"`
// ContainerID is the (truncated) container id the event was attributed to.
ContainerID string `json:"container_id,omitempty"`
// Argv is the full process command line for exec events.
Argv []string `json:"argv,omitempty"`
// Dst is "ip:port" for network_connect events.
Dst string `json:"dst,omitempty"`
// Path is the written file path for file_write events.
Path string `json:"path,omitempty"`
// Loader is true when the exec'd binary is a dynamic linker (ld-linux /
// ld-musl). This is the ld-linux/mmap bypass surfaced honestly: such an
// exec can load+run an arbitrary ELF the execve hook never named, so the
// argv of an ld-linux invocation is the real program. We FLAG it, we do
// not claim to block it.
Loader bool `json:"loader,omitempty"`
// Correlation is "mapped" or "unmapped".
Correlation Correlation `json:"correlation"`
// Reason carries a free-form explanation for sensor.blind / failure cases.
Reason string `json:"reason,omitempty"`
}
EventData is the JSON shape stored in audit_events.data for every event from this stream (audit_events.data is JSONB; this requires NO schema change). It is deliberately small and stable so SIEM rules can rely on it.
type Mapper ¶
type Mapper struct {
// contains filtered or unexported fields
}
Mapper converts a Tetragon JSON event into a Wardyn AuditEvent. It is stateless apart from the injected Correlator and a clock-free design (the ingest sidecar / control plane stamp the time). It is safe for concurrent use when the Correlator is.
func NewMapper ¶
func NewMapper(corr Correlator) *Mapper
NewMapper builds a Mapper over a Correlator. A nil Correlator treats every event as unmapped (run_id NULL, correlation="unmapped") — never a panic, so a mis-wired sidecar degrades to visible-blindness rather than crashing.
func (*Mapper) Map ¶
func (m *Mapper) Map(ev TetragonEvent) (types.AuditEvent, bool)
Map converts ev into an AuditEvent. ok is false when the event kind is one we do not record (so the caller skips it) or when a file_write does not touch a sensitive path (filtered noise). When ok is true the returned event always has Action with the kernel. prefix and Data with stream="ebpf".
Correlation: events that cannot be bound to a run are STILL returned (ok=true) with RunID nil and correlation="unmapped" — blindness must be visible, never a silent drop. The ONLY ok=false outcomes are: unknown event kind, and a non-sensitive file write.
type Subtype ¶
type Subtype string
Subtype is the fine-grained kernel-event kind carried in data.subtype. It is stable, machine-readable, and one-to-one with the action where applicable.
type TetragonArg ¶
type TetragonArg struct {
FileArg *TetragonFileArg `json:"file_arg,omitempty"`
PathArg *TetragonFileArg `json:"path_arg,omitempty"`
StringArg string `json:"string_arg,omitempty"`
SockArg *TetragonSockArg `json:"sock_arg,omitempty"`
}
TetragonArg is one kprobe argument. Tetragon emits typed argument objects; we read the path-bearing shapes (file_arg.path, path_arg.path, string_arg) and the socket shape (sock_arg, a KprobeSock) used for connect detection.
type TetragonEvent ¶
type TetragonEvent struct {
ProcessExec *TetragonProcessExec `json:"process_exec,omitempty"`
ProcessKprobe *TetragonProcessKprobe `json:"process_kprobe,omitempty"`
}
TetragonEvent is one line of the Tetragon JSON export. Exactly one of the event-kind fields is set per line. We map process_exec and process_kprobe; the kprobe handler multiplexes file-writes (security_file_permission / fd_install) and network connects (tcp_connect / security_socket_connect / __sys_connect) by function name + argument shape. Other kinds are ignored by the mapper (returns ok=false).
type TetragonFileArg ¶
type TetragonFileArg struct {
Path string `json:"path,omitempty"`
}
TetragonFileArg carries a path-bearing argument.
type TetragonProcess ¶
type TetragonProcess struct {
Binary string `json:"binary,omitempty"`
Arguments string `json:"arguments,omitempty"`
CgroupID uint64 `json:"cgroup_id,string,omitempty"`
Docker string `json:"docker,omitempty"` // container id (truncated)
}
TetragonProcess is the common process descriptor across event kinds. We read the binary, arguments, and container/cgroup correlation handles.
type TetragonProcessExec ¶
type TetragonProcessExec struct {
Process *TetragonProcess `json:"process,omitempty"`
}
TetragonProcessExec carries a process execve.
type TetragonProcessKprobe ¶
type TetragonProcessKprobe struct {
Process *TetragonProcess `json:"process,omitempty"`
FunctionName string `json:"function_name,omitempty"`
Args []TetragonArg `json:"args,omitempty"`
}
TetragonProcessKprobe carries a kprobe hit — the workhorse event kind for everything other than exec. The FunctionName identifies the hooked kernel function; the Args carry the typed argument objects. We use it for two things:
- sensitive file writes (security_file_permission / __x64_sys_write-style TracingPolicy): the path is in a file_arg / path_arg / string_arg.
- outbound network connects (tcp_connect / security_socket_connect / __sys_connect TracingPolicy): the destination is in a sock_arg.
type TetragonSockArg ¶
type TetragonSockArg struct {
DAddr string `json:"daddr,omitempty"` // destination address
DPort int `json:"dport,omitempty"` // destination port
}
TetragonSockArg is Tetragon's KprobeSock argument, emitted by a connect kprobe. Field names match Tetragon's protojson output. We read only the destination tuple (daddr/dport) used for connect detection.