Shell Audit Setup

Task-oriented recipes. Each section assumes you already have splam installed and running. See the Tutorial if not.

This page wires up real-time capture of sudo commands into incident markdown files. See Shell Audit Feedback Loop for why it’s built this way. Everything here needs auditd and root, and none of it needs a pip extra: redact, incidents, and shell_notify ship with splam and use only the standard library.

Add the audit rule

sudo tee /etc/audit/rules.d/splam-shell.rules <<'EOF'
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid!=unset -k splam-shell
EOF
sudo augenrules --load

Confirm it loaded:

sudo auditctl -l | grep splam-shell

Find the installed notify script

shell_notify.py ships inside the splam package itself, next to redact.py. Find its real path once, from whatever environment runs splam:

.venv/bin/python3 -c "import os, splam; print(os.path.join(os.path.dirname(splam.__file__), 'shell_notify.py'))"

Make it executable; it already has a #!/usr/bin/env python3 shebang:

sudo chmod +x /path/from/the/command/above/shell_notify.py

Wire up real-time dispatch

Recent auditd (3.0+) reads this straight from /etc/audit/plugins.d/. Older systems route it through audispd, configured from /etc/audit/auditd.conf, but the plugin file is the same either way. Use the path the previous step printed:

sudo tee /etc/audit/plugins.d/splam-shell.conf <<'EOF'
active = yes
direction = out
path = /path/from/the/previous/step/shell_notify.py
type = always
format = string
EOF
sudo systemctl restart auditd

shell_notify.py finds redact.py by looking next to itself, which is enough as long as both stay inside the installed splam package where pip put them. If something ever separates them, shell_notify.py also checks SPLAM_REDACT_MODULE, an environment variable it reads with os.environ, not a command-line flag. The plugin config above has no way to set one, so point the plugin at a two-line wrapper instead of at shell_notify.py directly:

sudo tee /usr/local/libexec/splam-shell-notify <<EOF
#!/bin/sh
SPLAM_REDACT_MODULE=/path/to/redact.py exec /path/from/the/earlier/step/shell_notify.py
EOF
sudo chmod +x /usr/local/libexec/splam-shell-notify

and use /usr/local/libexec/splam-shell-notify as the plugin’s path instead.

Enable the sweep timer

sudo tee /etc/systemd/system/splam-incidents-sweep.service <<EOF
[Unit]
Description=Flush idle splam shell-audit buffers

[Service]
Type=oneshot
ExecStart=$(pwd)/.venv/bin/splam-incidents sweep
EOF

sudo tee /etc/systemd/system/splam-incidents-sweep.timer <<'EOF'
[Unit]
Description=Run splam-incidents sweep every 60 seconds

[Timer]
OnBootSec=60
OnUnitActiveSec=60

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now splam-incidents-sweep.timer

Confirm it’s working

Run something splam-shell-tagged, then check the buffer:

sudo systemctl status cron.service
BUFFER=$(.venv/bin/python3 -c "from splam import incidents; print(incidents.buffer_path('$(logname)'))")
cat "$BUFFER"

One redacted line should be there. Close the window by hand rather than waiting for the sweep:

.venv/bin/splam-incidents close "$(logname)"
INCIDENTS_DIR=$(.venv/bin/python3 -c "from splam import incidents; print(incidents.INCIDENTS_DIR)")
ls "$INCIDENTS_DIR"

The buffer file is gone and a new <timestamp>-<service>.md is sitting in incidents/, with the command you just ran and the journalctl lines around it. Do this again after any change to the rule or the dispatch config; it’s the whole checklist, and neither auditd nor systemd will tell you if you got a path wrong.

Flag an incident for review

For the ones you already know deserve a closer look before anyone treats them as final:

.venv/bin/splam-incidents close "$(logname)" --review

Lands in incidents/pending/ instead of incidents/. Move it (or edit it, then move it) once you’ve checked it:

mv incidents/pending/2026-08-16T1430-sshd.service.md incidents/

Change the idle-gap threshold

Default is 600 seconds. It’s read by splam-incidents sweep, so it belongs on the timer’s service, not on splam itself:

sudo systemctl edit splam-incidents-sweep.service

Add:

[Service]
Environment="SPLAM_INCIDENT_IDLE_GAP=900"
sudo systemctl daemon-reload

When it doesn’t work

What you see What it means
Buffer file never appears after a sudo command Rule didn’t load (auditctl -l) or the plugin path is wrong (journalctl -u auditd)
RuntimeError: could not locate redact.py shell_notify.py and redact.py aren’t next to each other; set SPLAM_REDACT_MODULE
Buffer filename doesn’t match the account you expect It’s keyed on auid, the original login uid; if you su or sudo -i between sessions, auid follows the first login, not the current shell
incidents/ file exists but the ## Logs section says -- No entries -- journalctl returned nothing for that service in that window; check the service name correlate_and_write detected matches the unit’s real name
Everything under .buffer/ for the same user, always splam itself is running interactively under that account rather than as its own systemd service; see the deployment note in Shell Audit Feedback Loop
splam-incidents: command not found Reinstall with .venv/bin/pip install -e . so the [project.scripts] entry point registers

Further reading