groundtruth

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

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

View Source
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".

View Source
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).

View Source
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.

View Source
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

func IsDynamicLinker(binary string) bool

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

func IsSensitivePath(path string) bool

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.

func (*Mapper) MapLine

func (m *Mapper) MapLine(line []byte) (types.AuditEvent, bool)

MapLine parses one JSON line of the Tetragon export and maps it. ok is false on a parse error or an unrecorded/filtered event.

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.

const (
	SubtypeProcessExec    Subtype = "process_exec"
	SubtypeNetworkConnect Subtype = "network_connect"
	SubtypeFileWrite      Subtype = "file_write"
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL