Skip to content

Latest commit

 

History

History
377 lines (325 loc) · 61.1 KB

File metadata and controls

377 lines (325 loc) · 61.1 KB

Changelog

All notable changes to AegisBPF will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Added — Prometheus metrics endpoint

  • Opt-in HTTP /metrics endpoint (AEGIS_METRICS_ADDR=<host:port>, src/metrics_server.{hpp,cpp}, src/daemon.cpp wiring, docs/METRICS.md) — serves the agent's Prometheus exposition over HTTP so Prometheus / kube-prometheus can scrape it directly, reusing the daemon's already-loaded BPF state (no per-scrape reload). Off by default; routes GET /metrics and GET /healthz. Binds loopback by default; bind :9635 to expose and restrict with a firewall / NetworkPolicy (no auth, standard for a scrape target). The Prometheus builder was refactored out of the metrics CLI command into a shared build_metrics_report(BpfState&, bool) so the CLI, the textfile collector, and the HTTP endpoint all emit identical output. A new aegisbpf_deny_ttl_entries gauge exposes the count of control-API denies awaiting TTL expiry.
  • node_exporter textfile-collector units (packaging/systemd/aegisbpf-metrics.{service,timer}) — the no-open-port alternative: a 30 s timer writes the exposition atomically to ${AEGIS_METRICS_TEXTFILE} for node_exporter to serve.
  • New GTest suite tests/test_metrics_server.cpp (bind-addr parsing, real loopback round-trip for /metrics /healthz 404, no-callback 503, non-GET 405).

[0.10.0] - 2026-08-11

Ecosystem integration and programmatic enforcement: AegisBPF now plugs into the detection/response and SIEM stack. Highlights since v0.9.0 — a root-only node control API, a Falco → AegisBPF enforcement adapter, an OCSF → SIEM/data-lake pipeline, and TTL auto-expiry so automated response can't wedge a deny forever.

Added — Auto-expiry (TTL) for dynamic denies

  • Timed denies over the control API (src/ttl_registry.{hpp,cpp}, src/daemon.cpp reaper wiring, tests/test_ttl_registry.cpp, docs/CONTROL_API.md) — any add verb now accepts an optional trailing ttl=<seconds> token (POST /block/add /p ttl=300), and the deny is removed automatically when it expires. Closes the standing safety gap in automated response: a transient signal (e.g. a Falco detection relayed by aegis-responder) can no longer wedge a path or IP permanently. Timed denies are persisted to /var/lib/aegisbpf/deny_ttl.db and reaped by a dedicated thread (5 s granularity) that re-issues the same del command the CLI uses; expiry is wall-clock so it survives a daemon restart. Re-adding with a fresh TTL extends it, re-adding with no TTL makes it permanent, and del/clear drop the timer. The aegis-responder config gains a per-rule ttl_seconds that is forwarded as the ttl= token. Parse/partition/persistence are a kernel-free module with an 11-case GTest suite; the responder's Go tests cover TTL passthrough and the wire-format token.

Added — Node control API

  • Root-only Unix-socket control API (AEGIS_API_SOCKET=<path> on run, src/socket_api.{hpp,cpp}, src/daemon.cpp wiring, tests/test_socket_api.cpp, docs/CONTROL_API.md) — lets a co-located process drive AegisBPF enforcement programmatically instead of shelling out to the CLI. Opt-in (off by default); the pre-existing (never-wired) read-only socket server is now instantiated by the daemon and extended with control verbs: POST /block/add|del|clear, POST /network/deny/ip|cidr. Control ops are authorized by SO_PEERCRED (peer uid must equal control_uid, default 0) on top of the 0600 socket, and reuse the same cmd_block_* / cmd_network_deny_* code paths as the CLI, so a POST /block/add takes effect on the running daemon's pinned maps immediately. Verified end-to-end (driving POST /block/add over the socket made the target file -EPERM on the next read) plus a 5-case GTest suite (health, verb/arg routing incl. spaces in paths, control-disabled-without-handler, unauthorized peer-uid rejection, unknown-verb). This is the foundation for the Falco Talon / Falcosidekick response-engine integration — detect elsewhere, enforce here.

