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:
1 parent
4ef2dfb352
commit
744813f4ac
18 files changed
+236
-11
No files matched your search
+48
@@ -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
|
||||
|
||||
Reference in new issue
Block a user