Add compliance audit log

Immutable audit_log table in the scanner DB records every significant
admin action (profile save/delete, token create/revoke, PIN changes,
source add/update/delete, scheduler job changes, scan start/stop, SMTP
save, dispositions, item delete/redact). GET /api/audit_log exposes
entries newest-first. New Audit Log tab in the Settings modal renders
the table on demand. Settings modal widened 540→640 px and tab labels
set to white-space:nowrap so the six-tab row fits on one line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
StyxX65andClaude Sonnet 4.6 committed 2026-05-28 10:51:23 +02:00
1 parent 4ef2dfb352
commit 744813f4ac
18 files changed
+236 -11

No files matched your search

+48
View File
@@ -180,6 +180,17 @@ CREATE INDEX IF NOT EXISTS idx_dellog_time ON deletion_log(deleted_at);
CREATE INDEX IF NOT EXISTS idx_dellog_item ON deletion_log(item_id);
CREATE INDEX IF NOT EXISTS idx_dellog_reason ON deletion_log(reason);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts REAL NOT NULL,
action TEXT NOT NULL DEFAULT '',
actor TEXT NOT NULL DEFAULT '',
detail TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(ts);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_items_scan ON flagged_items(scan_id);
CREATE INDEX IF NOT EXISTS idx_items_source ON flagged_items(source_type);
@@ -809,6 +820,34 @@ class ScanDB:
).fetchone()[0] or 0
return {"total": total, "by_reason": by_reason, "cpr_hits_deleted": cpr_deleted}
# ── Compliance audit log ──────────────────────────────────────────────────
def log_audit(self, action: str, detail: str = "",
actor: str = "", ip: str = "") -> None:
"""Write an immutable compliance audit record."""
c = self._connect()
c.execute(
"INSERT INTO audit_log (ts, action, actor, detail, ip) VALUES (?,?,?,?,?)",
(time.time(), action, actor, detail, ip),
)
c.commit()
def get_audit_log(self, limit: int = 200,
action: str | None = None) -> list[dict]:
"""Return audit records, most recent first."""
c = self._connect()
if action:
rows = c.execute(
"SELECT * FROM audit_log WHERE action=? ORDER BY ts DESC LIMIT ?",
(action, limit),
).fetchall()
else:
rows = c.execute(
"SELECT * FROM audit_log ORDER BY ts DESC LIMIT ?",
(limit,),
).fetchall()
return [dict(r) for r in rows]
def delete_item_record(self, item_id: str, scan_id: int | None = None) -> None:
"""Remove a flagged item from the DB (after it has been deleted in M365)."""
c = self._connect()
@@ -1057,6 +1096,15 @@ class ScanDB:
_db: ScanDB | None = None
def log_audit_event(action: str, detail: str = "",
actor: str = "", ip: str = "") -> None:
"""Write an audit record to the shared DB. Silently no-ops if DB unavailable."""
try:
get_db().log_audit(action, detail, actor=actor, ip=ip)
except Exception:
pass
def get_db(path: Path = DB_PATH) -> ScanDB:
"""Return the module-level ScanDB singleton, creating it if needed."""
global _db