Added — Falco integration

  • aegis-responder: Falco → AegisBPF enforcement adapter (integrations/falco/aegis-responder/, .github/workflows/falco-responder.yml) — a stdlib-only Go webhook responder that turns Falco detections into AegisBPF kernel enforcement via the node-local control API. Falcosidekick posts alerts to it; it maps configured rules to POST /block/add / POST /network/deny/* on the agent's control socket, installing a race-free in-kernel -EPERM on the node where the alert fired. Safe by default: an explicit rule allowlist (only named Falco rules act), a min_priority gate, and a dry_run mode. Ships a config schema + example, a DaemonSet/Service manifest, a distroless Dockerfile, Go unit tests (rule→verb mapping, priority gate, socket-protocol round-trip), and its own CI (gofmt/vet/test/build). Verified end-to-end: a Falco webhook for "Write below binary dir" drove POST /block/add <path> and the target file became -EPERM on the next read. Pairs Falco (CNCF-graduated detection) with AegisBPF enforcement; the standalone equivalent of a Falco Talon actionner. Hardened at the trust boundary: every enforcement target is shape-validated (absolute path / parseable IP / CIDR) with control-character (\n\r\x00) rejection to block control-protocol injection from Falco fields, an optional X-Aegis-Token shared-secret (constant-time compare), and a 64 KiB request-body cap.

Added — Integrations

  • OCSF → SIEM / data-lake pipeline (integrations/vector/aegisbpf-ocsf.yaml, integrations/vector/README.md, integrations/vector/sample-ocsf-event.json) — a ready Vector pipeline that ships AegisBPF's native OCSF 1.1.0 output to any OCSF consumer (AWS Security Lake, Splunk HEC, Microsoft Sentinel, or generic OCSF/OTLP-HTTP). Vector remap transforms enrich each event with cloud/region, Kubernetes pod/namespace/node (Downward API), OCSF observables[], and a process-lineage correlation_uid — the context the kernel agent can't know — then fan out to env-gated sinks. Includes a live-captured File Activity (1001) sample event verifying all OCSF/Security-Lake required fields are present and AegisBPF forensics (inode/dev/cgroup/exec-lineage) ride under OCSF unmapped. No agent code change — the "ready now" data integration.

Changed — CI / Dependencies

  • Held the untested major bumps of the two release.yml signing-path actionssigstore/cosign-installer (pinned back to v3 from Dependabot's v4.1.2 bump) and actions/attest-build-provenance (kept at v2, holding the proposed v4). release.yml is tag-triggered and gated on the (currently offline) self-hosted signing runners, so these can't run in normal push CI, and their major bumps can change signing/attestation behavior (cosign sign-blob / cosign sign / provenance). Both are held via a Dependabot ignore for their semver-major updates until they can be validated in a real release run — remove an entry to re-propose that major. All other Dependabot action bumps were merged (exercised green in push CI, or dormant); actions/setup-go v7 and a red actions/checkout 7.0.1 were left open pending, not merged.

Added — Soak Evidence

  • 168-hour (7-day) enforce-mode soak on v0.9.0 (scripts/soak_laptop_168h.sh, scripts/soak_status.sh, evidence/soak-168h-laptop/) — a full week of continuous BPF-LSM enforcement on real hardware (kernel 6.17). An in-band enforcement canary was checked every poll: 0 misses / 57,310 checks over 7.13 billion enforcement decisions, with +784 KB RSS growth across the whole run (no leak), zero crashes, and no suspend interruptions. Telemetry drops (~21.7% under the tight-loop workload) are expected and decoupled from enforcement — the canary proves dropping telemetry never drops a decision. Closes the long-standing "168 h soak evidence not yet published" gap.

Added — Hardening

  • Pin auto-heal watchdog (AEGIS_PIN_HEAL, default 1 whenever AEGIS_ENFORCE_PIN_LINKS=1, src/bpf_link_pin.{hpp,cpp} heal_pinned_hooks(), src/bpf_ops.hpp enable_pin_heal + pin_heal_{attempts,successes,failures}, src/daemon_runtime.cpp heartbeat dispatch, src/daemon.cpp startup gate, tests/test_bpf_link_pin.cpp HealPinnedHooks*) — upgrade of the pinned-link fail-safe watchdog from warn-only to self-healing, closing the "auto re-attach intentionally deferred" follow-up noted in the parent feature. When a heartbeat tick observes a missing pin (operator rm, bpftool link detach, bpffs unmount/remount), the watchdog re-issues bpf_link__pin() on the still-live userspace bpf_link* (owned by BpfState::links, never destroyed for the daemon's lifetime), restoring the bpffs entry in-place. No kernel attach syscall is invoked — the kernel link object survived the missing-pin window because userspace held an fd to it; only the bpffs path needs rewriting. This is the intended fail-safe semantics (link object stays alive while any reference holds it: bpffs ref or userspace fd ref), and it side-steps the integration-test gap that motivated the deferral in the parent PR (no need for a kernel-equipped runner to validate a fresh bpf_program__attach() call in the heartbeat path). AEGIS_PIN_HEAL=0 falls back to the warn-only verify behaviour from the parent PR (useful while debugging a flaky bpffs). Counters pin_heal_{attempts,successes,failures} are aggregated across the daemon's lifetime and surfaced via INFO logs on every successful heal so a SIEM can alert on "heal rate non-zero" as drift-detection. An orphan-pin path (entry with link == nullptr, e.g. a future "adopt pins from previous run on cold start" feature) is explicitly handled: it counts as still-missing and emits an ERROR Pinned LSM hook missing and link handle unavailable — cannot heal without incrementing pin_heal_attempts, so the stats remain a faithful measure of "active heal effort" rather than "drift surface area". The heartbeat structured log now carries heal_enabled=<bool> alongside missing_count / total_pinned, so an operator alert rule can branch on which mode the daemon is in. New 3-test GTest extension to the BpfLinkPin.* suite: HealPinnedHooksReportsZeroOnHealthy (no-op fast path, no log spam, no counter movement), HealPinnedHooksReportsMissingWhenLinkIsNull (orphan-pin path emits ERROR but does not count an attempt), HealPinnedHooksLeavesHealthyEntriesUntouched (mixed-vector partition test). The 15 parent-PR tests pass byte-identically. Out of scope (deferred to follow-up PRs): full cold-start re-attach when the daemon comes up and finds existing pins with no userspace bpf_link* handle (would need bpf_program__attach() + a kernel-equipped CI runner), per-hook heal-failure cooldown (current heartbeat re-tries every tick, which is the right behaviour while we have no telemetry on permanent vs transient failures), Prometheus exposure of pin_heal_* counters (the daemon does not yet expose its own HTTP metrics endpoint; surfaced via structured logs today).
  • Pinned-link daemon-crash fail-safe (AEGIS_ENFORCE_PIN_LINKS=1, optional AEGIS_PIN_ROOT=/sys/fs/bpf/aegisbpf, src/bpf_link_pin.{hpp,cpp}, src/bpf_ops.hpp PinnedHook + enforce_pin_links / pin_root / pinned_hooks, src/bpf_attach.cpp attach_prog hook, src/daemon_runtime.cpp heartbeat verify tick, src/daemon.cpp startup gate, tests/test_bpf_link_pin.cpp) — Datadog Workload Protection lesson #1 ("hooks that silently fail … minimum required programs don't load") and the Cloudflare-style "LSM filter gets pinned on start, this means that it will remain active even if the userspace component gets killed" pattern, applied to AegisBPF. Off by default; operators opt in via env var so existing deployments see no behaviour change. When enabled, every successful attach in attach_prog() is immediately followed by bpf_link__pin(link, "<pin_root>/<program_name>") so the kernel link object holds an independent bpffs ref — closing the userspace fd (via OOM-kill, segfault, systemctl stop, sysadmin SIGKILL -9, etc.) no longer detaches the LSM hook. Enforcement survives daemon crash until a sysadmin explicitly bpftool link detaches or rms the pin. Startup is fail-loud: if AEGIS_ENFORCE_PIN_LINKS=1 is set but /sys/fs/bpf is not a mounted bpffs (statfs(2).f_type != BPF_FS_MAGIC) the daemon refuses to start with a remediation message (mount -t bpf bpf /sys/fs/bpf) rather than silently running unpinned. The pin root is mkdir(0700)-ed before first use. The heartbeat thread additionally runs a read-only verify watchdog every tick: each state.pinned_hooks entry is stat()-ed, and a structured ERROR Pinned LSM hooks missing — daemon-crash fail-safe degraded {missing_count, total_pinned} is emitted if any pin has disappeared (kernel module reload, operator rm, bpftool link detach); auto re-attach is intentionally deferred to a follow-up PR so this timer never invokes kernel attach syscalls without a dedicated integration test on a kernel-equipped runner. Program-name validation in pin_attached_link() rejects empty names, names containing / or NUL, and .. traversal so a buggy or hostile BPF object can never escape pin_root — defense in depth on top of libbpf's own C-identifier constraint on program names. New 14-test GTest suite (BpfLinkPin.*) exercises every free function in bpf_link_pin.cpp without needing root or a real bpf_link: bpffs probe against /tmp and missing paths (must be false), ensure_pin_root create / idempotent / fails on regular-file collision, count_existing_pins zero on empty, matches file count, ignores ./.., zero on missing dir, pin_attached_link input validation (null link, empty name, ../etc/passwd traversal, handle/execve slash), verify_pinned_hooks zero on healthy and counts missing correctly. Out of scope (deferred to follow-up PRs): auto re-attach of missing pins from the watchdog, an AEGIS_UNPIN_ON_EXIT env var for clean operator-driven shutdown, stale-pin recovery on startup when /sys/fs/bpf/aegisbpf/ already has pins from a previously-crashed daemon (operator must rm -rf today), and a Helm values.yaml toggle for one-command enable on Kubernetes deployments. The pin-survives-cleanup property is documented in cleanup_bpf(): bpf_link__destroy() only closes our fd, the kernel link object stays alive as long as bpffs holds the ref — exactly the fail-safe guarantee we want. Verified with grep bpf_link__pin src/ (zero matches before this PR, three matches after, all in bpf_link_pin.cpp).

Added — Event Output

  • ArcSight Common Event Format (CEF) event format (--event-format=cef, src/cef_formatter.{hpp,cpp}, tests/test_cef_formatter.cpp) — second downstream-parser-friendly format alongside OCSF, partial close of the §3.3 CEF Roadmap row in docs/POSITIONING.md. Opt-in single-line CEF records for the same two highest-volume event classes OCSF already covers: BlockEvent → signature aegis:file:open, NetBlockEvent → signature aegis:net:{connect,bind,listen,accept,send,recv}. Header layout follows the ArcSight Implementation Standard exactly: CEF:0|AegisBPF Project|AegisBPF|<version>|<sigID>|<name>|<severity>|<extension> with \ and | escaped in header fields, \ and = escaped in extension values, and CR/LF escaped everywhere so a record is single-line by construction (regression-tested by RecordIsSingleLineNoEmbeddedNewlines). Severity remaps the audit-vs-enforce split into CEF's 0-10 scale: audit-only is 4 (Medium) and enforce (BLOCK / TERM / KILL) is 8 (High). Extension uses standard ArcSight dictionary keys where they exist (act, outcome, msg, dvchost, spid, sproc, fname, filePath, proto, src/dst/spt/dpt, externalId, rt) and surfaces AegisBPF-specific forensic context under custom slots with explicit labels (cs1=cgroup_path + cs1Label=AegisCgroupPath, cs2=parent_exec_id + cs2Label=AegisParentExecId, cs3=rule_type + cs3Label=AegisRuleType for net only, cs4=event_type + cs4Label=AegisEventType for net only, cn1=cgid, cn2=inode for file / cn2=direction for net, cn3=device for file). Endpoint orientation matches the OCSF formatter: egress/send put the peer in dst/dpt, accept/recv put the peer in src/spt, and bind/listen surface only the local port as dpt (no remote peer yet). 0.0.0.0 and :: are suppressed from src/dst so a wildcard bind doesn't pollute SIEM dashboards with bogus indicators. CLI flag now accepts aegis|ocsf|cef (also CEF, cef-1.0); the format dispatch is global and orthogonal to the sink (--log=stdout|journald|both) — journald path stores the CEF payload in MESSAGE= while keeping the existing AEGIS_* field set on the journal entry, so an operator can swap formats without touching the field-based filters. New 14-test GTest suite (CefFormatterTest.*) covers keyword recognition, set_event_format dispatch, canonical header layout (exactly seven unescaped pipes), required dictionary keys, audit-vs-enforce severity + name flip, resolved-path preference, escape correctness for =/\ in extension values and |/\ in header fields, all six direction codes (egress→dst, accept→src, bind→dpt-only with 0.0.0.0 suppression), audit severity demotion, UDP send mapping, IPv6 address preservation through the formatter, and the single-line invariant under embedded-newline input. Out of scope (still emitted in AegisBPF-native shape today and deferred to a follow-up PR once OCSF Process Activity #140 lands): CEF for ExecEvent, ForensicEvent, KernelBlockEvent, OverlayCopyUpEvent, state_change, control_change. CEF severity intentionally compresses the OCSF severity ladder (Informational/Low/High → Medium/High) because CEF parsers historically alert at sev≥7; emitting audit at sev=2 would silently drop those records from default ArcSight / Splunk Enterprise Security correlation rules.

Added — Community Rule Library

  • 25 audited, MITRE-tagged rule packs (rules/*/*.conf + sibling README.md per pack, top-level rules/README.md, .github/workflows/rule-library.yml) — partial close of docs/POSITIONING.md §4.5 #22 ("No community rule library"). Every pack ships an INI policy file in the format aegisbpf policy validate accepts, plus a README documenting the threat model, MITRE ATT&CK / CIS Benchmark coverage, false-positive vectors, and the exact aegisbpf policy apply invocation. The packs deliberately use only the documented INI keywords (parser rejects wildcards) and are designed to be loaded individually or composed by an operator-curated top-level policy. See rules/README.md for the current pack inventory and coverage. The new rule-library CI workflow runs on every PR that touches rules/, the policy parser, or itself: builds aegisbpf with SKIP_BPF_BUILD=ON (parser-only, no root needed), runs aegisbpf policy validate on every shipped .conf, fails the PR if any pack fails to parse, and additionally enforces a structural contract that every rules/<pack>/ directory carries a README.md and at least one .conf file. Provenance discipline: hash-based rules (deny_binary_hash) are intentionally absent from the starter packs because the project does not publish hashes of malware binaries it has not directly verified — the per-pack READMEs document the extension recipe so operators can layer in hashes from their own threat-intel feeds. Out of scope (deferred to follow-up): extracting these in-tree packs into a standalone aegisbpf/rules repo (the in-tree home lets us iterate on the contribution flow against real CI before splitting), wildcard / glob support in the parser (would require BPF-side trie work), reverse-shell exec packs (path-only matching is unreliable for shell binaries; requires future deny_argv support).

