monitor: tie the four monitors together with a manager (mod.rs)
ober
151cd39a583a6c9120fdf9983a5b67671231770a
--- 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) new file mode 100644 --- /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))) new file mode 100644 --- /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)))))