commit: | 16f5a70c5b876c787083766728ef5b6a2016e32c |
author: | Trevor Bentley |
committer: | Trevor Bentley |
date: | Wed Dec 2 18:10:52 2020 +0100 |
parents: |
diff --git a/snitch-backtrace.el b/snitch-backtrace.el line changes: +141/-0 index 0000000..5454116 --- /dev/null +++ b/snitch-backtrace.el
@@ -0,0 +1,141 @@ +;;; snitch-backtrace.el -*- lexical-binding: t; -*- +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; See snitch.el for full details. +;; +;; Copyright (C) 2020 Trevor Bentley +;; Author: Trevor Bentley <snitch.el@x.mrmekon.com> +;; URL: https://github.com/mrmekon/snitch-el +;; +;; This file is not part of GNU Emacs. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; This file provides backtrace analysis for snitch.el. It is used to +;; attempt to determine the most likely original source of an event. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +;; find all directories in elisp load path that are NOT in the user dir +;; returns a list of strings +(defun snitch--site-lisp-dirs () + (let ((user-dir (expand-file-name package-user-dir))) + (cl-loop for dir in (elisp-load-path-roots) + unless (or (string-prefix-p user-dir dir) + (string-prefix-p package-user-dir dir)) + collect dir))) + +;; find the 'root' directories of (site-lisp-dirs), which is hopefully +;; a list of all of the system-wide base dirs that contain elisp +;; returns a list of strings +(defun snitch--site-lisp-roots () + (cl-loop for dir in (snitch--site-lisp-dirs) + if (or (string-equal "lisp" (file-name-base dir)) + (string-equal "site-lisp" (file-name-base dir))) + collect dir)) + +;; check if a directory is a subdirectory of a system-wide elisp dir +;; returns a boolean +(defun snitch--dir-in-site-lisp (dir) + (not (null (cl-loop for site-dir in (snitch--site-lisp-roots) + if (string-prefix-p site-dir dir) + collect site-dir)))) + +;; check if a directory belongs to a package tracked by the package manager. +;; if so, returns its name as a symbol +(defun snitch--package-from-dir (dir) + (nth 0 + (cl-loop for (pkgname . pkgdesc) in (package--alist) + if (string-equal + (file-name-as-directory dir) + (file-name-as-directory (package-desc-dir (car pkgdesc)))) + collect pkgname))) + +;; try to guess a package name for a full path to a file. returns a symbol, +;; which is either an installed package name, or one of the three fixed values: +;; - 'built-in, if registered as a built-in package +;; - 'site-lisp, if found in a system-wide elisp directory +;; - 'user, if its source is unknown +(defun snitch--package-from-path (path) + (let* ((dir (file-name-directory path)) + ;; twice to handle .el.gz + (base (file-name-base (file-name-base path))) + (package (snitch--package-from-dir dir))) + (if package + package + (if (package-built-in-p (intern base)) + 'built-in + (if (snitch--dir-in-site-lisp dir) + 'site-lisp + 'user))))) + +(defun snitch--backtrace () + (setq stack '()) + (let ((frames (backtrace-get-frames))) + (dotimes (idx (length frames)) + (if (> idx 5) ; skip frames in snitch + (let* ((frame (nth idx frames)) + (fun (backtrace-frame-fun frame)) + (path (find-lisp-object-file-name fun 'defun)) + (file (if path (file-name-base path) nil)) + (dir (if path (file-name-directory path) nil)) + (package (if path (snitch--package-from-path path) nil))) + ;;(message "frame %d: %s (%s) [%s]" idx fun path package) + (add-to-list 'stack (list fun path package)))))) + (reverse stack)) + +;; return true of package type 'a' is "more important", i.e. more likely +;; to be the package responsible for a request. Used to traverse a +;; backtrace looking for the "most important" function -- the most recent +;; function that should be considered the triggering cause. +(defun snitch--package-type-more-important (a b) + (cond + ;; nil only greater than nil + ((null a) (member b (list nil))) + ;; site-lisp more important than nil and itself + ((eq 'site-lisp a) (member b (list nil 'site-lisp))) + ;; built-in more important than nil, site-lisp, and itself + ((eq 'built-in a) (member b (list nil 'site-lisp 'built-in))) + ;; user more important than earlier, but not more important + ;; than itself. + ((eq 'user a) (member b (list nil 'site-lisp 'built-in))) + ;; installed package is most important, traversal stops here. + ((symbolp a) (member b (list nil 'site-lisp 'built-in 'user))) + ;; anything else is unknown + (t nil))) + + +(defun snitch--responsible-caller (backtrace) + (cl-loop for caller in backtrace with result = nil + when (snitch--package-type-more-important + (nth 2 caller) + (if (null result) nil + (nth 2 (car result)))) + do + (push caller result) + finally return (car result))) + +(provide 'snitch-backtrace) + +;;; snitch-backtrace.el ends here
diff --git a/snitch-custom.el b/snitch-custom.el line changes: +329/-0 index 0000000..3d3b0ed --- /dev/null +++ b/snitch-custom.el
@@ -0,0 +1,329 @@ +;;; snitch-custom.el -*- lexical-binding: t; -*- +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; See snitch.el for full details. +;; +;; Copyright (C) 2020 Trevor Bentley +;; Author: Trevor Bentley <snitch.el@x.mrmekon.com> +;; URL: https://github.com/mrmekon/snitch-el +;; +;; This file is not part of GNU Emacs. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; This file provides the customizable user options for snitch.el. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +;; +;; +;; Customizable variables +;; +;; + +(defgroup snitch nil + "Customization options for the snitch firewall" + :group 'communication + :prefix "snitch-") + + +(defgroup snitch-log nil + "Logging options for snitch firewall" + :group 'snitch + :prefix "snitch-") + +;;;###autoload +(defcustom snitch-log-policy '(all blocked) + "Specifies types of actions that snitch should log. Provided +as a list of symbols defined in snitch-log-policies" + :type '(repeat (choice (const all) + (const blocked) + (const allowed) + (const whitelisted) + (const blacklisted))) + :group 'snitch-log) + +(defcustom snitch-log-verbose nil + "Whether the log output should be extra verbose (pretty-printed +multi-line event logs)." + :type 'boolean + :group 'snitch-log) + +(defcustom snitch--log-buffer-max-lines 5000 + "Maximum number of lines to keep in the snitch event log +buffer. When it grows larger than this, the least recent lines +are periodically truncated by a timer. + +Since trimming is timer-based, the log buffer can temporarily +grow larger than the requested value. It is only trimmed after a +period of emacs idle time. + +Set to 0 for unlimited." + :type 'number + :group 'snitch-log) + +(defcustom snitch-enable-notifications nil + "Whether snitch should raise notifications for each log +message, in addition to printing them in the log buffer. + +This feature requires the ‘alert’ package to be available. + +Users can define custom styles for alert with +‘alert-define-style’. All snitch alerts set ‘category’ to +‘snitch’, provide an ‘id’ field unique to each event, and provide +the event object in ‘data’." + :type 'boolean + :group 'snitch-log) + + +(defgroup snitch-policy nil + "Default firewall policy options for snitch" + :group 'snitch + :prefix "snitch-") + +;;;###autoload +(defcustom snitch-process-policy 'allow + "Default firewall policy for subprocesses. When set to allow, +exceptions can be specified in snitch-process-blacklist. When +set to deny, exceptions can be specified in +snitch-process-whitelist." + :type '(choice (const deny) + (const allow)) + :group 'snitch-policy) + +;;;###autoload +(defcustom snitch-network-policy 'allow + "Default firewall policy for network connections. When set to +allow, exceptions can be specified in snitch-network-blacklist. +When set to deny, exceptions can be specified in +snitch-network-whitelist." + :type '(choice (const deny) + (const allow)) + :group 'snitch-policy) + + +(defgroup snitch-rules nil + "Firewall rules for snitch (blacklists/whitelists)" + :group 'snitch + :prefix "snitch-") + +;;;###autoload +(defcustom snitch-network-blacklist + '() + "" + :group 'snitch-rules + :type '(alist :key-type function + :value-type (repeat sexp))) + +;;;###autoload +(defcustom snitch-network-whitelist + '( + (snitch-filter/src-pkg . (user)) + ) + "" + :group 'snitch-rules + :type '(alist :key-type function + :value-type (repeat sexp))) + +;;;###autoload +(defcustom snitch-process-blacklist + '( + ;; Example: block processes from elfeed + (snitch-filter/src-pkg . (elfeed)) + + ;; Example: block processes from system packages + ;;(snitch-filter/src-pkg . (site-lisp)) + + ;; Example: block processes from emacs built-ins + ;;(snitch-filter/src-pkg . (built-in)) + + ;; Example: block processes from an unknown user package + (snitch-filter/src-pkg . (user)) + ) + "" + :group 'snitch-rules + :type '(alist :key-type function + :value-type (repeat sexp))) + +;;;###autoload +(defcustom snitch-process-whitelist + '() + "A list of rules defining which subprocess calls are permitted +when snitch.el is configured to deny subprocesses by default. + +If any filter returns true, the process is immediately allowed +without checking any remaining rules. + +Format is an alist of filter function and argument lists, in the +form: + + '((filter-fn1 . (arg1)) + (filter-fn2 . (arg2 arg3)) + (filter-fn3 . (arg4 arg5 arg6))) + +Each filter function must take a snitch-network-entry eieio +object as its first parameter, and any number of subsequent +arguments which are specified as the arguments in this alist. + +In the above example, filter-fn2 might be defined: + + (defun filter-fn2 (net-event fn-arg pkg-arg) + (or (string-equal (oref net-event :src-fn) fn-arg) + (string-equal (oref net-event :src-pkg) pkg-arg))) + +This allows any arbitrary filtering rules, at the expense of +efficiency. Keep short-circuiting in mind, and put more general +rules earlier in the list." + :group 'snitch-rules + :type '(alist :key-type function + :value-type (repeat sexp))) + + +;; +;; +;; Hooks +;; +;; + +(defgroup snitch-hooks nil + "Hooks (callbacks) for snitch firewall events." + :group 'snitch + :prefix "snitch-") + +;;;###autoload +(defcustom snitch-on-event-functions '() + "Hooks called for every event that snitch can intercept. + +Note that every event that is not blocked by these hooks is sent +twice: once to these hooks on initial reception, and again to one +of the other hooks with snitch's final decision. + +Callback functions must take two arguments: + + 1) a snitch-actions symbol describing the event type ('event) + + 2) an event object, either a snitch-process-entry or + snitch-network-entry. + +Returning nil blocks the event, terminating processing." + :group 'snitch-hooks + :type 'hook) + +;;;###autoload +(defcustom snitch-on-block-functions '() + "Hooks called for events that are about to be blocked by policy. + +Callback functions must take two arguments: + + 1) a snitch-actions symbol describing the event type ('block) + + 2) an event object, either a snitch-process-entry or + snitch-network-entry. + +Returning nil interrupts the block, allowing the event to pass." + :group 'snitch-hooks + :type 'hook) + +;;;###autoload +(defcustom snitch-on-allow-functions '() + "Hooks called for events that are about to be allowed by policy. + +Callback functions must take two arguments: + + 1) a snitch-actions symbol describing the event type ('allow) + + 2) an event object, either a snitch-process-entry or + snitch-network-entry. + +Returning nil blocks the event, terminating processing." + :group 'snitch-hooks + :type 'hook) + +;;;###autoload +(defcustom snitch-on-whitelist-functions '() + "Hooks called for events that are about to be allowed by whitelist. + +Callback functions must take two arguments: + + 1) a snitch-actions symbol describing the event type ('whitelist) + + 2) an event object, either a snitch-process-entry or + snitch-network-entry. + +Returning nil blocks the event, terminating processing." + :group 'snitch-hooks + :type 'hook) + +;;;###autoload +(defcustom snitch-on-blacklist-functions '() + "Hooks called for events that are about to be blocked by blacklist. + +Callback functions must take two arguments: + + 1) a snitch-actions symbol describing the event type ('blacklist) + + 2) an event object, either a snitch-process-entry or + snitch-network-entry. + +Returning nil interrupts the block, allowing the event to pass." + :group 'snitch-hooks + :type 'hook) + +;;;###autoload +(defcustom snitch-log-filter-window-open-hook '() + "Called immediately after log filter window opens." + :group 'snitch-hooks + :type 'hook) + +;;;###autoload +(defcustom snitch-log-filter-window-close-hook '() + "Called immediately after log filter window closes." + :group 'snitch-hooks + :type 'hook) + + +;; +;; +;; Fonts +;; +;; + +(defgroup snitch-faces nil + "Faces for snitch firewall windows" + :group 'snitch + :prefix "snitch-") + +(defface snitch--log-filter-face + '((t . (:inherit default))) + "Face for log filter wizard" + :group 'snitch-faces) + +(defface snitch--log-filter-active-face + '((t . (:inherit snitch--log-filter-face :inverse-video t :weight bold))) + "Face for log filter wizard, selected entries" + :group 'snitch-faces) + +(provide 'snitch-custom) + +;;; snitch-custom.el ends here
diff --git a/snitch-filter.el b/snitch-filter.el line changes: +80/-0 index 0000000..c7c4418 --- /dev/null +++ b/snitch-filter.el
@@ -0,0 +1,80 @@ +;;; snitch-filter.el -*- lexical-binding: t; -*- +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; See snitch.el for full details. +;; +;; Copyright (C) 2020 Trevor Bentley +;; Author: Trevor Bentley <snitch.el@x.mrmekon.com> +;; URL: https://github.com/mrmekon/snitch-el +;; +;; This file is not part of GNU Emacs. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; This file provides some filter functions for snitch.el. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +;; +;; +;; Filter functions +;; +;; + +(defun snitch-filter/name (event name) + "Filter function for snitch rules. + +Takes the process name as a string. Applies to both network and +subprocess events." + (string-equal (oref event proc-name) name)) + +(defun snitch-filter/src-pkg (event pkg) + "Filter function for snitch rules. + +Takes the emacs package that originated the event as a symbol. +Applies to both network and subprocess events." + (eq (oref event src-pkg) pkg)) + +(defun snitch-filter/log-filter (event &rest alist) + "Filter function for snitch rules. + +Takes an alist generated by the snitch log filter wizard, +filtering on all specified fields." + (cl-loop for (aslot . avalue) in alist + with accept = t + do + (let ((evalue (eieio-oref event aslot)) + (val-type (type-of avalue))) + (unless (cond + ((eq val-type 'string) (string-equal avalue evalue)) + (t (eq avalue evalue))) + (setq accept nil))) + ;; short-circuit, stop checking after first failure + when (null accept) + return nil + finally return accept)) + +(provide 'snitch-filter) + +;;; snitch-filter.el ends here
diff --git a/snitch-log.el b/snitch-log.el line changes: +487/-0 index 0000000..22c6e14 --- /dev/null +++ b/snitch-log.el
@@ -0,0 +1,487 @@ +;;; snitch-log.el -*- lexical-binding: t; -*- +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; See snitch.el for full details. +;; +;; Copyright (C) 2020 Trevor Bentley +;; Author: Trevor Bentley <snitch.el@x.mrmekon.com> +;; URL: https://github.com/mrmekon/snitch-el +;; +;; This file is not part of GNU Emacs. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; This file provides logging, notification, and log-to-filter +;; functionality for snitch.el. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +;; optional dependency on alert package +(defvar snitch--have-alert (require 'alert nil t)) + +(defvar snitch--log-buffer-name "*snitch firewall log*" + "Name of the buffer for the snitch firewall log.") +(defvar snitch--log-filter-buffer-name "*snitch filter wizard*" + "Name of the buffer for the log filter 'wizard' popup window.") + +(defvar snitch--log-filter-buffer nil + "Buffer in the log filter 'wizard' popup window") + +(defvar snitch--log-prune-timer nil + "Periodic timer to prune snitch log buffer to its maximum +permitted size.") + +(defun snitch--exact-log-match (policies) + "Return true if any of policies are explicitly defined in +snitch-log-policy." + (seq-some 'identity + (mapcar (lambda (l) (member l snitch-log-policy)) + policies))) + +(defun snitch--log-policy-match (policies) + "Return true of any of the log policies in POLICIES are +covered by one of the currently enabled policies in +‘snitch-log-policy’. + +This does not require exact matches. For instance, if POLICIES +contains ‘process-whitelisted’ and ‘snitch-log-policy’ contains +‘whitelisted’, this function returns true, as ‘whitelisted’ is a +larger set including both ‘process-whitelisted’ and +‘network-whitelisted’." + (cond + ;; all in policy, everything true + ((member 'all snitch-log-policy) t) + ;; exact match between requested and configured policies + ((snitch--exact-log-match policies) t) + ;; generalize whitelist policies + ((and (or (member 'process-whitelisted policies) + (member 'network-whitelisted policies)) + (member 'whitelisted snitch-log-policy)) t) + ;; generalize blacklist policies + ((and (or (member 'process-blacklisted policies) + (member 'network-blacklisted policies)) + (member 'blacklisted snitch-log-policy)) t) + ;; generalize allowed policies + ((and (or (member 'process-allowed policies) + (member 'network-allowed policies)) + (member 'allowed snitch-log-policy)) t) + ;; generalize blocked policies + ((and (or (member 'process-blocked policies) + (member 'network-blocked policies)) + (member 'blocked snitch-log-policy)) t))) + +(defun snitch--pretty-obj-string (event) + "Return an event eieio object in a 'pretty-printed' form, which +can be used to deserialize back into an object with eval." + ;; write eieio object out as a pretty string by redirecting + ;; standard output stream to a function that consumes the output + ;; char by char. This must be reversed and concatenated to + ;; produce the final string. + (setq pretty-obj nil) + (let ((old-std standard-output)) + (setq standard-output (lambda (c) (setq pretty-obj (cons c pretty-obj)))) + (object-write event) + (setq pretty-obj (concat (nreverse pretty-obj))) + (setq standard-output old-std)) + pretty-obj) + +(defun snitch--propertize (logmsg event) + "Add text properties to LOGMSG with elements from EVENT. This +allows the log filter commands to re-assemble an event from its +log message. " + (cond + ;; process events + ((snitch-process-entry-p event) + (propertize logmsg + 'snitch-class snitch-process-entry + 'snitch-src-fn (oref event src-fn) + 'snitch-src-path (oref event src-path) + 'snitch-src-pkg (oref event src-pkg) + 'snitch-proc-name (oref event proc-name) + 'snitch-executable (oref event executable) + 'snitch-args (oref event args))) + ;; network events + ((snitch-network-entry-p event) + (propertize logmsg + 'snitch-class snitch-network-entry + 'snitch-src-fn (oref event src-fn) + 'snitch-src-path (oref event src-path) + 'snitch-src-pkg (oref event src-pkg) + 'snitch-proc-name (oref event proc-name) + 'snitch-host (oref event host) + 'snitch-port (oref event port) + 'snitch-family (oref event family))))) + +(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 +type from ‘snitch-log-policies’." + (when (snitch--log-policy-match '(evt-type)) + (let* ((name (cond ((eq evt-type 'all) "event") + ((eq evt-type 'whitelisted) "whitelisted") + ((eq evt-type 'process-whitelisted) "whitelisted") + ((eq evt-type 'network-whitelisted) "whitelisted") + ((eq evt-type 'blacklisted) "blacklisted") + ((eq evt-type 'process-blacklisted) "blacklisted") + ((eq evt-type 'network-blacklisted) "blacklisted") + ((eq evt-type 'allowed) "allowed") + ((eq evt-type 'process-allowed) "allowed") + ((eq evt-type 'network-allowed) "allowed") + ((eq evt-type 'blocked) "blocked") + ((eq evt-type 'process-blocked) "blocked") + ((eq evt-type 'network-blocked) "blocked") + (t "other"))) + (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 + (cond (snitch-log-verbose (format "[%s] (%s) --\n%s" + timestamp name pretty-obj)) + (t (format "[%s] (%s) -- %s\n" + timestamp name event))) + event))) + ;; 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)) + ;; if the alert package is available and notifications are + ;; enabled, also raise a notification + (when (and snitch--have-alert snitch-enable-notifications) + (alert logmsg + :title (format "Snitch Event: %s" name) + :severity 'normal + :category 'snitch + ;; :id allows alert to replace notifications with + ;; updated ones. Since it is possible to get two + ;; alerts for one object with snitch (if ’all logging + ;; policy is enabled along with any other policy), we + ;; pass the internal eieio object name, which is the + ;; same if this event is raised again later + :id (eieio-object-name-string event) + ;; We also pass the raw event, so custom alert + ;; handlers can parse it. There is no way to get + ;; feedback from an alert, so this is only + ;; informative. + :data event))))) + +(defun snitch--prune-log-buffer () + ;; ensure timer is stopped. it will be started again by the next + ;; log event. it’s wasteful to have a timer running when we know + ;; the buffer isn’t growing. + (snitch--stop-log-prune-timer) + (let ((buf (get-buffer-create snitch--log-buffer-name))) + (with-current-buffer buf + (let ((line-count (count-lines (point-min) (point-max)))) + (when (and (> snitch--log-buffer-max-lines 0) + (> line-count snitch--log-buffer-max-lines)) + (setq buffer-read-only nil) + (buffer-disable-undo) + (save-excursion + (goto-char (point-min)) + (forward-line (+ (- line-count snitch--log-buffer-max-lines) 1)) + (delete-region (point-min) (point)) + (goto-char (point-min)) + (insert "[log trimmed]\n") + (goto-char (point-max))) + (setq buffer-read-only t)))))) + +(defun snitch--maybe-start-log-prune-timer () + "Start the snitch log pruning timer if it is not already +running." + (unless snitch--log-prune-timer + (snitch--start-log-prune-timer))) + +(defun snitch--start-log-prune-timer () + "Start the snitch log pruning timer. This is a non-repeating +timer that calls snitch--prune-log-buffer after a period of +idle." + (setq snitch--log-prune-timer + (run-with-idle-timer 30 nil #'snitch--prune-log-buffer))) + +(defun snitch--stop-log-prune-timer () + "Stop the snitch log pruning timer if it is running." + (when snitch--log-prune-timer + (cancel-timer snitch--log-prune-timer) + (setq snitch--log-prune-timer nil))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; +;; Log filter ’wizard’ +;; +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;###autoload +(defun snitch-filter-from-log () + "Opens an interactive 'wizard' to create a new snitch +whitelist/blacklist rule based on the event log under the cursor. + +To use the wizard, move the cursor over an item in the snitch +firewall log buffer (default: ‘*snitch firewall log*’), and run +this command (‘M-x snitch-filter-from-log’). A window will appear +with contents populated from the selected log line. Typing the +highlighted mnemonic characters toggles fields on and off. When +all desired fields are selected, typing ‘C-c C-c’ appends the new +filter to the existing blacklist or whitelist, and saves it +persistently as a customized variable." + (interactive) + (let ((cls (get-text-property (point) 'snitch-class)) + (fn (get-text-property (point) 'snitch-src-fn)) + (path (get-text-property (point) 'snitch-src-path)) + (pkg (get-text-property (point) 'snitch-src-pkg)) + (name (get-text-property (point) 'snitch-proc-name))) + (cond + ((eq cls 'snitch-network-entry) + (let ((host (get-text-property (point) 'snitch-host)) + (port (get-text-property (point) 'snitch-port)) + (family (get-text-property (point) 'snitch-family))) + (snitch--run-log-filter-wizard (snitch-network-entry + :src-fn fn + :src-path path + :src-pkg pkg + :proc-name name + :host host + :port port + :family family)))) + ((eq cls 'snitch-process-entry) + (let ((exec (get-text-property (point) 'snitch-executable)) + (args (get-text-property (point) 'snitch-args))) + (snitch--run-log-filter-wizard (snitch-process-entry + :src-fn fn + :src-path path + :src-pkg pkg + :proc-name name + :executable exec + :args args)))) + ))) + +(defun snitch--run-log-filter-wizard (event) + "Runs the snitch log filter 'wizard', an interactive popup +window to help a user create a new blacklist or whitelist filter +based on a log entry. This function sets up the window, +populates it, loops over user keypresses, and eventually saves +the filter to the customization variable if appropriate." + ;; create buffer if needed + (when (null snitch--log-filter-buffer) + (snitch--init-log-filter-buffer)) + ;; set initial contents of buffer so it opens to the correct size + (snitch--redraw-log-filter-buffer event fields) + ;; display window + (snitch--show-log-filter-window) + ;; read user input continuously until saved or aborted + (setq finished nil) + (setq fields '()) + (let ((key-map (snitch--log-filter-map event))) + (while (not finished) + ;; redraw to update font properties + (snitch--redraw-log-filter-buffer event fields) + (let* ((key (read-key-sequence "Enter field: "))) + (cond + ;; ignore, probably a control character (arrow keys, etc) + ;; must come first to short-circuit before string comparisons + ((not (stringp key)) nil) + ;; abort and exit + ((string-equal key (kbd "C-c C-k")) (setq fields '() finished t)) + ((string-equal key (kbd "C-g")) (setq fields '() finished t)) + ;; save and exit + ((string-equal key (kbd "C-c C-c")) (setq finished t)) + ;; some other string. check if string is in field map, and + ;; if so toggle that slot of the event in the list of slots + ;; to filter on + ((stringp key) + (let ((slot (snitch--log-filter-map-slot-from-key key-map key))) + (when slot + (if (member slot fields) + (setq fields (delete slot fields)) + (setq fields (cons slot fields)))))))))) + ;; close filter window + (snitch--hide-log-filter-window snitch--log-filter-buffer) + ;; generate filter + (when fields + (setq slot-value-alist '()) + ;; make an alist of (slot . value) pairs for the filter function + ;; to match against + (cl-loop for slot in fields + do + (setq slot-value-alist + (cons (cons slot (eieio-oref event slot)) slot-value-alist))) + ;; query user for whether this should go in blacklist or whitelist + (setq black-white nil) + (while (null black-white) + (let* ((key (read-key-sequence "[b]lacklist or [w]hitelist? "))) + (cond + ;; ignore, probably a control character (arrow keys, etc) + ;; must come first to short-circuit before string comparisons + ((not (stringp key)) nil) + ((string-equal key "b") (setq black-white "blacklist")) + ((string-equal key "w") (setq black-white "whitelist"))))) + ;; append the new entry to the correct defcustom list, and + ;; save as default customization. + (let* ((filter (cons #'snitch-filter/log-filter slot-value-alist)) + (orig-list (cond + ((snitch-network-entry-p event) + (intern-soft (format "snitch-network-%s" black-white))) + ((snitch-process-entry-p event) + (intern-soft (format "snitch-process-%s" black-white))) + (t nil))) + (orig-val (eval orig-list)) + (new-list (cons filter orig-val))) + (customize-save-variable orig-list new-list)))) + +(defun snitch--log-filter-map-slot-from-key (map key) + "Given a map from ‘snitch--log-filter-map’, returns the slot +matching to the given keypress, or nil." + (cl-loop for (slot . plist) in map + when (string-equal (plist-get plist 'key) key) + return slot + finally return nil)) + +(defun snitch--log-filter-map (event) + "Returns an alist of (SLOT . PLIST) pairs, where each PLIST +contains a field name, a key to press to select it, and a +‘mnemonic’ version of the name with the key highlighted in square +brackets. The correct set of fields is returned based on the +given event type. All of this stuff is used to display the +fields, and to interpret which field to select when receiving +user keypresses." + (setq common-alist + '((src-fn . (key "f" name "function" + mnemonic-name "[f]unction")) + (src-path . (key "p" name "path" + mnemonic-name "[p]ath")) + (src-pkg . (key "k" name "package" + mnemonic-name "pac[k]age")) + (proc-name . (key "n" name "name" + mnemonic-name "[n]ame")))) + (setq network-alist + '((host . (key "h" name "host" + mnemonic-name "[h]ost")) + (port . (key "o" name "host" + mnemonic-name "p[o]rt")) + (family . (key "m" name "family" + mnemonic-name "fa[m]ily")))) + (setq process-alist + '((executable . (key "x"name "executable" + mnemonic-name "e[x]ecutable")) + (args . (key "g" name "args" + mnemonic-name "ar[g]s")))) + (cond + ((snitch-network-entry-p event) (append common-alist network-alist)) + ((snitch-process-entry-p event) (append common-alist process-alist)) + (t common-alist))) + +(defun snitch--redraw-log-filter-buffer (evt selected) + "Draw the text contents of the log-filter menu based on the +given event and list of currently selected fields. Each field +name is drawn on a separate line, along with its value in the +current event. The ‘mnemonic’ version of the field name is +displayed, with the character to press surrounded by square +brackets. Fields that are currently selected display in a +different font." + (with-current-buffer snitch--log-filter-buffer + (erase-buffer) + (let ((evt-type (if (snitch-network-entry-p evt) + "network" + "process"))) + (insert (format "Creating new snitch %s filter from template:\n" evt-type)) + (cl-loop for (slot . plist) in (snitch--log-filter-map evt) + do + (let* ((msg (format "%-12s: %s" (plist-get plist 'mnemonic-name) + (eieio-oref evt slot))) + (styled-msg (propertize + msg 'face + (if (member slot selected) + 'snitch--log-filter-active-face + 'snitch--log-filter-face)))) + (insert "\n") + (insert styled-msg))) + (insert "\n") + (insert "\nSave: C-c C-c / Abort: C-c C-k") + (goto-char (point-min))))) + +(defun snitch--init-log-filter-buffer () + "Initialize buffer for displaying UI to generate a snitch +filter from an existing log line." + ;; logic looted from which-key + (unless (buffer-live-p snitch--log-filter-buffer) + (setq snitch--log-filter-buffer + (get-buffer-create snitch--log-filter-buffer-name)) + (with-current-buffer snitch--log-filter-buffer + (let (message-log-max) + (toggle-truncate-lines 1) + (message "")) + (setq-local cursor-type nil) + (setq-local cursor-in-non-selected-windows nil) + (setq-local mode-line-format nil) + (setq-local word-wrap nil) + (setq-local show-trailing-whitespace nil)))) + +(defun snitch--hide-log-filter-window (buffer) + "Hide which-key buffer when side-window popup is used." + ;; based on which-key + (when (buffer-live-p buffer) + (quit-windows-on buffer) + (run-hooks snitch-log-filter-window-close-hook))) + +(defun snitch--log-filter-window-size-to-fit (window) + "Resize log filter window to a reasonable height and maximum +width." + ;; based on which-key + ;; cap at 30% of the vertical height + (let ((fit-window-to-buffer-horizontally t) + (window-min-height 5) + (max-height (round (* .3 (window-total-height (frame-root-window)))))) + (fit-window-to-buffer window max-height))) + +(defun snitch--show-log-filter-window () + "Open or switch focus to the log filter window, resizing it as +necessary." + ;; based on which-key + (let* ((alist + `((window-width . snitch--log-filter-window-size-to-fit) + (window-height . snitch--log-filter-window-size-to-fit) + (side . bottom) + (slot . 0)))) + ;; Comment preserved from which-key: + ;; Previously used `display-buffer-in-major-side-window' here, but + ;; apparently that is meant to be an internal function. See emacs bug #24828 + ;; and advice given there. + (cond + ((get-buffer-window snitch--log-filter-buffer) + (display-buffer-reuse-window snitch--log-filter-buffer alist)) + (t + (display-buffer-in-side-window snitch--log-filter-buffer alist))) + (run-hooks snitch-log-filter-window-open-hook))) + +(provide 'snitch-log) + +;;; snitch-log.el ends here
diff --git a/snitch.el b/snitch.el line changes: +626/-0 index 0000000..dad3691 --- /dev/null +++ b/snitch.el
@@ -0,0 +1,626 @@ +;;; snitch.el --- an emacs firewall -*- lexical-binding: t; -*- +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Copyright (C) 2020 Trevor Bentley +;; Author: Trevor Bentley <snitch.el@x.mrmekon.com> +;; Created: 01 Dec 2020 +;; Version: 0.1 +;; +;; Keywords: processes, comm +;; URL: https://github.com/mrmekon/snitch-el +;; +;; This file is not part of GNU Emacs. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; snitch.el (pronounced like schnitzel) is a firewall for emacs. +;; +;; snitch intercepts calls to create network connections or launch +;; subprocesses. Through user-configured default policies, filter +;; rules, and user hooks it is able to log and potentially block each +;; action. It can be configured with ‘M-x customize-group <RET> +;; snitch’. +;; +;; Subprocesses and network connections are handled independently, +;; with their own separate default policies, blacklist and whitelist, +;; and logging policies. +;; +;; The main purpose of snitch is network monitoring. Subprocesses are +;; included because it is extremely common for emacs packages to +;; "shell out" to an external program for network access, commonly to +;; ‘curl’. As a side effect, snitch can also effectively audit and +;; prevent undesired access to other programs. +;; +;; Notifications can be raised on each logged event by ensuring the +;; ’alert’ package is installed and customizing +;; ‘snitch-enable-notifications’ to t. +;; +;; === MECHANISM === +;; +;; The underlying ’firewall’ mechanism is built on function advice +;; surrounding emacs’s lowest-level core functions for spawning +;; connections or subprocesses. When an emacs package or script makes +;; such a request, snitch receives it first, and either passes it +;; through or rejects it based on the current rules. Once a +;; connection or process is accepted, snitch is no longer involved for +;; the duration of that particular communication stream. +;; +;; For each intercepted call, snitch first builds an event object +;; defining everything snitch knows about the call. The metadata +;; differs for network connections (host, port, family) and processes +;; (executable and argument list), but all events share a common set: +;; calling function, calling function’s file path, calling package, +;; and request name. +;; +;; Once an event object is created, it is passed to any hooks defined +;; in ‘snitch-on-event-functions’ for early processing. If a hook +;; returns nil, the event is dropped immediately. Otherwise, snitch +;; then checks the corresponding whitelist (if the default policy is +;; deny) or the blacklist (if the default policy is allow) and makes +;; its internal decision. Before executing the decision, it calls the +;; corresponding hook functions to give the user hooks one more +;; opportunity to change the decision. Finally, only if the decision +;; was ‘allow’, snitch executes the original request and passes the +;; result back to the caller. +;; +;; As the event flows through the decision tree, it also triggers log +;; events. There are several different types defined in +;; ‘snitch-log-policies’, and users can subscribe to any combination +;; of them by customizing ‘snitch-log-policy’. Logs are displayed in +;; text format in a dedicated log buffer (by default: ‘*snitch +;; firewall log*’), along with text properties that allow extracting +;; the event information programatically from a log line with +;; ‘get-text-property’. The text lines can be "pretty printed" by +;; customizing ‘snitch-log-verbose’. +;; +;; +;; === USAGE === +;; +;; Enabling snitch is as simple as calling ‘(snitch-init)’. +;; Initialization does very little, so this is safe to call in your +;; emacs init without worrying about deferral or negative consequences +;; on startup time. +;; +;; An example initialization using ‘use-package’ might look like so: +;; +;; (use-package snitch +;; :ensure t +;; :init +;; (snitch-init)) +;; +;; snitch then runs in the background, performing its duties according +;; to your configuration, and logging in its dedicated buffer. +;; +;; You may add firewall exception rules manually, as covered in the +;; CONFIGURATION section below. Alternatively, you can also build +;; filters with a guided UI by switching to the firewall log buffer +;; (‘*snitch firewall log*’), highlighting an entry that you wish to +;; filter on, and execute ‘M-x snitch-filter-from-log’. This launches +;; a popup window that allows you to configure a new filter based on +;; one or more fields of the selected log line, and add it to either +;; your blacklist or whitelist. +;; +;; To disable snitch, call ‘(snitch-deinit)’. +;; +;; +;; === CONFIGURATION === +;; +;; Customize snitch with ‘M-x customize-group <RET> snitch’, or +;; manually in your emacs initialization file. +;; +;; Most users will have five variables that need to be configured +;; before use: +;; +;; - ‘snitch-network-policy’ -- whether to allow or deny network +;; connections by default. +;; +;; - ‘snitch-process-policy’ -- whether to allow or deny subprocesses +;; by default. +;; +;; - ‘snitch-log-policy’ -- which events to log (to see the options, +;; run ‘M-x describe-variable <RET> snitch-log-policies’) +;; +;; - ‘snitch-network-*list’ -- filter rules containing exceptions to +;; the default network policy. See FILTER RULES below. Use +;; ‘-whitelist’ if the default policy is ‘deny’, or ‘-blacklist’ if +;; the default policy is ‘allow’ +;; +;; - ‘snitch-process-*list’ -- filter rules containing exceptions to +;; the default process policy. See FILTER RULES below. Use +;; ‘-whitelist’ if the default policy is ‘deny’, or ‘-blacklist’ if +;; the default policy is ‘allow’ +;; +;; +;; ==== COMMON CONFIG: DENY ==== +;; +;; A useful configuration is to deny all external communication by +;; default, but allow certain packages to communicate. This example +;; demonstrates permitting only the ’elfeed’ package to create network +;; connections: +;; +;; (use-package snitch +;; :ensure t +;; :init +;; (setq snitch-network-policy 'deny) +;; (setq snitch-process-policy 'deny) +;; (setq snitch-log-policy '(blocked whitelisted allowed)) +;; (add-to-list 'snitch-network-whitelist +;; (cons #'snitch-filter/src-pkg '(elfeed))) +;; (snitch-init)) +;; +;; +;; ==== COMMON CONFIG: ALLOW + AUDIT ==== +;; +;; Another useful configuration is to allow all accesses, but log them +;; to keep an audit trail. This might look like so: +;; +;; (use-package snitch +;; :ensure t +;; :init +;; (setq snitch-network-policy 'allow) +;; (setq snitch-process-policy 'allow) +;; (setq snitch-log-policy '(allowed blocked whitelisted blacklisted)) +;; (setq snitch-log-verbose t) +;; (snitch-init)) +;; +;; +;; ==== FILTER RULES ==== +;; +;; Filter rules, as specified in ‘snitch-(process|network)-*list’ +;; variables, are specified as cons cells where the car is a filtering +;; function, and the cdr is a list of arguments to pass to the +;; function in addition to the event object: +;; +;; (setq snitch-network-whitelist +;; '( +;; (filter-fn1 . (argQ argL)) +;; (filter-fn2 . (argN argP)) +;; )) +;; +;; Each filter function should have a prototype accepting EVENT as the +;; snitch event object in consideration, and ARGS as the list of +;; arguments from the cdr of the rules entry: +;; +;; (defun filter-fn1 (event &rest args)) +;; +;; A trivial function which matches if a single string in the event +;; object matches a known value might look like so: +;; +;; (defun filter-fn1 (event name) +;; (string-equal (oref event proc-name) name)) +;; +;; While a more complex filter function might treat ARGS as an +;; associative list of key/value pairs: +;; +;; (defun filter-fn2 (event &rest alist) +;; (cl-loop for (aslot . avalue) in alist with accept = t +;; do +;; (let ((evalue (eieio-oref event aslot)) +;; (val-type (type-of avalue))) +;; (unless (cond +;; ((eq val-type 'string) (string-equal avalue evalue)) +;; (t (eq avalue evalue))) +;; (setq accept nil))) +;; when (null accept) +;; return nil +;; finally return accept)) +;; +;; The return value of a filter function determines whether the filter +;; should take effect. t means "take effect" and nil means "do not +;; take effect". What that means for the event depends on which list +;; the filter rule is in. If the rule is in a whitelist, t means +;; allow and nil means block. If it is in a blacklist, t means block +;; and nil means allow. +;; +;; +;; ==== HOOKS ==== +;; +;; Events are passed to user-provided hook functions, if specified. +;; These hooks can subscribe to receive events either immediately on +;; arrival, upon a final decision, or both. The hooks can change +;; snitch’s final decision. +;; +;; Hook functions take a single argument, the event object: +;; +;; (defun snitch-hook (event)) +;; +;; Hooks should return t to allow snitch to continue processing as it +;; would have, or return nil to reverse snitch’s decision. For hooks +;; in ‘snitch-on-event-functions’, returning nil cancels all further +;; processing of the event and blocks it immediately. For other hook +;; lists, returning nil reverses the action implied by the list name: +;; returning nil in a ‘snitch-on-allow-functions’ hook causes the +;; event to be blocked, returning nil in a ‘snitch-on-block-functions’ +;; hook causes it to be allowed. +;; +;; +;; === SECURITY === +;; +;; snitch provides, effectively, zero security. +;; +;; If you were to ask your Principal Security Engineer friends, they +;; might say that an effective security boundary must be +;; "tamper-proof" and provide "complete mediation." snitch does +;; neither. +;; +;; Tamper-proof: none at all. Any other emacs package can simply +;; disable snitch, or modify it to pass malicious traffic undetected. +;; +;; Complete mediation: no attempt has been made to verify that *all* +;; network and subprocess accesses must go through the functions that +;; snitch hooks. Given the complexity of emacs, it is extremely +;; unlikely that they do. +;; +;; However, your Principal Security Engineer friends also like to +;; blather on about ’defining your security model’, and a fun game to +;; play with them is to define your security model such that none of +;; the insecurities are in it. As so: +;; +;; Security model: includes malicious adversaries +;; snitch effectiveness: zero. +;; +;; Security model: includes no malicious adversaries +;; snitch effectiveness: great! +;; +;; snitch is useful for auditing and blocking unwanted features in an +;; otherwise well-behaving ecosystem. It is handy for getting a +;; record of exactly what your emacs is doing, and for fine-tuning +;; accesses beyond emacs’s boundaries a little bit better. It will +;; not, however, save you from the bad guys. +;; +;; +;; === TODO === +;; +;; - send notifications in batches? +;; - interactive prompts? +;; - handle service strings as port numbers +;; - ensure the inverted negation rules make sense +;; - automated test suite +;; - publish on gitwhatever +;; - publish on MELPA? +;; - profit! +;; +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth +;; Floor, Boston, MA 02110-1301, USA. +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +(require 'eieio) ; class objects +(require 'cl-macs) ; cl loops + +(require 'snitch-backtrace) +(require 'snitch-custom) +(require 'snitch-filter) +(require 'snitch-log) + +;; +;; +;; Classes +;; +;; + +(defclass snitch-source () + ((timestamp :initarg :timestamp :type number :initform 0) + (src-fn :initarg :src-fn :type (or null symbol) :initform nil) + (src-path :initarg :src-path :type (or null string) :initform nil) + (src-pkg :initarg :src-pkg :type (or null symbol) :initform nil)) + "Common base class for snitch entries. Supplies information +about snitch's best guess for which emacs function/file/package +is ultimately responsible for the event that snitch is +considering.") + +(defclass snitch-process-entry (snitch-source) + ((proc-name :initarg :proc-name :type (or null string) :initform nil) + (executable :initarg :executable :type (or null string) :initform nil) + (args :initarg :args :type list :initform ())) + "snitch entry for events attempting to spawn a +subprocess. Supplies information about the name, executable +binary, and arguments being provided to the subprocess that +snitch is considering.") + +(defclass snitch-network-entry (snitch-source) + ((proc-name :initarg :proc-name :type (or null string) :initform nil) + (host :initarg :host :type (or null string symbol) :initform nil) + (port :initarg :port :type (or null number symbol) :initform nil) + (family :initarg :family :type (or null symbol) :initform nil)) + "snitch entry for events attempting to create a network +connection. Supplies information about the name, host, port, and +protocol family of the connection that snitch is considering.") + + +;; +;; +;; Constants +;; +;; + +(defconst snitch-source-package-types + '(built-in site-lisp user) + "Possible types for a snitch event's package source, as found +in the ‘src-pkg’ field of each event object. In addition to +these pre-defined types, any loaded package name (as a symbol) is +a permitted type as well. + + 'built-in' -- package provided by emacs, and responds true to +the ‘package-built-in-p’ function. + + 'site-lisp' -- package is found in one of the emacs common +directories (i.e. a system-wide shared elisp directory), but does +not report itself as a built-in. + + 'user' -- a package from an unknown source, possibly manually +installed by the user. + + anything else -- a package registered in ‘package--alist’, +typically including those installed by package managers.") + +(defconst snitch-hook-types + '(event block allow whitelist blacklist) + "Types provided to user-defined hooks registered with snitch. + +The types match with the hook callbacks that can receive +them (i.e. ‘snitch-on-event-functions’), but are also provided as +arguments so the same function can be used for multiple hook +types. + + 'event' -- any event type + + 'block' -- log events that are blocked by policy + + 'allow' -- log events that are permitted by policy + + 'whitelist' -- log events that would have been blocked, but +were permitted by a whitelist rule + + 'blacklist' -- log events that would have been allowed, but +were blocked by a blacklist rule") + +(defconst snitch-log-policies + '( + ;; log absolutely everything + all + + ;; log actions for both subprocesses and networks + blocked + allowed + whitelisted + blacklisted + + ;; log actions for only subprocesses + process-blocked + process-allowed + process-whitelisted + process-blacklisted + + ;; log actions for only network connections + network-blocked + network-allowed + network-whitelisted + network-blacklisted + ) + "All of the logging policies for snitch. Provide a list of +these symbols to ‘snitch-log-policy’ to enable logging of events of +the corresponding type. + + 'all' -- logs every event, before a decision is made. + + 'blocked' -- log events that are blocked by policy + + 'allowed' -- log events that are permitted by policy + + 'whitelisted' -- log events that would have been blocked, but +were permitted by a whitelist rule + + 'blacklisted' -- log events that would have been allowed, but +were blocked by a blacklist rule + + 'process-*' -- only log subprocess events of the matching type + + 'network-*' -- only log network connection events of the +matching type") + +(defconst snitch-firewall-policies + '(deny allow) + "Default firewall policies. + + 'allow' -- allow all processes/connections unless overridden by +a blacklist rule or registered hook. + + 'deny' -- deny all processes/connections unless overridden by a +whitelist rule or registered hook.") + + +;; +;; +;; Internal functions +;; +;; + +(defun snitch--service-to-port (service) + "Convert SERVICE argument of ‘make-network-process’ into a symbol +or number." + (cond + ((symbolp service) service) + ;; TODO: handle special service names, ex: "https" + ((stringp service) (string-to-number service)) + ((numberp service) service) + (t (progn + (message "ERROR: unknown network service: %s" service) + nil)))) + +(defun snitch--decide (event + decision-list + list-evt-type + list-hook-fns + default-evt-type + default-hook-fns) + "Return t if EVENT is to be filtered differently from the +default policy, nil if default action is to be taken. The choice +of DECISION-LIST (whitelist or blacklist) and the event types +(LIST-EVT-TYPE and DEFAULT-EVT-TYPE) determines whether default +is block/allow. Registered user hooks are called, and potentially +alter the decision. + +This function only generates a decision. It does not perform the +actual block or pass action. + +Example: if DEFAULT-EVT-TYPE is ‘block’ and DECISION-LIST is +‘snitch-network-whitelist’, this function will check each entry +in the network whitelist for an exception. If no exception is +found, it will call the user hooks in +‘snitch-on-block-functions’. If one of those hooks returns nil, +‘snitch--decide’ returns t, indicating that the user hook has +changed the default behavior for this event (it should allow +instead of block). On the other hand, if every user hook returns +t, ‘snitch--decide’ returns nil, indicating that the default +block action should be taken." + (cl-loop for (f-fn . f-args) in decision-list + ;; when event is in the white/blacklist, and no + ;; hooks override the list, return t. + when (and (apply f-fn (cons event f-args)) + (run-hook-with-args-until-failure list-hook-fns + 'list-evt-type + event)) + return t + ;; otherwise fall back on default policy + finally return + (if (run-hook-with-args-until-failure default-hook-fns + default-evt-type + event) + nil + t))) + +(defun snitch--wrap-internal (event prefix orig-fun args) + "Execute the wrapped function, ORIG-FUN with its original +arguments ARGS if EVENT is allowed by default policy or +whitelist. PREFIX is the string 'process' or 'network' to +indicate the type of event. Registered hooks are called before +making the final decision, and the decision is logged based on +the globally configured log filters." + (when (run-hook-with-args-until-failure snitch-on-event-functions + 'event + event) + (snitch--log 'all event) + (let* ((wl (symbol-value (intern-soft + (format "snitch-%s-whitelist" prefix)))) + (bl (symbol-value (intern-soft + (format "snitch-%s-blacklist" prefix)))) + (wled (intern-soft (format "%s-whitelisted" prefix))) + (bled (intern-soft (format "%s-blacklisted" prefix))) + (alw (intern-soft (format "%s-allowed" prefix))) + (blk (intern-soft (format "%s-blocked" prefix))) + (decision (cond ((eq snitch-process-policy 'deny) + (snitch--decide event + wl + 'whitelist + snitch-on-whitelist-functions + 'block + snitch-on-block-functions)) + (t ;; policy allow + (snitch--decide event + bl + 'blacklist + snitch-on-blacklist-functions + 'allow + snitch-on-allow-functions))))) + (cond ((eq snitch-process-policy 'deny) + (progn + (snitch--log (if decision wled blk) event) + (when decision (apply orig-fun args)))) + (t ;; policy allow + (progn + (snitch--log (if decision bled alw) event) + (unless decision (apply orig-fun args)))))))) + + +(defun snitch--wrap-make-process (orig-fun &rest args) + "Wrap a call to make-process in the snitch firewall decision +engine. ORIG-FUN is called only if the snitch firewall rules +permit it." + (let* ((caller (snitch--responsible-caller (snitch--backtrace))) + (event (snitch-process-entry + :timestamp (time-to-seconds (current-time)) + :src-fn (nth 0 caller) + :src-path (nth 1 caller) + :src-pkg (nth 2 caller) + :proc-name (plist-get args :name) + :executable (car (plist-get args :command)) + :args (cdr (plist-get args :command))) + )) + (snitch--wrap-internal event "process" orig-fun args))) + +(defun snitch--wrap-make-network-process (orig-fun &rest args) + "Wrap a call to make-network-process in the snitch firewall +decision engine. ORIG-FUN is called only if the snitch firewall +rules permit it." + (let* ((caller (snitch--responsible-caller (snitch--backtrace))) + (event (snitch-network-entry + :timestamp (time-to-seconds (current-time)) + :src-fn (nth 0 caller) + :src-path (nth 1 caller) + :src-pkg (nth 2 caller) + :proc-name (plist-get args :name) + :host (plist-get args :host) + :port (snitch--service-to-port (plist-get args :service)) + :family (plist-get args :family)))) + (snitch--wrap-internal event "network" orig-fun args))) + +(defun snitch--register-wrapper-fns () + "Add snitch decision engine around the lowest-level emacs +functions responsible for launching subprocesses and opening +network connections." + ;; lowest-level functions, implemented in C + (add-function :around (symbol-function 'make-network-process) + #'snitch--wrap-make-network-process) + (add-function :around (symbol-function 'make-process) + #'snitch--wrap-make-process) + ;; TODO: are all of these covered? + ;; call-process + ;; start-process + ;; url-retrieve + ;; open-network-stream + ) + +(defun snitch-unload-function () + "Unload the snitch decision engine wrapping functions." + (remove-function (symbol-function 'make-network-process) + #'snitch--wrap-make-network-process) + (remove-function (symbol-function 'make-process) + #'snitch--wrap-make-process)) + + +;;;###autoload +(defun snitch-init () + "Initialize snitch.el firewall, enabling globally." + (interactive) + (snitch--register-wrapper-fns)) + +(defun snitch-deinit () + "Unload snitch.el firewall, disabling globally." + (interactive) + (snitch--stop-log-prune-timer) + (unload-feature 'snitch) + (require 'snitch)) + +(provide 'snitch) + +;;; snitch.el ends here