Skip to content

Audit log

/audit/ (needs audit_log.manage) records every request, including plain GET/view traffic and not just state-changing ones, plus every login attempt. Each entry captures the actor (user, username, and role at the time), action, HTTP method, path, status code, related engagement/object, client IP, query string, referer, user agent, and request duration.

Only HEAD/OPTIONS requests and a short list of specifically-excluded views (the live report preview endpoint, the health check, and the CSP report endpoint) are skipped. Everything else, including plain page loads, is logged. Two sensitive URLs (password_reset_confirm, account_setup_confirm) have their path redacted in the log, since the URL itself contains a single-use token.

The Audit Log page is filterable by free-text search (matches actor username, action, path, object reference, IP, user agent, or referer), HTTP method, actor, status-code bucket (2xx/3xx/4xx/5xx), and a from/to date range, with a configurable page size (25/50/100/250).

AUDIT_LOG_RETENTION_DAYS (default 2190, which is 6 years and matches HIPAA’s general documentation-retention rule; see config/settings/base.py’s comment for how that compares to PCI DSS/NIST/CIS) controls how far back entries are kept. Override it in .env per your own compliance obligations.

Purging is never automatic, since nothing in RedScribe runs on a schedule of its own, so wire it into your host’s own cron for a self-maintaining instance:

Terminal window
0 3 * * * cd /path/to/redscribe && docker compose exec -T web python manage.py purge_audit_log

Add --dry-run to preview the delete count first, or --days N to purge against a one-off window instead of the configured default. A Superadmin can also trigger the same purge on demand from the Purge entries older than N days button on the Audit Log page. It’s the same underlying logic either way, and the purge action itself is recorded in the audit trail like any other state-changing request.

Separate from the age-based purge above, the Audit Log page also offers Clear all logs, an unconditional wipe of every audit-log and login-attempt row, regardless of age. Because this is destructive and irreversible, confirming it requires typing the exact phrase DELETE ALL LOGS into a confirmation field first; there’s no equivalent one-step CLI flag for it.

Every entry’s entry_hash is a keyed hash (HMAC-SHA256, keyed with the instance’s Django secret key) computed over its own fields, chained onto the previous entry’s hash. Changing a stored row anywhere in the chain, including through direct database access rather than the app, invalidates every hash after it. A brand-new instance’s very first entry chains from a genesis hash of 64 zeros.

Check the whole chain at any time with:

Terminal window
docker compose exec web python manage.py verify_audit_log

The oldest surviving row can’t be checked against a prior link. Either it’s genuinely the first entry ever, or everything before it was legitimately removed by purge_audit_log. The command reports that row as an anchor, not a failure, and flags the first row after it (if any) whose recomputed hash doesn’t match what’s stored.