Added — Distro Packaging

  • Installable .deb and .rpm artefacts via CPack (CMakeLists.txt CPACK_DEBIAN_* / CPACK_RPM_* blocks, packaging/maintainer-scripts/{postinst,prerm,postrm}, .github/workflows/packaging.yml, docs/PACKAGING.md) — partial close of docs/POSITIONING.md §4.3 #11 ("No distro packages") and the matching Phase 1 GA exit-criteria bullet ("Ubuntu PPA, Fedora COPR, OpenSUSE OBS, Arch AUR packages"). Every release now ships aegisbpf_<ver>_<arch>.deb and aegisbpf-<ver>-<rel>.<arch>.rpm, both produced from the same cmake --build && cpack -G {DEB,RPM} source-of-truth. The Debian and RPM metadata reuse a single set of three maintainer scripts (packaging/maintainer-scripts/{postinst,prerm,postrm}) that branch on the first arg to honour both ABIs (Debian: configure/remove/purge/...; RPM: 1 for install, 2 for upgrade, 0 for final removal) — a fix to one ABI cannot silently drift from the other. The scripts honour distro convention on auto-enable (Debian: deb-systemd-helper enable matching dh_installsystemd default; Fedora: do not auto-enable per Fedora packaging guidelines), all systemd interaction is conditional on /run/systemd/system existing so install on non-systemd hosts (containers, chroots, OpenRC) succeeds cleanly. The new packaging CI workflow runs on every PR that touches CMakeLists.txt, packaging/**, or itself: builds the binary + BPF object on ubuntu-24.04, runs cpack -G DEB && cpack -G RPM, enforces a required-files contract (binary + BPF object + sha256 sidecar + systemd unit + /etc/default/aegisbpf conffile + /etc/aegisbpf/policy.example must all be present, else fail), verifies RPM scriptlets are wired (%post/%preun/%postun), then spins up a clean container per matrix entry (debian:12, ubuntu:24.04, fedora:40, rockylinux:9), runs dpkg -i / rpm -ivh, asserts aegisbpf --version returns 0, and exercises the remove and purge paths (purge must clean /var/lib/aegisbpf; remove must preserve /etc/default/aegisbpf as a Debian conffile / RPM %config(noreplace)). The aegisbpf-packages artefact (14-day retention) is uploaded so a release engineer can dput / copr-cli from the same bytes the smoke test verified. Out of scope (deferred to follow-up PRs): hosted-repo upload (Launchpad PPA, COPR, OBS, AUR — all require maintainer-account credentials), a debian/ source tree for proper PPA sponsorship (Launchpad insists on source builds, not binary upload), and a hand-written aegisbpf.spec for Fedora official-repo sponsorship. docs/PACKAGING.md documents the full maintainer workflow including the §5.1–§5.4 hosted-repo recipes.

