Port config/mod.rs (AgentConfig) pure parts to untyped Jerboa
ober
1df902fee9df58adb0782aa5ca475e3918aaf2e8
--- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ SCHEME ?= $(JERBOA)/.chez/bin/scheme BUILD ?= build/rust TYPED := $(wildcard typed/*.ss) -.PHONY: rust test ffi-demo kernels-check triage-check triage-store-check analytics-check detect-check storage-check threats-check geoip-check sigma-check yaml-rules-check buffer-check dns-sniffer-check suspicious-check netconn-check kernmod-check selinux-check container-check dns-servers-check sensitive-path-check dtrace-parse-check proc-linux-check freebsd-parse-check event-meta-check checks clean +.PHONY: rust test ffi-demo kernels-check triage-check triage-store-check analytics-check detect-check storage-check threats-check geoip-check sigma-check yaml-rules-check buffer-check dns-sniffer-check suspicious-check netconn-check kernmod-check selinux-check container-check dns-servers-check sensitive-path-check dtrace-parse-check proc-linux-check freebsd-parse-check event-meta-check config-check checks clean # Combined libdir path so sibling libraries `(jsecmon ...)` resolve to ./jsecmon # (a second --libdirs would replace, not append, the jerboa one). LIBDIRS := "$(JERBOA)/lib:$(CURDIR)" @@ -181,6 +181,13 @@ freebsd-parse-check: event-meta-check: $(SCHEME) --libdirs $(LIBDIRS) --script examples/event_meta_check.ss +# Agent configuration (secmon src/config/mod.rs): AgentConfig defaults, the +# from_env merge (bad numeric values keep the default, not zero), and the +# platform-defaulted local db/key paths. Pure — env reads and build-embedded +# secrets are deferred — parameterized over a getenv callback. No native lib. +config-check: + $(SCHEME) --libdirs $(LIBDIRS) --script examples/config_check.ss + # Everything that runs through the Jerboa side of the bridge, one shot. checks: kernels-check $(SCHEME) --libdirs $(LIBDIRS) --script examples/triage_check.ss @@ -205,6 +212,7 @@ checks: kernels-check $(SCHEME) --libdirs $(LIBDIRS) --script examples/proc_linux_check.ss $(SCHEME) --libdirs $(LIBDIRS) --script examples/freebsd_parse_check.ss $(SCHEME) --libdirs $(LIBDIRS) --script examples/event_meta_check.ss + $(SCHEME) --libdirs $(LIBDIRS) --script examples/config_check.ss clean: rm -rf $(BUILD) --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ make dtrace-parse-check # DTrace SECMON|TYPE|... line parser (exec/exit/connect/ make proc-linux-check # Linux /proc parsers: stat ppid+comm, uid, TCP state, net hex IP make freebsd-parse-check # FreeBSD kldstat row + sockstat addr:port (decimal, wildcard, v6) make event-meta-check # event-type -> display severity + coarse store-priority u8 tables +make config-check # AgentConfig defaults + from_env merge + platform db/key paths make checks # every Jerboa-side check in one shot ``` @@ -114,5 +115,6 @@ then crypto orchestration, then I/O / async / FFI (monitors, server, storage). | `platform::linux` (/proc parsers) | `jsecmon/proc-linux.ss` | ✅ **untyped layer** — the pure parsing helpers with the file reads stripped: `parse_stat` (comm between first `(` and **last** `)`, ppid the 2nd field after `") "`), `parse_uid` (first `Uid:` line, 2nd field), `hex_to_state` (TCP state table → `UNKNOWN`), `parse_ipv4` (little-endian hex → dotted quad), `parse_ipv6` (32-hex → 8 groups), `parse_addr` (`HEXADDR:HEXPORT`, ipv6 when protocol contains `6`). Pure text/number parsing, so untyped. `parse_stat`/`parse_uid` use `.parse::<u32>().ok()` so failure is `#f` (not 0) and negatives are rejected; `parse_ipv4` rejects >`0xFFFFFFFF`; ports are u16. `make proc-linux-check` reproduces secmon's five linux.rs tests + ipv6/parse-addr + a comm-with-paren corner. (The `/proc` reads and inode→pid scan are the deferred I/O.) | | `platform::freebsd` (parsers) | `jsecmon/freebsd-parse.ss` | ✅ **untyped layer** — the pure parsing helpers with the command/file reads stripped: `parse_kldstat_line` (≥5 whitespace fields, name is `parts[4]`, size is `parts[3]` as hex with optional `0x`, size `None` on non-hex via `.ok()`, action always `Loaded`) and `parse_address` (`addr:port` split at the **last** `:`, `[ipv6]:port` split at the first `]`, `*` address → `0.0.0.0`, `*` port → `0`). Ports here are **DECIMAL** u16 (`.parse()`), unlike Linux's hex `/proc/net`. Pure text/number parsing, so untyped. `make freebsd-parse-check` reproduces secmon's three freebsd.rs tests + ipv6/wildcard/negatives. (The `kldstat`/`sockstat` command runs are the deferred I/O.) | | `event_json` + `local_store` (tables) | `jsecmon/event-meta.ss` | ✅ **untyped layer** — the pure classification tables lifted out of the payload-carrying `EventType` enum: `event_json.rs` `get_event_json_data`'s **display severity** (25 constant arms as a name→severity table, + the 7 payload-dependent arms as named helpers taking the deciding field — `auth`/`privilege_change`/`mount`/`capability`/`podman`/`selinux`/`lateral_movement`), and `local_store.rs` `event_severity_u8`'s **coarse store priority** 0..3, which is an *independent* scale (e.g. `privilege_escalation` is `critical` for display but `0` for the store). secmon has no `#[test]` here, so `make event-meta-check` asserts both full tables arm-for-arm against the Rust source. (The JSON payload bodies stay with the I/O layer that owns the event structs.) | +| `config` | `jsecmon/config.ss` | ✅ **untyped layer** — `AgentConfig`'s pure parts: the defaults (`0.0.0.0:31337`, poll `100`ms, buffer `10000`), the `from_env` merge (overwrites `listen_addr` on any present value but only overwrites poll/buffer when the value parses as strict u64 — a bad value **keeps the default**, it is not zeroed), and `local_db_path`/`local_key_path` (env override, else `/opt/secmon/{events.db,local.key}` on linux+freebsd, else the `./secmon_*` cwd fallback). Parameterized over a `getenv` callback + a `platform` symbol so the env reads stay deferred I/O; the build.rs-embedded secrets (`get_public_key`/`get_psk`/`is_debug_mode`) belong to the build/FFI phase, not this layer. secmon has no tests here, so `make config-check` asserts the behaviour against the Rust source. | | `monitor::dns_sniffer` (DNS wire parser + dedup) | `jsecmon/dns-sniffer.ss` | ✅ **untyped layer** — the platform-independent half of secmon's `src/monitor/dns_sniffer.rs`: the DNS wire-format parser (QNAME decoding with compression-pointer chasing capped at 128 steps, QTYPE→string, question + A/AAAA answer-RR extraction) and the 5s dedup / 30s cleanup state machine. Every bounds check is preserved — a truncated/malformed/looping packet yields `#f`, never a bad read. Pure byte parsing → untyped, like geoip. The AF_PACKET raw-socket capture + `/proc` PID lookup stay for the monitor I/O driver. `make dns-sniffer-check` reproduces secmon's parser + dedup tests (+ AAAA, qtype table, pointer-loop/qdcount guards). | | monitors / server / ebpf / dtrace | — | ⏳ I/O+async+FFI, last | new file mode 100644 --- /dev/null +++ b/examples/config_check.ss @@ -0,0 +1,79 @@ +;;; Parity check for (jsecmon config) against secmon's src/config/mod.rs +;;; (AgentConfig::default / from_env / local_db_path / local_key_path). secmon +;;; has no #[test] for config, so this derives every expectation from the Rust +;;; source and IS the spec for the port. +;;; +;;; scheme --libdirs "$JERBOA/lib:." --script examples/config_check.ss + +(import (jerboa prelude) + (jsecmon config)) + +(def fails 0) +(def (check name got want) + (let ((ok (equal? got want))) + (unless ok (set! fails (+ fails 1))) + (displayln (if ok " ok " " FAIL ") name + (if ok "" (str " got " got " want " want))))) + +;; env getter from an alist; absent key -> #f (like std::env::var Err). +(def (env-of . pairs) + (lambda (k) (cond ((assoc k pairs) => cdr) (else #f)))) +(def none (env-of)) +(def (field cfg k) (cdr (assq k cfg))) + +;; ── AgentConfig::default ────────────────────────────────────────────────────── +(displayln "default:") +(def d (default-config)) +(check "default listen" (field d 'listen-addr) "0.0.0.0:31337") +(check "default poll" (field d 'poll-interval-ms) 100) +(check "default buffer" (field d 'max-buffer-size) 10000) + +;; ── from_env: empty env keeps defaults ─────────────────────────────────────── +(displayln "from_env empty:") +(def e0 (config-from-env none)) +(check "empty listen" (field e0 'listen-addr) "0.0.0.0:31337") +(check "empty poll" (field e0 'poll-interval-ms) 100) +(check "empty buffer" (field e0 'max-buffer-size) 10000) + +;; ── from_env: overrides ────────────────────────────────────────────────────── +(displayln "from_env overrides:") +(def e1 (config-from-env (env-of '("SECMON_LISTEN" . "127.0.0.1:9999") + '("SECMON_POLL_MS" . "250") + '("SECMON_BUFFER_SIZE" . "50000")))) +(check "listen set" (field e1 'listen-addr) "127.0.0.1:9999") +(check "poll set" (field e1 'poll-interval-ms) 250) +(check "buffer set" (field e1 'max-buffer-size) 50000) +;; SECMON_LISTEN present but empty -> still used (Ok("") overrides) +(check "listen empty string" (field (config-from-env (env-of '("SECMON_LISTEN" . ""))) 'listen-addr) "") + +;; ── from_env: bad numeric values keep the default (parse Err) ──────────────── +(displayln "from_env bad parse keeps default:") +(for-each + (lambda (bad) + (check (str "poll " bad " -> 100") + (field (config-from-env (env-of (cons "SECMON_POLL_MS" bad))) 'poll-interval-ms) + 100)) + '("abc" "-5" "1.5" "1e3" "" " 100 " "0x10" "100abc")) +(check "buffer bad -> 10000" + (field (config-from-env (env-of '("SECMON_BUFFER_SIZE" . "xx"))) 'max-buffer-size) + 10000) +;; "0" is a valid u64 and DOES override +(check "poll 0 valid" (field (config-from-env (env-of '("SECMON_POLL_MS" . "0"))) 'poll-interval-ms) 0) + +;; ── local_db_path / local_key_path ────────────────────────────────────────── +(displayln "paths:") +(check "db env override" + (local-db-path (env-of '("SECMON_DB_PATH" . "/tmp/x.db")) 'linux) "/tmp/x.db") +(check "db linux" (local-db-path none 'linux) "/opt/secmon/events.db") +(check "db freebsd" (local-db-path none 'freebsd) "/opt/secmon/events.db") +(check "db macos" (local-db-path none 'macos) "./secmon_events.db") +(check "key env override" + (local-key-path (env-of '("SECMON_KEY_PATH" . "/tmp/x.key")) 'macos) "/tmp/x.key") +(check "key linux" (local-key-path none 'linux) "/opt/secmon/local.key") +(check "key freebsd" (local-key-path none 'freebsd) "/opt/secmon/local.key") +(check "key macos" (local-key-path none 'macos) "./secmon_local.key") + +(newline) +(if (= fails 0) + (displayln "OK: config matches secmon's config/mod.rs behaviour.") + (begin (displayln fails " FAILURES") (exit 1))) new file mode 100644 --- /dev/null +++ b/jsecmon/config.ss @@ -0,0 +1,85 @@ +#!chezscheme +;;; jsecmon agent configuration (secmon src/config/mod.rs), untyped. +;;; +;;; The pure parts of secmon's AgentConfig: the default values, the env-var +;;; merge (from_env), and the platform-defaulted local store paths. The actual +;;; env reads and the build.rs-embedded secrets are NOT here: +;;; * get_public_key/get_psk/get_private_key/is_debug_mode are build-time +;;; constants injected by build.rs (EMBEDDED_*), not source logic — they +;;; belong to the build/FFI phase, not this pure layer. +;;; * std::env::var reads are the deferred I/O: every function here takes a +;;; `getenv` callback (name -> string | #f) so the merge logic is pure and +;;; checkable, and a `platform` symbol (linux|freebsd|macos|...) instead of +;;; Rust's cfg!(target_os). +;;; +;;; A config is the alist ((listen-addr . s) (poll-interval-ms . n) +;;; (max-buffer-size . n)). +;;; +;;; Faithfulness notes: +;;; * from_env overwrites listen_addr whenever SECMON_LISTEN is *present* +;;; (any string, even ""), but only overwrites poll/buffer when the value +;;; PARSES (Rust `if let Ok(x) = s.parse()`) — a bad value keeps the +;;; default, it is NOT zeroed. parse is strict u64: a decimal non-negative +;;; integer in [0, 2^64); "-5", "1.5", "1e3", "abc", "", " 100 " all fail. +;;; * local_db_path/local_key_path: SECMON_DB_PATH/SECMON_KEY_PATH override; +;;; else /opt/secmon/{events.db,local.key} on linux+freebsd, else the +;;; ./secmon_{events.db,local.key} cwd-relative fallback. +;;; +;;; secmon has no #[test] for config, so examples/config_check.ss derives every +;;; expectation from the Rust source and IS the spec for this port. + +(library (jsecmon config) + (export default-config config-from-env local-db-path local-key-path + *default-listen-addr* *default-poll-interval-ms* *default-max-buffer-size*) + (import (except (chezscheme) + make-hash-table hash-table? + sort sort! + printf fprintf + path-extension path-absolute? + with-input-from-string with-output-to-string + iota 1+ 1- + partition + make-date make-time) + (except (jerboa prelude) meta atom?)) + + (def *default-listen-addr* "0.0.0.0:31337") + (def *default-poll-interval-ms* 100) + (def *default-max-buffer-size* 10000) + + ;; AgentConfig::default(). + (def (default-config) + (list (cons 'listen-addr *default-listen-addr*) + (cons 'poll-interval-ms *default-poll-interval-ms*) + (cons 'max-buffer-size *default-max-buffer-size*))) + + ;; s.parse::<u64>().ok(): a strict decimal non-negative integer in [0, 2^64), + ;; or #f. exact? rejects "1e3"/"1.5" (parse to inexact integers/floats). + (def (parse-u64-opt s) + (let ((n (string->number s))) + (if (and n (integer? n) (exact? n) (>= n 0) (< n (expt 2 64))) n #f))) + + ;; AgentConfig::from_env(): merge env over the defaults. `getenv` maps an env + ;; name to its string value or #f when unset. + (def (config-from-env getenv) + (let* ((listen (getenv "SECMON_LISTEN")) + (poll (getenv "SECMON_POLL_MS")) + (buf (getenv "SECMON_BUFFER_SIZE")) + (poll-n (and (string? poll) (parse-u64-opt poll))) + (buf-n (and (string? buf) (parse-u64-opt buf)))) + (list (cons 'listen-addr (if (string? listen) listen *default-listen-addr*)) + (cons 'poll-interval-ms (or poll-n *default-poll-interval-ms*)) + (cons 'max-buffer-size (or buf-n *default-max-buffer-size*))))) + + ;; AgentConfig::local_db_path(): SECMON_DB_PATH override else platform default. + (def (local-db-path getenv platform) + (let ((p (getenv "SECMON_DB_PATH"))) + (cond ((string? p) p) + ((or (eq? platform 'linux) (eq? platform 'freebsd)) "/opt/secmon/events.db") + (else "./secmon_events.db")))) + + ;; AgentConfig::local_key_path(): SECMON_KEY_PATH override else platform default. + (def (local-key-path getenv platform) + (let ((p (getenv "SECMON_KEY_PATH"))) + (cond ((string? p) p) + ((or (eq? platform 'linux) (eq? platform 'freebsd)) "/opt/secmon/local.key") + (else "./secmon_local.key")))))