fix: threats sequence-pair binds type/json-filter as ? params (P1 #23)

ober

97265347ed513ae99ffe5b7b79a79e65e9123786

diff --git a/jsecmon/threats.ss b/jsecmon/threats.ss
index d7853f5..7d98104 100644
--- a/jsecmon/threats.ss
+++ b/jsecmon/threats.ss
@@ -227,17 +227,28 @@
 
   ;; Generic: event_type A (optional json equality on data) followed by event_type
   ;; B within window_ms on the same host. json-filter is (path . literal), e.g.
-  ;; ("$.success" . "1") -> AND json_extract(data,'$.success')=1.
+  ;; ("$.success" . "1") -> AND json_extract(data,'$.success')=1. type-a/type-b
+  ;; and the json-filter path+value are bound as ? params (never interpolated),
+  ;; so a value that reaches this helper can't inject SQL. Numeric literals stay
+  ;; numeric so `= ?` keeps the original `=1` integer match against json_extract.
+  (def (json-literal v)
+    (if (string? v) (or (string->number v) v) v))
   (def (sequence-pair db type-a json-filter type-b window-ms rule-name severity filter)
-    (let ((as (filtered-query db
-                (str "SELECT host, timestamp_ms FROM events WHERE event_type='" type-a "'"
-                     (if json-filter
-                         (str " AND json_extract(data,'" (car json-filter) "')=" (cdr json-filter))
-                         ""))
-                " ORDER BY host, timestamp_ms" filter))
-          (bs (filtered-query db
-                (str "SELECT host, timestamp_ms FROM events WHERE event_type='" type-b "'")
-                " ORDER BY host, timestamp_ms" filter)))
+    (let* ((a-binds (if json-filter
+                        (list type-a (car json-filter) (json-literal (cdr json-filter)))
+                        (list type-a)))
+           (aw (build-where filter (+ 1 (length a-binds))))
+           (as (apply sqlite-query db
+                 (str (if json-filter
+                          "SELECT host, timestamp_ms FROM events WHERE event_type = ?1 AND json_extract(data, ?2) = ?3"
+                          "SELECT host, timestamp_ms FROM events WHERE event_type = ?1")
+                      (car aw) " ORDER BY host, timestamp_ms")
+                 (append a-binds (cdr aw))))
+           (bw (build-where filter 2))
+           (bs (apply sqlite-query db
+                 (str "SELECT host, timestamp_ms FROM events WHERE event_type = ?1"
+                      (car bw) " ORDER BY host, timestamp_ms")
+                 (cons type-b (cdr bw)))))
       (pair-sequence as bs window-ms
         (lambda (a b)
           (make-anomaly rule-name (a-str a "host") severity (a-num a "timestamp_ms")