Added — Soak Evidence

  • Laptop 24 h soak wrapper + aborted-run evidence pack (scripts/soak_laptop_24h.sh, evidence/soak-24h-laptop/) — partial close of docs/POSITIONING.md §4.4 #16 ("168 h soak evidence not yet published") and the matching Phase 1 GA bullet ("Fix soak harness bug … publish 168 h bare-metal soak evidence"). The harness disk-fill bug itself was fixed in scripts/soak_reliability.sh (commit 813a68c: cap+rotate daemon.log at 100 MiB, disk-free pre-flight at 2 GiB, in-loop watchdog that aborts the run rather than fill the root filesystem); this PR publishes the laptop wrapper that exercised the bug and the evidence captured before it was patched, so the failure mode is reproducible from the repo. scripts/soak_laptop_24h.sh runs the underlying reliability harness with i9-13900H-class env defaults (16 workers, audit mode, UDP workload on, 24 h duration, 128 MiB RSS budget, 0.1 % drop-ratio cap) under a tmux/systemd-inhibit shell so a 24 h run survives lid-close / idle suspend on a workstation. evidence/soak-24h-laptop/NOTES.md documents the original ~14.5 h aborted run on feat/event-dedup-window precursor: RSS stayed flat (50 804 kB → 49 956 kB; well inside noise) across 16 workers + UDP workload, no daemon crashes, systemd-inhibit and performance governor held throughout — i.e. the failure was harness-side disk capture, not an AegisBPF defect. Bundled host snapshot (kernel.txt, lsm.txt, cpu.txt, os-release.txt, memory-start.txt, original-governor.txt, commit.txt, start_utc.txt) plus the truncated soak.log and tmux-final.txt proves the run actually happened on the recorded host. Out of scope (deferred to a follow-up): the actual 168 h bare-metal soak run that requires 7 contiguous days of dedicated machine time on a host with adequate disk + thermal headroom; the evidence layout in evidence/soak-24h-laptop/ is shaped so the 168 h run can drop into a sibling evidence/soak-168h-baremetal/ directory with the same file set.

Added — Operator Tooling

  • Bounded time-window event dedup extended to NetBlockEvent (src/events.{hpp,cpp} configure_net_block_event_dedup + net_block_event_deduper() + net_block_event_dedup_key(), print_net_block_event integration, src/cli_run.cpp shared-flag wiring, tests/test_net_block_event_dedup.cpp) — closes the NetBlockEvent follow-up the original BlockEvent dedup PR explicitly listed as deferred. Uses the same --event-dedup-window-ms=N --event-dedup-max-entries=N flags an operator already configures for file-block events; the two dedupers maintain independent state but operators do not need a second pair of knobs (they think in terms of "duplicate suppression window", not per-class windows). The key includes a per-event-class tag (kNetBlockEventTag = 2, distinct from the file-block tag 1) so collisions across the two domains are mathematically impossible. The hash mixes (cgid, pid|direction|protocol|family, addr ^ ((remote_port << 32) | local_port)) so semantically distinct events never collapse: each of the six directions (egress / bind / listen / accept / send / recv) is its own key, IPv4 vs IPv6 are distinct, TCP vs UDP are distinct, and IPv6 destinations differing only in the upper 64 bits (e.g. ::1 vs ::2 after the XOR-fold) stay distinct — earlier draft used (addr_slot << 32) which silently aliased those; the regression contract is locked in by DistinctRemoteIPv6DoNotCollapse. The first emit after window expiry carries "suppressed_during_prior_window": N in the Aegis-native JSON shape (mirrors the BlockEvent field exactly); OCSF Network Activity payloads still suppress correctly but do not surface the count, since OCSF 1.1 has no analog field and we will not invent one off-spec. Disabled by default; existing deployments see no behaviour change unless they set --event-dedup-window-ms > 0. New 12-test GTest suite (NetBlockDedupTest) pins the contract end-to-end through print_net_block_event (stdout-capture): disabled-by-default emits every event, enabled suppresses true duplicates, all six directions stay distinct, distinct (protocol / family / cgid / pid / remote_ipv4 / remote_ipv6 / remote_port / local_port) tuples never collapse, and reconfiguring back to window_ms=0 restores the disabled contract. Resurface-after-expiry behaviour is covered by the existing EventDeduper unit tests with synthetic time. Out of scope (still deferred): ExecEvent, ForensicEvent, KernelBlockEvent, OverlayCopyUpEvent.
  • Bounded time-window event dedup for BlockEvent (--event-dedup-window-ms=N --event-dedup-max-entries=N, env vars AEGIS_EVENT_DEDUP_WINDOW_MS / AEGIS_EVENT_DEDUP_MAX_ENTRIES, src/event_dedup.{hpp,cpp}, src/events.{hpp,cpp} configure_block_event_dedup + integration in print_block_event, tests/test_event_dedup.cpp, docs/EVENT_LOSS_AND_BACKPRESSURE.md §Bounded time-window event dedup) — partial close of docs/POSITIONING.md §4.2 #8 ("No event dedup / aggregation on the agent"). Disabled by default (--event-dedup-window-ms=0); existing deployments see no behaviour change unless they opt in. When enabled, identical block events keyed on (event_class, cgid, ino, pid, dev) inside the active window are coalesced — the kernel still returns -EPERM on every duplicate so enforcement is unaffected; only the userspace log line is suppressed. The first emit after window expiry carries a "suppressed_during_prior_window": N field in the Aegis-native JSON so the prior-window count is always reported on the next emit, never silently dropped. Capacity is bounded at --event-dedup-max-entries (default 4096); when full, the entry with the oldest first-seen timestamp is evicted and an evictions() counter increments so under-sized tables are observable, not silent. The deduper is owned by the ringbuf consumer thread (no locking), and the timer uses CLOCK_MONOTONIC so wall-clock skew cannot extend or collapse a window. Hash uses a deterministic FNV-1a-style mix over (event_type_tag, cgid, ino, pid<<32|dev) — collisions are bounded for non-adversarial inputs (the daemon is the trusted producer of these keys; they are derived from kernel-side fields). New 10-test GTest suite (tests/test_event_dedup.cpp) pins the contract: default-constructed deduper is disabled, zero-window/zero-capacity remain disabled even if the other knob is set, first sighting always emits with suppressed=0, duplicates inside the window are suppressed and counted, the next emit after window expiry surfaces the accumulated count and resets, distinct keys are independent, eviction at full capacity preserves the eviction counter and re-promotes the evicted key cleanly on next sighting, the hash is order-sensitive ((1,2,3,4) ≠ (1,3,2,4)), and clock-skew style non-monotonic timestamps do not underflow. CLI startup logs a structured INFO Block-event dedup enabled {window_ms, max_entries} only when actually enabled, so an operator's journald pipeline can assert on the line. Out of scope (deferred to follow-ups): NetBlockEvent, ExecEvent, ForensicEvent, KernelBlockEvent, OverlayCopyUpEvent, OCSF payload augmentation (OCSF suppression still works, but the prior-window count appears only in the Aegis-native JSON shape today), and a Prometheus userspace counter (the daemon does not expose its own HTTP metrics endpoint; the aegisbpf metrics CLI reads BPF maps and a userspace counter would not be visible to it without a separate IPC mechanism).
  • aegisbpf simulate now replays net_*_block events (src/commands_explain.{hpp,cpp} NetExplainEvent / NetExplainResult / parse_net_explain_event / evaluate_net_event_against_policy, src/commands_simulate.{hpp,cpp} SimulateNetRecord and dispatch in simulate_one_event, tests/test_commands_simulate.cpp) — extends the dry-run replay so an audit-mode JSONL stream that mixes file block events and network net_connect_block / net_bind_block / net_listen_block / net_accept_block / net_sendmsg_block / net_recvmsg_block events can be evaluated end-to-end against a candidate policy in one pass. The new evaluator mirrors the BPF runtime's match precedence exactly: allow_cgroup (early-return, parallel to is_cgroup_allowed() in every BPF network hook) → deny_ip_port (exact remote IP:port tuple) → deny_ip (exact remote IP) → deny_cidr (LPM range, IPv4 and IPv6) → deny_port (port + protocol + direction). Direction-aware port matching mirrors port_rule_matches(): egress-class events (egress / send / recv) check remote_port with rule direction 0; bind-class events (bind / listen / accept) check local_port with rule direction 1; rule.direction == 2 (both) and rule.protocol == 0 (any) are wildcard fallbacks. Protocol strings (tcp → 6, udp → 17) are normalized; numeric protocols and unknowns degrade to wildcard rather than silent mis-classification. CIDR matching is implemented in pure userspace (ipv4_in_cidr / ipv6_in_cidr) by reusing parse_cidr_v4 / parse_cidr_v6 from network_ops.cpp so test correctness depends only on standard inet_pton. SimulateSummary gains a parallel set of network counters with the same partition invariant the file-event counters guarantee: net_would_block + net_would_allow + net_no_match == net_block_events, with net_would_block further broken down into net_would_block_ip / net_would_block_cidr / net_would_block_port / net_would_block_ip_port. The two invariants hold simultaneously on a mixed stream, so operators can reason about file and network drift independently. --per-event adds a separate net_events[] array (and a "Per-event detail (network)" text section) with the original action, original rule_type, simulated rule, and the four raw match flags so an operator can drill into surprising verdicts. New 14-test GTest suite (SimulateNetEvent.*) covers IPv4 exact deny, IPv6 exact deny, IPv4 CIDR contained / outside-range, IPv6 CIDR contained, egress port match on remote port, bind port match on local port, both-direction rule against egress event, direction-mismatch must NOT block, exact IpPortRule tuple match, allow_cgroup overrides network deny, no-match counted, mixed file+network stream where each partition invariant holds, and any-protocol rule matching a UDP event (mirrors the BPF (port, 0, dir) fallback). Cgroup-scoped network deny rules (policy.cgroup.deny_ips / deny_ports) are intentionally out of scope for this PR and documented as such in evaluate_net_event_against_policy()'s contract; the v1 file-event evaluator's contract that protect_* flags are not consulted is unchanged. The pre-existing 10 file-event tests still pass byte-identically, including the partition invariant for block_events.
  • aegisbpf simulate — policy dry-run / would-break report (src/commands_simulate.{hpp,cpp}, src/cli_dispatch.cpp, tests/test_commands_simulate.cpp) — closes Honest Limitation #10 in docs/POSITIONING.md §4.2 ("No policy simulation / dry-run diffing"). Replays an audit-mode JSONL event stream against a candidate enforce policy and reports what would change without touching BPF or any pinned maps. Pure userspace; safe to run from a developer laptop, an admission-controller pod, or CI. Usage: aegisbpf simulate <events.jsonl>|- --policy <candidate.conf> [--per-event] [--json]. Reads - from stdin so it composes with journalctl -o cat -u aegisbpfd | aegisbpf simulate - --policy …. Reuses the live agent's allow/deny precedence (allow_cgroup → deny_inode → deny_path → no_policy_match) by extracting a pure evaluate_event_against_policy() helper from commands_explain.cpp so simulator verdicts can never silently drift from the daemon's actual decisions. Output partitions every parsed block event into exactly one of would_block (further broken down into would_block_inode / would_block_path), would_allow, no_match. Also reports skipped_non_json (lines that didn't start with {), skipped_non_block (other event types), and parse_errors (JSON missing the required type field). --per-event adds a events[] array with the original action, simulated rule, and the three raw match flags so operators can drill into surprising verdicts. New 10-test GTest suite in tests/test_commands_simulate.cpp covers deny-path matching, allow-cgroup-path override of deny-path, allow-cgroup-id numeric matching, no-match counting, resolved_path fallback when the raw path misses, non-block-event skipping, non-JSON / empty-line handling, missing-type parse-error detection, and the partition invariant would_block + would_allow + no_match == block_events. The pre-existing cmd_explain rule-match logic is now a thin wrapper around the same helper, so the regression risk is symmetric: a refactor that breaks simulate would also fail explain and vice versa. Updates the CLI usage string in src/cli_common.cpp.

