add hooks for blocking and filtering log messages
: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)
+
;;
;;
'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
(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
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"
(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"
(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)))
(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)
(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)))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;