Files
magnus919_agent-skills/binary-analysis/scripts/binary_analysis/projects/diagnostics.py
T

145 lines
4.5 KiB
Python

"""Diagnostics persistence — accumulate and retrieve diagnostics across commands.
Diagnostics are persisted as JSONL in project/diagnostics.jsonl, one
JSON object per line. Each entry has: severity, category, message, recoverable,
command, and timestamp.
The diagnostics file grows across the project lifecycle: warnings and errors
from analyze, triage, suspicious-apis, and other commands are accumulated
and retrievable via the `binary diagnostics` command.
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from typing import Any
from binary_analysis.projects.atomic import atomic_append_text
DIAGNOSTICS_FILENAME = "diagnostics.jsonl"
def _diagnostics_path(project_path: str) -> str:
"""Return the path to the diagnostics file within a project workspace."""
return os.path.join(project_path, DIAGNOSTICS_FILENAME)
def persist_diagnostics(
project_path: str,
diagnostics: list[dict[str, Any]],
command: str = "unknown",
) -> None:
"""Persist diagnostic entries to the project's diagnostics file.
Each diagnostic entry is augmented with a command field and timestamp
before being appended atomically to the JSONL file.
Args:
project_path: Absolute path to the project workspace directory.
diagnostics: List of diagnostic dicts to persist.
command: Name of the command that produced these diagnostics.
"""
if not diagnostics:
return
path = _diagnostics_path(project_path)
timestamp = datetime.now(timezone.utc).isoformat()
for diag in diagnostics:
entry = {
"severity": diag.get("severity", "INFO"),
"category": diag.get("category", "general"),
"message": diag.get("message", ""),
"recoverable": diag.get("recoverable", True),
"command": command,
"timestamp": timestamp,
}
# Preserve optional fields
if "component" in diag:
entry["component"] = diag["component"]
if "remediation" in diag:
entry["remediation"] = diag["remediation"]
line = json.dumps(entry, ensure_ascii=False)
atomic_append_text(path, line)
def load_diagnostics(project_path: str) -> list[dict[str, Any]]:
"""Load all accumulated diagnostics from the project's diagnostics file.
Returns an empty list if the file does not exist or is empty.
Args:
project_path: Absolute path to the project workspace directory.
Returns:
List of diagnostic dicts ordered by appearance in the file
(oldest first).
"""
path = _diagnostics_path(project_path)
if not os.path.exists(path):
return []
diagnostics: list[dict[str, Any]] = []
try:
with open(path, encoding="utf-8") as f:
for line_num, line in enumerate(f, start=1):
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
diagnostics.append(entry)
except json.JSONDecodeError:
# Skip corrupted lines but note in a diagnostic
diagnostics.append(
{
"severity": "WARNING",
"category": "diagnostics-file",
"message": f"Corrupted diagnostics entry at line {line_num}",
"recoverable": True,
"command": "diagnostics",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
)
except OSError:
return []
return diagnostics
def clear_diagnostics(project_path: str) -> None:
"""Remove the diagnostics file (e.g., on project clean).
Args:
project_path: Absolute path to the project workspace directory.
"""
path = _diagnostics_path(project_path)
if os.path.exists(path):
os.unlink(path)
def get_diagnostics_summary(
diagnostics: list[dict[str, Any]],
) -> dict[str, Any]:
"""Compute a summary of diagnostic entries.
Args:
diagnostics: List of diagnostic dicts.
Returns:
Dict with total count and breakdown by severity.
"""
by_severity: dict[str, int] = {"INFO": 0, "WARNING": 0, "ERROR": 0}
for d in diagnostics:
sev = d.get("severity", "INFO")
if sev in by_severity:
by_severity[sev] += 1
return {
"total": len(diagnostics),
"by_severity": by_severity,
}