monitor: tie the four monitors together with a manager (mod.rs)

ober

151cd39a583a6c9120fdf9983a5b67671231770a

diff --git a/Makefile b/Makefile
index b2ed486..fa57ecd 100644
--- 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 entity-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 event-danger-check persistence-check file-change-check webshell-check platform-mounts-check analyze-cli-check collector-cli-check event-summary-check ioc-check frame-check correlate-check revshell-check cron-check logtamper-check detection-rules-check ipaddr-check auth-check lolbin-check dga-check calendar-check monitor-process-check monitor-network-check monitor-files-check monitor-dns-check checks clean
+.PHONY: rust test ffi-demo kernels-check triage-check triage-store-check analytics-check detect-check storage-check entity-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 event-danger-check persistence-check file-change-check webshell-check platform-mounts-check analyze-cli-check collector-cli-check event-summary-check ioc-check frame-check correlate-check revshell-check cron-check logtamper-check detection-rules-check ipaddr-check auth-check lolbin-check dga-check calendar-check monitor-process-check monitor-network-check monitor-files-check monitor-dns-check monitor-manager-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)"
@@ -368,6 +368,11 @@ monitor-files-check:
 monitor-dns-check:
 	$(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_dns_check.ss
 
+# Monitor manager (secmon mod.rs): boot + one poll cycle running all four
+# monitors into a single merged event stream, over fixture providers. No dylib.
+monitor-manager-check:
+	$(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_manager_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
@@ -420,6 +425,7 @@ checks: kernels-check
 	$(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_network_check.ss
 	$(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_files_check.ss
 	$(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_dns_check.ss
+	$(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_manager_check.ss
 
 clean:
 	rm -rf $(BUILD)
diff --git a/examples/monitor_manager_check.ss b/examples/monitor_manager_check.ss
new file mode 100644
index 0000000..2311d64
--- /dev/null
+++ b/examples/monitor_manager_check.ss
@@ -0,0 +1,74 @@
+;;; Behaviour check for the monitor manager (secmon mod.rs MonitorManager).
+;;;
+;;; Wires all four monitors to fixture providers and asserts that `monitor-boot`
+;;; baselines + emits agent_start, and that one `monitor-tick` runs every scan
+;;; and merges the events in the fixed process/network/files/dns order — i.e.
+;;; the whole agent body works end to end without any OS access.
+;;;
+;;; Run from the repo root with the repo on the libdir path:
+;;;   scheme --libdirs $JERBOA/lib --libdirs . --script examples/monitor_manager_check.ss
+
+(import (jerboa prelude)
+        (jsecmon monitor-process)
+        (jsecmon monitor-network)
+        (jsecmon monitor-files)
+        (jsecmon monitor-dns)
+        (jsecmon monitor-manager))
+
+(def fails 0)
+(def (check label got want)
+  (let ((ok (equal? got want)))
+    (unless ok (set! fails (+ fails 1)))
+    (displayln (if ok "  ok   " "  FAIL ") label " => " got
+               (if ok "" (str "  (want " want ")")))))
+(def (types evs) (map (lambda (e) (hash-get e "type")) evs))
+
+;; --- process provider: init + gitea, both benign -----------------------------
+(def *pids* '(1 100))
+(def procs (make-hash-table))
+(hash-put! procs 1   (make-proc-info 1   0    0 "/sbin/init"     "systemd" '("/sbin/init") #f '()))
+(hash-put! procs 100 (make-proc-info 100 1 1000 "/usr/bin/gitea" "gitea"   '("/usr/bin/gitea") #f '()))
+(def pp (make-mon-provider (lambda () *pids*) (lambda (p) (hash-get procs p)) "host"))
+
+;; --- network provider: one listener + a normal conn + a DNS conn (shared) ----
+(def l-ssh  (make-conn-info "tcp" "0.0.0.0" 22 "0.0.0.0" 0 "LISTEN" #f "sshd"))
+(def c-web  (make-conn-info "tcp" "10.0.0.2" 50000 "93.184.216.34" 443 "ESTABLISHED" 1234 "curl"))
+(def c-dns  (make-conn-info "udp" "10.0.0.2" 40000 "8.8.8.8"       53  "ESTABLISHED" 1234 "curl"))
+(def np (make-net-provider (lambda () (list c-web c-dns)) (lambda () (list l-ssh)) "host"))
+
+;; --- file provider: one critical file, mutated between boot and tick ----------
+(def fs (make-hash-table))
+(def (fs-get p) (or (hash-get fs p) (make-file-state #f 0 0 0 0 #f)))
+(hash-put! fs "/etc/passwd" (make-file-state "p1" #o644 0 0 100 #t))
+(def fp (make-file-provider fs-get (lambda (p) #f) "host"))
+
+;; --- assemble the set ---------------------------------------------------------
+(def mset (make-monitor-set
+           "host"
+           (make-monitor pp) pp
+           (make-network-monitor np) np
+           (make-file-monitor fp 'linux) fp '("/etc/passwd")
+           (make-dns-monitor '("8.8.8.8") "host")))
+
+(displayln "boot baselines files and emits agent_start:")
+(check "agent_start only" (types (monitor-boot mset "0.1.0" 1000)) '("agent_start"))
+
+;; a critical file changes after baseline -> the tick should flag it
+(hash-put! fs "/etc/passwd" (make-file-state "p2" #o644 0 0 100 #t))
+
+(displayln "first tick runs every monitor, merged in fixed order:")
+(def t1 (monitor-tick mset 2000))
+(check "merged event stream"
+       (types t1)
+       '("process_start" "process_start"          ;; init, gitea
+         "listening_port" "network_connection" "network_connection"  ;; sshd; 443; :53
+         "suspicious_file_change"                  ;; /etc/passwd critical
+         "dns_query"))                             ;; the :53 conn, seen by dns too
+
+(displayln "second tick with no changes (within dns dedup window) is silent:")
+(check "idempotent" (monitor-tick mset 2500) '())
+
+(newline)
+(if (= fails 0)
+    (displayln "OK: the manager boots and ticks all four monitors into one event stream.")
+    (begin (displayln fails " FAILURES") (exit 1)))
diff --git a/jsecmon/monitor-manager.ss b/jsecmon/monitor-manager.ss
new file mode 100644
index 0000000..82ff6d2
--- /dev/null
+++ b/jsecmon/monitor-manager.ss
@@ -0,0 +1,77 @@
+#!chezscheme
+;;; jsecmon monitor manager (secmon src/monitor/mod.rs), untyped.
+;;;
+;;; secmon's MonitorManager spawns each monitor as an async task feeding one
+;;; mpsc channel of SecurityEvents. jsecmon has no async runtime, so the
+;;; equivalent is a synchronous poll cycle: `monitor-tick` runs every monitor's
+;;; scan against its provider for a given `now` and returns the merged event
+;;; stream; `monitor-boot` does the one-time startup secmon's run() methods do
+;;; (baseline the file set, emit agent_start). The caller drains the events to
+;;; whatever sink it likes (storage, a socket, stdout) — that sink is the thin
+;;; shell; the cycle itself is pure over the injected providers, so the whole
+;;; agent body is fixture-testable.
+;;;
+;;; Monitors run in a fixed order (process, network, files, dns) so the merged
+;;; stream is deterministic. The network and dns monitors share one
+;;; net-provider, exactly as secmon shares a NetworkProvider between them.
+
+(library (jsecmon monitor-manager)
+  (export make-monitor-set monitor-set?
+          monitor-set-hostname
+          monitor-boot monitor-tick
+          make-linux-monitor-set)
+  (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?)
+          (jsecmon monitor-process)
+          (jsecmon monitor-network)
+          (jsecmon monitor-files)
+          (jsecmon monitor-dns))
+
+  ;; the full agent: each monitor's state + the providers feeding it. The dns
+  ;; monitor reuses net-provider (secmon shares a NetworkProvider).
+  (defstruct monitor-set
+    (hostname
+     proc-state proc-provider
+     net-state net-provider
+     file-state file-provider file-paths
+     dns-state))
+
+  ;; one-time startup (secmon's per-monitor run() preambles): baseline the
+  ;; tracked files and emit the single agent_start event.
+  (def (monitor-boot mset version now)
+    (baseline-files (monitor-set-file-state mset)
+                    (monitor-set-file-provider mset)
+                    (monitor-set-file-paths mset))
+    (list (agent-start-event (monitor-set-hostname mset) version now)))
+
+  ;; one poll cycle: every monitor scans for `now`, results merged in a fixed
+  ;; order. The dns dedup map is aged afterwards (secmon cleanup_old_queries).
+  (def (monitor-tick mset now)
+    (let ((evs (append
+                (scan-processes  (monitor-set-proc-state mset) (monitor-set-proc-provider mset) now)
+                (scan-connections (monitor-set-net-state mset) (monitor-set-net-provider mset) now)
+                (scan-files      (monitor-set-file-state mset) (monitor-set-file-provider mset) now)
+                (scan-dns-connections (monitor-set-dns-state mset) (monitor-set-net-provider mset) now))))
+      (cleanup-dns-queries (monitor-set-dns-state mset) now)
+      evs))
+
+  ;; wire the set to the live platform providers (Linux). The thin shell.
+  (def (make-linux-monitor-set)
+    (let* ((pp (make-linux-provider))
+           (np (make-linux-network-provider))
+           (fp (make-linux-file-provider))
+           (host (mon-provider-hostname pp)))
+      (make-monitor-set
+       host
+       (make-monitor pp) pp
+       (make-network-monitor np) np
+       (make-file-monitor fp 'linux) fp (linux-monitored-paths)
+       (make-dns-monitor (linux-dns-servers) host)))))