Fixed — Optional LSM Hook Attachment

  • All optional LSM programs were silently disabled on every supported kernel (src/bpf_ops.cpp detect_missing_optional_lsm_hooks, bpf/aegis_exec.bpf.h). The capability detector looked up bare hook names (bprm_check_security, file_mmap, socket_connect, socket_bind, socket_listen, socket_accept, socket_sendmsg) in vmlinux BTF as BTF_KIND_FUNC, but those names appear only as struct members of the LSM hooks list — the actual BPF-LSM trampoline FUNC entries are bpf_lsm_<hook>. Every lookup returned -ENOENT, so bpf_program__set_autoload(false) was called on each optional program. Subsequent attach attempts then failed with libbpf: prog 'handle_bprm_check_security': can't attach before loaded (and the same for handle_file_mmap); the daemon logged a single WARN per hook and continued, leaving exec-identity verification, runtime-deps trust, and the entire network blocking path unattached even though lsm_enabled=true was reported. Two compounding bugs are fixed: (1) the BTF lookup now uses the bpf_lsm_<hook> symbol via a per-hook catalog mirroring src/hook_capabilities.cpp, and the catalog now also includes socket_recvmsg and inode_copy_up that were previously omitted; (2) bpf/aegis_exec.bpf.h now uses SEC("lsm/mmap_file") (the kernel hook was renamed from file_mmap pre-5.6) so the trampoline name matches bpf_lsm_mmap_file. Verified end-to-end on Linux 6.17: aegisbpf capabilities --json now reports runtime_deps_hook_attached: true and hooks.lsm_{bprm_check_security,file_mmap,socket_*,inode_copy_up,bprm_ima_check}: true; the previous Disabling optional LSM program WARN cluster (×7) and the can't attach before loaded ERROR pair are gone. The operator-facing posture key lsm_file_mmap and the BPF program function name handle_file_mmap are unchanged so JSON consumers and runtime telemetry stay byte-stable across the rename.

Added — Operator Tooling

  • Pre-install hook capability probe (aegisbpf probe, src/hook_capabilities.{hpp,cpp}, docs/HOOK_CAPABILITY_PROBE.md) — closes Honest Limitation #3. Operators can now run aegisbpf probe before installing or rolling out AegisBPF on a fleet to find out, for each of the 14 LSM hooks AegisBPF wants to attach, whether the target kernel will let it. The probe loads vmlinux BTF (/sys/kernel/btf/vmlinux) and asks libbpf whether each bpf_lsm_<hook> trampoline is present as a BTF_KIND_FUNC — the exact symbol BPF-LSM attach needs. Output JSON gains a hook_probe.hooks.<name> block per hook with kernel_supported, required, btf_symbol, and description fields, plus a hook_probe.btf_available summary so callers can distinguish "BTF was unavailable" from "BTF was there but symbol was missing". Catalog covers lsm_file_open, lsm_inode_permission (required), and lsm_bprm_check_security, lsm_bprm_ima_check, lsm_file_mmap, lsm_socket_{connect,bind,listen,accept,sendmsg,recvmsg}, lsm_ptrace_access_check, lsm_locked_down, lsm_inode_copy_up (optional). Names mirror the keys in the daemon's runtime /var/lib/aegisbpf/capabilities.json so consumers can join "predicted attachable" against "actually attached". Probe needs no privileges beyond reading /sys/kernel/btf/vmlinux and loads no BPF programs. New 5-test GTest suite (tests/test_hook_capabilities.cpp) pins the catalog shape (size + name set), enforces the bpf_lsm_* BTF-symbol prefix invariant, asserts only lsm_file_open/lsm_inode_permission are required, exercises the no-BTF path on hosts without /sys/kernel/btf/vmlinux (GTEST_SKIP() otherwise), and verifies the two required hooks resolve in vmlinux BTF on hosts that have it. Updates README Limitation #3 from "Runtime probing today; a machine-readable capability report is on the roadmap" to point at aegisbpf probe and the new doc.

