summary history branches tags files
commit:16f5a70c5b876c787083766728ef5b6a2016e32c
author:Trevor Bentley
committer:Trevor Bentley
date:Wed Dec 2 18:10:52 2020 +0100
parents:
Initial check-in of a mostly working snitch
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