summary history branches tags files
commit:fcee6a66eadc83bf485d8c2efceafc18b5ddf3c6
author:Trevor Bentley
committer:Trevor Bentley
date:Wed Dec 9 18:25:44 2020 +0100
parents:0321f364e5c9b86abef0fd1a16ecd560a64ed894
add hooks for blocking and filtering log messages
diff --git a/snitch-custom.el b/snitch-custom.el
line changes: +23/-0
index d8f2723..b2255c0
--- a/snitch-custom.el
+++ b/snitch-custom.el
@@ -308,6 +308,29 @@ Returning nil interrupts the block, allowing the event to pass."
   :group 'snitch-hooks
   :type 'hook)
 
+;;;###autoload
+(defcustom snitch-log-functions '()
+  "Hooks called for snitch log entries.
+
+These hooks can be used to filter snitch's log output.  One
+possible use is removing potentially sensitive information from
+the log, such as authentication tokens passed to curl as
+arguments.
+
+Callback functions must take one argument: a log message string,
+propertized with details about the event that generated it.
+
+Return t to keep the log unchanged, nil to block the log entry,
+or a new propertized string to replace the log line.
+
+If several hooks are registered, the first hook to return nil or
+a modified string terminates processing.
+
+Hooks that modify the message are strongly encouraged to keep the
+timestamp and trailing newline intact."
+  :group 'snitch-hooks
+  :type 'hook)
+
 
 ;;
 ;;

diff --git a/snitch-log.el b/snitch-log.el
line changes: +35/-10
index f23155c..7de0820
--- a/snitch-log.el
+++ b/snitch-log.el
@@ -133,6 +133,21 @@ log message. "
                 'snitch-port (oref event port)
                 'snitch-family (oref event family)))))
 
+(defun snitch--run-filter-log-hooks (logmsg)
+  "Run all hooks registered in ‘snitch-log-functions’ with the
+given log message.  Return the original log message if all hooks
+return t (or none are defined), or return nil or a modified new
+log string based on the first hook to return something other than
+t."
+  (if (null snitch-log-functions)
+      logmsg
+    (cl-loop for fn in snitch-log-functions with res = nil
+             do (setq res (funcall fn logmsg))
+             when (or (null res)
+                      (stringp res))
+             return res
+             finally return logmsg)))
+
 (defun snitch--log (evt-type event)
   "Log a snitch event to the dedicated snitch firewall log
 buffer.  EVENT is an event object, and EVT-TYPE is any policy
@@ -155,25 +170,35 @@ type from ‘snitch-log-policies’."
            (buf (get-buffer-create snitch--log-buffer-name))
            (pretty-obj (snitch--pretty-obj-string event))
            (timestamp (format-time-string "%Y-%m-%d %H:%M:%S"))
-           (logmsg (snitch--propertize
+           (rawmsg (snitch--propertize
                     (cond (snitch-log-verbose (format "[%s] (%s) --\n%s"
                                                       timestamp name pretty-obj))
                           (t (format "[%s] (%s) -- %s\n"
                                      timestamp name event)))
-                    event)))
+                    event))
+           (logmsg (snitch--run-filter-log-hooks rawmsg)))
       ;; start timer to keep log size limited
       (snitch--maybe-start-log-prune-timer)
       ;; write the formatted log entry to the log buffer
-      (with-current-buffer buf
-        (setq buffer-read-only nil)
-        (buffer-disable-undo)
-        (save-excursion
-          (goto-char (point-max))
-          (insert logmsg))
-        (setq buffer-read-only t))
+      (when logmsg
+        (with-current-buffer buf
+          (setq buffer-read-only nil)
+          (buffer-disable-undo)
+          (save-excursion
+            (goto-char (point-max))
+            (insert logmsg))
+          (setq buffer-read-only t)
+          ;; scroll log window to end if it is not active.  Don’t
+          ;; scroll when active to allow user to move around
+          ;; uninterrupted in the log.
+          (let ((log-win (get-buffer-window buf)))
+            (when log-win
+              (unless (eq (selected-window) log-win)
+                (with-selected-window log-win
+                  (goto-char (point-max))))))))
       ;; if the alert package is available and notifications are
       ;; enabled, also raise a notification