Added — Supply Chain

  • Bit-for-bit reproducible builds (cmake/Reproducibility.cmake, AEGIS_REPRODUCIBLE_BUILD=ON by default) — aegisbpf is now byte-identical across builds from differing absolute source paths, hostnames, users, and wall-clock times, given the same compiler version and SOURCE_DATE_EPOCH. Implemented via -ffile-prefix-map=<src>=. -ffile-prefix-map=<build>=. -fdebug-prefix-map=<src>=. -fdebug-prefix-map=<build>=. (strips absolute paths from __FILE__, DWARF, and assertion macros), -Wl,--build-id=sha1 (content-addressed build-id replacing the default uuid/random), and ar -D / ranlib -D (zeroed mtime/uid/gid/mode on .a archives, so libaegisbpf_lib.a is reproducible too). SOURCE_DATE_EPOCH is honoured and propagated. The flags module is included from the top-level CMakeLists.txt after all sanitizer/coverage/hardening flags so prefix-map applies to every TU. scripts/check_reproducible_build.sh was rewritten to do a real test: it stages two source-tree copies at distinctly different absolute paths (/tmp/aegis-repro-XXX/aaaaaa/src vs …/bbbbbbbbbbbbbb/src), builds each with SKIP_BPF_BUILD=ON BUILD_TESTING=OFF, and compares the full aegisbpf ELF with sha256sum — no objcopy --strip-debug, no section extraction. On failure it runs diffoscope and retains the scratch trees (KEEP_TMP=1) for inspection. The previous workaround that compared only .text/.rodata/.data.rel.ro payloads is gone. CI runs the same script via .github/workflows/reproducibility.yml. Documented end-to-end in docs/REPRODUCIBLE_BUILDS.md (knob table, what's covered, what isn't, release-binary verification recipe). Adds a "Supply chain: Bit-for-bit reproducible builds" row to the README Standards Alignment matrix.

Added — Portability

  • BTFhub fallback resolver (src/btf_loader.{hpp,cpp}, AEGIS_BTF_PATH env var) — explicit multi-tier lookup for the BTF blob handed to libbpf at BPF object load time, so kernels without /sys/kernel/btf/vmlinux (RHEL 7, very old embedded, stripped-down kernels) can still run aegisbpfd. Resolution order: AEGIS_BTF_PATH override → /sys/kernel/btf/vmlinux (kernel built-in) → /lib/modules/<release>/btf/vmlinux (Debian/Ubuntu linux-image-extra location) → /var/lib/aegisbpf/btfs/<release>.btf (runtime cache) → /usr/lib/aegisbpf/btfs/<release>.btf (package-shipped) → /etc/aegisbpf/btfs/<release>.btf (operator-managed). An override that points at an unreadable file fails fast with BpfLoadFailed rather than silently falling back to the kernel BTF — a typo is much more likely than a deliberate mid-run swap, and a mismatched BTF would cause subtle CO-RE field-offset drift. The "no BTF found" path logs every searched location so operators can see exactly where to drop the blob. Pulled the inline lookup out of bpf_ops.cpp into a pure function resolve_btf_path(kernel_release, override) -> BtfResolution{path, source, searched} so it's testable without spinning up libbpf. New 7-test GTest suite (tests/test_btf_loader.cpp) covers env-var pickup, readable/unreadable override semantics, kernel-built-in preference, empty-kernel-release safety (no /lib/modules//btf traversal), and searched list population. Documented end-to-end in docs/BTF_FALLBACK.md (resolution table, override semantics, scripts/btfgen.sh + BTFhub-archive workflow, log examples). Flips the "Portability: BTFhub fallback for kernels without /sys/kernel/btf/vmlinux" row in the Standards Alignment matrix from Roadmap to shipped, and rewrites the matching Honest Limitation #6 from "unsupported" to "requires per-kernel blobs (here's how)".

Added — Event Output

  • OCSF 1.1.0 event format (--event-format=ocsf, src/ocsf_formatter.{hpp,cpp}) — opt-in OCSF JSON output for two highest-volume event types: BlockEvent reshaped to OCSF File Activity (class_uid 1001, activity_id 14 Open) and NetBlockEvent reshaped to OCSF Network Activity (class_uid 4001, activity_id 1 Open for connect/bind/listen/accept, activity_id 6 Traffic for sendmsg/recvmsg). Audit-mode events emit action_id=1 (Allowed) with no disposition_id; enforce-mode events emit action_id=2 (Denied) + disposition_id=2 (Blocked). Severity scales (Low for audit, High for enforce). AegisBPF-specific forensic fields (inode/dev/cgroup id/exec id) are preserved under the OCSF unmapped extension so SIEM parsers see standard fields without losing evidence. Hostname cached at startup via gethostname(). New EventFormat enum + set_event_format() / current_event_format() helpers; CLI flag accepts aegis (default), ocsf, OCSF, ocsf-1.1, ocsf-1.1.0. Format dispatch is global and orthogonal to the sink (--log=stdout|journald|both); journald path stores the OCSF payload in MESSAGE= while preserving the existing AEGIS_* field set on the journal entry. New 9-test GTest suite (tests/test_ocsf_formatter.cpp) covers required fields per OCSF class, audit-vs-enforce semantics, file path resolution (raw vs resolved), root-file path handling, all six network direction codes, and CLI keyword acceptance. Out of scope (still emitted in AegisBPF-native shape today): ExecEvent, ExecArgvEvent, ForensicEvent, KernelBlockEvent, OverlayCopyUpEvent, state_change, control_change. Closes docs/POSITIONING.md §3.3 OCSF row from "Roadmap" to "shipped for File + Network Activity"; flips the matching README Standards Alignment row.

