Status#
Accepted (reaffirmed 2026-04-16)
Supersedes the database part of the original decision. The original ADR selected SQLite; the project has since migrated to redb (pure Rust embedded KV store). See Decision → Database below. Language selection (Rust) is unchanged.
Context#
The existing ProcMonD prototype was implemented in Python with SQLite as the database backend. While the Python implementation proved the concept, it faced several limitations in production environments:
- Performance bottlenecks. Process enumeration and hash calculation are CPU-intensive operations that suffer from Python's Global Interpreter Lock (GIL).
- Memory consumption. Python's object overhead and garbage collection patterns lead to higher memory usage than necessary.
- Deployment complexity. Requires Python runtime, virtual environments, and dependency management in production.
- Cross-platform challenges. Platform-specific imports and dependencies require complex fallback mechanisms.
- Security concerns. Dynamic typing and runtime interpretation increase attack surface compared to compiled alternatives.
Business requirements (purposes)#
These are the functional purposes that drive the language decision. Each is evaluated by what it needs to accomplish, not by which library implements it:
| # | Purpose | Constraint |
|---|---|---|
| 1 | Detect process anomalies without degrading host performance | <5% sustained CPU, <100MB RAM, <5s for 10k+ processes |
| 2 | Prevent injection in the custom SQL-like detection DSL | AST validation at load time, SELECT-only enforcement, function whitelist |
| 3 | Maintain tamper-evident forensic audit trail | Append-only ledger, BLAKE3 hash chains, Merkle tree inclusion proofs |
| 4 | Enforce privilege separation with immediate drop | Only procmond elevates; detection/alerting in user space |
| 5 | Operate offline-first in air-gapped environments | Embedded database, no external dependencies, bundle-based config |
| 6 | Collect kernel-level events on Linux, Windows, and macOS | eBPF (Linux), ETW (Windows), EndpointSecurity (macOS) |
| 7 | Scrub cryptographic secrets from memory after use | Deterministic zeroization, no secret lingering in RAM |
| 8 | Deploy as single binary to all target platforms | Linux, macOS, Windows, FreeBSD on x86_64 and ARM64 |
| 9 | Satisfy CISA/NSA memory-safe language guidance | STIG/CMMC/IC compliance narrative |
| 10 | Deliver open-source Rust crate connectors | powerbi-rs, qradar-rs, logrhythm-rs for ecosystem contribution |
Decision#
Language: Rust#
Implement DaemonEye using Rust as the primary language.
Rationale by purpose:
- Memory safety (Purposes 7, 9). Compile-time ownership model eliminates buffer overflows, use-after-free, and data races.
unsafe_code = "forbid"is enforceable at workspace level — no equivalent enforcement exists in Go. - Deterministic resource cleanup (Purpose 7). Rust's drop model guarantees secrets are scrubbed when values go out of scope.
zeroize+secrecycrates provide cross-platform deterministic zeroization. Go's GC cannot guarantee when or whether memory is reclaimed, andruntime/secret(Go 1.26) is experimental and Linux-only. - Predictable performance (Purpose 1). No garbage collector means no GC pauses competing with the <5% CPU budget. Under a tight <100MB RSS cap, Go's GC tuning (GOGC + GOMEMLIMIT) trades CPU for headroom — an inherent tension that Rust avoids.
- Cross-platform kernel collection (Purpose 6). Rust's FFI via
bindgen(no runtime overhead) is cleaner than CGO for wrapping platform-native APIs (ETW on Windows, EndpointSecurity on macOS). Windows gap is narrower than initially assessed; see Alternatives Considered → Go. - Single binary deployment (Purpose 8). Both Rust and Go produce static binaries for pure code. Platform collectors requiring native bindings use FFI in Rust vs CGO in Go — Rust's approach preserves the static binary while CGO typically does not.
- Ecosystem contributions (Purpose 10). Commercial roadmap includes open-source Rust crate deliverables (LogRhythm, QRadar, Power BI connectors). These are ecosystem plays specific to the Rust community.
Database: redb (updated from the original SQLite selection)#
The original ADR selected SQLite. The project has since migrated to redb (pure Rust embedded key-value store) for:
- In-process operation. No external dependencies, suitable for air-gapped deployment.
- ACID transactions. Full transactional guarantees for audit ledger integrity.
- Concurrent access. MVCC with multiple concurrent readers, unlike bbolt's single-writer model.
- Memory efficiency. Zero-copy deserialization keeps RSS within the <100MB budget.
- Pure Rust. No C dependencies, no linking complexity, auditable codebase.
See ADR-0002 for current crate selections and ADR-0006 for the Phase 2 SQL execution decision (DataFusion over redb via custom TableProviders).
Technology stack summary#
- Runtime: Rust 2024 Edition (MSRV 1.91+) with async/await via Tokio.
- Database: redb (pure Rust embedded KV store).
- Process enumeration: sysinfo (cross-platform, parallel enumeration).
- CLI: clap v4 with derive macros.
- Cryptography: BLAKE3, SHA-256, ed25519-dalek, zeroize, secrecy.
- Serialization: serde ecosystem, protobuf for IPC.
Migration approach#
- Functional parity. Implement all existing detection capabilities before adding enhancements.
- Configuration evolution. Migrate from INI to YAML/TOML with conversion utilities.
- Incremental validation. Phase-based approach allowing parallel operation during transition.
Consequences#
Positive#
- Performance gains. Expected 5-10x improvement in CPU efficiency and memory usage vs Python.
- Security enhancement. Compile-time safety eliminates buffer overflows and memory corruption.
- Deterministic secret handling. zeroize/secrecy provide cross-platform scrubbing guarantees.
- Operational simplicity. Single binary deployment with no runtime dependencies.
- Cross-platform consistency. Uniform behavior across all supported operating systems.
- Development velocity. Strong typing and comprehensive testing reduce debugging time.
- Ecosystem contribution. Open-source Rust crate connectors (powerbi-rs, qradar-rs, logrhythm-rs).
- Compliance. Rust is listed as a memory-safe language in the June 2025 CISA/NSA CSI.
Negative#
- Learning curve. Team must become proficient in Rust development patterns. Mitigation: dedicated Rust learning resources and gradual team onboarding.
- Migration effort. Existing Python codebase cannot be directly translated. Mitigation: phase-based approach allowing parallel operation during transition.
- Ecosystem risk. Some specialized libraries may be less mature than Python equivalents. Mitigation: prototype critical dependencies early to validate maturity.
- Compilation time. Rust compilation is slower than Python interpretation during development. Mitigation: optimize build pipeline with cargo-watch and incremental compilation.
Neutral#
- Success metrics. Process enumeration <5 seconds for 10,000+ processes; <100 MB resident memory under normal operation; <5% sustained CPU usage during continuous monitoring; >99.9% uptime target; functional parity with Python prototype within 12 weeks of start.
- Review triggers. Revisit at v1.0 completion (target 2026-10-09), or if Go's
runtime/secretreaches stable with cross-platform support, or if a Linux-first product variant is pursued (hybrid architecture becomes favored), or if team size grows beyond single maintainer (hybrid toolchain cost becomes acceptable). - Review history.
- 2025-01-13 — Accepted (original decision, Rust + SQLite).
- 2026-04-16 — Reaffirmed (purpose-driven Go feasibility review; DB migrated to redb; Go rejected on Purposes 6 and 7).
- 2026-04-16 — Deep-dive addendum (ETW consuming proven pure-Go; ESF gap narrowed to one Obj-C block; hybrid architecture documented as future option).
- 2026-04-18 — Backfilled to Nygard-style template.
References#
Go ecosystem (ETW): microsoft/go-winio (MIT, Microsoft-maintained, no CGO); 0xrawsec/golang-etw (GPL-3, incompatible, but proves the technique); Microsoft ETW docs; Go issues #20823, #9240 on foreign-thread callbacks.
Go ecosystem (macOS): xorrior/goesf (CGO+ObjC shim); gatkinso/gomac/endpointsecurity (CGO wrapper); 0x4D31/santamon (sidesteps ES via Santa telemetry); ebitengine/purego (dlopen without CGO).
Go ecosystem (general): cilium/ebpf-go (foundation of Tetragon/Falco/Cilium); cilium/tetragon (CNCF eBPF agent, <1% CPU); awnumar/memguard (off-heap mlock'd secrets).
Compliance: CISA/NSA Memory-Safe Languages CSI (June 2025).
Alternatives Considered#
Go — purpose-driven analysis (2026-04-16)#
A thorough feasibility review evaluated Go against each functional purpose rather than comparing libraries.
Purposes Go fulfills well (11 of 13): The majority of DaemonEye's functional purposes are language-agnostic and Go can fulfill them — memory safety (via GC), privilege separation, audit trail with hash chains and Merkle proofs, custom detection DSL (participle/ANTLR4-Go/pigeon), alert delivery, offline-first operation, single-binary cross-compile for pure Go, CISA/NSA compliance (Go and Rust listed equivalently), extensible collector framework (interfaces), event bus (channels), and Linux eBPF (where Go is actually stronger via cilium/ebpf-go and Tetragon precedent).
Purposes with moderate gaps (2 of 13): Embedded database under 100 MB RSS (bbolt ACID-but-single-writer, Badger has documented RSS blowups — achievable with care but less margin than redb); process enumeration at scale (stock gopsutil has documented performance issues; Datadog maintains a custom fork — achievable but requires custom work).
Platform kernel collection deep-dive:
- Linux eBPF — Go is stronger. cilium/ebpf-go is the foundation of Tetragon, Falco, Pixie, and Cilium.
- Windows ETW — Gap closed. Pure-Go ETW consuming is proven via mkwinsyscall +
NewCallbackCDecl;ProcessTraceinvokes callbacks on the caller's thread, sidestepping Go's foreign-thread callback limitation. go-winio (MIT) provides named pipes and ETW producing without CGO. A clean-room implementation of ETW consuming is feasible (the technique is unencumbered — Microsoft public docs). - macOS EndpointSecurity — Narrow gap.
es_new_client()takes an Objective-C block, not a C function pointer. Mitigations: 30-line CGO shim; separate native process with protobuf IPC; the santamon pattern (read Santa telemetry from a spool directory). - Windows IPC / ETW producing — viable without CGO via go-winio.
Genuine remaining gaps: Windows ETW consuming (closed — proven pure-Go technique, clean-room required); macOS EndpointSecurity (narrow — one Obj-C block, solvable with a 30-line CGO shim or IPC pattern); secret zeroization (open — Go'sruntime/secretis experimental and Linux-only;memguardexists but adds complexity; Rust'szeroize/secrecyremain superior for FIPS-adjacent key handling).
Verdict: Go can fulfill the vast majority of DaemonEye's functional purposes. The only genuinely open gap is cross-platform deterministic secret zeroization. Rejected as the primary language — but the gap is narrower than initially assessed, which enables the hybrid option below.
Hybrid Go + Rust architecture (evaluated 2026-04-16)#
Go handles the bulk of DaemonEye; Rust handles only the components where Go has genuine limitations. DaemonEye's existing protobuf IPC between separate processes means the language boundary and the process boundary are the same line — the cleanest possible hybrid, no FFI or CGO in the hot path.
Proposed component split:
- Go: daemoneye-agent (detection engine, alert delivery, event bus, storage); daemoneye-cli (operator interface, queries, rule management); Linux eBPF (cilium/ebpf-go); Windows ETW (clean-room mkwinsyscall consumer).
- Rust: procmond (elevated process collector, binary hashing, audit ledger, privilege drop); macOS ESF (EndpointSecurity client with Obj-C block); crypto-core (secret handling, key derivation, signing via zeroize/secrecy).
- Communication: protobuf IPC over Unix sockets / named pipes, unchanged.
Gains: better Linux eBPF ecosystem; faster development for agent/CLI/detection engine; simpler concurrency via Go channels; broader contributor pool for SIEM integrations. Costs: two toolchains (cargo + go), two CI matrices, two dependency audit pipelines; two test ecosystems; integration testing complexity; open-source crate story changes (connectors as Go libs or Rust-only for procmond); cognitive overhead for contributors needing both languages.
Verdict: Technically viable and architecturally clean. For a single-maintainer project, the operational cost of two full toolchains outweighs the development velocity gains. Documented as a viable future option if team size grows or if a Linux-first product variant is pursued. See ADR-0003 (Polyglot Collector SDK Strategy) for the collector-specific approach that does adopt a hybrid model at the collector layer.
C++#
Maximum performance, mature ecosystem, no runtime overhead. Manual memory management increases development time and security risks; does not satisfy CISA memory-safe language guidance. Rejected — development velocity and safety requirements favor Rust.
Keeping Python#
Existing codebase, familiar technology, rapid development. Performance limitations (GIL, object overhead), deployment complexity (runtime + venvs + dependency management), security concerns (dynamic typing expands attack surface). Rejected — performance requirements (Purpose 1) cannot be met.