Files
magnus919_agent-skills/binary-analysis/scripts/binary_analysis/domain/entities.py
T

441 lines
10 KiB
Python

"""Canonical domain entities as dataclasses.
All entities use typed fields with proper defaults. Every entity can be
serialized to a JSON-compatible dict via asdict() or the schema helpers.
Address objects use the canonical structured format with space, offset,
display, and optional file_offset.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from uuid import UUID, uuid4
from binary_analysis.domain.enums import (
AuditResult,
Confidence,
DiagnosticSeverity,
Endianness,
FunctionNameSource,
ImportResolution,
ProjectState,
ReferenceKind,
ReportType,
)
# ---------------------------------------------------------------------------
# Canonical Address
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class Address:
"""Canonical structured address.
Attributes:
space: Address space name (e.g., "ram", "register", "const").
offset: Hex-prefixed offset string (e.g., "0x4018d0").
display: Human-readable display form (e.g., "0x4018d0").
file_offset: Optional byte offset within the file on disk.
"""
space: str
offset: str
display: str
file_offset: int | None = None
def __post_init__(self) -> None:
"""Validate offset format."""
if not self.offset.startswith("0x"):
raise ValueError(f"Address offset must start with '0x', got: {self.offset!r}")
def to_dict(self) -> dict[str, Any]:
"""Serialize to a canonical dict for JSON output."""
result: dict[str, Any] = {
"space": self.space,
"offset": self.offset,
"display": self.display,
}
if self.file_offset is not None:
result["file_offset"] = self.file_offset
return result
@classmethod
def from_dict(cls, data: dict[str, Any]) -> Address:
"""Deserialize from a canonical dict."""
return cls(
space=data["space"],
offset=data["offset"],
display=data["display"],
file_offset=data.get("file_offset"),
)
# ---------------------------------------------------------------------------
# Domain Entities
# ---------------------------------------------------------------------------
@dataclass
class Project:
"""Persistent analysis workspace.
Identity: UUID.
"""
id: UUID = field(default_factory=uuid4)
name: str = ""
state: ProjectState = ProjectState.CREATED
created_at: str = ""
updated_at: str = ""
workspace_version: str = "1"
binary_count: int = 0
is_stale: bool = False
lock: dict[str, Any] | None = None
description: str | None = None
max_binary_size_bytes: int | None = None
@dataclass
class Binary:
"""Imported artifact identified by SHA-256.
Identity: UUID + SHA-256.
"""
id: UUID = field(default_factory=uuid4)
sha256: str = ""
path: str = ""
format: str = ""
import_mode: str = "copy"
size_bytes: int = 0
architecture: str | None = None
endianness: Endianness | None = None
entry_point: Address | None = None
compiler: str | None = None
source_language: str | None = None
imported_at: str | None = None
analyzed_at: str | None = None
analysis_profile: str | None = None
is_stale: bool = False
@dataclass
class Section:
"""Mapped code or data region within a binary.
Identity: Name + binary ID.
"""
name: str = ""
binary_id: UUID | None = None
address: Address | None = None
virtual_size: int = 0
raw_size: int = 0
flags: list[str] = field(default_factory=list)
entropy: float | None = None
content_hash: str | None = None
@dataclass
class EntryPoint:
"""Process, library, boot, or firmware entry point.
Identity: Address within binary.
"""
address: Address | None = None
kind: str = "unknown"
confidence: Confidence = Confidence.UNKNOWN
name: str | None = None
binary_id: UUID | None = None
@dataclass
class Import:
"""External dependency symbol.
Identity: Address within binary.
"""
module: str = ""
symbol: str = ""
address: Address | None = None
resolution: ImportResolution = ImportResolution.UNRESOLVED
ordinal: int | None = None
binary_id: UUID | None = None
@dataclass
class Export:
"""Public symbol or forwarder.
Identity: Address or ordinal.
"""
name: str = ""
address: Address | None = None
ordinal: int | None = None
forwarder: str | None = None
kind: str = "function"
binary_id: UUID | None = None
@dataclass
class Symbol:
"""Named entity with source and scope.
Identity: Address within binary.
"""
name: str = ""
address: Address | None = None
source: FunctionNameSource = FunctionNameSource.UNKNOWN
scope: str = "unknown"
binary_id: UUID | None = None
@dataclass
class String:
"""Decoded string at a specific address.
Identity: Address + encoding + length.
"""
text: str = ""
encoding: str = "ASCII"
address: Address | None = None
length: int = 0
binary_id: UUID | None = None
@dataclass
class Function:
"""Callable code region.
Identity: Binary ID + entry address.
"""
name: str = ""
address: Address | None = None
size_bytes: int = 0
confidence: Confidence = Confidence.UNKNOWN
name_source: FunctionNameSource = FunctionNameSource.UNKNOWN
binary_id: UUID | None = None
is_external: bool = False
is_thunk: bool = False
signature: str | None = None
source_language: str | None = None
basic_block_count: int | None = None
instruction_count: int | None = None
cyclomatic_complexity: int | None = None
@dataclass
class Instruction:
"""Canonical machine instruction.
Identity: Address within function.
"""
mnemonic: str = ""
operands: str = ""
bytes_hex: str = ""
address: Address | None = None
size_bytes: int = 0
function_id: str | None = None
@dataclass
class BasicBlock:
"""Control-flow node within a function.
Identity: Start address within function.
"""
start_address: Address | None = None
end_address: Address | None = None
instruction_count: int = 0
function_id: str | None = None
is_entry: bool = False
is_exit: bool = False
@dataclass
class Reference:
"""Directed call, jump, read, write, or data relation.
Identity: Address pair + kind.
"""
from_addr: Address | None = None
to_addr: Address | None = None
kind: ReferenceKind = ReferenceKind.UNKNOWN
confidence: Confidence = Confidence.UNKNOWN
binary_id: UUID | None = None
@dataclass
class CallGraph:
"""Bounded call graph rooted at a function.
Identity: Derived from function references.
"""
root_address: Address | None = None
nodes: list[dict[str, Any]] = field(default_factory=list)
edges: list[dict[str, Any]] = field(default_factory=list)
max_depth: int = 3
total_nodes: int = 0
total_edges: int = 0
truncated: bool = False
binary_id: UUID | None = None
@dataclass
class Diagnostic:
"""Warning or limitation from an analysis run.
Identity: Unique within analysis run.
"""
severity: DiagnosticSeverity = DiagnosticSeverity.INFO
category: str = ""
message: str = ""
component: str | None = None
remediation: str | None = None
recoverable: bool = True
@dataclass
class Capability:
"""Rule-derived functional indicator.
Identity: Name within binary.
"""
name: str = ""
confidence: Confidence = Confidence.UNKNOWN
evidence: list[dict[str, Any]] = field(default_factory=list)
binary_id: UUID | None = None
@dataclass
class Observation:
"""Direct deterministic fact from analysis.
Identity: Unique within analysis run.
"""
category: str = ""
description: str = ""
source: str = ""
address: Address | None = None
evidence: Any | None = None
binary_id: UUID | None = None
@dataclass
class Heuristic:
"""Rule-derived interpretation with confidence.
Identity: Name within analysis run.
"""
name: str = ""
description: str = ""
confidence: Confidence = Confidence.UNKNOWN
rule_id: str | None = None
evidence: list[dict[str, Any]] = field(default_factory=list)
binary_id: UUID | None = None
@dataclass
class Inference:
"""Agent-generated interpretation.
Identity: Unique within analysis run.
"""
description: str = ""
confidence: Confidence = Confidence.UNKNOWN
basis: list[str] = field(default_factory=list)
binary_id: UUID | None = None
@dataclass
class Unknown:
"""Explicit unresolved question.
Identity: Address within binary.
"""
address: Address | None = None
question: str = ""
category: str | None = None
binary_id: UUID | None = None
@dataclass
class Report:
"""Durable handoff artifact.
Identity: UUID.
"""
id: UUID = field(default_factory=uuid4)
report_type: ReportType = ReportType.TRIAGE
project_id: UUID | None = None
binary_id: UUID | None = None
created_at: str = ""
format: str = "json"
summary: str | None = None
sections: list[dict[str, Any]] = field(default_factory=list)
@dataclass
class AuditEvent:
"""Append-only provenance event.
Identity: Timestamp sequence.
"""
timestamp: str = ""
event_type: str = ""
result: AuditResult = AuditResult.SUCCESS
project_id: UUID | None = None
binary_id: UUID | None = None
user: str | None = None
details: dict[str, Any] = field(default_factory=dict)
@dataclass
class TriageResult:
"""Aggregate result of a triage analysis.
Contains observations (facts), heuristics (interpretations),
unknowns (open questions), and engine diagnostics.
"""
observations: list[Observation] = field(default_factory=list)
heuristics: list[Heuristic] = field(default_factory=list)
unknowns: list[Unknown] = field(default_factory=list)
engine_diagnostics: list[dict[str, Any]] = field(default_factory=list)
partial: bool = False
@dataclass
class DiagnosticsResult:
"""Cumulative diagnostics across project lifecycle.
Contains all persistent diagnostics from analysis, triage,
and other commands, plus any current engine diagnostics.
"""
diagnostics: list[dict[str, Any]] = field(default_factory=list)
total: int = 0
by_severity: dict[str, int] = field(
default_factory=lambda: {"INFO": 0, "WARNING": 0, "ERROR": 0}
)