Added — Daemon Hardening

  • Post-attach capability drop (--drop-caps, src/capabilities.{hpp,cpp}) — opt-in defence-in-depth that runs after BPF programs are attached and reduces the daemon's capability surface to a tight keep set: CAP_NET_ADMIN (cgroup BPF + network policy map writes) and CAP_DAC_READ_SEARCH (cross-userns /proc/<pid>/{exe,cgroup,ns/*} reads). Everything else — CAP_SYS_ADMIN, CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE, CAP_SYS_RESOURCE, etc. — is cleared from effective/permitted/inheritable, lowered out of ambient, and dropped from the bounding set. Direct capget(2) / capset(2) syscalls (_LINUX_CAPABILITY_VERSION_3, two-u32 mask) since the glibc wrappers are deprecated. Per-cap drop sequence is capgetPR_CAP_AMBIENT_LOWER (EINVAL/ENOENT ignored) → capset (clears the three sets atomically) → PR_CAPBSET_DROP (EPERM/EINVAL ignored). Order matters: clearing effective/permitted before the bounding drop guarantees the cap is gone from runtime use even when the bounding drop is blocked (e.g. inside an unprivileged container or when setpcap is missing). apply_post_attach_cap_drop() enumerates caps from the live snapshot rather than hard-coding a list, so future kernels remain covered. Kernel-support probe (PR_CAPBSET_READ on CAP_BPF) ensures --drop-caps never fails startup on kernels < 5.8 — a WARN is logged and the layer is skipped. Stacks cleanly with --seccomp and --landlock (the in-process drop happens before Landlock's restrict_self and before the seccomp filter is loaded). Startup log records cap_drop=true caps_dropped=<n> for empirical verification via /proc/<pid>/status. The systemd unit's CapabilityBoundingSet= and AmbientCapabilities= already restrict the cap surface; this layer is the last shrink-wrap. New 7-test GTest suite (tests/test_capabilities.cpp) covers split-support probe, snapshot consistency (effective ⊆ permitted), keep-set shape, no-op empty-list drop, idempotent already-absent drop, and a fork+drop CAP_KILL verification (self-skips when CAP_KILL is not in the test runner's permitted set). Flips the "Daemon hardening: Split capabilities (CAP_BPF + CAP_PERFMON)" row in the Standards Alignment matrix and the matching honest-limitation in README.md §Honest Limitations #8; documented in docs/HARDENING.md §Capability splitting.
  • Landlock LSM filesystem self-sandbox (--landlock, src/landlock.{hpp,cpp}) — opt-in post-init confinement of the daemon's own filesystem access to a fixed allowlist (RO /etc/aegisbpf, /usr/lib/aegisbpf, /proc, /sys/kernel/btf, $AEGIS_KEYS_DIR, dirname($AEGIS_BPF_OBJ); RW /var/lib/aegisbpf, /sys/fs/bpf). Direct landlock_create_ruleset / landlock_add_rule / landlock_restrict_self syscalls (no glibc wrapper dependency); raw __NR_* fallbacks for older libc. ABI version probed via LANDLOCK_CREATE_RULESET_VERSION — ABI 2 picks up LANDLOCK_ACCESS_FS_REFER, ABI 3 adds LANDLOCK_ACCESS_FS_TRUNCATE. Sets NO_NEW_PRIVS unconditionally before landlock_restrict_self (idempotent with the seccomp path). Missing allowlist entries are logged and skipped, not fatal. Kernels without Landlock log a WARN and continue — --landlock never fails startup on unsupported hosts. New 6-test GTest suite (tests/test_landlock_sandbox.cpp) covers ABI probe, default config shape, AEGIS_KEYS_DIR pickup, and a fork+restrict EACCES verification; self-skips via GTEST_SKIP() when the kernel lacks Landlock. Closes the "daemon hardening: Landlock self-sandbox" row in the Standards Alignment matrix and docs/HARDENING.md.

Added — Operator Policy Model (v0.5.0)

  • Per-rule action field on FileRule and NetworkRule (Allow or Block, default Block) so a single policy can express both deny and allow semantics. Allow rules lower into the daemon's existing [allow_*] sections; no daemon change required.
  • Allow > Block merge precedence in MergePolicies: any literal that appears in an [allow_*] section is removed from the corresponding [deny_*] section across the merged ConfigMap, mirroring Tetragon and KubeArmor behaviour. Sections that are emptied by the sweep are dropped from the final output.
  • spec.workloadSelector with full Kubernetes LabelSelector support (matchLabels + matchExpressions: In/NotIn/Exists/DoesNotExist), plus a separate namespaceSelector and matchNamespaceNames shortcut. Replaces the v0.4.x PolicySelector for new policies.
  • internal/selector package that evaluates workloadSelector against the live cluster (resolving namespaces, then matching pods inside each), with fallback to the legacy spec.selector only when workloadSelector is unset.
  • Admission webhook validation for the new fields: rejects Action=Allow on inode-based or protect file rules, detects in-spec Allow/Block collisions on the same target (path / IP / CIDR / port / ip:port / binary hash), validates LabelSelector parseability, validates matchNamespaceNames as DNS-1123 labels, and rejects cross-namespace selection from a namespaced AegisPolicy.
  • Deprecated status condition raised on policies that still use spec.selector, with reason LegacySelectorInUse. The policy continues to reconcile normally; the condition is informational.
  • Pinned controller-gen Makefile workflow (make controller-gen / manifests / deepcopy / generate / verify-generated, controller-gen v0.21.0). verify-generated fails CI when CRD YAML or zz_generated.deepcopy.go drift from the markers in api/.
  • CRD schema regenerated for aegispolicies.aegisbpf.io and aegisclusterpolicies.aegisbpf.io: adds workloadSelector (podSelector, namespaceSelector, matchNamespaceNames) and the per-rule action enum defaulted to Block.
  • New example operator/examples/allow-override.yaml demonstrating the cross-policy Allow override flow (a global block + a namespaced allow carve-out).

Backwards compatibility (v0.5.0)

  • v0.4.x policies that use spec.selector continue to admit, reconcile, and translate to byte-identical INI output. They simply gain a Deprecated=True condition.
  • The per-rule action field defaults to Block, so existing rule lists keep their original semantics with no edits.
  • The CRD remains v1alpha1. No v1alpha2 bump.
  • Daemon (policy_parse.cpp and the BPF maps) is unchanged in v0.5.0; the operator translates per-rule Action into the existing [allow_*] and [deny_*] INI sections.

Added — Quality & Observability

  • Per-hook latency tracking (hook_latency PERCPU_ARRAY map) — records total, count, min, and max nanoseconds per LSM/tracepoint hook invocation for overhead benchmarking
  • In-kernel event pre-filtering (event_approver_inode, event_approver_path maps) — Datadog-style approver/discarder pattern to suppress noisy events in-kernel, reducing ring buffer pressure
  • Priority ring buffer (priority_events, 4 MB) — dedicated ring buffer for security-critical forensic events, isolated from the main events ring buffer to prevent drops
  • Forensic event capture (ForensicEvent / forensic_block) — enriched block events with UID/GID, exec identity stage, verified_exec flag, and process context, emitted via the priority ring buffer
  • Startup self-tests (src/selftest.{hpp,cpp}) — Datadog-pattern startup validation: map accessibility, ring buffer FD, config readability, and process_tree write/read/delete cycle
  • Map capacity monitoring (src/map_monitor.{hpp,cpp}) — iterates BPF map entries to compute usage ratios and log warnings when thresholds are exceeded
  • Process cache /proc reconciliation (src/proc_scan.{hpp,cpp}) — scans /proc at startup to populate process_tree with pre-existing processes
  • BPF program signing preparation (src/bpf_signing.{hpp,cpp}) — Ed25519 signature helper code for BPF object files; the active load-time SHA-256 gate lives in src/bpf_integrity.cpp with break-glass override via AEGIS_ALLOW_UNSIGNED_BPF
  • Binary hash verification (src/binary_hash.{hpp,cpp}) — SHA-256 integrity verification for binary allow-lists with recursive directory scanning
  • Hot-loadable detection rules (src/rule_engine.{hpp,cpp}) — JSON-based detection rule engine with comm/path matching, severity levels, and thread-safe hot-reload
  • Plugin/extension system (src/plugin.{hpp,cpp}) — abstract plugin interface with virtual event handlers, lifecycle management, and break-on-consume dispatch; ships with built-in JsonLoggerPlugin

Added — CI Quality Gates

  • Real kernel BPF testing (.github/workflows/kernel-bpf-test.yml) — virtme-ng boots a real kernel in CI to test BPF object loading and map creation
  • BPF code coverage analysis (.github/workflows/bpf-coverage.yml) — llvm-objdump instruction counting per BPF program with JSON summary artifact

