Files
ObsiGate/backend/audit.py
T
bruno 1673531b43
CI / lint (push) Failing after 9s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Failing after 12s
fix: resolve all CI lint and security issues
- ruff: 602→0 errors (428 auto-fixed, pyproject.toml ignores for FastAPI patterns)
- bandit: skip B310 (urllib for vault file access is intentional)
- Fixed SIM118 (dict.keys()→dict), PERF102, SIM113, SIM117
- Created pyproject.toml with ruff + bandit config
- 285 tests still pass
2026-07-24 10:38:44 -04:00

154 lines
4.2 KiB
Python

"""
Audit logging: traces sensitive operations to data/audit.log.
Logs all write, delete, and configuration change operations
with timestamp, username, IP address, action, and target.
Format: JSON lines (one JSON object per line) for easy parsing.
"""
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
logger = logging.getLogger("obsigate.audit")
AUDIT_LOG_FILE = Path("data/audit.log")
# Max file size before rotation (10 MB)
MAX_LOG_SIZE = int(os.environ.get("OBSIGATE_AUDIT_MAX_SIZE", str(10 * 1024 * 1024)))
def _rotate_if_needed():
"""Rotate audit log if it exceeds the max size."""
if not AUDIT_LOG_FILE.exists():
return
if AUDIT_LOG_FILE.stat().st_size > MAX_LOG_SIZE:
backup = AUDIT_LOG_FILE.with_suffix(".log.1")
if backup.exists():
backup.unlink()
AUDIT_LOG_FILE.rename(backup)
logger.info("Rotated audit log")
def _write_entry(entry: dict):
"""Append a JSON entry to the audit log."""
try:
AUDIT_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
_rotate_if_needed()
line = json.dumps(entry, default=str, ensure_ascii=False) + "\n"
with open(AUDIT_LOG_FILE, "a", encoding="utf-8") as f:
f.write(line)
except Exception as e:
logger.error(f"Failed to write audit log: {e}")
def log_file_save(
username: str,
vault_name: str,
file_path: str,
size: int,
ip: str | None = None,
):
"""Log a file save (PUT) operation."""
_write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "file_save",
"username": username,
"ip": ip or "unknown",
"vault": vault_name,
"path": file_path,
"size": size,
})
def log_file_delete(
username: str,
vault_name: str,
file_path: str,
ip: str | None = None,
):
"""Log a file delete operation."""
_write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "file_delete",
"username": username,
"ip": ip or "unknown",
"vault": vault_name,
"path": file_path,
})
def log_config_change(
username: str,
changes: dict,
ip: str | None = None,
):
"""Log a configuration change."""
_write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "config_change",
"username": username,
"ip": ip or "unknown",
"changes": changes,
})
def log_vault_add(username: str, vault_name: str, vault_path: str, ip: str | None = None):
"""Log a vault addition."""
_write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "vault_add",
"username": username,
"ip": ip or "unknown",
"vault": vault_name,
"vault_path": vault_path,
})
def log_vault_remove(username: str, vault_name: str, ip: str | None = None):
"""Log a vault removal."""
_write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": "vault_remove",
"username": username,
"ip": ip or "unknown",
"vault": vault_name,
})
def get_recent_entries(limit: int = 100, action: str | None = None) -> list:
"""Read the most recent audit log entries.
Args:
limit: Maximum number of entries to return.
action: Optional filter by action type.
Returns:
List of audit entries (most recent first).
"""
if not AUDIT_LOG_FILE.exists():
return []
entries = []
try:
with open(AUDIT_LOG_FILE, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if action and entry.get("action") != action:
continue
entries.append(entry)
except json.JSONDecodeError:
continue
# Return most recent first
entries.reverse()
return entries[:limit]
except Exception as e:
logger.error(f"Failed to read audit log: {e}")
return []