~ober/jerboa-secmon
Imported from ~/mine/jerboa-secmon
about
# jsecmon
A pure-Jerboa reimplementation of secmon, a host
security monitor. The goal: no hand-written Rust or C. Security-critical kernels
(crypto, secrets, detection scoring) are written in **Typed Jerboa** and
compiled to Rust by jerboa's typed→rust backend; the rest is ordinary Jerboa.
This is a long, incremental port. Each kernel is translated, compiled to Rust,
and **verified against secmon's own test vectors** before it counts as done.
## Layout
```
typed/ Typed Jerboa kernels (.ss) — compile to a Rust crate
tests/ Hand-written Rust tests asserting parity with secmon's vectors
build/rust/ Generated crate (gitignored; `make rust` regenerates)
```
## Build & verify
```
make rust # typed/*.ss → build/rust (a cargo crate)
make test # regenerate, then cargo test against secmon's vectors
make ffi-demo # build the cdylib + drive two kernels from a Jerboa script
make kernels-check # exercise the (jsecmon kernels) library against the vectors
make triage-check # verify the untyped (jsecmon triage) engine vs secmon vectors
make analytics-check # verify untyped risk-ranking + incident grouping vs vectors
make detect-check # full pipeline: events -> detect -> analytics (all kernels)
make storage-check # SQLite round-trip: store -> query -> detect -> analytics
make threats-check # SQL-aggregation threat rules vs secmon detection vectors
make geoip-check # geoip CSV vectors + geoip-gated impossible_travel detector
make sigma-check # Sigma YAML rule importer vs secmon conversion vectors
make yaml-rules-check # user YAML detection rules (threshold/distinct/sequence/match)
make buffer-check # the agent's encrypted event ring buffer (FIFO + priority eviction)
make dns-sniffer-check # DNS wire-format parser (QNAME/compression/answers) + dedup state
make suspicious-check # SuspiciousPatterns: shell/tool-from-service, revshell + miner
make netconn-check # connection classifier: bad-port, high-port-mult-1000, web→external
make kernmod-check # kernel-module classifier: rootkit substring, short name, no vowels
make selinux-check # SELinux audit-log parser: AVC + boolean/policy/role events
make container-check # container/jail escape mount classifier (host bind, docker sock)
make dns-servers-check # resolv.conf nameserver parse + public-resolver union
make sensitive-path-check # DTrace sensitive-path classifier (passwd/ssh/cron/...)
make dtrace-parse-check # DTrace SECMON|TYPE|... parser + stateful EventParser rows
make dtrace-runtime-check # DTrace direct libdtrace/subprocess runtime shell + EventParser drain
make stealth-check # stealth init helpers: anti-debug parsers/tables + runtime decisions
make ebpf-runtime-check # eBPF support guard + libbpf tracepoint/perf stream shell
make proc-linux-check # Linux /proc parsers: stat ppid+comm, uid, TCP state, net hex IP, environ scan
make freebsd-parse-check # FreeBSD kldstat/ps/address + sockstat & netstat connection lines
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 privdrop-check # agent post-bind privilege drop: passwd lookup + setgroups/setgid/setuid order
make event-danger-check # mount is_dangerous + capability dangerous_caps + namespace ns_types
make persistence-check # classify_path (-> persistence type) + suspicious-content line scan
make file-change-check # is_suspicious_change: setuid/setgid added, critical files, sensitive dirs
make webshell-check # web-server-spawned suspicious child: name/cmdline classifier + reason
make platform-mounts-check # is_dangerous_path (per-platform exact set) + get_mounts line parsers
make analyze-cli-check # analyze bin: parse_duration_ms + AlertSink::parse + --flag scanners
make collector-cli-check # collector bin: --after/--format/--db + host normalize + hosts-file
make event-summary-check # storage readers: extract_pid/extract_process_name/build_summary
make ioc-check # storage IOC: detect_ioc_type + is_ipv4 + parse_ioc_text + match field routing
make frame-check # server protocol: length-prefixed wire framing (encode/read_length/decode)
make correlate-check # storage detect_*: brute_force/cred_stuffing/dns_tunnel/suspicious_cron cores
make revshell-check # revshell: is_shell/is_c2_port/is_legitimate + classify_connection
make cron-check # cron: per-platform CRON/PERIODIC path tables + systemd/periodic route
make logtamper-check # logtamper: system-log/history tables + classify-tamper (trunc/mtime)
make detection-rules-check # DETECTION_RULES ATT&CK catalog: rule_attack + anomaly_rule_attack
make daemon-telemetry-check # daemon telemetry normalization + httpd/smtpd/sshd early exploit rules
make mux-telemetry-check # obersh-compatible mux telemetry intake + encrypted storage path
make ipaddr-check # lateral: faithful IpAddr parse (v4/v6) + is_internal_ip (RFC1918/fc00)
make auth-check # auth-log parsers: sshd/sudo/su/pam/useradd/userdel/passwd + extract_field
make monitor-process-check # process monitor scan loop: pid diff/classify/emit over fixture provider
make monitor-network-check # network monitor scan loop: listener/connection dedup+classify over fixture
make monitor-files-check # file integrity monitor: detect-change priority + new-file discovery
make monitor-dns-check # dns monitor capture stream + connection-polling fallback
make monitor-podman-check # podman event-stream parser + polling fallback deltas
make monitor-selinux-check # SELinux monitor loop: mode changes + tailed audit events
make monitor-lateral-check # lateral monitor loop: internal service use + port scan tracking
make monitor-webshell-check # webshell monitor loop: suspicious web-server descendants
make monitor-revshell-check # reverse-shell monitor loop: connection + cmdline signals
make monitor-manager-check # monitor manager: boot + one full polling cycle (fixture)
make event-json-check # event JSON serializer: every Rust EventType variant
make collector-check # agent collector loop: boot+tick into a real SQLite store, rows queried back
make agent-server-check # agent poll server: PSK auth, dispatcher, local-store status, socket lifecycle
make checks # every Jerboa-side check in one shot
```
### The C ABI bridge
The generated crate builds as a `cdylib` exposing each kernel with a C ABI
(`jt_<module>_<fn>(ptr, len, …)`, every Bytes/String argument passed as a
pointer + length). That is the boundary the untyped layer uses: ordinary
Jerboa monitors/storage stay in `.ss` and call the vetted typed kernels over
that ABI via Chez's `foreign-procedure`. `examples/ffi_bridge.ss` proves the
whole path — a Jerboa script `load-shared-object`s the dylib and calls the
`constant-time-eq?` and `score-cmdline` kernels, asserting their results.
`jsecmon/kernels.ss` is the **reusable** form of that bridge: an importable
`(library (jsecmon kernels) …)` that loads the dylib once on import and exports
clean Jerboa wrappers for all 22 kernels (`lolbin-score-cmdline`, `lolbin-match-bits`, `hex-decode`,
`host-risk-score`, `dga-score-domain`, `bytes-contains?`, `phantom-rootkit-race?`,
…). It hides the C ABI entirely — including the out-param dance for Bytes/String
results (alloc two cells, call, copy the buffer, `jt_byte_buffer_free`, free the
cells). The untyped I/O layer just does `(import (jsecmon kernels))`. The repo
root must be on the libdir path so the import resolves to `jsecmon/kernels.ss`.
`examples/kernels_check.ss` re-checks the secmon parity vectors through these
wrappers (`make kernels-check`).
`make` needs a built Jerboa checkout at `$JERBOA` (default `vendor/jerboa`),
whose `.chez/bin/scheme` and `support/typed-rust.ss` drive the backend.
## Binaries
The shippable artifacts are **compiled, self-contained executables** — never
`scheme --script foo.ss` launchers. `build-binary.ss` takes a `bin/*.ss` entry
point, whole-program-compiles it (any stdlib library that ships only a `.so`,
e.g. `(jerboa core)`, comes back in `compile-whole-program`'s missing list and
is bundled into the boot image instead of inlined), bundles the Chez kernel +
stdlib boot, and **statically links** the Rust kernels from
`libjerboa_typed_generated.a` — `-force_load` pulls the archive in whole (the C
`main` never names the `jt_*` symbols; Chez resolves them at runtime via
`dlsym`) and `-export_dynamic` makes them visible to that `dlsym`; `kernels.ss`
does `(load-shared-object #f)` in this mode. The result is one Mach-O/ELF file
that needs no scheme, no `.ss`, and no kernel `.dylib` at runtime.
`make keygen` builds `jsecmon-keygen` (the ECIES keypair + PSK generator, port of
`secmon-keygen`); `make telemetry` builds the mux daemon-telemetry intake;
`make binaries` builds all of them. The `make *-check` vector tests stay as
dev-time `.ss` scripts (the test harness, not shipped).
| secmon binary | jsecmon | status |
|------------------|--------------------|---------------------------------|
| `bin/keygen` | `bin/keygen.ss` → `make keygen` | ✅ **compiled binary** — creates ECIES public/private keys and a 32-byte PSK from the OS CSPRNG as exclusive, no-follow, descriptor-validated `0600` files. It refuses existing paths and prints no secret values. |
| `bin/analyze` | `bin/analyze.ss` → `make analyze` | ✅ **compiled binary** — assembles `run_detections` itself over `(jsecmon storage)`; summary/query/anomalies/first-seen/timeline/retention all wired to the ported detection + analytics layers. |
| `bin/collector` | `bin/collector.ss` → `make collector` | ✅ **compiled binary** — the agent **pull** client: PSK handshake (Challenge → ChallengeResponse) over the transport envelope, then `get_events_after`/`status` requests; each `SerializedEvent` is ECIES-decrypted to a `SecurityEvent` and printed (`--format human`/`json`, secmon-faithful) or persisted to SQLite (`--db`, advancing `collector_state`). `poll`/`status`/`watch` subcommands. Verified end-to-end over a real TCP socket (`examples/fake_agent.ss` loopback) and byte-wise without a socket (`make collector-pull-check`). Loads keys from env/file at runtime, never embedded. |
| `bin/agent` | `bin/agent.ss` → `make agent` | ✅ **compiled binary** — runtime shell: loads the collector public key + PSK from env/file, opens the local encrypted event store when configured, starts the PSK-authenticated pull server, ECIES-encrypts `SecurityEvent` bytes into the priority buffer, persists plaintext events locally, emits `agent_start`/`heartbeat`, initializes stealth before key/config work, and drops to `nobody` after privileged resources are open. On Linux it prefers the eBPF stream and falls back to the provider-backed polling set (`process`, `network`, `files`, `auth`, `kernel`, `scheduled`, `container`, `rootkit`, `podman`, `selinux`, `persistence`, `lateral`, `logtamper`, `webshell`, `revshell`, `dns`). On FreeBSD it wires the DTrace direct/subprocess stream before privilege drop. Verified with `jsecmon-collector status`/`poll` against the compiled agent on a real loopback TCP socket; `agent-server-check` also pins the local-store status fields. |
| daemon telemetry | `bin/telemetry.ss` → `make telemetry` | ✅ **compiled binary** — dedicated mux intake for `jerboa-smtp`/`jerboa-sshd`/httpd-style first-party daemons. Listens on `SECMON_TELEMETRY_LISTEN` (default `127.0.0.1:31338`), uses a fixed bounded worker pool with absolute pre-auth/frame deadlines and global/per-source budgets, opens mux `MSG-ENCRYPTED` frames with the PSK transport key, and stores validated telemetry. |
## Port status
Driven by what each module needs from the backend; pure-logic detection first,
then crypto orchestration, then I/O / async / FFI (monitors, server, storage).
| secmon module | jsecmon | status |
|--------------------------|--------------------|---------------------------------|
| `dga::max_consonant_run` | `typed/dga.ss` | ✅ ported, vectors pass |
| `dga::shannon_entropy` | `typed/dga.ss` | ✅ ported, vectors pass |
| `dga::score_domain` (+ `score_label_bits`, `dga_label`) | `typed/dga.ss` | ✅ full: lowercase + dot-trim + benign-suffix + label split + score; vectors pass. Sibling kernels `score-label-bits` (ORs one disjoint power-of-2 bit per reason, in the same source order as the weight table — `+` is bitwise-or since the bits don't overlap) and `dga-label` (the exact leftmost label scored, `""` for a benign CDN suffix / empty label) let the untyped layer recover `DgaVerdict.reasons`/`label`/`entropy`/`run`/`length` with no second copy of any scoring decision. |
| `&str` ops (lowercase/ends_with/starts_with/contains/split/whole-word) | `typed/strbytes.ss` | ✅ Bytes toolkit, vectors pass — shared by dga/lolbin/sigma |
| `lolbin::score` + `severity` | `typed/lolbin.ss` | ✅ full 25-pattern table + severity buckets, plus `match-bits` (a u64 bitmask of which patterns fired, bit 0…24 in PATTERNS order) so the diagnostic breakdown needs no second copy of the matchers; vectors pass (JSON-cmdline parse stays in untyped wrapper) |
| `lolbin::LolScore` (match breakdown + JSON) | `jsecmon/lolbin.ss` | ✅ **untyped layer** — the diagnostic companion to the typed kernel: `score-cmdline` returns a `lol-score` (total from `lolbin-score-cmdline`, per-pattern `matches` decoded from `lolbin-match-bits` against a static label/score/explanation table — data, not logic), `score-json-cmdline` adds the JSON-array parse (falls back to the raw string like `from_str::<Vec<String>>`), `lol-severity`/`lol-label-summary` mirror the Rust methods. No matcher is re-implemented, so total and matches can't drift. Rule 11 `detect-lolbin-cmdline` (`storage::detect_lolbin_cmdline`) also lives here rather than in `correlate.ss` — it's the one correlation rule that needs the native scorer, so hosting it beside `score-json-cmdline` keeps `correlate.ss` kernel-free: it scores each `process_start` row's cmdline (row `(host ts pid proc-name cmdline exe)`), skips rows whose cmdline **and** exe are both empty (Rust `continue`), and emits a `suspicious_cmdline` anomaly seed for totals ≥50 (`pname` falls back to `"?"`). `make lolbin-check` runs secmon's 10 `#[test]` vectors plus exact-total, label-order, bit↔label pins, and the Rule-11 threshold/skip/`"?"`-fallback edges (41 cases). |
| `dga::DgaVerdict` (diagnostic verdict + match breakdown) | `jsecmon/dga.ss` | ✅ **untyped layer** — the diagnostic companion to the typed DGA kernel, exactly mirroring `jsecmon/lolbin.ss`: `score-domain-verdict` returns a `dga-verdict` whose `score` is the headline kernel total, whose `reasons` are decoded from `dga-score-label-bits` against a static bit→reason table (data, not logic), and whose `entropy`/`max-consonant-run`/`length` are read off the SAME kernels applied to `dga-label` — so nothing here recomputes a scoring decision and the breakdown can't drift from the total. Rule 12 `detect-dga-domain` (`storage::detect_dga_domain`) also lives here rather than in `correlate.ss` — like Rule 11 it needs the native scorer, so hosting it beside the verdict keeps `correlate.ss` kernel-free: it scores each `dns_query` row (`(host ts pid proc-name query-name)`), skips a `#f`/empty query-name (Rust `Some(q) if !q.is_empty()`), emits a `dga_domain` seed for label scores ≥60, and dedups per `(host, process, scored-label)` keeping the first qualifying query (Rust `HashMap::or_insert`) so a host querying many subdomains of one DGA label alerts once (`pname` falls back to `"?"`). `make dga-check` runs secmon's 5 `dga.rs` `#[test]` vectors plus the bit↔reason pins and the Rule-12 threshold/skip/dedup/cross-host/`"?"`-fallback edges (39 cases). |
| `analytics::compute_host_risks` | `typed/analytics.ss` | ✅ risk-score kernel (clamped weighted sum); vectors pass |
| `analytics` grouping + `group_incidents` | `jsecmon/analytics.ss` | ✅ **untyped layer** — per-host accumulation/sort/top-N driving the risk-score kernel, plus incident dedup/collapse; secmon analytics vectors pass (`make analytics-check`) |
| `storage::detect_sequence_pair` (kill-chain core) | `jsecmon/analytics.ss` | ✅ **untyped layer** — the pure pairing primitive behind `detect_priv_escalation_chain`/`lateral_after_shell`/`persistence_after_access`/`log_cover`: given two event streams as `(host . ts-ms)` lists (the SQL `ORDER BY host,timestamp_ms` fetch is deferred I/O), pair each A with the **first** same-host B strictly later and within `window-ms` — at most one per A (Rust's inner `break`) — returning `((host …) (a-ts …) (b-ts …) (gap-seconds …))` for the caller to wrap as an Anomaly (`format_ts` is calendar-deferred). `gap-seconds` is integer ms/1000 (Rust i64 `/`). `make analytics-check` adds window-edge (≤ inclusive), strictly-later, cross-host, first-B-only, multi-A, and empty-stream cases. |
| `storage::detect_severity_clusters` (cluster core) | `jsecmon/analytics.ss` | ✅ **untyped layer** — the pure sliding-window clustering: given critical/high events as `(host ts-ms event-type)` pre-sorted by host then ts (SQL fetch deferred), slide from each i, greedily take the same-host run with `ts ≤ ts_i + window-ms`, and when it holds `≥ min-count` (5) events emit a cluster then skip past it (Rust `i = j`), else advance one. Emits `((host …) (window-start …) (window-end …) (count …) (event-types …))` for the caller to format (`format_ts` deferred). `make analytics-check` adds exactly-5, only-4, past-edge, run-of-6-then-skip, host-boundary, two-clusters-after-skip, and empty cases. |
| `storage::detect_frequency_spikes` (spike core) | `jsecmon/analytics.ss` | ✅ **untyped layer** — the pure per-(host,event-type) hourly-spike test: given `(host event-type hour count)` rows (the `hourly_counts` GROUP BY aggregate is deferred I/O), sum count and tally hours per key, then emit any row whose key average `> 0` and whose `count` strictly exceeds `3×` that average. Returns `((host …) (event-type …) (hour …) (count …) (average …) (ratio …))` in input row order for the caller to wrap (`parse_hour_to_ms` is calendar-deferred); `average`/`ratio` are f64 like Rust's `total/hours` and `count/avg`. `make analytics-check` adds 3×-spike, exact-3×-excluded (strict `>`), flat, avg-0-guard, key-independence, two-group-order, and empty cases. |
| `storage::detect_kill_chain` (chain core) | `jsecmon/analytics.ss` | ✅ **untyped layer** — the pure multi-phase kill-chain detector: given `(host ts-ms event-type)` rows pre-sorted by host then ts (SQL fetch deferred), slide from each i over the same-host run with `ts ≤ ts_i + window-ms`, map each type to an ATT&CK-ish phase via `event-type->attack-phase` (also exported; unmapped types skipped), and when the **distinct** phases reach `min-phases` (3) emit a chain then skip past it (Rust `i = j`), else advance one. Emits `((host …) (window-start …) (window-end …) (phases …) (event-types …))`; Rust collects phases from an unordered `HashSet`, so `phases` is canonicalized to first-seen order (treat as a set) while `event-types` keeps phase-mapped types in order. `make analytics-check` adds the classifier table, three-phases, two-distinct-only, unmapped-skip, host-boundary, window-edge, past-edge, two-chains-after-skip, and empty cases. |
| `storage::detect_off_hours` (predicate) | `jsecmon/analytics.ss` | ✅ **untyped layer** — `off-hours?`, the decision rule factored out of the SQL `WHERE`: a critical/high event is off-hours on a weekend or outside 08:00–18:00 UTC (`weekday` = strftime `%w` 0=Sun…6=Sat, `hour` = `%H` 0–23). The timestamp→(weekday,hour) decomposition is calendar-deferred. `make analytics-check` adds weekend, midday, and the 08:00/17:00/18:00 boundaries. |
| `storage::anomaly_rule_attack` (tactic tagger) | `jsecmon/analytics.ss` | ✅ **untyped layer** — the pure rule-name→ATT&CK-tactic table `detect_anomalies` stamps onto each anomaly: `kill_chain`→TA0001/TA0008/TA0010, `off_hours`→TA0005, every other rule→none. `make analytics-check` covers both tagged rules plus untagged/unknown. |
| `storage::detect_lolbin_cmdline` + `detect_dga_domain` | `jsecmon/detect.ss` | ✅ **untyped layer** — the kernel-driven detection rules: score every process_start cmdline (lolbin) / dns_query (dga) into anomalies above threshold. `make detect-check` runs the full events→detect→analytics pipeline; all three scoring kernels fire. This is the *live* pipeline shape (event hash-tables → anomaly hash-tables for `(jsecmon analytics)`); the faithful storage-Rule ports against secmon's `test_detect_*` vectors — with the per-pattern lolbin label breakdown and the label-level DGA dedup + reason list — live in `jsecmon/lolbin.ss` and `jsecmon/dga.ss`. The live DGA path now keys dedup by `(host, process_name, dga-label)`, matching the Rust storage rule instead of full query name. |
| `triage` classifiers | `typed/triage.ss` | ✅ pure predicates (transient-unit?, phantom-rootkit-race?); vectors pass |
| `triage` engine (rules + dispatch) | `jsecmon/triage.ss` | ✅ **untyped layer** — all 18 false-positive rules + first-match engine, in secmon's exact RULES order, dispatch in ordinary Jerboa delegating byte/string classification to the typed kernels; 40 triage vectors pass (`make triage-check`), incl. the security-relevant negatives (non-sshd reading host keys, systemd impersonated from /tmp, unknown daemon reading passwd). |
| `triage::compute_triaged_ids` (triage-aware mode) | `jsecmon/triage-store.ss` | ✅ **untyped layer** — the bridge above storage+triage: query every in-scope event, triage each, return the sorted benign/expected ID set to drop into a filter's `exclude_event_ids`. `make triage-store-check` proves the round-trip — detection then sees only the real attacks. |
| `storage::yaml_rules` (user rules: threshold/distinct/sequence/match) | `jsecmon/yaml-rules.ss` | ✅ **untyped layer** — port of secmon's `src/storage/yaml_rules.rs`: load YAML rule files (`(std text yaml)`), validate per type, and run them over a `(jsecmon storage)` handle into Anomaly row-hashes. A MatchSpec (event_type/severity/process_name/host + json_eq equality + data_contains LIKE) is inlined into SQL (quotes escaped); window strings (`10m`/`1h`/…) parse to ms; threshold/distinct are time-bucket GROUP BY/HAVING, sequence pairwise-chains steps within the window in Jerboa. `make yaml-rules-check` exercises all four types + window parsing + validation. This is the engine the `sigma` importer feeds. |
| `sigma` (Sigma rule importer) | `jsecmon/sigma.ss` | ✅ **untyped layer** — port of secmon's `src/sigma.rs`: parse a Sigma YAML rule (via `(std text yaml)`), map `logsource.category` → event_type, translate the BTreeMap-first selection's fields (`Field` → json_eq, `Field\|contains/startswith/endswith/re` → data_contains, Windows field names aliased to Linux JSON paths), `level` → severity, `attack.tNNNN` tags → ATT&CK IDs, and render secmon's `YamlRule` YAML back out. `make sigma-check` reproduces secmon's four conversion vectors (process_creation, network_connection, unsupported-category skip, safe-name). Pure YAML+strings, untyped. |
| `psk::constant_time_eq` | `typed/psk.ss` | ✅ ported, vectors pass |
| `psk::from_hex` (hex codec) | `typed/psk.ss` | ✅ hex encode + decode + 32-byte precondition; vectors pass (decode∘encode identity over all 256 byte values) |
| `stealth::obfuscate` (compile-time string XOR) | `typed/obfuscate.ss` | ✅ **typed kernel** — keeps sensitive strings out of the binary in plaintext. Lowers secmon's `obfuscate!`/`obfuscate_bytes!` scheme: length-derived wrapping-u8 key (`len*31+42` / `len*37+13`) + XOR. Since XOR preserves length, decode recomputes the key from the buffer — a clean involutive pair, no stored key. `make test` reproduces secmon's `test_obfuscate_roundtrip` + `test_obfuscated_not_plaintext` plus key-derivation and all-256-byte round-trip vectors. |
| `stealth::{anti_debug,integrity,init}` | `jsecmon/stealth.ss` | ✅ **untyped runtime shell** — TracerPid parsing/status-integrity comparison, parent debugger/tool detection tables, timing baseline rule, breakpoint-byte predicate, environment sanitization list, PID-stable process-name masquerade, `mlockall`, anti-debug watchdog, and best-effort self-integrity hashes are ported. `bin/agent.ss` initializes stealth before key/config work and starts the watchdog after privilege drop. `make stealth-check` pins the pure helpers/tables; runtime effects are best-effort through libc FFI and no-op when the platform primitive is unavailable. |
| `crypto` primitives (SHA256/HMAC/HKDF/x25519/AES-256-GCM) | `typed/crypto.ss` | ✅ **typed kernel** — FFI-delegated to vetted RustCrypto crates (sha2/hmac/hkdf/x25519-dalek/aes-gcm), never reimplemented; the backend emits the `use … ;` block per primitive and content-scans the Cargo deps. Vector-verified against NIST SP 800-38D GCM, RFC 7748/5869/4231, FIPS 180-4 (`make test`). |
| `psk` key-derivation + proof + transport (`from_bytes`, `compute_proof`, `encrypt_transport`) | `typed/psk.ss` + `jsecmon/crypto-psk.ss` | ✅ **typed kernel + untyped orchestration** — HKDF-derived auth/transport keys, the SHA256 challenge proof + constant-time `verify_proof`, and AES-256-GCM transport (12-byte nonce prepended) are typed kernels (`make test`); `(jsecmon crypto-psk)` adds the effects they omit — `transport-encrypt` draws a random nonce, `generate-challenge`/`respond-to-challenge`/`verify-response` add the random challenge nonce + clock + freshness check (`now` injected for testability). `make crypto-psk-check` pins the FFI path to the `psk_vectors.rs` digests then exercises round-trip / randomization / tamper→#f / wrong-key→#f / stale→#f. |
| `crypto::ecies` (x25519 ECDH + HKDF + AES-GCM) | `typed/ecies.ss` + `jsecmon/crypto-ecies.ss` | ✅ **typed kernel + untyped orchestration** — deterministic `ecies-seal`/`-open` are typed kernels (parity vs an independent Python impl, `ecies_vectors.rs`); `(jsecmon crypto-ecies)` adds `ecies-generate-keypair` + `ecies-encrypt` (random ephemeral keypair + nonce) + `ecies-decrypt`. Frame = secmon's bincode `EncryptedPayload` layout: `ephemeral_public(32) ‖ nonce(12) ‖ ciphertext_len(u64le) ‖ ciphertext`; decrypt also accepts the older jsecmon bare concat for local backward compatibility. `make crypto-ecies-check` pins x25519 to RFC 7748 + ecies-seal to the reference vector, then bincode-frame layout / round-trip / randomization / wrong-recipient→#f / tamper→#f. |
| `storage` (events table, store/query/filters) | `jsecmon/storage.ss` | ✅ **untyped layer** — SQLite event store on vendored `jsqlite`: secmon's schema (events + indexes + collector_state), `store-event` INSERT-OR-IGNORE dedup, and the full EventFilter WHERE builder (host/type/severity/since/until/pid/process_name LIKE/search/exclude_event_ids). `query-events` returns row hashes with `data` parsed from JSON, so detect/triage/analytics consume them directly. `make storage-check` round-trips store→query→detect→analytics (host risk 30, same as `detect-check`). `entity-where`/`entity-timeline` port `storage::entity_timeline`: the per-category WHERE-group builder (Process = `process_name LIKE` + six json fields with LIKE `%value%`; Ip/User/Domain = exact `json_extract` `=` over their field sets) plus the assembled oldest-first query, with the filter clauses continuing the positional `?N` numbering after the category binds (`build-where` gained an optional start index). `make entity-check` pins each category's exact SQL fragment + binds and routes/orders/narrows/limits over a live store (18 cases). |
| `storage` SQL-aggregation detectors (brute_force, credential_stuffing, dns_tunnel, suspicious_cron, recon_port_scan, data_exfil) | `jsecmon/threats.ss` | ✅ **untyped layer** — secmon's `run_detections` family: the time-bucket GROUP BY/HAVING rules and the two 5-min sliding-window rules, run as SQL (json_extract) over a `(jsecmon storage)` handle. `make threats-check` reproduces secmon's six detection-rule test vectors; the companion sequence/statistical families are covered by the rows below. |
| `storage` sequence/chain detectors (priv_escalation_chain, persistence_after_access, log_cover, lateral_after_shell) | `jsecmon/threats.ss` | ✅ **untyped layer** — secmon's `detect_sequence_pair` family: event A then event B within a window on the same host (auth-success→priv-esc /5min, reverse-shell/webshell→persistence /1h, any-critical→log-tampering /1h, shell→lateral /1h). Reproduces secmon's chain test vectors incl. the outside-window negative. |
| `storage` time-window aggregates (frequency_spike, severity_cluster, off_hours, kill_chain) | `jsecmon/threats.ss` | ✅ **untyped layer** — secmon's full `detect_anomalies` family: per-(host,event_type) hour count 3x above its own average, 5+ crit/high on a host /5min, crit/high outside 08:00-18:00 UTC weekday (SQLite `strftime`), and 3+ distinct kill-chain phases /1h. `run-anomaly-detections` is the dispatcher (frequency_spike first, as secmon runs it). `make threats-check` covers each with threshold/negative cases. |
| `geoip` (CSV GeoIP/ASN, IPv4+IPv6, binary-search range lookup, is_private) | `jsecmon/geoip.ss` | ✅ **untyped layer** — full port of secmon's `src/geoip.rs`: parse `start,end,country,asn,name` CSV rows (v4 + one-`::`-expanding v6), sort-by-start + binary-search lookup, RFC1918/loopback/link-local/multicast/ULA/CGNAT → synthetic `PRIVATE`. Pure parsing + integer math + file read, so untyped. `make geoip-check` runs secmon's geoip vectors. |
| `storage` impossible_travel | `jsecmon/threats.ss` | ✅ **untyped layer** — geoip-gated (reads `SECMON_GEOIP_CSV`): pair a user's consecutive successful `auth_event`s, fire `high` when the two source IPs resolve to different countries within `SECMON_TRAVEL_GAP_MIN` (default 30). Private IPs are dropped before pairing. `make geoip-check` proves the fire + the gap/same-country/private/cross-user negatives. |
| `buffer::ring` (StoredEvent ring buffer) | `jsecmon/buffer.ss` | ✅ **untyped layer** — port of secmon's `src/buffer/ring.rs`: the agent's bounded in-memory event ring. FIFO list + monotonic seq numbering, priority eviction (`event_severity_u8` table, drop lowest-severity oldest-first, oldest-critical last), seq/time-range polling, FIFO delivery-ack (`clear_before`), and the little-endian header codec (`seq u64 ∥ ts i64 ∥ sev u8 ∥ payload`). Pure mechanics, so untyped — the one security step, ECIES payload encryption, is FFI-deferred: the caller hands `buffer-store!` opaque ciphertext bytes. `make buffer-check` reproduces secmon's three ring tests (store/seq, priority eviction, FIFO-oldest) + codec round-trip. |
| `storage` correlation rules (`detect_*`) | `jsecmon/correlate.ss` | ✅ **untyped layer** — secmon's anomaly detectors are "run a SQL query, then map result rows → Anomaly". The SQL fetch (with its `json_extract` / `GROUP BY … HAVING`) is the deferred storage I/O; the pure, portable part is the correlation algorithm over the fetched rows + the Anomaly description, reimplemented here over pre-shaped row lists (which is exactly what `test_detect_*` exercise). **Batch 1** = the GROUP-BY rules: `detect_brute_force` (≥5 failed-auth per user / 10-min bucket), `detect_credential_stuffing` (≥5 **distinct** users per remote / 10-min), `detect_dns_tunnel` (≥50 queries per process / 5-min), `detect_suspicious_cron` (per-row: `scheduled_task_change` by a non-root user, missing user → `""` ≠ root → kept). `timestamp_ms / N` is integer division → `quotient`, bucket anomaly ts is `bucket*N`; COUNT(*) vs COUNT(DISTINCT) become first-seen-order tallies (treat the set, not order, as significant). Each detector returns "anomaly seeds" (an Anomaly minus the calendar `format_ts` fields, which the caller adds, and minus the `attack` tags from `anomaly-rule-attack`). **Batch 2** = the sliding-window / sequence-pair rules over caller-sorted rows: `detect_recon_port_scan` (≥10 distinct remote ports / 5-min, same process) and `detect_data_exfil` (≥20 outbound connections / 5-min) share a `slide` helper reproducing secmon's `while i<len { expand j; if hit { i=j } else { i+=1 } }`; `detect_sequence_pair` (event A then first B on the same host with `a.ts < b.ts ≤ a.ts+window`, one match per A) backs the three chain rules `detect_priv_escalation_chain` (auth-success→priv-esc / 5-min, caller pre-filters A to `success=1`), `detect_persistence_after_access` (reverse_shell **or** webshell→persistence / 1h — two arms appended), `detect_lateral_after_shell` (reverse_shell→lateral / 1h); and `detect_log_cover` (any critical event→log_tampering / 1h, one per critical) carries the event type into the description. `gap_seconds` is `(quotient (- b a) 1000)`. **Batch 3** = the geo-correlation rule `detect_impossible_travel` (Rule 13): over auth points the caller has already geo-resolved (the `GeoDb` lookup is the deferred I/O, like the SQL fetch) and sorted by `(user, ts)`, it walks every adjacent pair (Rust `i += 1` always) and emits a `high` seed when the same user logs in from two **different** countries within `min-gap-ms` (default 30 min = `SECMON_TRAVEL_GAP_MIN`, an optional 2nd arg). The `PRIVATE`/empty-country drop (Rust's `filter_map` before pairing) is pure, so it stays here; `gap_minutes` = `(quotient ms 60000)`; the seed carries raw `first-seen-ms`/`second-seen-ms` for the caller's `format_ts`. `make correlate-check` reproduces all ten `test_detect_*` (incl. `_below_threshold` and `_outside_window`) + the threshold / bucket-boundary / distinct-dedup / window-edge / cross-host / B-before-A edges, plus the impossible-travel country/gap-edge/cross-user/PRIVATE-drop/override edges (76 cases). |
| `server::protocol` (length-prefixed framing) | `jsecmon/frame.ss` | ✅ **untyped layer** — the wire framing the collector↔agent transport wraps every `ProtocolMessage` in: `frame_encode` prepends a 4-byte little-endian u32 length to the payload (`to_bytes`'s framing half), `frame_read_length` reads that header as an LE u32 (`read_length`), and `frame_decode` reproduces `from_bytes`'s two guards — `< 4` bytes → `data too short`, `< 4 + declared len` → `incomplete message` — then slices out `data[4 .. 4+len]`. Pure byte mechanics, so untyped; the bincode (de)serialization of the message *body* is the Rust-specific deferred piece (the caller (de)serializes the payload `frame_decode` hands back). LE u32 verified to round-trip `0x12345678` and `0xFFFFFFFF`. `make frame-check` pins the byte layout, the encode∘decode round-trip, both error guards, and the empty/zero-length-payload edge. |
| `monitor::events::SuspiciousPatterns` (process-spawn classifier) | `jsecmon/suspicious.ss` | ✅ **untyped layer** — `check_suspicious(process, parent)`: shell-from-service, attack-tool-from-service (name exact-match or exe suffix), reverse-shell command-line patterns, and crypto-miner name/cmdline patterns, in secmon's order, returning the same reason string. Pure string classification like triage. Pins two corners the Rust depends on: a missing parent short-circuits to "clean" before any check, and `str::contains` is a *literal* substring test (so `python -c.*socket` is literal, not a regex). `make suspicious-check` reproduces secmon's two events.rs tests + the other three signals + both corners. |
| `monitor::network::NetworkMonitor` (connection classifier) | `jsecmon/netconn.ss` | ✅ **untyped layer** — `check_suspicious(port, addr, process)`: known reverse-shell/C2/l33t port, ephemeral port (49152..65535) that is a round multiple of 1000, and a web-server process (nginx/apache/httpd/php-fpm) connecting to a non-private address, in secmon's order with the same reason string. Pure metadata classification. secmon hides the web-server names with `obfstr!` (same scheme as `typed/obfuscate.ss`); they decode to these plaintext literals at runtime. Pins the faithfulness quirk that the "private" prefix set is literal `{127. 10. 192.168. 172.}`, so `172.` matches all of 172.x, not just RFC1918 172.16/12. `make netconn-check` reproduces secmon's two network.rs tests + the full bad-port list + the high-port and web-server rules with private-address negatives. |
| `monitor::kernel::KernelModuleMonitor` (kernel-module classifier) | `jsecmon/kernmod.ss` | ✅ **untyped layer** — `is_suspicious_module(name)`: lower-cased name contains a known-rootkit substring (diamorphine/reptile/hide/rootkit/keylog/…), or a 1-2 char name not on the legitimate-short allow-list (ip dm sd sr nf if), or a >4 char name with no vowel, in secmon's order. Pure string classification like the other classifiers. obfstr!-hidden name lists decode to these plaintext literals. Pins the faithfulness corner that only the substring test lower-cases the name — the short-name and vowel tests use the original case, and the vowel set is both-case `aeiouAEIOU`. `make kernmod-check` reproduces secmon's two kernel.rs tests + each signal exercised independently + the case corners. |
| `monitor::selinux::SELinuxMonitor` (audit-log parser) | `jsecmon/selinux.ss` | ✅ **untyped layer** — the line-parsing core: `parse_audit_line` dispatches on the `type=` tag (AVC → `parse_avc_event`, MAC_CONFIG_CHANGE → boolean change, MAC_POLICY_LOAD → policy load, USER_ROLE_CHANGE → role change) into a `selinux-event` record mirroring `SELinuxEventInfo`, plus the `extract_field` helper. A text-format parser yielding a structured record, like the DNS parser, so untyped. secmon's AVC regex is reused verbatim through Jerboa's `(std pregexp)` `pregexp-match` (capture order 1=decision 2=permission 3=pid 4=comm 5=scontext 6=tcontext 7=tclass — verified identical). Pins `extract_field`'s quoting/empty/missing-quote corners and the `val=` default-empty. `make selinux-check` reproduces secmon's two selinux.rs tests + the dispatcher + all four event kinds. (The I/O — tailing the audit log, mode polling — is the deferred monitor loop.) |
| `monitor::container::ContainerEscapeMonitor` (mount classifier) | `jsecmon/container.ss` | ✅ **untyped layer** — `is_suspicious_mount(mount)`: a mount that starts with `/host` or `/mnt/host`, is exactly `/`, or contains `/var/run/docker` / `/run/docker` / `devd.pipe` (FreeBSD jail), flagging a container/jail escape. Pure string classification like the other monitor classifiers, so untyped; obfstr!-hidden patterns decode to these plaintext literals. `make container-check` reproduces secmon's `test_suspicious_mount_detection` + each escape signal + negatives. (Isolation detection and mount/path/cap polling are provider-driven I/O — the deferred monitor loop.) |
| `monitor::dns::read_dns_servers` (resolver-set builder) | `jsecmon/dns-servers.ss` | ✅ **untyped layer** — `parse_dns_servers(content)`: collect each `nameserver <ip>` entry from resolv.conf text (the 2nd whitespace field of a trimmed line starting with `nameserver`) and union with the fixed public-resolver set (Google/Cloudflare/Quad9/OpenDNS). Pure parsing, so untyped; the `/etc/resolv.conf` read is the deferred I/O wrapper (split off like the selinux log tail). Folds tabs/CR to spaces to match Rust's `split_whitespace`. `make dns-servers-check` reproduces secmon's `test_read_dns_servers` (publics always present) + the nameserver parsing with multi-space/tab/indented lines and dedup. |
| `dtrace::scripts::is_sensitive_path` (sensitive-path classifier) | `jsecmon/sensitive-path.ss` | ✅ **untyped layer** — `is_sensitive_path(path)`: a sensitive prefix (passwd/shadow/sudoers/ssh dirs/cron/periodic/ld.so.preload/`/boot/`/…), with `/home/` special-cased to only `/.ssh/` subpaths, else `authorized_keys` anywhere, else `/cron` or `/periodic`. Pure string classification, so untyped; obfstr!-hidden literals decode to these plaintexts. Ported with secmon's loop-with-early-return so the **load-bearing corner** holds: a `/home/` path short-circuits before the `authorized_keys` check, so `/home/user/authorized_keys` (no `/.ssh/`) is **not** sensitive. `make sensitive-path-check` reproduces secmon's `test_sensitive_path_detection` + each signal + that corner. |
| `dtrace::consumer::EventParser` (DTrace line parser) | `jsecmon/dtrace-parse.ss` | ✅ **untyped layer** — `parse_dtrace_line(line)`: split a `SECMON\|TYPE\|…` DTrace line on `\|` and dispatch on `parts[1]` into a per-type structured record (EXEC/EXIT/CONNECT/LISTEN/OPEN/WRITE) with each handler's exact field extraction; a <2-field / unknown-type / too-few-fields line yields no record (`#f`), matching secmon's `return Ok(())` no-ops. `dtrace-process-line` ports the stateful `EventParser`: process cache, parent lookup, suspicious EXEC dispatch, process_exit cache removal, CONNECT/LISTEN event rows, and OPEN/WRITE no-event behavior. Numeric fields use `.parse().unwrap_or(0)` (u32 rejects negatives → 0; exit code is i32), and the EXEC cmdline is `split_whitespace`. **Composes** `(jsecmon sensitive-path)` for the OPEN `sensitive?` gate. `make dtrace-parse-check` reproduces secmon's parser tests plus the stateful process-cache/suspicious/network/exit behavior; runtime startup/work loops live in `(jsecmon dtrace-runtime)`. |
| `dtrace::{mod,consumer}` runtime shell | `jsecmon/dtrace-runtime.ss` | ✅ **untyped runtime shell** — ports the FreeBSD DTrace runtime around `EventParser`: secmon's combined D script, support-check guard ordering (root, `/dev/dtrace/dtrace`, smoke test), direct libdtrace open/setopt/compile/exec/go/work-loop path, buffered callback line queue, stop/close cleanup, subprocess `dtrace -n <script>` fallback, stdout line pump, EOF marker, and `SECMON|...` drain through `dtrace-process-line`. `make-freebsd-monitor-set` runs the same preference order before privilege drop. `make dtrace-runtime-check` verifies the guard order, exported entrypoints, script probes, noise filtering, and stream-to-event rows; the direct libdtrace path still needs FreeBSD root/libdtrace for live verification. |
| `platform::{linux,freebsd}` (`is_dangerous_path` + `get_mounts` parsers) | `jsecmon/platform-mounts.ss` | ✅ **untyped layer** — the pure halves of each `IsolationProvider`, reads stripped: `is_dangerous_path` is **exact** membership in the platform's dangerous-path set (Linux 12 entries incl. `/proc/kcore`, `/dev/mem`, the docker/crio/containerd sockets; FreeBSD 6 incl. `/dev/io`, `devd.pipe`; unknown platforms empty, per the `UnsupportedProvider` default), and `parse_mounts` reproduces each `get_mounts` line loop — Linux `split_whitespace` keeping field[1], FreeBSD `split(" on ")` keeping piece[1] minus a trailing ` (opts)`. Pins that membership is exact not prefix (`/host/foo` is clean), and that the FreeBSD split takes piece[1] of a multi-`" on "` split (not everything-after-first). obfstr!-hidden lists decode to these plaintexts. Pure — the `/proc/self/mounts` read / `mount` exec is the deferred I/O — no native lib; secmon has no `#[test]` here so `make platform-mounts-check` asserts against the Rust source. (The `container.rs` test-only mock `is_dangerous_path` is `#[cfg(test)]` scaffolding, not ported.) |
| `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. Also `parse_security_environ` (from `ebpf/loader.rs`, but functionally a `/proc/[pid]/environ` parser): splits the NUL-separated blob, drops empty and non-UTF-8 entries (faithful to `std::str::from_utf8(..).ok()`, reproduced exception-free by a lossy-decode→re-encode→compare round trip), and keeps any var whose name `starts_with` one of 19 security-relevant prefixes **or** whose `to_uppercase()` does — so a lower-cased `path=`/`ld_…` still matches the upper-cased prefixes, while `http_proxy`/`https_proxy` match only literally-lower vars. Also `parse_module_line` (a `/proc/modules` row: ≥4 ws fields → the same `KernelModuleInfo` alist as freebsd's `parse_kldstat_line`, name `parts[0]`, size `parts[1]` as DECIMAL u64 via `.parse().ok()` so a non-u64 size is `#f` but the row still parses, action always Loaded, no loader pid). `make proc-linux-check` reproduces secmon's five linux.rs tests + ipv6/parse-addr + a comm-with-paren corner + 10 environ-scan cases (invalid-UTF-8 drop, uppercase-arm, trailing NUL) + the module-line rows. (The `/proc` reads and inode→pid scan are the deferred I/O.) |
| `ebpf::{events, loader::parse_event}` | `jsecmon/ebpf-events.ss` | ✅ **untyped layer** — decodes the packed userspace eBPF `Event` layout (`kind`, `pid`, `uid`, `data1`, `data2`, `comm[16]`, `filename[32]`) and maps every Rust `parse_event` kind into the same SecurityEvent-shaped row: exec, exit, connect/accept, file_open, setuid, kmod, ptrace, namespace clone/enter/unshare, mount, and capset. DNS kind stays `#f` like Rust because DNS is handled by the sniffer; unknown/zero-address connect cases are skipped. `make ebpf-events-check` pins packed-field decoding, invalid UTF-8 handling, ptrace request names, IP byte order, and all event-kind mappings. |
| `ebpf::loader` runtime shell | `jsecmon/ebpf-runtime.ss` | ✅ **untyped runtime shell** — Linux support guard (`/proc/version`, root/CAP_BPF approximation, BTF warning parity), eBPF object search, the full 18-program tracepoint attach table, libbpf object load/attach, perf-buffer callback queue, stop cleanup, and drain-through-`parse_event` are ported. `make-linux-monitor-set` tries this stream first; while active, `monitor-tick` drains eBPF process/network/file-open events and suppresses the process/network polling monitors, matching secmon's high-fidelity mode shape while keeping the auxiliary monitors. `make ebpf-runtime-check` pins the guard/table/stream-drain pieces; live attach still requires Linux, root/CAP_BPF, libbpf, and a `secmon.bpf.o` object. |
| `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`. Plus `parse_ps_line` (the `ps -axo pid,ppid,uid,comm,args` fallback parser: ≥5 ws fields, `pid`/`ppid`/`uid` as u32 via `.parse().ok()?` so a non-u32 field rejects the whole line, `comm` is field[3], `args` is field[4..] re-joined with single spaces). Pure text/number parsing, so untyped. Also `parse_sockstat_line` (cols `USER COMMAND PID FD PROTO LOCAL FOREIGN`, ≥7, `pid` as u32-or-reject, protocol lower-cased, a `FOREIGN` of exactly `*:*` short-circuits to `("0.0.0.0" . 0)` **without** `parse_address`, `state` = LISTEN when remote is `0.0.0.0`/`::`/port 0 else ESTABLISHED) and `parse_netstat_line` (cols `Proto Recv-Q Send-Q LOCAL FOREIGN [state]`, ≥5, here `*:*` **does** go through `parse_address`, `state` = `parts[5]` or `UNKNOWN`, no pid/name) — both producing a connection alist mirroring `ConnectionInfo`. Plus `parse_freebsd_status` (the procfs `/proc/[pid]/status` columns: ≥13 ws fields → `(name ppid uid)`, name is `parts[0]`, ppid is `parts[2]`, uid is `parts[12]` = ruid, both u32-or-reject, trailing group columns ignored). `make freebsd-parse-check` reproduces secmon's three freebsd.rs tests + ipv6/wildcard/negatives + the ps-line cases + the sockstat/netstat rows + the status columns traced from source. (The `kldstat`/`sockstat`/`netstat`/`ps`/`status` command/file reads 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.) |
| `local_store::LocalEventStore` | `jsecmon/local-store.ss` | ✅ **untyped layer** — encrypted SQLite local event store whose 32-byte key is exclusively created or opened no-follow and validated by descriptor for type, owner, link count, exact `0600` mode, and length. Unsafe existing keys fail closed; `secure-key-check` covers permissions, symlink/hardlink/directory, malformed length, wrong-owner policy, and pathname replacement. |
| `config` | `jsecmon/config.ss` | ✅ **untyped layer** — `AgentConfig` defaults to loopback (`127.0.0.1:31337`), poll `100`ms, and buffer `10000`; an explicit `SECMON_LISTEN` may select a numeric remote interface. |
| `monitor/events` (danger predicates) | `jsecmon/event-danger.ss` | ✅ **untyped layer** — the payload predicates that drive a mount/capability event's severity, lifted off their structs: `MountEventInfo::is_dangerous` (`mount-danger-reason source target` → reason string, with the faithful corner that the `/` source entry's prefix is `//` so a plain `/foo` is **not** flagged, and dangerous *targets* match exact-only) and `CapabilityEventInfo::dangerous_caps` (`cap_effective` bits → cap names in the Rust push order, full u64 so bits 38/39 work). These compute the booleans `event-meta`'s mount/capability severity helpers consume. Plus `NamespaceEventInfo::ns_types` (`ns-types ns-flags` → the namespace names whose `CLONE_NEW*` mask is set, in Rust push order mnt/uts/ipc/user/pid/net/cgroup/time — masks 0x20000/0x04000000…0x40000000/0x80/0x100, not bit indices). Pure, no native lib; `make event-danger-check` asserts against the Rust source. |
| `monitor/persistence` (helpers) | `jsecmon/persistence.ss` | ✅ **untyped layer** — `classify_path` (path → `PersistenceType` symbol via an ordered first-match substring chain; `systemd` before `cron`, `.timer` vs service, and the shell-profile arm == the default) and `extract_suspicious_content` (first line matching `SUSPICIOUS_PATTERNS`, returned in original case, truncated to 200 chars + `...`). Faithfully preserves secmon's dead-pattern bug: the line is lowercased before `contains`, so the uppercase patterns `NOPASSWD`/`ALL=(ALL)` can never match. Pure — the directory walk + baseline hashing are the deferred I/O — no native lib; `make persistence-check` asserts against the Rust source. |
| `monitor/files` (`FileIntegrityMonitor::is_suspicious_change`) | `jsecmon/file-change.ss` | ✅ **untyped layer** — the deciding logic with stat/hashing stripped (modes + change-type + platform passed in): ordered first-match — setuid then setgid bit *added* (both modes known), exact platform critical file, `authorized_keys`/`cron` substrings, then a platform sensitive dir on `created` only. Pins the order corner that the `cron` substring precedes the sensitive-dir step, so a created `/etc/cron.d/x` reports "Cron configuration modified", never the sensitive-dir message; the critical-files/sensitive-dirs sets switch on `cfg!(target_os)` (linux/freebsd/other). Pure — the `stat`/SHA-256 baseline is the deferred I/O — no native lib; secmon has no `#[test]` here so `make file-change-check` asserts against the Rust source. |
| `monitor/webshell` (`WebshellMonitor` classifiers) | `jsecmon/webshell.ss` | ✅ **untyped layer** — the three pure deciders with the `/proc` scan + parent/child PID walk + event emission stripped: `is_web_server` (lower-cased name **substring** vs the server list, so `php-fpm` matches `php`), `is_suspicious_child` (process name by **exact** lower-cased equality — `bashx` is clean — OR the joined+lowercased cmdline **substring**-matched against the pattern list), and `get_detection_reason` (scans only the cmdline patterns, **in list order**, first match → `Suspicious command pattern: {pat}`, else the default `Web server spawned suspicious process: {name}` with the **original-case** name). obfstr!-hidden lists decode to these plaintext literals. Pins the corner that the reason is chosen by pattern-list order, not cmdline-token order, and that a name-only hit yields the default reason. Pure — the PID walk is the deferred monitor loop — no native lib; secmon has no `#[test]` here so `make webshell-check` asserts against the Rust source. |
| `monitor::dns_sniffer` (DNS wire parser + live capture queue) | `jsecmon/dns-sniffer.ss` | ✅ **untyped runtime shell** — the platform-independent half of secmon's `src/monitor/dns_sniffer.rs`: DNS wire-format parser (QNAME compression-pointer cap, QTYPE table, question + A/AAAA answers), IPv4+UDP header peel for AF_PACKET/SOCK_DGRAM-style IP bytes, Ethernet/VLAN peel for Jerboa pcap bytes, 5s dedup / 30s cleanup, local-port→PID/name callback, and a best-effort pcap-backed live capture stream feeding the monitor. Every bounds check is preserved: truncated/malformed/looping packets yield `#f`. `make dns-sniffer-check` reproduces parser/dedup tests; `make monitor-dns-check` verifies the stream-to-monitor path. |
| `bin/analyze` (CLI parse helpers) | `jsecmon/analyze-cli.ss` | ✅ **untyped layer** — the pure argument parsers of the `analyze` binary, returning the prelude Result (ok/err) to mirror Rust's `Result<_, String>` **including the exact error text**: `parse_duration_ms` (`10m`/`2h`/`1d`/bare-seconds → ms; splits leading ASCII digits from the unit; empty → `empty duration`, bad number/leading-non-digit → `invalid duration: {s}`, bad unit like `m5` → `unknown duration unit: …`; the number must fit i64) and `AlertSink::parse` (`stdout` / `file:PATH` / `webhook:URL` / `syslog` / `syslog:TAG`, first-match in order, remainder taken verbatim so `file:` → empty path) and `parse_alert_sinks` (collect every `--alert-to <spec>`, parsing each and short-circuiting on the first bad spec like Rust's `?`; a trailing `--alert-to` with no value is skipped, and no flags → the empty list — the watch-time default-to-stdout lives in `cmd_watch`), plus the generic `--flag` scanners shared across the CLI (`parse_flag_value` → the arg after the **first** `flag`, or `#f` even when the flag is last; `has_flag` → membership; `is_json_format` → the first `--format` that has a value decides, a trailing `--format` is skipped). Pure string→Result/bool; the sink dispatch (stdout/file append/curl webhook/`logger` syslog) and query dispatch are the deferred I/O. secmon has no `#[test]` here so `make analyze-cli-check` asserts against the Rust source. (`format_ts`/`format_ts_iso` display helpers now live in `jsecmon/calendar.ss`; only the actual stdout/file/webhook/syslog emission stays deferred.) |
| `bin/collector` (CLI/hosts parse helpers) | `jsecmon/collector-cli.ss` | ✅ **untyped layer** — the pure argument/hosts parsing of the `collector` binary, with the async polling + ECIES/PSK key loading + SQLite I/O deferred: `parse_after_seq` (first `--after` value as u64, `unwrap_or(0)` so junk/negative/≥2⁶⁴ → 0), `parse_format` (→ `'json`/`'human`/`'quiet`; a per-index scan where an unknown `--format` value does **not** consume the value — differs from analyze's `is_json_format` — and the no-flag default is `quiet` when a `--db` is present else `human`), `parse_db_path`, `normalize_host` (append `:31337` unless the host already contains **any** `:`, so bare IPv6 is left as-is, faithfully), `collect_positional_hosts` (skip the four value flags **and** their values, drop other `--` args, normalize the rest), and `parse_hosts_file`'s pure contents→hosts core (trim, drop blanks/`#` comments, normalize). secmon has no `#[test]` here so `make collector-cli-check` asserts against the Rust source. |
| `storage` event readers (`extract_pid` / `extract_process_name` / `build_summary`) | `jsecmon/event-summary.ss` | ✅ **untyped layer** — the pure readers that turn an event's flat JSON `data` (a hash table, as `string->json-object` yields) back into a pid / process name / one-line summary, with the SQLite query + serde plumbing left to storage. Each field is read through a **typed** getter so only a JSON value of the right type counts (`as_u64`/`as_i64`/`as_str`/`as_bool`); `extract_pid` walks pid→source_pid→spawned_pid→web_server_pid and truncates the first hit to **u32** (Rust `v as u32`, so ≥2³² wraps, and pid 0 is a real hit); `extract_process_name` walks process_name→name→exe→source_process→spawned_process. `build_summary` reproduces every per-type format with the exact `unwrap_or` defaults (`"?"`/`0`), the `process_exit` exit-code *option* (Some(0) still prints `(0)`), the nested `selinux_event` perm/class/path-vs-message branches with the 80-char message cap, and the catch-all that scans values **in sorted key order** (serde's default BTreeMap) for the first string longer than 3 chars (capped at 80) else the event type. `make event-summary-check` (43 cases) reproduces secmon's `test_extract_helpers` + `test_build_summary` and adds every per-type / typed-getter corner derived from the source. |
| `storage` IOC parsing (`detect_ioc_type` / `is_ipv4` / `parse_ioc_text`) | `jsecmon/ioc.ss` | ✅ **untyped layer** — the pure indicator classifier behind threat-list ingestion (the file read in `load_ioc_file` is the deferred I/O). `detect_ioc_type` is first-match ip→hash→domain→process: `is_ipv4` (split on `.`, exactly 4 non-empty ≤3-char all-digit groups — **no** 0–255 range check, so `999.999.999.999` is still Ip and `1.2.3.4444` is not), then IPv6 (`:` present and every char hex-or-`:`), then a 32/40/64-length all-hex Hash (MD5/SHA1/SHA256, case-insensitive), then a `.`-bearing space-free Domain, else Process. `parse_ioc_text` trims, drops blanks and `#` comments, and tags each remaining line (order preserved). `ioc_type_json_fields` / `ioc_type_column_fields` expose the per-type JSON-path and column lists `match_iocs` probes (pure routing data; Process is the only type with a plain column, `process_name`). `make ioc-check` reproduces the `test_ioc_type_detection` #[test] plus the is_ipv4 / parse / field-routing corners. |
| `monitor/revshell` reverse-shell classifiers (`classify_connection` + helpers) | `jsecmon/revshell.ss` | ✅ **untyped layer** — the pure deciders of the reverse-shell monitor (connection/PID enumeration, event emission, and the dedup set stay in the monitor loop). `is_shell` / `is_revshell_tool` are **exact** lower-cased name membership; `is_c2_port` tests the 17-port C2 set; `is_legitimate_service` fires only on 443/8080/8443 and matches a **substring** of the name. `extract_addr_from_cmdline` finds `/dev/tcp/`, splits the remainder on `/`, and on ≥2 pieces returns `(addr . u16-port)` (first whitespace token of piece 1, junk/out-of-range→0), else `("unknown" . 0)`. `classify_connection` runs checks 1–4 first-match: shell→`shell-outbound`, C2-port-and-not-legit→`known-c2-port`, revshell-tool→`shell-outbound`, any `REVSHELL_PATTERNS` substring→`suspicious-redirect`, else `#f`. revshell.rs has no #[test], so `make revshell-check` (47 cases) **is** the spec. |
| `monitor/cron` scheduled-task path tables + classifier | `jsecmon/cron.ss` | ✅ **untyped layer** — the pure pieces of the cron / systemd-timer / periodic monitor (the baseline walk + change detection stay in the loop). secmon keys `CRON_PATHS` / `PERIODIC_PATHS` off `#[cfg(target_os)]`, so `cron-paths` / `periodic-paths` are functions of a platform symbol (`'linux` / `'freebsd` / `'other`) reproducing the three cfg arms verbatim. `is-systemd-or-periodic-path` is the routing predicate `baseline_all` uses to decide whether a `PERIODIC_PATHS` entry is a systemd unit dir vs another cron-like dir — a plain **substring** test for `"systemd"` OR `"periodic"`. cron.rs has no #[test], so `make cron-check` asserts the full tables + the classifier and **is** the spec. |
| `monitor/logtamper` log-tamper decision core | `jsecmon/logtamper.ss` | ✅ **untyped layer** — the pure pieces of the log-tampering monitor (the `fs::metadata` polling + size/mtime tracking map stay in the loop). Exposes the `SYSTEM_LOGS` / `HISTORY_FILES` constant tables and `TRUNCATION_THRESHOLD` (1000). `is-history-file` is any-`HISTORY_FILES`-**substring**. `classify-tamper old-size new-size old-mtime new-mtime path` reproduces `check_tampering`'s Ok-arm in push order: size dropped by **>** threshold → `history-cleared` (if a history file) else `truncated`; mtime went backwards and `> 0` → `timestamp-modified` (both can fire for one file). The `deleted` case is the `fs::metadata` Err arm (deferred I/O). logtamper.rs has no #[test], so `make logtamper-check` **is** the spec. |
| `storage` detection-rule ATT&CK catalog (`DETECTION_RULES` / `rule_attack` / `anomaly_rule_attack`) | `jsecmon/detection-rules.ss` | ✅ **untyped layer** — the static `DETECTION_RULES` table (13 rows: name / description / severity / MITRE techniques) and its pure lookups, used to annotate findings with ATT&CK IDs and to list/validate rule names (the SQL detectors that fire these live in `(jsecmon threats)`). `rule-attack` finds the row by exact name and returns its `attack` list, else `()` (unwrap_or_default); `rule-names` / `rule-known?` mirror the listing/validation paths. `anomaly-rule-attack` is the separate match for the `detect_anomalies` **statistical** rules (not in the table): `kill_chain` → recon/lateral/exfil tactics, `off_hours` → Defense Evasion, others (frequency_spike, severity_cluster) → `()`. const has no #[test], so `make detection-rules-check` asserts every row + both lookups and **is** the spec. |
| `monitor/lateral` IP parsing + internal-network test (`is_internal_ip`) | `jsecmon/ipaddr.ss` | ✅ **untyped layer** — `is_internal_ip` parses `ip_str.parse::<IpAddr>()` first and only classifies on success, so `parse-ipv4` / `parse-ipv6` faithfully reproduce **Rust std's `IpAddr` FromStr** boundary: IPv4 = exactly 4 octets, 1–3 digits, **no leading zeros**, ≤255; IPv6 = colon-separated 1–4-digit hex groups with at most one `::` (eliding ≥1 zero group) and an optional trailing embedded IPv4 (forbidden before `::`). `is-internal-ip` then mirrors lateral.rs: V4 `10/8` · `172.16/12` · `192.168/16` · `127/8`, V6 `fc00::/7` (`seg0 & 0xfe00 == 0xfc00`) or loopback. lateral.rs has no #[test]; expectations were generated by a **std-only Rust oracle** over ~70 inputs, so `make ipaddr-check` (88 cases) pins the port to real Rust and **is** the spec. |
| `monitor/auth` log-line parsers (`parse_auth_line` + 7 sub-parsers) | `jsecmon/auth.ss` | ✅ **untyped layer** — the dispatcher lowercases and routes on substrings (`sshd`→ssh, `sudo`, `su[`/`su:`, `authentication failure`→pam, `useradd`/`adduser`, `userdel`/`deluser`, `password changed`/`passwd`), each sub-parser pulling fields with `extract_field(line, start, end)`. Faithful corners: `extract_field` searches the **raw** line (case-sensitive) while dispatch/success use the lower-cased copy, so `Invalid user admin` → username `unknown`; the SSH "accepted" branch `?`-propagates a missing `for ` to **#f** (no fall-through); and the documented quirks where the username captures `TTY=pts/0` / `authentication failure; TTY=…` / `root)` are preserved. AuthEventType → `'ssh-key-auth 'login 'failed-login 'sudo-attempt 'su-attempt 'user-created 'user-deleted 'password-change`; None → `#f`. auth.rs has no #[test]; `make auth-check` (24 cases) derives every expectation from a **std-only Rust oracle** over the verbatim bodies and **is** the spec. |
| `format_ts` / `format_ts_iso` / `parse_hour_to_ms` / `parse_datetime` (chrono UTC calendar) | `jsecmon/calendar.ss` | ✅ **untyped layer** — the pure epoch-ms ↔ civil-date boundary the detect/analytics seeds defer to. secmon formats/parses timestamps with chrono always in UTC (`DateTime::from_timestamp_millis`, `NaiveDateTime`), which is pure proleptic-Gregorian arithmetic, reproduced exactly with Howard Hinnant's `days_from_civil`/`civil_from_days`: `format-ts` (`%Y-%m-%d %H:%M:%S`, storage+analyze), `format-ts-iso` (`%Y-%m-%dT%H:%M:%S%.3fZ`, analyze), `parse-hour-to-ms` (`%Y-%m-%d %H`+`:00:00`, the frequency-spike inverse of `%H` decomposition, bad input → 0), and `parse-datetime-ms` (returns the prelude Result: relative `Nh`/`Nd` from an optional `at`/now, then RFC3339 with `Z`/`±HH:MM` offset and optional `.fff`, then naive ISO assumed-UTC, then date-only; else `err` with the exact Rust message). Faithful corners: Hinnant's `/` truncates toward zero (`quotient`) but the ms→day split must **floor** (a pre-1970 ms keeps a non-negative ms-of-day), so the parts use exact `floor`; the export is `parse-datetime-ms`/`char-index` to avoid shadowing the prelude's own `parse-datetime` (a datetime record) and `string-index`. `make calendar-check` pins the civil round-trip, both formatters (incl. leap-day, negative-ms flooring), `parse_hour_to_ms`, and secmon's `test_parse_datetime` invariants (28 cases). |
| `monitor::process::ProcessMonitor` scan loop | `jsecmon/monitor-process.ss` | ✅ **untyped layer** — secmon's process-monitor scan in pure Jerboa over an injected `mon-provider` seam (list-pids / get-process / hostname), fixture-tested: diff pid sets → classify each new spawn (check-suspicious delegates to `(jsecmon suspicious)`) → emit process_start/suspicious_exec; exited pids → process_exit; list-pids error → no-op. Faithfully preserves secmon's `continue` before insert (a failed get-process is not added to known and is retried), the parent-cache lookup, and the 1000-entry cache prune. `make-linux-provider` is the thin /proc shell via `(jsecmon proc-linux)` parsers plus live `/proc/[pid]/{exe,cwd}` readlinks. `make monitor-process-check` (30 cases). |
| `monitor::network::NetworkMonitor` scan loop | `jsecmon/monitor-network.ss` | ✅ **untyped layer** — the network-monitor scan over an injected `net-provider` seam: listeners → listening_port/info (de-dup key `proto:laddr:lport`); connections → `(jsecmon netconn)` connection-suspicious? → suspicious_connection/high or network_connection/info (de-dup key `proto:laddr:lport:raddr:rport:state`). Provider Err skips that half; never ages out (secmon no-op TODO). `make-linux-network-provider` reads /proc/net/{tcp,udp,tcp6,udp6} via proc-linux parsers and resolves socket inode → `(pid . process-name)` by scanning `/proc/*/fd`, enabling the live web-server outbound check. `make monitor-network-check` (13 cases). |
| `monitor::files::FileIntegrityMonitor` scan loop | `jsecmon/monitor-files.ss` | ✅ **untyped layer** — file-integrity monitor over an injected `file-provider` seam: `baseline-files` records initial state; `detect-change` produces the ordered diff (deleted > created > permission-changed > owner-changed > modified) — pinned secmon quirk: owner-changed reports `old_mode = new_mode = new.mode`; `scan-files` re-stats tracked paths + discovers new files in watched dirs (Created), classifying via `(jsecmon file-change)` is-suspicious-change. `make-linux-file-provider` now uses POSIX stat mode/uid/gid/mtime and SHA-256 for regular files under 10 MiB; `linux-monitored-paths` = secmon's Linux PLATFORM_PATHS. `make monitor-files-check` (18 cases). |
| `monitor::dns::DnsMonitor` sniffer + connection fallback | `jsecmon/dns-sniffer.ss`, `jsecmon/monitor-dns.ss` | ✅ **untyped runtime shell** — tries the live packet-capture stream first, feeds captured DNS packets through `DnsSnifferState` for real query names/answers + local-port PID/name lookup, and uses the connection-polling fallback when capture is unavailable/ended. The fallback reuses net-provider's list-connections; keeps only remote-port==53; dedups by `pid:remote_addr:protocol:local_port`; emits `query_name="<unknown>"`; and ages entries ≥30 s. `make dns-sniffer-check` pins the wire parser; `make monitor-dns-check` covers sniffer delivery/lookup, fallback suppression, fallback polling, dedup, and cleanup. |
| `monitor::podman::PodmanMonitor` | `jsecmon/monitor-podman.ss` | ✅ **untyped runtime shell** — ports the preferred `podman events --format json` stream (`create`/`start`/`stop`/`kill`/`remove`/`cleanup`/`exec`/`attach`, container-only, malformed lines ignored), starts it with the live Linux provider, queues event lines into the synchronous agent tick, and falls back to `podman ps -a` polling only after spawn failure or stream EOF. `make monitor-podman-check` covers stream parser actions/ignores, queued stream delivery, EOF-to-polling fallback baseline, and start/remove polling deltas. |
| `monitor::mod::MonitorManager` (boot + poll cycle) | `jsecmon/monitor-manager.ss` | ✅ **untyped layer** — `monitor-boot` baselines stateful monitors and emits agent_start; `monitor-tick` runs the full polling set in a fixed order and returns the merged event stream; cleanup-dns-queries runs after tick. `make-linux-monitor-set` wires the live Linux providers, sharing process/network providers where the Rust monitors do, and prefers the live eBPF stream when available (skipping only process/network polling in that mode). `make-freebsd-monitor-set` wires the DTrace direct/subprocess stream and suppresses process/network polling while it is active, matching the high-fidelity mode shape. `make monitor-manager-check` drives the fallback cycle end to end with fixture providers and the DTrace stream drain. |
| `event_json.rs get_event_json_data` | `jsecmon/event-json.ss` | ✅ **untyped layer** — the flat JSON `data` body for every Rust `EventType` variant, matching secmon's json!{...} field SETS exactly: suspicious_exec drops cwd/name; listening_port keeps only local side; suspicious_connection drops state/process_name; mount_event drops target/fstype but computes `dangerous`; namespace/capability derive `ns_types`/`dangerous_capabilities`; all enum-ish fields use Rust Debug spelling. Option::None → JSON null via `(if #f #f)` void sentinel; parent process → nested {pid,name,exe} or null. `make event-json-check` drives monitor events plus representative eBPF/podman/SELinux/revshell/lateral/webshell rows through the serializer, parses back, and asserts the tricky corners. |
| `monitor::events::SecurityEvent` bincode | `jsecmon/event-codec.ss` | ✅ **untyped layer** — byte-exact bincode reader/writer for the top-level `SecurityEvent` and all 32 `EventType` discriminants, with golden secmon bytes for the process/network base cases and round-trip coverage for every remaining variant. This is the compiled agent's event payload codec feeding the encrypted buffer and collector pull path. |
| `bin/agent` storage path (collector loop) | `jsecmon/collector.ss` | ✅ **untyped layer** — ties monitor runtime to storage: `collect-event!` derives the full storage row (data = event-data-json, summary = build-summary, pid/pname = extract-pid/extract-process-name), assigns a monotonic seq, INSERT-OR-IGNORE dedup; `collect-events!` drains a batch; `run-boot!`/`run-tick!` tie monitor-boot/monitor-tick to the store. `run-agent` is the thin live shell (real ms clock, platform monitor-set, poll/sleep). `make collector-check` drives boot+tick of the full fixture monitor set into a real temp SQLite store, queries rows back to assert event count, derived pid/name/summary, parsed data, and dedup. |
| `server::PollServer` | `jsecmon/agent-server.ss` | ✅ **untyped layer** — server-side pull protocol: creates PSK challenge, verifies ChallengeResponse freshness/proof, handles `get_events_after`, `get_events_in_range`, `status`, `acknowledge`, and `ping`, serializes ECIES-sealed `SerializedEvent`s from the priority buffer, assigns monotonic event IDs, persists to the optional local encrypted store, and reports local DB count/latest seq in status. `make agent-server-check` covers direct buffer decrypt, dispatcher requests, local-store status, and socket start/stop lifecycle. |
| `bin/agent` privilege drop | `jsecmon/privdrop.ss`, `bin/agent.ss` | ✅ **untyped runtime shell** — after binding the poll server, the agent resolves `nobody` from passwd text and calls `setgroups`, `setgid`, then `setuid`, matching secmon's required order. `make privdrop-check` verifies lookup strictness and call ordering without changing the test process credentials. |
| live-platform verification limits | — | ⚠️ The provider-backed polling agent path, wire protocol, encrypted local store, ECIES bincode frame, eBPF libbpf runtime shell, DTrace direct/subprocess runtime shells, live DNS capture stream/PID association, live Podman event stream/fallback, stealth init/watchdog, and post-bind `nobody` privilege drop are ported, wired into the agent, and fixture-checked. This macOS workspace cannot prove the privileged Linux root/libbpf path or FreeBSD root/libdtrace path live; final live parity needs those target OS/root environments. |
recent commits
- Document local mine repository authority ffbe127 Jaime Fournier
- Update jpkg metadata for Forgejo 8a00a3a Jaime Fournier
- docs: remove jerboa-emacs restriction f4c7554 Jaime Fournier
- Remove GitHub workflow metadata bf3dc6e Jaime Fournier
- update AGENTS.md 4ed34a7 Jaime Fournier
- perf(buffer): O(1) store/evict via per-priority FIFO queues (P2) 5a11729 Jaime Fournier
- perf(network-guard): peer table as a hash keyed by peer (P2) 6b7c0a9 Jaime Fournier
- perf(agent-server): run ECIES encryption outside the agent lock (P2) 50497d2 Jaime Fournier
- ci: set umask 022 in package/verify tasks too (jerbuild creates group-writable cache dirs during pkg operations) 2ee7973 Jaime Fournier
- ci: set umask 022 to prevent group-writable cache dirs (sr.ht default umask creates 0775 dirs) ab77daa Jaime Fournier