monitor: port secmon's DNS monitor polling fallback (dns.rs)
ober
98e620a1c017aa6003553f681ab39381adc76b3f
--- 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 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 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)" @@ -363,6 +363,11 @@ monitor-network-check: monitor-files-check: $(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_files_check.ss +# DNS monitor connection-polling fallback (secmon dns.rs): port-53 filter, 5 s +# dedup window, 30 s cleanup — pure over the shared net-provider seam. No dylib. +monitor-dns-check: + $(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_dns_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 @@ -414,6 +419,7 @@ checks: kernels-check $(SCHEME) --libdirs $(LIBDIRS) --script examples/monitor_process_check.ss $(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 clean: rm -rf $(BUILD) new file mode 100644 --- /dev/null +++ b/examples/monitor_dns_check.ss @@ -0,0 +1,70 @@ +;;; Behaviour check for the DNS monitor's connection-polling fallback (dns.rs). +;;; +;;; `scan-dns-connections` is pure over an injected net-provider (shared with +;;; the network monitor) and `now`, so we drive it with fixture connections and +;;; assert the port-53 filter, the 5 s dedup window, the key composition, and +;;; the 30 s cleanup — all derived from secmon's scan_dns_connections. +;;; +;;; Run from the repo root with the repo on the libdir path: +;;; scheme --libdirs $JERBOA/lib --libdirs . --script examples/monitor_dns_check.ss + +(import (jerboa prelude) + (jsecmon monitor-network) ;; make-conn-info / make-net-provider + (jsecmon monitor-dns)) + +(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 (field k evs) (map (lambda (e) (hash-get e k)) evs)) +(def (recent-count st) (length (hash-keys (dns-mon-state-recent st)))) + +;; fixtures: a UDP query to 8.8.8.8:53, a TCP query to 1.1.1.1:53, and a plain +;; HTTPS connection that must be ignored. +(def q-udp (make-conn-info "udp" "10.0.0.2" 40000 "8.8.8.8" 53 "ESTABLISHED" 1234 "curl")) +(def q-tcp (make-conn-info "tcp" "10.0.0.2" 40002 "1.1.1.1" 53 "ESTABLISHED" 1234 "curl")) +(def c-web (make-conn-info "tcp" "10.0.0.2" 40001 "93.184.216.34" 443 "ESTABLISHED" 1234 "curl")) + +(def *conns* (list q-udp c-web q-tcp)) +(def netp (make-net-provider (lambda () *conns*) (lambda () '()) "dhost")) +(def st (make-dns-monitor '("8.8.8.8" "1.1.1.1") "dhost")) + +(displayln "A: only port-53 connections, typed by protocol:") +(def ev1 (scan-dns-connections st netp 1000)) +(check "two dns_query" (field "type" ev1) '("dns_query" "dns_query")) +(check "UDP then TCP" (field "query_type" ev1) '("UDP" "TCP")) +(check "server addrs" (field "server_addr" ev1) '("8.8.8.8" "1.1.1.1")) +(check "query_name unknown" (hash-get (car ev1) "query_name") "<unknown>") +(check "web conn ignored" (length ev1) 2) + +(displayln "B: a repeat within the 5 s window is deduped:") +(check "silent at +1s" (scan-dns-connections st netp 2000) '()) + +(displayln "C: past the 5 s window it re-fires:") +(check "re-fires at +6s" (field "type" (scan-dns-connections st netp 7000)) + '("dns_query" "dns_query")) + +(displayln "D: the dedup key includes local_port — a new socket is distinct:") +(def q-udp2 (make-conn-info "udp" "10.0.0.2" 49999 "8.8.8.8" 53 "ESTABLISHED" 1234 "curl")) +(set! *conns* (list q-udp c-web q-tcp q-udp2)) +(check "new local_port emits once" (length (scan-dns-connections st netp 7100)) 1) + +(displayln "E: a provider error makes the scan a no-op:") +(def errp (make-net-provider (lambda () #f) (lambda () '()) "dhost")) +(check "no events" (scan-dns-connections st errp 8000) '()) + +(displayln "F: cleanup ages out dedup entries older than 30 s:") +(def before (recent-count st)) +(cleanup-dns-queries st 8000) ;; newest entries ~7100, diff < 30 s -> kept +(check "young entries kept" (recent-count st) before) +(cleanup-dns-queries st 40000) ;; now all entries are > 30 s old -> dropped +(check "old entries dropped" (recent-count st) 0) +;; with the dedup map cleared, the same connections fire again +(check "fires after cleanup" (length (scan-dns-connections st netp 41000)) 3) + +(newline) +(if (= fails 0) + (displayln "OK: dns fallback filters port 53, dedups within 5 s, and ages out at 30 s.") + (begin (displayln fails " FAILURES") (exit 1))) new file mode 100644 --- /dev/null +++ b/jsecmon/monitor-dns.ss @@ -0,0 +1,114 @@ +#!chezscheme +;;; jsecmon DNS monitor — connection-polling fallback (secmon monitor/dns.rs). +;;; +;;; secmon's DnsMonitor has two strategies: an AF_PACKET sniffer (Linux+root, +;;; in dns_sniffer.rs — its parser is already ported, examples/dns_sniffer_ +;;; check.ss) and a connection-polling fallback that watches /proc/net for +;;; connections to port 53. This ports the fallback's scan loop: it reuses the +;;; NetworkProvider seam (so it shares (jsecmon monitor-network)'s conn-info / +;;; net-provider) and the trusted-resolver set from (jsecmon dns-servers). +;;; +;;; `scan-dns-connections` is pure over an injected net-provider + `now` +;;; (epoch ms), so the port-53 filter, the dedup window, and the event build +;;; are fixture-testable; `cleanup-dns-queries` ages the dedup map. +;;; +;;; Faithfulness points the Rust pins: +;;; * only remote_port == 53 connections are considered (others `continue`). +;;; * dedup key = "pid:remote_addr:protocol:local_port" (pid defaults to 0); +;;; a key seen < 5000 ms ago is skipped. +;;; * query_type = "UDP" if protocol contains "udp" else "TCP"; query_name is +;;; literally "<unknown>" (the fallback can't see the domain). Category/ +;;; severity per event_json.rs: dns_query / info. +;;; * cleanup drops dedup entries older than 30000 ms. +;;; * secmon computes is_dns = servers ∋ addr || port == 53; since we already +;;; filtered to port 53 that is always true, so the resolver set does not +;;; gate the fallback — kept verbatim for fidelity. + +(library (jsecmon monitor-dns) + (export make-dns-monitor dns-mon-state-hostname dns-mon-state-servers + dns-mon-state-recent + scan-dns-connections cleanup-dns-queries + linux-dns-servers linux-dns-hostname) + (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-network) + (jsecmon dns-servers)) + + ;; servers : list of trusted resolver addrs; recent : key -> last-seen ms. + (defstruct dns-mon-state (hostname servers recent)) + + (def (make-dns-monitor servers hostname) + (make-dns-mon-state hostname servers (make-hash-table))) + + (def (query-type-of c) + (if (string-contains (conn-info-protocol c) "udp") "UDP" "TCP")) + + (def (dns-query-event host now c) + (let ((h (make-hash-table))) + (hash-put! h "host" host) + (hash-put! h "ts" now) + (hash-put! h "type" "dns_query") + (hash-put! h "severity" "info") + (hash-put! h "query_name" "<unknown>") + (hash-put! h "query_type" (query-type-of c)) + (hash-put! h "server_addr" (conn-info-remote-addr c)) + (hash-put! h "pid" (conn-info-pid c)) + (hash-put! h "pname" (conn-info-process-name c)) + (hash-put! h "connection" c) + h)) + + ;; secmon scan_dns_connections (fallback): emit a deduped DnsQuery for each + ;; live connection to a remote port 53. Pure over the net-provider and `now`. + (def (scan-dns-connections state net-provider now) + (let ((conns ((net-provider-list-connections net-provider)))) + (if (not conns) + '() ;; Err(list_connections) -> no-op + (let ((recent (dns-mon-state-recent state)) + (servers (dns-mon-state-servers state)) + (host (dns-mon-state-hostname state)) + (events '())) + (for-each + (lambda (c) + (when (= (conn-info-remote-port c) 53) + ;; is_dns = servers ∋ addr || port==53 — always true here (see header) + (let ((is-dns (or (member (conn-info-remote-addr c) servers) + (= (conn-info-remote-port c) 53)))) + (when is-dns + (let ((key (str (or (conn-info-pid c) 0) ":" + (conn-info-remote-addr c) ":" + (conn-info-protocol c) ":" + (conn-info-local-port c)))) + (let ((last (hash-get recent key))) + (unless (and last (< (- now last) 5000)) + (hash-put! recent key now) + (set! events (cons (dns-query-event host now c) events))))))))) + conns) + (reverse events))))) + + ;; secmon cleanup_old_queries: drop dedup entries older than 30 s. + (def (cleanup-dns-queries state now) + (let ((recent (dns-mon-state-recent state))) + (for-each (lambda (k) + (when (>= (- now (hash-get recent k)) 30000) + (hash-remove! recent k))) + (hash-keys recent)))) + + ;; --- the thin /etc/resolv.conf shell (Linux) ------------------------------- + (def (slurp path) (guard (e (#t #f)) (read-file-string path))) + + (def (linux-dns-servers) + (let ((content (slurp "/etc/resolv.conf"))) + (if content (parse-dns-servers content) *public-dns-servers*))) + + (def (linux-dns-hostname) + (or (let ((h (slurp "/etc/hostname"))) (and h (string-trim h))) + (let ((h (slurp "/proc/sys/kernel/hostname"))) (and h (string-trim h))) + "unknown"))) --- a/jsecmon/monitor-network.ss +++ b/jsecmon/monitor-network.ss @@ -30,6 +30,7 @@ conn-info-remote-addr conn-info-remote-port conn-info-state conn-info-pid conn-info-process-name make-net-provider net-provider-hostname + net-provider-list-connections net-provider-list-listeners make-net-state net-state-hostname net-state-known-connections net-state-known-listeners make-network-monitor scan-connections