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.
- Opt-in HTTP
/metricsendpoint (AEGIS_METRICS_ADDR=<host:port>,src/metrics_server.{hpp,cpp},src/daemon.cppwiring,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; routesGET /metricsandGET /healthz. Binds loopback by default; bind:9635to expose and restrict with a firewall / NetworkPolicy (no auth, standard for a scrape target). The Prometheus builder was refactored out of themetricsCLI command into a sharedbuild_metrics_report(BpfState&, bool)so the CLI, the textfile collector, and the HTTP endpoint all emit identical output. A newaegisbpf_deny_ttl_entriesgauge 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/healthz404, no-callback 503, non-GET 405).
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.
- Timed denies over the control API (
src/ttl_registry.{hpp,cpp},src/daemon.cppreaper wiring,tests/test_ttl_registry.cpp,docs/CONTROL_API.md) — any add verb now accepts an optional trailingttl=<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 byaegis-responder) can no longer wedge a path or IP permanently. Timed denies are persisted to/var/lib/aegisbpf/deny_ttl.dband reaped by a dedicated thread (5 s granularity) that re-issues the samedelcommand 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, anddel/cleardrop the timer. Theaegis-responderconfig gains a per-rulettl_secondsthat is forwarded as thettl=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.
- Root-only Unix-socket control API (
AEGIS_API_SOCKET=<path>onrun,src/socket_api.{hpp,cpp},src/daemon.cppwiring,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 bySO_PEERCRED(peer uid must equalcontrol_uid, default 0) on top of the0600socket, and reuse the samecmd_block_*/cmd_network_deny_*code paths as the CLI, so aPOST /block/addtakes effect on the running daemon's pinned maps immediately. Verified end-to-end (drivingPOST /block/addover the socket made the target file-EPERMon 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.
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 toPOST /block/add/POST /network/deny/*on the agent's control socket, installing a race-free in-kernel-EPERMon the node where the alert fired. Safe by default: an explicit rule allowlist (only named Falco rules act), amin_prioritygate, and adry_runmode. 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" drovePOST /block/add <path>and the target file became-EPERMon 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 optionalX-Aegis-Tokenshared-secret (constant-time compare), and a 64 KiB request-body cap.
- 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). Vectorremaptransforms enrich each event with cloud/region, Kubernetes pod/namespace/node (Downward API), OCSFobservables[], and a process-lineagecorrelation_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 OCSFunmapped. No agent code change — the "ready now" data integration.
- Held the untested major bumps of the two
release.ymlsigning-path actions —sigstore/cosign-installer(pinned back to v3 from Dependabot's v4.1.2 bump) andactions/attest-build-provenance(kept at v2, holding the proposed v4).release.ymlis 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 Dependabotignorefor 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-gov7 and a redactions/checkout7.0.1 were left open pending, not merged.
- 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.
- Pin auto-heal watchdog (
AEGIS_PIN_HEAL, default1wheneverAEGIS_ENFORCE_PIN_LINKS=1,src/bpf_link_pin.{hpp,cpp}heal_pinned_hooks(),src/bpf_ops.hppenable_pin_heal+pin_heal_{attempts,successes,failures},src/daemon_runtime.cppheartbeat dispatch,src/daemon.cppstartup gate,tests/test_bpf_link_pin.cppHealPinnedHooks*) — 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 (operatorrm,bpftool link detach, bpffs unmount/remount), the watchdog re-issuesbpf_link__pin()on the still-live userspacebpf_link*(owned byBpfState::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 freshbpf_program__attach()call in the heartbeat path).AEGIS_PIN_HEAL=0falls back to the warn-only verify behaviour from the parent PR (useful while debugging a flaky bpffs). Counterspin_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 withlink == nullptr, e.g. a future "adopt pins from previous run on cold start" feature) is explicitly handled: it counts as still-missing and emits anERROR Pinned LSM hook missing and link handle unavailable — cannot healwithout incrementingpin_heal_attempts, so the stats remain a faithful measure of "active heal effort" rather than "drift surface area". The heartbeat structured log now carriesheal_enabled=<bool>alongsidemissing_count/total_pinned, so an operator alert rule can branch on which mode the daemon is in. New 3-test GTest extension to theBpfLinkPin.*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 userspacebpf_link*handle (would needbpf_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 ofpin_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, optionalAEGIS_PIN_ROOT=/sys/fs/bpf/aegisbpf,src/bpf_link_pin.{hpp,cpp},src/bpf_ops.hppPinnedHook+enforce_pin_links/pin_root/pinned_hooks,src/bpf_attach.cppattach_proghook,src/daemon_runtime.cppheartbeat verify tick,src/daemon.cppstartup 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 inattach_prog()is immediately followed bybpf_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, sysadminSIGKILL -9, etc.) no longer detaches the LSM hook. Enforcement survives daemon crash until a sysadmin explicitlybpftool link detaches orrms the pin. Startup is fail-loud: ifAEGIS_ENFORCE_PIN_LINKS=1is set but/sys/fs/bpfis 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 ismkdir(0700)-ed before first use. The heartbeat thread additionally runs a read-only verify watchdog every tick: eachstate.pinned_hooksentry isstat()-ed, and a structuredERROR Pinned LSM hooks missing — daemon-crash fail-safe degraded {missing_count, total_pinned}is emitted if any pin has disappeared (kernel module reload, operatorrm,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 inpin_attached_link()rejects empty names, names containing/or NUL, and..traversal so a buggy or hostile BPF object can never escapepin_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 inbpf_link_pin.cppwithout needing root or a real bpf_link: bpffs probe against/tmpand missing paths (must be false),ensure_pin_rootcreate / idempotent / fails on regular-file collision,count_existing_pinszero on empty, matches file count, ignores./.., zero on missing dir,pin_attached_linkinput validation (null link, empty name,../etc/passwdtraversal,handle/execveslash),verify_pinned_hookszero on healthy and counts missing correctly. Out of scope (deferred to follow-up PRs): auto re-attach of missing pins from the watchdog, anAEGIS_UNPIN_ON_EXITenv 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 mustrm -rftoday), and a Helmvalues.yamltoggle for one-command enable on Kubernetes deployments. The pin-survives-cleanup property is documented incleanup_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 withgrep bpf_link__pin src/(zero matches before this PR, three matches after, all inbpf_link_pin.cpp).
- 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 indocs/POSITIONING.md. Opt-in single-line CEF records for the same two highest-volume event classes OCSF already covers:BlockEvent→ signatureaegis:file:open,NetBlockEvent→ signatureaegis: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 byRecordIsSingleLineNoEmbeddedNewlines). 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=AegisRuleTypefor net only,cs4=event_type+cs4Label=AegisEventTypefor net only,cn1=cgid,cn2=inodefor file /cn2=directionfor net,cn3=devicefor file). Endpoint orientation matches the OCSF formatter: egress/send put the peer indst/dpt, accept/recv put the peer insrc/spt, and bind/listen surface only the local port asdpt(no remote peer yet).0.0.0.0and::are suppressed fromsrc/dstso a wildcard bind doesn't pollute SIEM dashboards with bogus indicators. CLI flag now acceptsaegis|ocsf|cef(alsoCEF,cef-1.0); the format dispatch is global and orthogonal to the sink (--log=stdout|journald|both) — journald path stores the CEF payload inMESSAGE=while keeping the existingAEGIS_*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 forExecEvent,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.
- 25 audited, MITRE-tagged rule packs (
rules/*/*.conf+ siblingREADME.mdper pack, top-levelrules/README.md,.github/workflows/rule-library.yml) — partial close ofdocs/POSITIONING.md§4.5 #22 ("No community rule library"). Every pack ships an INI policy file in the formataegisbpf policy validateaccepts, plus a README documenting the threat model, MITRE ATT&CK / CIS Benchmark coverage, false-positive vectors, and the exactaegisbpf policy applyinvocation. 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. Seerules/README.mdfor the current pack inventory and coverage. The newrule-libraryCI workflow runs on every PR that touchesrules/, the policy parser, or itself: buildsaegisbpfwithSKIP_BPF_BUILD=ON(parser-only, no root needed), runsaegisbpf policy validateon every shipped.conf, fails the PR if any pack fails to parse, and additionally enforces a structural contract that everyrules/<pack>/directory carries aREADME.mdand at least one.conffile. 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 standaloneaegisbpf/rulesrepo (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 futuredeny_argvsupport).
- Installable
.deband.rpmartefacts via CPack (CMakeLists.txtCPACK_DEBIAN_*/CPACK_RPM_*blocks,packaging/maintainer-scripts/{postinst,prerm,postrm},.github/workflows/packaging.yml,docs/PACKAGING.md) — partial close ofdocs/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 shipsaegisbpf_<ver>_<arch>.debandaegisbpf-<ver>-<rel>.<arch>.rpm, both produced from the samecmake --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:1for install,2for upgrade,0for 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 enablematchingdh_installsystemddefault; Fedora: do not auto-enable per Fedora packaging guidelines), all systemd interaction is conditional on/run/systemd/systemexisting so install on non-systemd hosts (containers, chroots, OpenRC) succeeds cleanly. The newpackagingCI workflow runs on every PR that touchesCMakeLists.txt,packaging/**, or itself: builds the binary + BPF object onubuntu-24.04, runscpack -G DEB && cpack -G RPM, enforces a required-files contract (binary + BPF object + sha256 sidecar + systemd unit +/etc/default/aegisbpfconffile +/etc/aegisbpf/policy.examplemust 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), runsdpkg -i/rpm -ivh, assertsaegisbpf --versionreturns 0, and exercises the remove and purge paths (purge must clean/var/lib/aegisbpf; remove must preserve/etc/default/aegisbpfas a Debian conffile / RPM%config(noreplace)). Theaegisbpf-packagesartefact (14-day retention) is uploaded so a release engineer candput/copr-clifrom 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), adebian/source tree for proper PPA sponsorship (Launchpad insists on source builds, not binary upload), and a hand-writtenaegisbpf.specfor Fedora official-repo sponsorship.docs/PACKAGING.mddocuments the full maintainer workflow including the §5.1–§5.4 hosted-repo recipes.
- Laptop 24 h soak wrapper + aborted-run evidence pack (
scripts/soak_laptop_24h.sh,evidence/soak-24h-laptop/) — partial close ofdocs/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 inscripts/soak_reliability.sh(commit 813a68c: cap+rotatedaemon.logat 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.shruns 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-inhibitshell so a 24 h run survives lid-close / idle suspend on a workstation.evidence/soak-24h-laptop/NOTES.mddocuments the original ~14.5 h aborted run onfeat/event-dedup-windowprecursor: RSS stayed flat (50 804 kB → 49 956 kB; well inside noise) across 16 workers + UDP workload, no daemon crashes,systemd-inhibitandperformancegovernor 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 truncatedsoak.logandtmux-final.txtproves 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 inevidence/soak-24h-laptop/is shaped so the 168 h run can drop into a siblingevidence/soak-168h-baremetal/directory with the same file set.
- 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_eventintegration,src/cli_run.cppshared-flag wiring,tests/test_net_block_event_dedup.cpp) — closes theNetBlockEventfollow-up the originalBlockEventdedup PR explicitly listed as deferred. Uses the same--event-dedup-window-ms=N --event-dedup-max-entries=Nflags 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 tag1) 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.::1vs::2after the XOR-fold) stay distinct — earlier draft used(addr_slot << 32)which silently aliased those; the regression contract is locked in byDistinctRemoteIPv6DoNotCollapse. The first emit after window expiry carries"suppressed_during_prior_window": Nin the Aegis-native JSON shape (mirrors theBlockEventfield 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 throughprint_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 towindow_ms=0restores the disabled contract. Resurface-after-expiry behaviour is covered by the existingEventDeduperunit 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 varsAEGIS_EVENT_DEDUP_WINDOW_MS/AEGIS_EVENT_DEDUP_MAX_ENTRIES,src/event_dedup.{hpp,cpp},src/events.{hpp,cpp}configure_block_event_dedup+ integration inprint_block_event,tests/test_event_dedup.cpp,docs/EVENT_LOSS_AND_BACKPRESSURE.md§Bounded time-window event dedup) — partial close ofdocs/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-EPERMon 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": Nfield 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 anevictions()counter increments so under-sized tables are observable, not silent. The deduper is owned by the ringbuf consumer thread (no locking), and the timer usesCLOCK_MONOTONICso 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 withsuppressed=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 INFOBlock-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; theaegisbpf metricsCLI reads BPF maps and a userspace counter would not be visible to it without a separate IPC mechanism). aegisbpf simulatenow replaysnet_*_blockevents (src/commands_explain.{hpp,cpp}NetExplainEvent/NetExplainResult/parse_net_explain_event/evaluate_net_event_against_policy,src/commands_simulate.{hpp,cpp}SimulateNetRecordand dispatch insimulate_one_event,tests/test_commands_simulate.cpp) — extends the dry-run replay so an audit-mode JSONL stream that mixes fileblockevents and networknet_connect_block/net_bind_block/net_listen_block/net_accept_block/net_sendmsg_block/net_recvmsg_blockevents 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 tois_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 mirrorsport_rule_matches(): egress-class events (egress/send/recv) checkremote_portwith rule direction 0; bind-class events (bind/listen/accept) checklocal_portwith 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 reusingparse_cidr_v4/parse_cidr_v6fromnetwork_ops.cppso test correctness depends only on standardinet_pton.SimulateSummarygains 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, withnet_would_blockfurther broken down intonet_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-eventadds a separatenet_events[]array (and a "Per-event detail (network)" text section) with the original action, originalrule_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, exactIpPortRuletuple 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 inevaluate_net_event_against_policy()'s contract; the v1 file-event evaluator's contract thatprotect_*flags are not consulted is unchanged. The pre-existing 10 file-event tests still pass byte-identically, including the partition invariant forblock_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 indocs/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 withjournalctl -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 pureevaluate_event_against_policy()helper fromcommands_explain.cppso simulator verdicts can never silently drift from the daemon's actual decisions. Output partitions every parsedblockevent into exactly one ofwould_block(further broken down intowould_block_inode/would_block_path),would_allow,no_match. Also reportsskipped_non_json(lines that didn't start with{),skipped_non_block(other event types), andparse_errors(JSON missing the requiredtypefield).--per-eventadds aevents[]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 intests/test_commands_simulate.cppcovers deny-path matching, allow-cgroup-path override of deny-path, allow-cgroup-id numeric matching, no-match counting,resolved_pathfallback when the raw path misses, non-block-event skipping, non-JSON / empty-line handling, missing-typeparse-error detection, and the partition invariantwould_block + would_allow + no_match == block_events. The pre-existingcmd_explainrule-match logic is now a thin wrapper around the same helper, so the regression risk is symmetric: a refactor that breakssimulatewould also failexplainand vice versa. Updates the CLI usage string insrc/cli_common.cpp.
- All optional LSM programs were silently disabled on every supported kernel (
src/bpf_ops.cppdetect_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 asBTF_KIND_FUNC, but those names appear only as struct members of the LSM hooks list — the actual BPF-LSM trampoline FUNC entries arebpf_lsm_<hook>. Every lookup returned-ENOENT, sobpf_program__set_autoload(false)was called on each optional program. Subsequent attach attempts then failed withlibbpf: prog 'handle_bprm_check_security': can't attach before loaded(and the same forhandle_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 thoughlsm_enabled=truewas reported. Two compounding bugs are fixed: (1) the BTF lookup now uses thebpf_lsm_<hook>symbol via a per-hook catalog mirroringsrc/hook_capabilities.cpp, and the catalog now also includessocket_recvmsgandinode_copy_upthat were previously omitted; (2)bpf/aegis_exec.bpf.hnow usesSEC("lsm/mmap_file")(the kernel hook was renamed fromfile_mmappre-5.6) so the trampoline name matchesbpf_lsm_mmap_file. Verified end-to-end on Linux 6.17:aegisbpf capabilities --jsonnow reportsruntime_deps_hook_attached: trueandhooks.lsm_{bprm_check_security,file_mmap,socket_*,inode_copy_up,bprm_ima_check}: true; the previousDisabling optional LSM programWARN cluster (×7) and thecan't attach before loadedERROR pair are gone. The operator-facing posture keylsm_file_mmapand the BPF program function namehandle_file_mmapare unchanged so JSON consumers and runtime telemetry stay byte-stable across the rename.
- Pre-install hook capability probe (
aegisbpf probe,src/hook_capabilities.{hpp,cpp},docs/HOOK_CAPABILITY_PROBE.md) — closes Honest Limitation #3. Operators can now runaegisbpf probebefore 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 eachbpf_lsm_<hook>trampoline is present as aBTF_KIND_FUNC— the exact symbol BPF-LSM attach needs. Output JSON gains ahook_probe.hooks.<name>block per hook withkernel_supported,required,btf_symbol, anddescriptionfields, plus ahook_probe.btf_availablesummary so callers can distinguish "BTF was unavailable" from "BTF was there but symbol was missing". Catalog coverslsm_file_open,lsm_inode_permission(required), andlsm_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.jsonso consumers can join "predicted attachable" against "actually attached". Probe needs no privileges beyond reading/sys/kernel/btf/vmlinuxand loads no BPF programs. New 5-test GTest suite (tests/test_hook_capabilities.cpp) pins the catalog shape (size + name set), enforces thebpf_lsm_*BTF-symbol prefix invariant, asserts onlylsm_file_open/lsm_inode_permissionare 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 ataegisbpf probeand the new doc.
- Bit-for-bit reproducible builds (
cmake/Reproducibility.cmake,AEGIS_REPRODUCIBLE_BUILD=ONby default) —aegisbpfis now byte-identical across builds from differing absolute source paths, hostnames, users, and wall-clock times, given the same compiler version andSOURCE_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), andar -D/ranlib -D(zeroed mtime/uid/gid/mode on.aarchives, solibaegisbpf_lib.ais reproducible too).SOURCE_DATE_EPOCHis honoured and propagated. The flags module is included from the top-levelCMakeLists.txtafter all sanitizer/coverage/hardening flags so prefix-map applies to every TU.scripts/check_reproducible_build.shwas rewritten to do a real test: it stages two source-tree copies at distinctly different absolute paths (/tmp/aegis-repro-XXX/aaaaaa/srcvs…/bbbbbbbbbbbbbb/src), builds each withSKIP_BPF_BUILD=ON BUILD_TESTING=OFF, and compares the fullaegisbpfELF withsha256sum— no objcopy --strip-debug, no section extraction. On failure it runsdiffoscopeand retains the scratch trees (KEEP_TMP=1) for inspection. The previous workaround that compared only.text/.rodata/.data.rel.ropayloads is gone. CI runs the same script via.github/workflows/reproducibility.yml. Documented end-to-end indocs/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.
- BTFhub fallback resolver (
src/btf_loader.{hpp,cpp},AEGIS_BTF_PATHenv 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 runaegisbpfd. Resolution order:AEGIS_BTF_PATHoverride →/sys/kernel/btf/vmlinux(kernel built-in) →/lib/modules/<release>/btf/vmlinux(Debian/Ubuntulinux-image-extralocation) →/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 withBpfLoadFailedrather 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 ofbpf_ops.cppinto a pure functionresolve_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//btftraversal), andsearchedlist population. Documented end-to-end indocs/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)".
- 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:BlockEventreshaped to OCSF File Activity (class_uid 1001, activity_id 14 Open) andNetBlockEventreshaped 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 emitaction_id=1 (Allowed)with nodisposition_id; enforce-mode events emitaction_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 OCSFunmappedextension so SIEM parsers see standard fields without losing evidence. Hostname cached at startup viagethostname(). NewEventFormatenum +set_event_format()/current_event_format()helpers; CLI flag acceptsaegis(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 inMESSAGE=while preserving the existingAEGIS_*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. Closesdocs/POSITIONING.md§3.3 OCSF row from "Roadmap" to "shipped for File + Network Activity"; flips the matching README Standards Alignment row.
- 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) andCAP_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. Directcapget(2)/capset(2)syscalls (_LINUX_CAPABILITY_VERSION_3, two-u32 mask) since the glibc wrappers are deprecated. Per-cap drop sequence iscapget→PR_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 whensetpcapis 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_READonCAP_BPF) ensures--drop-capsnever fails startup on kernels < 5.8 — a WARN is logged and the layer is skipped. Stacks cleanly with--seccompand--landlock(the in-process drop happens before Landlock'srestrict_selfand before the seccomp filter is loaded). Startup log recordscap_drop=true caps_dropped=<n>for empirical verification via/proc/<pid>/status. The systemd unit'sCapabilityBoundingSet=andAmbientCapabilities=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 inREADME.md§Honest Limitations #8; documented indocs/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). Directlandlock_create_ruleset/landlock_add_rule/landlock_restrict_selfsyscalls (no glibc wrapper dependency); raw__NR_*fallbacks for older libc. ABI version probed viaLANDLOCK_CREATE_RULESET_VERSION— ABI 2 picks upLANDLOCK_ACCESS_FS_REFER, ABI 3 addsLANDLOCK_ACCESS_FS_TRUNCATE. SetsNO_NEW_PRIVSunconditionally beforelandlock_restrict_self(idempotent with the seccomp path). Missing allowlist entries are logged and skipped, not fatal. Kernels without Landlock log a WARN and continue —--landlocknever fails startup on unsupported hosts. New 6-test GTest suite (tests/test_landlock_sandbox.cpp) covers ABI probe, default config shape,AEGIS_KEYS_DIRpickup, and a fork+restrict EACCES verification; self-skips viaGTEST_SKIP()when the kernel lacks Landlock. Closes the "daemon hardening: Landlock self-sandbox" row in the Standards Alignment matrix anddocs/HARDENING.md.
- Per-rule
actionfield onFileRuleandNetworkRule(AlloworBlock, defaultBlock) 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 > Blockmerge precedence inMergePolicies: 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.workloadSelectorwith full KubernetesLabelSelectorsupport (matchLabels+matchExpressions: In/NotIn/Exists/DoesNotExist), plus a separatenamespaceSelectorandmatchNamespaceNamesshortcut. Replaces the v0.4.xPolicySelectorfor new policies.internal/selectorpackage that evaluatesworkloadSelectoragainst the live cluster (resolving namespaces, then matching pods inside each), with fallback to the legacyspec.selectoronly whenworkloadSelectoris unset.- Admission webhook validation for the new fields: rejects
Action=Allowon inode-based or protect file rules, detects in-specAllow/Blockcollisions on the same target (path / IP / CIDR / port / ip:port / binary hash), validatesLabelSelectorparseability, validatesmatchNamespaceNamesas DNS-1123 labels, and rejects cross-namespace selection from a namespaced AegisPolicy. Deprecatedstatus condition raised on policies that still usespec.selector, with reasonLegacySelectorInUse. The policy continues to reconcile normally; the condition is informational.- Pinned
controller-genMakefile workflow (make controller-gen / manifests / deepcopy / generate / verify-generated, controller-gen v0.21.0).verify-generatedfails CI when CRD YAML orzz_generated.deepcopy.godrift from the markers inapi/. - CRD schema regenerated for
aegispolicies.aegisbpf.ioandaegisclusterpolicies.aegisbpf.io: addsworkloadSelector(podSelector,namespaceSelector,matchNamespaceNames) and the per-ruleactionenum defaulted toBlock. - New example
operator/examples/allow-override.yamldemonstrating the cross-policy Allow override flow (a global block + a namespaced allow carve-out).
- v0.4.x policies that use
spec.selectorcontinue to admit, reconcile, and translate to byte-identical INI output. They simply gain aDeprecated=Truecondition. - The per-rule
actionfield defaults toBlock, so existing rule lists keep their original semantics with no edits. - The CRD remains
v1alpha1. Nov1alpha2bump. - Daemon (
policy_parse.cppand the BPF maps) is unchanged in v0.5.0; the operator translates per-rule Action into the existing[allow_*]and[deny_*]INI sections.
- Per-hook latency tracking (
hook_latencyPERCPU_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_pathmaps) — 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 insrc/bpf_integrity.cppwith break-glass override viaAEGIS_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
- 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
- BPF hook functions instrumented with
record_hook_latency()calls at every return point acrossaegis_exec.bpf.h,aegis_file.bpf.h, andaegis_net.bpf.h handle_event()now processesEVENT_FORENSIC_BLOCKevents from the priority ring bufferBpfStateextended withhook_latency,event_approver_inode,event_approver_path, andpriority_eventsmap pointers- Daemon startup now runs self-tests, reconciles /proc, and checks map capacity after ring buffer creation
- Event union extended with
ForensicEvent forensicmember - Event schema (
config/event-schema.json) extended withForensicBlockEventdefinition - BPF map schema (
docs/BPF_MAP_SCHEMA.md) updated with 4 new maps and memory budget - Feature surface contract updated to validate new components
ForensicEventstatic_assert corrected to 104 bytes (was 112)
- Test suite: 210/210 passing
- Feature surface contract: passing
- Build: zero errors, zero warnings
Cross-kernel enforcement hardening, on-hardware trust evidence, and supply-chain CI hygiene. Highlights since v0.8.0:
- Agent now starts and enforces on kernels < 6.1 (#265): the
handle_exitprocess-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_upverifies on all kernels (#267): fixed a non-monotonic 6.8 verifier rejection (R0out of[-4095,0]) via thebarrier_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.
- 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).
- Least-privilege
GITHUB_TOKENpermissions 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
authoritativeflip 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.
- Added a categorized documentation index (
docs/README.md) and consolidated the docs tree (#271).
- 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.mdfor full details
- New safe crypto wrapper functions (
src/tweetnacl_safe.hpp)crypto_sign_detached_safe()- Stack-based signature generationcrypto_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 analysisSECURITY_FIX_SUMMARY.md- Implementation summaryscripts/verify_security_fix.sh- Automated verification
- Updated
crypto.cppto use safe crypto wrappers exclusively - Enhanced error messages to indicate size limit constraints
- Updated
SECURITY.mdwith security fixes history section
- Neutral to positive impact: stack allocation faster than heap
- Predictable memory usage with no fragmentation
- No measurable difference in test suite runtime
- Test suite expanded: 153 → 157 tests (all passing)
- Added edge case tests: empty messages, invalid signatures, boundary conditions
- Full backward compatibility verified
- OWASP Top 10 2021 compliant
- CERT Secure Coding Standards compliant
- CWE/SANS Top 25 compliant
- Memory safety guaranteed
- 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
- 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-leveland--log-formatCLI options--seccompflag 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
- 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
- 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
- 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_VERIFYbypass 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
- 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