-      (when (and snitch--have-alert snitch-enable-notifications)
+      (when (and logmsg snitch--have-alert snitch-enable-notifications)
         (alert logmsg
                :title (format "Snitch Event: %s" name)
                :severity 'normal

diff --git a/snitch-test.el b/snitch-test.el
line changes: +75/-3
index 20b0611..85e9678
--- a/snitch-test.el
+++ b/snitch-test.el
@@ -78,7 +78,8 @@
         snitch-on-allow-functions
         snitch-on-block-functions
         snitch-on-whitelist-functions
-        snitch-on-blacklist-functions))
+        snitch-on-blacklist-functions
+        snitch-log-functions))
 
 (defun snitch-test--restore-vars (vars)
   "restore saved vars after a test"
@@ -95,7 +96,8 @@
   (setq snitch-on-allow-functions (nth 10 vars))
   (setq snitch-on-block-functions (nth 11 vars))
   (setq snitch-on-whitelist-functions (nth 12 vars))
-  (setq snitch-on-blacklist-functions (nth 13 vars)))
+  (setq snitch-on-blacklist-functions (nth 13 vars))
+  (setq snitch-log-functions (nth 14 vars)))
 
 (defun snitch-test--clear-vars (net-policy proc-policy &optional init)
   "set global vars to known defaults for duration of a test"
@@ -107,12 +109,13 @@
   (setq snitch-process-whitelist '())
   (setq snitch-log-policy '())
   (setq snitch-log-verbose nil)
-  (setq snitch--log-buffer-max-lines 5000)
+  (setq snitch--log-buffer-max-lines 1000)
   (setq snitch-on-event-functions '())
   (setq snitch-on-allow-functions '())
   (setq snitch-on-block-functions '())
   (setq snitch-on-whitelist-functions '())
   (setq snitch-on-blacklist-functions '())
+  (setq snitch-log-functions '())
   (when init
     (snitch-init)))
 
@@ -187,6 +190,16 @@ matches EXPECTED-SUCCESS: t if allowed through, nil if blocked."
                (props (text-properties-at (point))))
           (list event class props))))))
 
+(defun snitch-test--get-log-line-raw (line)
+  "get a single line from the log buffer, unparsed"
+  (with-current-buffer (get-buffer-create snitch--log-buffer-name)
+    (let ((line-count (count-lines (point-min) (point-max))))
+      (when (> line-count line)
+        (goto-char (point-min))
+        (forward-line line)
+        (beginning-of-line)
+        (thing-at-point 'line)))))
+
 (defun snitch-test--log-lines ()
   "get the total number of lines in the snitch log buffer."
   (with-current-buffer (get-buffer-create snitch--log-buffer-name)
@@ -895,6 +908,65 @@ snitch to accept it."
     (snitch-test--restore-vars orig-vars)
     (snitch-test--cleanup)))
 
+(ert-deftest snitch-test-log-hooks ()
+  "Test that hooks are called when snitch emits a log message.
+Tests passing, blocking, and modifying log messages."
+  (setq hook1-var 0)
+  (setq hook2-var 0)
+  (let ((orig-vars (snitch-test--save-vars t))
+        (hook1 (lambda (msg) (setq hook1-var (1+ hook1-var)) t))
+        (hook2 (lambda (msg)
+                 (setq hook2-var (1+ hook2-var))
+                 (cond
+                  ((equal (get-text-property 0 'snitch-executable msg) "curl")
+                   "filtered out curl message\n")
+                  ((equal (get-text-property 0 'snitch-executable msg) "ls")
+                   nil)
+                  (t t)))))
+    (snitch-test--clear-vars 'allow 'allow t)
+    (setq snitch-log-policy '(allowed))
+
+    ;; All messages allowed
+    (setq snitch-log-functions (list hook1))
+    (snitch-test--clear-logs)
+
+    (snitch-test--process "ls" t)
+    (snitch-test--process "curl" t)
+    (snitch-test--process "whoami" t)
+    (should (eq hook1-var 3))
+    (should (eq hook2-var 0))
+    (should (eq (snitch-test--log-lines) 3))
+
+    ;; Some messages filtered
+    (setq snitch-log-functions (list hook1 hook2 hook1))
+    (snitch-test--clear-logs)
+
+    ;; hook1 run once (hook2 terminates)
+    (snitch-test--process "ls" t)
+    (should (eq hook1-var 4))
+    (should (eq hook2-var 1))
+    ;; ls blocked, nothing in log
+    (should (eq (snitch-test--log-lines) 0))
+
+    ;; hook1 run once (hook2 terminates)
+    (snitch-test--process "curl" t)
+    (should (eq hook1-var 5))
+    (should (eq hook2-var 2))
+    (should (eq (snitch-test--log-lines) 1))
+    (should (string-match "filtered out curl"
+                          (snitch-test--get-log-line-raw 0)))
+
+
+    ;; hook1 run twice (hook2 passes)
+    (snitch-test--process "whoami" t)
+    (should (eq hook1-var 7))
+    (should (eq hook2-var 3))
+    (should (eq (snitch-test--log-lines) 2))
+
+    ;; cleanup
+    (snitch-test--restore-vars orig-vars)
+    (snitch-test--cleanup)))
+
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;