# Shell Audit Setup

Task-oriented recipes. Each section assumes you already have `splam` installed and running. See the [Tutorial](tutorial-getting-started.md) if not.

This page wires up real-time capture of `sudo` commands into incident markdown files. See [Shell Audit Feedback Loop](02.08-construction-shell-audit-feedback-loop.md) for why it's built this way. Everything here needs `auditd` and root, and none of it needs a pip extra: [redact](../reference/redact.md#splam.redact), `incidents`, and `shell_notify` ship with `splam` and use only the standard library.


# Add the audit rule

``` bash
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:

``` bash
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`:

``` bash
.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:

``` bash
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:

``` bash
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:

``` bash
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

``` bash
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:

``` bash
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:

``` bash
.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:

``` bash
.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:

``` bash
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:

``` bash
sudo systemctl edit splam-incidents-sweep.service
```

Add:

``` ini
[Service]
Environment="SPLAM_INCIDENT_IDLE_GAP=900"
```

``` bash
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](02.08-construction-shell-audit-feedback-loop.md#setting-it-up) |
| `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](02.08-construction-shell-audit-feedback-loop.md), for why this is built this way

- [How-To: Audit Trail](01.07-how-to-audit-trail.md), for what `splam` already records on its own

- [How-To: Sudo Privileges](01.02-how-to-sudo-privileges.md), for the privilege `splam` itself needs to run service actions
