Add 'annotate-usecode' command to import USECODE IR JSON annotations

- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors.
- Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option.
- Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra.
- Enhanced JSON schema to include response structure for the new command.
This commit is contained in:
MaddoScientisto 2026-03-24 18:14:20 +01:00
commit daa363c3d2
39 changed files with 41450 additions and 871 deletions

View file

@ -214,6 +214,15 @@ JSON_SCHEMAS = {
"list-classes": {"type": "array", "items": CLASS_SCHEMA},
"run-script": STATUS_SCHEMA,
"apply-plan": STATUS_SCHEMA,
"annotate-usecode": {
"type": "object",
"required": ["annotated", "skipped", "files"],
"properties": {
"annotated": {"type": "integer"},
"skipped": {"type": "integer"},
"files": {"type": "integer"},
},
},
}
@ -523,6 +532,31 @@ def build_parser() -> argparse.ArgumentParser:
help="Validate and print the plan without modifying the project.",
)
annotate_usecode_parser = subparsers.add_parser(
"annotate-usecode",
aliases=["annotate_usecode"],
help="Import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors.",
)
annotate_usecode_parser.add_argument(
"--ir-file",
dest="ir_files",
metavar="IR_FILE",
action="append",
required=True,
help="Path to an IR JSON file produced by poc_crusader_usecode_parser.py. May be given multiple times.",
)
annotate_usecode_parser.add_argument(
"--comment-type",
choices=["pre", "plate", "eol", "repeatable", "post"],
default="plate",
help="Ghidra comment type to use for anchor annotations (default: plate).",
)
annotate_usecode_parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be annotated without modifying the project.",
)
return parser
@ -947,6 +981,67 @@ def _print_plan(plan: dict) -> None:
print(json.dumps(plan, indent=2, sort_keys=True))
def command_annotate_usecode(config: ProjectConfig, args: argparse.Namespace) -> int:
"""Import USECODE IR JSON files and set Ghidra comments on compiled anchor addresses."""
import json as _json
# Collect all annotation records from every IR file.
pending: list[tuple[str, str]] = [] # (address, comment_text)
for ir_path_str in args.ir_files:
ir_path = Path(ir_path_str).resolve()
if not ir_path.is_file():
raise RuntimeError(f"IR file not found: {ir_path}")
ir = _json.loads(ir_path.read_text(encoding="utf-8"))
cls = ir.get("class", {})
evt = ir.get("event", {})
hints = ir.get("annotation_hints", {})
class_name = cls.get("class_name", "?")
slot = evt.get("slot", 0)
event_name_hint = evt.get("event_name_hint") or f"slot_0x{slot:02X}"
body_start = evt.get("derived_body_start", 0)
body_end = evt.get("derived_body_end", 0)
raw_word = evt.get("raw_event_entry_word", 0)
for anchor in hints.get("compiled_anchors", []):
address = anchor.get("address", "")
role = anchor.get("role", "")
if not address:
continue
comment = (
f"POC USECODE: {class_name} slot=0x{slot:02X} [{event_name_hint}]"
f" body=0x{body_start:04X}..0x{body_end:04X}"
f" raw_word=0x{raw_word:04X} | {role}"
)
pending.append((address, comment))
if args.dry_run:
for address, comment in pending:
print(f"DRY-RUN {address} {comment}")
return _emit(
args,
{"annotated": 0, "skipped": len(pending), "files": len(args.ir_files)},
f"dry-run: {len(pending)} annotations would be written from {len(args.ir_files)} file(s)",
)
annotated = 0
skipped = 0
with open_program(config, read_only=False) as (project, program):
with transaction(program, "USECODE annotation import"):
for address, comment in pending:
try:
set_comment(program, address, comment, args.comment_type)
annotated += 1
except Exception as exc:
print(f"SKIP {address}: {exc}")
skipped += 1
save_program(project, program)
return _emit(
args,
{"annotated": annotated, "skipped": skipped, "files": len(args.ir_files)},
f"annotated {annotated} anchors ({skipped} skipped) from {len(args.ir_files)} file(s)",
)
def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int:
plan = _load_plan(args.plan)
if args.dry_run:
@ -1037,6 +1132,7 @@ def main(argv: list[str] | None = None) -> int:
"get-function-xrefs": command_get_function_xrefs,
"run-script": command_run_script,
"apply-plan": command_apply_plan,
"annotate-usecode": command_annotate_usecode,
}
return command_map[args.command](config, args)