Files
magnus919_agent-skills/binary-analysis/scripts/binary_analysis/reporting/audit.py
T

156 lines
4.8 KiB
Python

"""Audit event persistence — append-only events.jsonl.
Provides atomic audit event writing and reading for the project audit log.
Each event is a single-line JSON object appended atomically. Events are
immutable and append-only; no modification or deletion is supported.
Events include: timestamp, command, args, result (AuditResult enum),
duration_ms, project_id, and optional details.
Key guarantees:
- Atomic append: no partial lines, no interleaving.
- Every line is valid JSON (single-line object).
- Events ordered by timestamp (ISO 8601 with timezone).
- File only grows; never shrinks or overwrites existing entries.
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from typing import Any
from binary_analysis.domain.enums import AuditResult
from binary_analysis.projects.atomic import atomic_append_text
AUDIT_FILENAME = "events.jsonl"
def _audit_path(project_path: str) -> str:
"""Return the path to the audit events file within a project workspace.
Args:
project_path: Absolute path to the project workspace directory.
Returns:
Full path to the events.jsonl file.
"""
return os.path.join(project_path, "audit", AUDIT_FILENAME)
def write_audit_event(
project_path: str,
command: str,
result: AuditResult,
duration_ms: int,
*,
args: dict[str, Any] | None = None,
project_id: str | None = None,
binary_id: str | None = None,
details: dict[str, Any] | None = None,
) -> None:
"""Atomically append a single audit event to events.jsonl.
Each event is written as a single JSON line. Uses atomic_append_text
to guarantee no partial lines or interleaving.
Args:
project_path: Absolute path to the project workspace directory.
command: The command name (e.g., "project create", "import", "analyze").
result: Outcome from AuditResult enum.
duration_ms: Wall-clock duration in milliseconds.
args: Non-sensitive command arguments (flags, selectors).
project_id: Optional project UUID.
binary_id: Optional binary UUID.
details: Optional additional event details.
"""
timestamp = datetime.now(timezone.utc).isoformat()
event: dict[str, Any] = {
"timestamp": timestamp,
"command": command,
"args": args if args is not None else {},
"result": result.value,
"duration_ms": duration_ms,
}
if project_id is not None:
event["project_id"] = project_id
if binary_id is not None:
event["binary_id"] = binary_id
if details is not None:
event["details"] = details
line = json.dumps(event, ensure_ascii=False)
path = _audit_path(project_path)
atomic_append_text(path, line)
def read_audit_events(project_path: str) -> list[dict[str, Any]]:
"""Read all audit events from events.jsonl, ordered by appearance.
Events are returned in file order (oldest first), which corresponds to
timestamp order since events are appended chronologically.
Args:
project_path: Absolute path to the project workspace directory.
Returns:
List of audit event dicts ordered by timestamp. Empty list if the
file does not exist or is empty.
"""
path = _audit_path(project_path)
if not os.path.exists(path):
return []
events: list[dict[str, Any]] = []
try:
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
events.append(event)
except json.JSONDecodeError:
# Skip corrupted lines but emit a placeholder
events.append(
{
"timestamp": datetime.now(timezone.utc).isoformat(),
"command": "unknown",
"args": {},
"result": AuditResult.FAILED.value,
"duration_ms": 0,
"details": {"error": f"Corrupted audit event: {line[:100]}"},
}
)
except OSError:
return []
return events
def clear_audit(project_path: str) -> None:
"""Remove the audit events file (e.g., on project clean).
Args:
project_path: Absolute path to the project workspace directory.
"""
path = _audit_path(project_path)
if os.path.exists(path):
os.unlink(path)
def audit_file_exists(project_path: str) -> bool:
"""Check if the audit events file exists.
Args:
project_path: Absolute path to the project workspace directory.
Returns:
True if the events.jsonl file exists.
"""
return os.path.exists(_audit_path(project_path))