mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
156 lines
4.8 KiB
Python
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))
|