mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
454 lines
11 KiB
Python
454 lines
11 KiB
Python
"""JSON serialization helpers for the canonical domain model.
|
|
|
|
Key serialization rules:
|
|
- Addresses: structured objects (space, offset, display, optional file_offset)
|
|
- Sizes: integer bytes (JSON number), never strings
|
|
- Unknown/null fields: serialize as JSON null, not "" or 0
|
|
- Enum values: UPPER_CASE strings matching documented enum members
|
|
- Entity objects: only canonical fields; no backend-specific keys
|
|
- Strings: correct JSON escaping of embedded quotes, backslashes, control chars
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import json
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from binary_analysis.domain.entities import Address
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Address serialization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def serialize_address(addr: Address | None) -> dict[str, Any] | None:
|
|
"""Serialize an Address to its canonical dict form, or null."""
|
|
if addr is None:
|
|
return None
|
|
return addr.to_dict()
|
|
|
|
|
|
def deserialize_address(data: dict[str, Any] | None) -> Address | None:
|
|
"""Deserialize a canonical dict back to an Address, or null."""
|
|
if data is None:
|
|
return None
|
|
return Address.from_dict(data)
|
|
|
|
|
|
def canonical_address(
|
|
space: str, offset: str, display: str | None = None, file_offset: int | None = None
|
|
) -> Address:
|
|
"""Factory for creating canonical addresses with validated format.
|
|
|
|
Args:
|
|
space: Address space name (e.g., "ram", "register").
|
|
offset: Hex-prefixed offset string (e.g., "0x401000").
|
|
display: Display string. Defaults to offset if not provided.
|
|
file_offset: Optional byte offset within the file.
|
|
"""
|
|
if not offset.startswith("0x"):
|
|
offset = f"0x{offset}"
|
|
if display is None:
|
|
display = offset
|
|
return Address(space=space, offset=offset, display=display, file_offset=file_offset)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Enum serialization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def serialize_enum(value: Enum | None) -> str | None:
|
|
"""Serialize an enum member to its UPPER_CASE string name, or null."""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, str):
|
|
return value.upper()
|
|
return str(value.value)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entity serialization (generic)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def entity_to_dict(
|
|
entity: Any,
|
|
canonical_fields: set[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Convert a dataclass entity to a dict using only canonical fields.
|
|
|
|
Args:
|
|
entity: The dataclass entity to serialize.
|
|
canonical_fields: Optional whitelist of field names to include.
|
|
If not provided, all dataclass fields are serialized.
|
|
|
|
Returns:
|
|
A dict with only canonical fields, with proper serialization:
|
|
- Addresses become structured dicts or null
|
|
- Enums become UPPER_CASE strings or null
|
|
- UUIDs become strings
|
|
- None values remain as null
|
|
- Sizes remain as integers (never converted to strings)
|
|
"""
|
|
result: dict[str, Any] = {}
|
|
fields_dict = {f.name: f for f in dataclasses.fields(entity)}
|
|
|
|
for field_name in fields_dict:
|
|
# Skip non-canonical fields if a whitelist is provided
|
|
if canonical_fields is not None and field_name not in canonical_fields:
|
|
continue
|
|
|
|
value = getattr(entity, field_name)
|
|
|
|
# Serialize based on type
|
|
serialized = _serialize_value(value)
|
|
|
|
# Only include optional fields if they have a non-None value,
|
|
# to keep the JSON minimal
|
|
result[field_name] = serialized
|
|
|
|
return result
|
|
|
|
|
|
def _serialize_value(value: Any) -> Any:
|
|
"""Serialize a single value to its JSON-compatible form.
|
|
|
|
Rules:
|
|
- None → None (JSON null)
|
|
- Address → structured dict or None
|
|
- Enum → UPPER_CASE string or None
|
|
- UUID → string
|
|
- list → list of serialized values
|
|
- dict → dict of serialized values
|
|
- booleans → remain booleans
|
|
- integers → remain integers (never strings)
|
|
- floats → remain floats
|
|
- strings → remain strings
|
|
"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, Address):
|
|
return value.to_dict()
|
|
if isinstance(value, Enum):
|
|
return value.value
|
|
if isinstance(value, list):
|
|
return [_serialize_value(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {k: _serialize_value(v) for k, v in value.items()}
|
|
# Primitives pass through as-is
|
|
return value
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Canonical field whitelists per entity type
|
|
# These ensure no backend-specific keys leak into entity objects.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PROJECT_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"id",
|
|
"name",
|
|
"state",
|
|
"created_at",
|
|
"updated_at",
|
|
"workspace_version",
|
|
"binary_count",
|
|
"is_stale",
|
|
"lock",
|
|
"description",
|
|
"max_binary_size_bytes",
|
|
}
|
|
)
|
|
|
|
BINARY_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"id",
|
|
"sha256",
|
|
"path",
|
|
"format",
|
|
"import_mode",
|
|
"size_bytes",
|
|
"architecture",
|
|
"endianness",
|
|
"entry_point",
|
|
"compiler",
|
|
"source_language",
|
|
"imported_at",
|
|
"analyzed_at",
|
|
"analysis_profile",
|
|
"is_stale",
|
|
}
|
|
)
|
|
|
|
SECTION_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"binary_id",
|
|
"address",
|
|
"virtual_size",
|
|
"raw_size",
|
|
"flags",
|
|
"entropy",
|
|
"content_hash",
|
|
}
|
|
)
|
|
|
|
ENTRYPOINT_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"address",
|
|
"kind",
|
|
"confidence",
|
|
"name",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
IMPORT_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"module",
|
|
"symbol",
|
|
"address",
|
|
"resolution",
|
|
"ordinal",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
EXPORT_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"address",
|
|
"ordinal",
|
|
"forwarder",
|
|
"kind",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
SYMBOL_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"address",
|
|
"source",
|
|
"scope",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
STRING_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"text",
|
|
"encoding",
|
|
"address",
|
|
"length",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
FUNCTION_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"address",
|
|
"size_bytes",
|
|
"confidence",
|
|
"name_source",
|
|
"binary_id",
|
|
"is_external",
|
|
"is_thunk",
|
|
"signature",
|
|
"source_language",
|
|
"basic_block_count",
|
|
"instruction_count",
|
|
"cyclomatic_complexity",
|
|
}
|
|
)
|
|
|
|
INSTRUCTION_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"mnemonic",
|
|
"operands",
|
|
"bytes_hex",
|
|
"address",
|
|
"size_bytes",
|
|
"function_id",
|
|
}
|
|
)
|
|
|
|
BASIC_BLOCK_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"start_address",
|
|
"end_address",
|
|
"instruction_count",
|
|
"function_id",
|
|
"is_entry",
|
|
"is_exit",
|
|
}
|
|
)
|
|
|
|
REFERENCE_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"from_addr",
|
|
"to_addr",
|
|
"kind",
|
|
"confidence",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
CALLGRAPH_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"root_address",
|
|
"nodes",
|
|
"edges",
|
|
"max_depth",
|
|
"total_nodes",
|
|
"total_edges",
|
|
"truncated",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
DIAGNOSTIC_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"severity",
|
|
"category",
|
|
"message",
|
|
"component",
|
|
"remediation",
|
|
"recoverable",
|
|
}
|
|
)
|
|
|
|
CAPABILITY_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"confidence",
|
|
"evidence",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
OBSERVATION_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"category",
|
|
"description",
|
|
"source",
|
|
"address",
|
|
"evidence",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
HEURISTIC_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"description",
|
|
"confidence",
|
|
"rule_id",
|
|
"evidence",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
INFERENCE_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"description",
|
|
"confidence",
|
|
"basis",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
UNKNOWN_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"address",
|
|
"question",
|
|
"category",
|
|
"binary_id",
|
|
}
|
|
)
|
|
|
|
REPORT_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"id",
|
|
"report_type",
|
|
"project_id",
|
|
"binary_id",
|
|
"created_at",
|
|
"format",
|
|
"summary",
|
|
"sections",
|
|
}
|
|
)
|
|
|
|
AUDIT_EVENT_CANONICAL_FIELDS = frozenset(
|
|
{
|
|
"timestamp",
|
|
"event_type",
|
|
"result",
|
|
"project_id",
|
|
"binary_id",
|
|
"user",
|
|
"details",
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JSON encoding with correct string escaping
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def safe_json_dumps(obj: Any, indent: int = 2, ensure_ascii: bool = False) -> str:
|
|
"""Serialize to JSON with correct escaping of embedded quotes, backslashes,
|
|
and control characters.
|
|
|
|
Uses json.dumps with ensure_ascii=False (preserving Unicode) unless
|
|
ensure_ascii is explicitly True. The standard library json module
|
|
correctly escapes ", \\, and control characters by default, but we
|
|
document the expected behavior here.
|
|
|
|
Args:
|
|
obj: The object to serialize.
|
|
indent: Indentation level (default 2 spaces).
|
|
ensure_ascii: Whether to escape non-ASCII characters.
|
|
|
|
Returns:
|
|
A valid JSON string.
|
|
|
|
Serialization rules enforced:
|
|
- Double quotes in strings → \\"
|
|
- Backslashes in strings → \\\\
|
|
- Control characters → \\uXXXX
|
|
- Unicode preserved by default (ensure_ascii=False)
|
|
"""
|
|
return json.dumps(obj, indent=indent, ensure_ascii=ensure_ascii)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Serializable entity mixin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class SerializableEntity:
|
|
"""Mixin for entities that need JSON serialization.
|
|
|
|
Subclasses must implement to_dict() and can override _canonical_fields
|
|
to restrict which fields are serialized.
|
|
"""
|
|
|
|
_canonical_fields: frozenset[str] | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Convert to a JSON-compatible dict."""
|
|
if self._canonical_fields is not None:
|
|
return entity_to_dict(self, set(self._canonical_fields))
|
|
return entity_to_dict(self)
|
|
|
|
def to_json(self, indent: int = 2) -> str:
|
|
"""Serialize to JSON string with correct escaping."""
|
|
return safe_json_dumps(self.to_dict(), indent=indent)
|