TradingAgents/.claude/lib/install_audit.py

494 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Install Audit - Audit logging for GenAI-first installation system
This module provides audit trail logging for installation operations,
tracking protected files, conflicts, resolutions, and outcomes.
Key Features:
- JSONL format audit logs (one JSON per line)
- Installation attempt tracking with unique IDs
- Protected file recording
- Conflict tracking and resolution logging
- Report generation from audit trail
- Crash-resistant (append-only, recoverable)
Usage:
from install_audit import InstallAudit
# Start installation
audit = InstallAudit(Path.home() / ".autonomous-dev" / "install_audit.jsonl")
install_id = audit.start_installation("fresh")
# Log events
audit.record_protected_file(install_id, ".env", "secrets")
audit.log_success(install_id, files_copied=42)
# Generate report
report = audit.generate_report(install_id)
Date: 2025-12-09
Issue: #106 (GenAI-first installation system)
Agent: implementer
Design Patterns:
See library-design-patterns skill for standardized design patterns.
"""
import json
import uuid
from pathlib import Path
from typing import Dict, Any, List, Optional
from datetime import datetime
# Security utilities
try:
from plugins.autonomous_dev.lib.security_utils import audit_log
except ImportError:
from security_utils import audit_log
class AuditEntry:
"""Audit log entry data class."""
def __init__(
self,
event: str,
install_id: str,
timestamp: Optional[str] = None,
**kwargs
):
"""Initialize audit entry.
Args:
event: Event type (installation_start, protected_file, etc.)
install_id: Unique installation ID
timestamp: ISO 8601 timestamp (auto-generated if None)
**kwargs: Additional event-specific data
"""
self.event = event
self.install_id = install_id
self.timestamp = timestamp or (datetime.utcnow().isoformat() + "Z")
self.data = kwargs
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
"event": self.event,
"install_id": self.install_id,
"timestamp": self.timestamp,
**self.data
}
class InstallAudit:
"""Audit logging for installation operations.
This class provides append-only audit logging in JSONL format,
tracking all installation events for security and debugging.
Attributes:
audit_file: Path to audit log file (JSONL format)
Examples:
>>> audit = InstallAudit(Path("install_audit.jsonl"))
>>> install_id = audit.start_installation("fresh")
>>> audit.log_success(install_id, files_copied=42)
"""
def __init__(self, audit_file: Path | str):
"""Initialize audit logger.
Args:
audit_file: Path to audit log file
Note:
Parent directories are created automatically.
File is created in append mode (preserves existing entries).
"""
self.audit_file = Path(audit_file) if isinstance(audit_file, str) else audit_file
# Create parent directories
self.audit_file.parent.mkdir(parents=True, exist_ok=True)
# Security audit log
audit_log("install_audit", "initialized", {
"audit_file": str(self.audit_file)
})
def start_installation(self, install_type: str) -> str:
"""Log installation start and return unique install ID.
Args:
install_type: Installation type (fresh, brownfield, upgrade)
Returns:
Unique installation ID (UUID)
Examples:
>>> audit = InstallAudit(Path("audit.jsonl"))
>>> install_id = audit.start_installation("fresh")
"""
install_id = str(uuid.uuid4())
entry = AuditEntry(
event="installation_start",
install_id=install_id,
install_type=install_type
)
self._write_entry(entry)
return install_id
def log_success(self, install_id: str, files_copied: int, **kwargs) -> None:
"""Log successful installation completion.
Args:
install_id: Installation ID from start_installation()
files_copied: Number of files copied
**kwargs: Additional context (files_skipped, files_backed_up, etc.)
Examples:
>>> audit.log_success(install_id, files_copied=42, files_skipped=2)
"""
entry = AuditEntry(
event="installation_success",
install_id=install_id,
files_copied=files_copied,
**kwargs
)
self._write_entry(entry)
def log_failure(self, install_id: str, error: str, **kwargs) -> None:
"""Log failed installation.
Args:
install_id: Installation ID from start_installation()
error: Error message
**kwargs: Additional context
Examples:
>>> audit.log_failure(install_id, error="Permission denied")
"""
entry = AuditEntry(
event="installation_failure",
install_id=install_id,
error=error,
**kwargs
)
self._write_entry(entry)
def record_protected_file(
self,
install_id: str,
file_path: str,
reason: str,
metadata: Optional[Dict[str, Any]] = None
) -> None:
"""Record a protected file.
Args:
install_id: Installation ID
file_path: Relative path to protected file
reason: Why file is protected
metadata: Optional additional metadata
Examples:
>>> audit.record_protected_file(
... install_id,
... ".env",
... "secrets",
... metadata={"size": 1024}
... )
"""
# Validate path for security
self._validate_path(file_path)
entry = AuditEntry(
event="protected_file",
install_id=install_id,
file=file_path,
reason=reason
)
if metadata:
entry.data["metadata"] = metadata
self._write_entry(entry)
def record_conflict(
self,
install_id: str,
file_path: str,
existing_hash: str,
staging_hash: str,
**kwargs
) -> None:
"""Record a file conflict.
Args:
install_id: Installation ID
file_path: Relative path to conflicting file
existing_hash: Hash of existing file
staging_hash: Hash of staging file
**kwargs: Additional context
Examples:
>>> audit.record_conflict(
... install_id,
... "file.py",
... existing_hash="abc",
... staging_hash="def"
... )
"""
self._validate_path(file_path)
entry = AuditEntry(
event="conflict",
install_id=install_id,
file=file_path,
existing_hash=existing_hash,
staging_hash=staging_hash,
**kwargs
)
self._write_entry(entry)
def record_conflict_resolution(
self,
install_id: str,
file_path: str,
action: str,
**kwargs
) -> None:
"""Record conflict resolution action.
Args:
install_id: Installation ID
file_path: Relative path to file
action: Action taken (backup, skip, overwrite)
**kwargs: Additional context (backup_path, etc.)
Examples:
>>> audit.record_conflict_resolution(
... install_id,
... "file.py",
... action="backup",
... backup_path="file.py.bak"
... )
"""
self._validate_path(file_path)
entry = AuditEntry(
event="conflict_resolution",
install_id=install_id,
file=file_path,
action=action,
**kwargs
)
self._write_entry(entry)
def generate_report(self, install_id: str) -> Dict[str, Any]:
"""Generate installation report from audit trail.
Args:
install_id: Installation ID to generate report for
Returns:
Dict with installation report:
- install_id: Installation ID
- status: Status (success, failure, in_progress)
- timeline: Chronological list of events
- summary: Summary statistics
- protected_files: List of protected files
- conflicts: List of conflicts
Raises:
ValueError: If install ID not found in audit log
Examples:
>>> report = audit.generate_report(install_id)
>>> print(f"Status: {report['status']}")
"""
entries = self._read_entries_for_install(install_id)
if not entries:
raise ValueError(f"Install ID not found: {install_id}")
# Parse entries
status = "in_progress"
timeline = []
protected_files = []
conflicts = []
stats = {
"total_protected_files": 0,
"total_conflicts": 0,
"files_copied": 0
}
for entry_dict in entries:
event = entry_dict["event"]
timeline.append(entry_dict)
if event == "installation_success":
status = "success"
stats["files_copied"] = entry_dict.get("files_copied", 0)
elif event == "installation_failure":
status = "failure"
elif event == "protected_file":
protected_files.append(entry_dict["file"])
stats["total_protected_files"] += 1
elif event == "conflict":
conflicts.append(entry_dict["file"])
stats["total_conflicts"] += 1
return {
"install_id": install_id,
"status": status,
"timeline": timeline,
"summary": stats,
"protected_files": protected_files,
"conflicts": conflicts
}
def export_report(self, install_id: str, report_file: Path | str) -> None:
"""Export installation report to JSON file.
Args:
install_id: Installation ID
report_file: Path to output report file
Examples:
>>> audit.export_report(install_id, Path("report.json"))
"""
report = self.generate_report(install_id)
report_path = Path(report_file) if isinstance(report_file, str) else report_file
report_path.parent.mkdir(parents=True, exist_ok=True)
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
def get_all_installations(self) -> List[Dict[str, Any]]:
"""Get all installation attempts from audit log.
Returns:
List of installation info dicts (one per install_id)
Examples:
>>> history = audit.get_all_installations()
>>> print(f"Found {len(history)} installations")
"""
if not self.audit_file.exists():
return []
installations = {}
with open(self.audit_file, "r") as f:
for line in f:
try:
entry = json.loads(line.strip())
install_id = entry.get("install_id")
if not install_id:
continue
# Track start entries
if entry["event"] == "installation_start":
installations[install_id] = {
"install_id": install_id,
"install_type": entry.get("install_type"),
"timestamp": entry.get("timestamp")
}
except json.JSONDecodeError:
# Skip corrupted lines
continue
return list(installations.values())
def get_installations_by_status(self, status: str) -> List[Dict[str, Any]]:
"""Get installations filtered by status.
Args:
status: Status to filter by (success, failure)
Returns:
List of installation info dicts matching status
Examples:
>>> successful = audit.get_installations_by_status("success")
"""
installations = []
for install_info in self.get_all_installations():
install_id = install_info["install_id"]
try:
report = self.generate_report(install_id)
if report["status"] == status:
installations.append(install_info)
except ValueError:
continue
return installations
def _write_entry(self, entry: AuditEntry) -> None:
"""Write audit entry to log file.
Args:
entry: AuditEntry to write
"""
# Append to audit file
with open(self.audit_file, "a") as f:
f.write(json.dumps(entry.to_dict()) + "\n")
def _read_entries_for_install(self, install_id: str) -> List[Dict[str, Any]]:
"""Read all entries for a specific installation.
Args:
install_id: Installation ID
Returns:
List of entry dicts for this installation
"""
if not self.audit_file.exists():
return []
entries = []
with open(self.audit_file, "r") as f:
for line in f:
try:
entry = json.loads(line.strip())
if entry.get("install_id") == install_id:
entries.append(entry)
except json.JSONDecodeError:
# Skip corrupted lines
continue
return entries
def _validate_path(self, file_path: str) -> None:
"""Validate file path for security.
Args:
file_path: Relative file path
Raises:
ValueError: If path contains traversal or is absolute
"""
# Check for path traversal
if ".." in file_path:
raise ValueError(f"Path traversal not allowed (invalid path): {file_path}")
# Check for absolute paths
if Path(file_path).is_absolute():
raise ValueError(f"Absolute paths not allowed (invalid path): {file_path}")