minimost.audit
minimost.audit
Security audit logging for MiniMost.
The Application Security and Development (ASD) STIG requires an application to generate audit records for security-relevant events, and that each record identify what happened, when (timestamp), where it came from (source address), who triggered it (user identity), and the outcome (success/failure). This module is the single source of truth for producing those records.
Design
Standard library only. Records are emitted through
logging, so there is no new dependency and nothing newer than Python 3.6 is required.Durable local trail. Records are appended to
audit.login the MiniMost data root (seeminimost.paths.data_dir()). On a packaged install that is the systemdStateDirectory(e.g./var/lib/minimost), so the file persists across restarts and can be shipped to a central aggregator/SIEM by the host’s log forwarder (journald, rsyslog, …).One line per event, machine-parseable. Each record is a single
key=valueline prefixed with an ISO-8601 UTC timestamp, e.g.:2026-06-29T18:04:11Z event=login outcome=failure user=alice src=10.0.0.5
Append-only, multi-worker safe. The file handler is created lazily on first use after Gunicorn forks its workers, so every worker opens its own
O_APPENDdescriptor; on POSIX, single-line appends are atomic, so the workers never corrupt each other’s records.Bounded by rotation. The handler rotates the log by size and/or age (see
audit_log_max_size_mb/audit_log_max_age_days/audit_log_backupsinsettings.json), keeping a configurable number of timestamped archives. Rotation is coordinated across workers so they never clobber each other (see_RotatingAuditHandler). Pruning deletes the oldest archives, so a deployment with audit-retention requirements should off-load records to a central aggregator/SIEM before they age out.Log-injection resistant. All interpolated values are stripped of control characters (notably CR/LF) before they reach the log, so an attacker cannot forge or split records by smuggling newlines through a username or path.
Module-level attributes
- AUDIT_LOGstr
Absolute path to
audit.log. Resolved once at import time fromminimost.paths.data_dir(). Exposed as a module attribute (mirroringauth.AUTH_DB/presence.PRESENCE_DB) so the test suite can redirect it to a temp directory; changing it re-points the logger on the next event.
- class minimost.audit._UTCFormatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)[source]
Bases:
FormatterFormat timestamps as ISO-8601 UTC with a trailing
Z.The default
logging.Formatteremits local time with millisecond precision; the STIG audit-content requirement is cleaner to satisfy with an unambiguous UTC instant, so the converter is pinned totime.gmtime().Initialize the formatter with specified format strings.
Initialize the formatter either with the specified format string, or a default as described above. Allow for specialized date formatting with the optional datefmt argument. If datefmt is omitted, you get an ISO8601-like (or RFC 3339-like) format.
Use a style parameter of ‘%’, ‘{’ or ‘$’ to specify that you want to use one of %-formatting,
str.format()({}) formatting orstring.Templateformatting in your format string.Changed in version 3.2: Added the
styleparameter.- converter()
- gmtime([seconds]) -> (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
tm_sec, tm_wday, tm_yday, tm_isdst)
Convert seconds since the Epoch to a time tuple expressing UTC (a.k.a. GMT). When ‘seconds’ is not passed in, convert the current time instead.
If the platform supports the tm_gmtoff and tm_zone, they are available as attributes only.
- formatTime(record, datefmt=None)[source]
Return the creation time of the specified LogRecord as formatted text.
This method should be called from format() by a formatter which wants to make use of a formatted time. This method can be overridden in formatters to provide for any specific requirement, but the basic behaviour is as follows: if datefmt (a string) is specified, it is used with time.strftime() to format the creation time of the record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used. The resulting string is returned. This function uses a user-configurable function to convert the creation time to a tuple. By default, time.localtime() is used; to change this for a particular formatter instance, set the ‘converter’ attribute to a function with the same signature as time.localtime() or time.gmtime(). To change it for all formatters, for example if you want all logging times to be shown in GMT, set the ‘converter’ attribute in the Formatter class.
- minimost.audit._setting_num(value, default)[source]
Return value if it is a non-negative, non-bool number, else default.
- minimost.audit._rotation_config()[source]
Return
(max_bytes, max_age_seconds, backup_count)from settings.json.Keys (all optional):
audit_log_max_size_mb— rotate once the live log reaches this size.audit_log_max_age_days— rotate this long after the previous rotation.audit_log_backups— number of rotated archives to keep; older ones are deleted.0keeps every archive.
A
0(or absent) size/age disables that trigger; with both disabled the file is never rotated. A read/parse error falls back to the shipped defaults.
- class minimost.audit._RotatingAuditHandler(filename, max_bytes, max_age_seconds, backup_count, encoding=None, delay=False)[source]
Bases:
WatchedFileHandlerAppend-only audit handler that rotates by size and/or age.
Rotation is safe across several Gunicorn workers, each of which holds its own handler.
WatchedFileHandlerreopens the log whenever its inode changes, so when one worker rotates the file (by renaming it) the others transparently switch to the freshly created one — no records are lost, and, unlikeRotatingFileHandler, the workers never clobber each other’s rotation. The rename is guarded by an atomically-created<log>.lockfile (O_CREAT | O_EXCL, which works on both POSIX and Windows) and re-checked under the lock, so exactly one worker rotates and a file already rotated by a peer is left alone. A lock left behind by a crashed rotation is treated as stale after a minute so it can never deadlock.Age is measured from a companion
<log>.rotated_atmarker file (touched at each rotation), independent of write activity, so a quiet day does not reset the clock.Open the specified file and use it as the stream for logging.
- __init__(filename, max_bytes, max_age_seconds, backup_count, encoding=None, delay=False)[source]
Open the specified file and use it as the stream for logging.
- _close_stream()[source]
Release this handler’s open file descriptor on the log.
Windows refuses to rename a file that is open in the process, so the log must be closed before it can be rotated (POSIX allows renaming an open file, so this is harmless there). The stream is left as
Noneso the nextemit()reopens a fresh file via the base handler.
- _LOCK_STALE_SECONDS = 60
- minimost.audit._make_handler(path)[source]
Return a rotating audit handler for path, or
Noneif unopenable.Audit logging must never take the request down: if the log file cannot be created (read-only data dir, permissions, …) the failure is swallowed and the caller proceeds without a durable record rather than raising. The handler rotates by size and/or age per
_rotation_config()so the trail does not grow without bound.
- minimost.audit._get_logger()[source]
Return the configured audit logger, (re)attaching the file handler.
Idempotent: the handler is built only when missing or when
AUDIT_LOGhas changed since it was last built.propagateis disabled so audit records do not bleed into Gunicorn’s root/error logger.
- minimost.audit._sanitize(value, limit=256)[source]
Neutralise a value for safe single-line logging.
Control characters are replaced with spaces (defeating log injection) and the result is length-capped so an oversized field cannot bloat the trail.
- minimost.audit._client_ip()[source]
Return the request’s source IP, or
Noneoutside a request context.When the app sits behind a reverse proxy the real client is the leftmost
X-Forwarded-Forentry; otherwise it is the peer address.XFFis attacker-controllable when the app is exposed directly, so deployments that must trust it should terminate at a proxy that overwrites the header.
- minimost.audit.log_event(event, outcome, user=None, source=None, detail=None)[source]
Append one security audit record.
- Parameters:
event – Short event type, e.g.
"login"or"account_create".outcome –
"success"or"failure".user – The user identity the event concerns (
None→-).source – Source address; resolved from the request when omitted.
detail – Optional free-text context (sanitised and quoted).
- minimost.audit.account_lockout(user)[source]
Record an account being locked after repeated failed logins.
- minimost.audit.account_deleted(user, delete_type)[source]
Record removal (soft/hard) of an account.
- minimost.audit.password_changed(user, outcome='success')[source]
Record a password change by the logged-in user.
- minimost.audit.password_reset(user, outcome='success')[source]
Record a password reset via a reset token.