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 --loadConfirm it loaded:
sudo auditctl -l | grep splam-shellFind 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.pyWire 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 auditdshell_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-notifyand 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.timerConfirm 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)" --reviewLands 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.serviceAdd:
[Service]
Environment="SPLAM_INCIDENT_IDLE_GAP=900"sudo systemctl daemon-reloadWhen 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
Shell Audit Feedback Loop, for why this is built this way
How-To: Audit Trail, for what
splamalready records on its ownHow-To: Sudo Privileges, for the privilege
splamitself needs to run service actions