Changed

  • BPF hook functions instrumented with record_hook_latency() calls at every return point across aegis_exec.bpf.h, aegis_file.bpf.h, and aegis_net.bpf.h
  • handle_event() now processes EVENT_FORENSIC_BLOCK events from the priority ring buffer
  • BpfState extended with hook_latency, event_approver_inode, event_approver_path, and priority_events map pointers
  • Daemon startup now runs self-tests, reconciles /proc, and checks map capacity after ring buffer creation
  • Event union extended with ForensicEvent forensic member
  • Event schema (config/event-schema.json) extended with ForensicBlockEvent definition
  • BPF map schema (docs/BPF_MAP_SCHEMA.md) updated with 4 new maps and memory budget
  • Feature surface contract updated to validate new components
  • ForensicEvent static_assert corrected to 104 bytes (was 112)

Testing

  • Test suite: 210/210 passing
  • Feature surface contract: passing
  • Build: zero errors, zero warnings

[0.9.0] - 2026-07-03

Cross-kernel enforcement hardening, on-hardware trust evidence, and supply-chain CI hygiene. Highlights since v0.8.0:

Fixed — Cross-kernel enforcement

  • Agent now starts and enforces on kernels < 6.1 (#265): the handle_exit process-exit tracepoint is verifier-gated below 6.1 and was still attached via the fatal path, aborting all enforcement on 5.4/5.10/5.15. The attach now honors the autoload gate.
  • Resilient BPF object load (#266): a single verifier-fragile optional hook can no longer take down core enforcement — load retries once with known-fragile optional hooks disabled; required enforcement hooks are never disabled.
  • handle_inode_copy_up verifies on all kernels (#267): fixed a non-monotonic 6.8 verifier rejection (R0 out of [-4095,0]) via the barrier_var() + clamp idiom, restoring overlay copy-up enforcement with zero degradation on 6.8.
  • Net effect: clean load + enforcement across the 5.15 → 6.17 LTS range.

Added — Trust evidence (reproducible, on-hardware)

  • Free cross-kernel enforcement matrix (5.15/6.1/6.8/6.17 via qemu/KVM) (#265).
  • Red-team alternate-read-path bypass battery — io_uring / open_by_handle_at / openat2 (#263).
  • Backpressure-saturation battery — enforcement holds when telemetry drops (#264).
  • Enforcement-grade canary + red-team soak evidence (#241, #242).
  • Pilot evidence contract + template and validation test (#252).

Added — Security posture & CI supply-chain hardening

  • Least-privilege GITHUB_TOKEN permissions across all workflows; write scopes moved to the jobs that need them (#268). Cleared all OSSF Scorecard Token-Permissions alerts.
  • Future-LSM / next-gen BPF posture (#245); trusted key lookup hardening (#243); policy authoritative flip behind a default-off mode (#240).
  • Self-hosted PR workflows gated (#247); required-check reporting hardened (#246).
  • KPI-threshold, degraded-mode, rollback-failure, and e2e-matrix coverage contracts locked in.
  • ~20 pinned GitHub Action dependency bumps.

Changed — Docs

  • Added a categorized documentation index (docs/README.md) and consolidated the docs tree (#271).

[0.1.1] - 2026-02-07

Security

  • CRITICAL FIX: Eliminated TweetNaCl memory exhaustion vulnerability
    • Replaced unbounded heap allocation with fixed 4KB stack-based buffers
    • Added size validation to prevent memory exhaustion DoS attacks
    • Implemented secure buffer zeroing with volatile pointers
    • See docs/SECURITY_FIX_TWEETNACL_MEMORY.md for full details

Added

  • New safe crypto wrapper functions (src/tweetnacl_safe.hpp)
    • crypto_sign_detached_safe() - Stack-based signature generation
    • crypto_sign_verify_detached_safe() - Stack-based signature verification
    • Size limit: 4096 bytes (33× larger than actual usage)
  • Comprehensive test suite for crypto safety (tests/test_crypto_safe.cpp)
    • 13 new tests covering edge cases and security boundaries
    • Tests for empty messages, invalid signatures, size limits
  • Security fix documentation and verification script
    • docs/SECURITY_FIX_TWEETNACL_MEMORY.md - Detailed security analysis
    • SECURITY_FIX_SUMMARY.md - Implementation summary
    • scripts/verify_security_fix.sh - Automated verification

Changed

  • Updated crypto.cpp to use safe crypto wrappers exclusively
  • Enhanced error messages to indicate size limit constraints
  • Updated SECURITY.md with security fixes history section

Performance

  • Neutral to positive impact: stack allocation faster than heap
  • Predictable memory usage with no fragmentation
  • No measurable difference in test suite runtime

Testing

  • Test suite expanded: 153 → 157 tests (all passing)
  • Added edge case tests: empty messages, invalid signatures, boundary conditions
  • Full backward compatibility verified

Compliance

  • OWASP Top 10 2021 compliant
  • CERT Secure Coding Standards compliant
  • CWE/SANS Top 25 compliant
  • Memory safety guaranteed

Migration Notes

  • No breaking changes - fully backward compatible
  • New limitation: Messages > 4096 bytes rejected (no legitimate use cases affected)
  • All existing functionality preserved

0.1.0 - Previous Release

Added

  • Result error handling throughout the codebase
  • Constant-time hash comparison (constant_time_hex_compare()) to prevent timing side-channel attacks
  • Structured logging with text and JSON output formats
  • --log-level and --log-format CLI options
  • --seccomp flag for runtime syscall filtering
  • Thread-safe caching for cgroup and path resolution
  • RAII wrappers for popen (PipeGuard) and ring_buffer (RingBufferGuard)
  • Input validation for CLI path arguments
  • Google Test unit tests for core components
  • Google Benchmark performance tests
  • Sanitizer builds (ASAN, UBSAN, TSAN)
  • Code coverage reporting with gcovr and Codecov
  • Comprehensive CI pipeline with test, sanitizer, and coverage jobs
  • AppArmor profile for runtime confinement
  • SELinux policy module
  • Sigstore/Cosign code signing for releases
  • SBOM generation (SPDX and CycloneDX)
  • Prometheus alert rules
  • Grafana dashboard
  • JSON Schema for event validation
  • Event schema validation tests and sample payloads
  • SIEM integration documentation
  • Dockerfile for containerized deployment
  • Helm chart for Kubernetes deployment
  • Architecture documentation
  • Troubleshooting guide
  • Man page
  • Dev check and environment verification scripts
  • Enforce-mode smoke test script
  • Nightly fuzz workflow, perf regression workflow, and kernel matrix workflow

Changed

  • All functions now return Result instead of int/bool
  • Replaced std::cerr/std::cout with structured logging
  • Improved error messages with context
  • Event schema aligned with emitted JSON fields
  • README/architecture diagrams updated to file-open enforcement

Fixed

  • popen() file descriptor leak in kernel config check
  • Race conditions in cgroup path cache
  • Race conditions in CWD resolution cache
  • Thread-safety issue in journal error reporting

Security

  • Added seccomp-bpf syscall filter
  • Added AppArmor and SELinux policies
  • Added input validation for all user-provided paths
  • Added constant-time comparison for all hash verification (BPF integrity, policy SHA256, bundle verification)
  • Disabled AEGIS_SKIP_BPF_VERIFY bypass in Release builds (only available in Debug builds)
  • Added try-catch exception handling in signed bundle parser to prevent crashes on malformed input
  • Extended json_escape() to handle all control characters, preventing JSON injection in logs

0.1.0 - 2024-01-01

Added

  • Initial release
  • BPF LSM-based execution blocking
  • Tracepoint-based audit mode (fallback)
  • Policy file support with deny_path, deny_inode, allow_cgroup sections
  • SHA256 hash-based blocking
  • Prometheus metrics endpoint
  • Journald integration
  • CLI commands: run, block, allow, policy, stats, metrics, health