enodia-sentinal/docs/GO_PORT_HANDOFF.md

8.1 KiB

Go Port Handoff

Saved: 2026-07-22T02:48:00-07:00 Branch: main Base commit: 6355fb4 (feat(go): expose retained incident views) Status: implemented and green, but uncommitted

Worktree warning

The checkout is intentionally dirty and contains work from multiple related continuations. Do not reset, clean, or broadly restage it.

  • The validation-sidecar, incident-persistence, bounded-enrichment, local- assurance, asynchronous-enrichment, and incident-reader tranches are signed commits f85c2e8, 538d1d9, fd2bbba, 1ab4add, eff31b3, and 6355fb4. The package-ownership enrichment slice is uncommitted.
  • At this checkpoint, git status --short has 20 entries with untracked directories collapsed.
  • The Python GUI files and tests are separate pre-existing work. Preserve them while continuing the Go port.
  • build/ is ignored and contains only local build output.

Implemented Go surface

The Go module requires Go 1.25 or newer and is currently verified with go1.26.5-X:nodwarf5. It uses github.com/cilium/ebpf v0.22.0 and builds as a static binary.

Polling and lifecycle

  • All current Python poll detectors have Go parity: reverse_shell, ld_preload, deleted_exe, input_snooper, credential_access, stealth_network, memory_obfuscation, egress, first_seen, new_listener, persistence, and new_suid.
  • --state-dir enables isolated listener/SUID baselines, startup grace, persistence scans, and atomic first-seen retention.
  • The Go state directory must never be the Python daemon's /var/log/enodia-sentinel directory.

Event rules and native sources

  • Exec rules: built-in SIDs 100001-100004 plus configured rules.
  • Syscall rules: SIDs 100060-100066.
  • Typed host rules: SIDs 100067-100073 and 100076-100078.
  • Mixed JSONL routing, rule catalog operations, correlation SID 100080, and one shared cooldown gate are implemented.
  • Embedded CO-RE exec and filtered raw-syscall probes use cilium/ebpf and fail open to polling on load/attach/read failures.
  • Native typed transports include UID/GID changes, chmod/chown variants, capset, finit_module, confirmed interpreter write/writev/pwrite64/ pwritev/pwritev2, and successful TCP connect/bind/listen/accept calls with /proc endpoint resolution.
  • Generated little- and big-endian BPF objects live under go-agent/internal/ebpfsource/. Run make generate-go only after changing go-agent/internal/ebpfsource/bpf/*.bpf.c.

Production validation path

  • make build-go creates a stripped, static, VCS-independent binary.
  • make install-go installs the separate binary and enodia-sentinel-go-sidecar.service without replacing or enabling the Python service.
  • The hardened systemd unit has a current systemd-analyze security exposure score of 4.3 (OK). It preserves the host /proc, /tmp, and network view because detectors require them, while hiding the Python state tree.
  • The unit uses Type=notify; READY is sent only after baseline setup, event log preflight, and both requested probe-load attempts.
  • JSONL remains visible in journald and is also retained as private bounded /var/lib/enodia-sentinel-go/events.jsonl plus events.jsonl.1 (64 MiB per active segment boundary). Non-status events are synchronized to disk.
  • /var/lib/enodia-sentinel-go/heartbeat is atomically refreshed after each successful status emission.
  • enodia-sentinel-go --health returns enodia.go.health.v1 JSON and a watchdog-friendly exit status.
  • enodia-sentinel-go --events-tail N reads active and rotated retained events chronologically, validates JSON, fails closed on corruption, and deliberately works without loading the agent configuration.
  • The unit now retains private alert-YYYYMMDD-HHMMSS.{json,log} pairs using the required enodia.alert.snapshot.v1 fields. Same-second alerts merge into one pair, process context is collected best-effort from /proc, and shared max_snapshots / max_snapshot_age_days limits are applied at startup, capture, and status time.
  • enodia.status.v1 totals, severity counts, and last-alert time are populated only from parseable retained Go snapshots. Python's unchanged list_alerts and get_alert data layer successfully consumed a Go-produced pair.
  • Snapshot incident IDs now reference an atomic private incidents.json using the required enodia.incident.v1 fields. Grouping mirrors Python's process- lineage-first, time-window fallback behavior, the index is bounded to 1000 recent incidents, and same-second alert batches update one incident without duplicating the snapshot name.
  • Incident severity/signatures/SIDs/PIDs/lineage/timestamps are updated additively and correlation SID 100080 is persisted. Python's unchanged list_incidents and get_incident consumers successfully loaded a live Go-produced index, snapshot, and timeline.
  • A bounded no-I/O enrichment stage now derives Python-shaped process/parent context, lineage, remote-IP classification, and candidate paths entirely from already-captured records. Alert/process/indicator caps bound attacker- controlled work on the emission path.
  • Snapshot log/JSON revisions now append synchronized private enodia.hash_chain.v1 records. Artifact SHA-256 is streamed, previous-hash lookup reads a bounded chain tail, and append completion is synced before the alert event is emitted.
  • A single 16-job asynchronous worker now streams SHA-256 only for regular executables at most 8 MiB and adds lstat file metadata. Slow I/O occurs outside the snapshot lock; a full queue simply leaves optional fields unknown.
  • The asynchronous worker now resolves pacman/dpkg/rpm package-owner strings with a two-second bound and cache; ownership lookup never holds a detector lock or blocks alert capture.
  • Richer integrity anchors and notification fan-out are not yet ported.
  • --incidents-list and --incident-show <id> now read the sidecar's isolated state without starting the agent. The latter returns enodia.incident.view.v1 with the incident, available snapshots, and time-ordered timeline.
  • No live system service was installed or enabled during development.

Last green verification

Run from the repository root:

python3 -m unittest discover -s tests -q
cd go-agent
GOCACHE=/tmp/enodia-go-cache go test ./...
GOCACHE=/tmp/enodia-go-cache go test -race ./...
GOCACHE=/tmp/enodia-go-cache go vet ./...
go mod verify
cd ..
make parity-go
make check-go-service
git diff --check

Results at save time:

  • Python: 377 tests passed.
  • Go unit and race suites: all packages passed.
  • go vet and go mod verify: passed.
  • Parity: 22 poll alerts, 5 exec alerts, 7 syscall alerts, 10 typed-host alerts, 22 catalog records, 3 mixed-stream alerts, and 2 correlations.
  • systemd-analyze verify and the three Go sidecar packaging/isolation tests: passed.
  • Static builds: linux/amd64, linux/arm64, linux/riscv64, and linux/s390x passed.
  • Integration smoke tests passed for static binary identity, JSON emission, READY/STOPPING notification, heartbeat freshness/staleness, retained event equivalence, tail selection, corruption rejection, and evidence reading with malformed TOML. Snapshot smoke tests also passed for same-second batching, severity aggregation, private JSON/text pairs, truthful status counts, startup/quiet-period retention, process context, and Python dashboard-reader compatibility.

The Python suite prints three expected CLI error lines from negative tests and one existing ResourceWarning for an implicitly cleaned-up HTTP 401 object; the suite still exits successfully.

Resume here

  1. Add bounded integrity-anchor collectors to the existing asynchronous worker without destructive response behavior.
  2. Add a read-only management API consumer for isolated Go snapshot/event state while keeping Python authoritative.
  3. Continue broader Phase 3 rule metadata/state parity before considering any default-service or package cutover.

Keep the validation sidecar opt-in, preserve its separate state tree, and keep new attack/event primitives disconnected from destructive response paths.