#!/usr/bin/env python3 """Convert peek-a-bin ExportSchemaV1 JSON into markdown reports or CSV. Reads an analysis export produced by peek-a-bin's ``export_analysis`` MCP tool and converts it into a human-readable markdown report or a flat CSV suitable for spreadsheet analysis. """ from __future__ import annotations import argparse import csv import io import json import sys from pathlib import Path from typing import Any # --------------------------------------------------------------------------- # Loaders # --------------------------------------------------------------------------- def load_export(input_path: Path) -> dict[str, Any]: """Load and validate an ExportSchemaV1 JSON file.""" data = json.loads(input_path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise ValueError("Expected a JSON object at the top level") return data # --------------------------------------------------------------------------- # Markdown formatter # --------------------------------------------------------------------------- def to_markdown(data: dict[str, Any]) -> str: """Render the export as a structured markdown report.""" lines: list[str] = [] file_name = data.get("fileName", "unknown") exported_at = data.get("exportedAt", "unknown") lines.append(f"# PE Analysis Report: {file_name}") lines.append("") lines.append(f"**Exported:** {exported_at} ") lines.append(f"**Schema version:** {data.get('version', '?')}") lines.append("") # Bookmarks bookmarks = data.get("bookmarks", []) if bookmarks: lines.append("## Bookmarks") lines.append("") lines.append("| Address | Label |") lines.append("|---------|-------|") for bm in bookmarks: addr = bm.get("address", "?") label = bm.get("label", "") lines.append(f"| `{addr}` | {label} |") lines.append("") # Renames renames = data.get("renames", []) if renames: lines.append("## Function Renames") lines.append("") lines.append("| Address | Name |") lines.append("|---------|------|") for rn in renames: addr = rn.get("address", "?") name = rn.get("name", "") lines.append(f"| `{addr}` | `{name}` |") lines.append("") # Comments comments = data.get("comments", []) if comments: lines.append("## Comments") lines.append("") lines.append("| Address | Comment |") lines.append("|---------|---------|") for cm in comments: addr = cm.get("address", "?") text = cm.get("text", "").replace("|", "\\|") lines.append(f"| `{addr}` | {text} |") lines.append("") # Functions functions = data.get("functions", []) if functions: lines.append("## Functions") lines.append("") lines.append("| Address | Name | Size |") lines.append("|---------|------|------|") for fn in functions: addr = fn.get("address", "?") name = fn.get("name", "unknown") size = fn.get("size", "?") lines.append(f"| `{addr}` | `{name}` | {size} |") lines.append("") # Hex patches patches = data.get("hexPatches", []) if patches: lines.append("## Hex Patches") lines.append("") lines.append("| Address | Original | Patched |") lines.append("|---------|----------|---------|") for p in patches: addr = p.get("address", "?") orig = p.get("original", "") patched = p.get("patched", "") lines.append(f"| `{addr}` | `{orig}` | `{patched}` |") lines.append("") # Summary lines.append("## Summary") lines.append("") lines.append(f"- **Bookmarks:** {len(bookmarks)}") lines.append(f"- **Renames:** {len(renames)}") lines.append(f"- **Comments:** {len(comments)}") lines.append(f"- **Functions:** {len(functions)}") lines.append(f"- **Hex patches:** {len(patches)}") lines.append("") return "\n".join(lines) # --------------------------------------------------------------------------- # CSV formatter # --------------------------------------------------------------------------- def to_csv(data: dict[str, Any]) -> str: """Flatten all annotations into a single CSV table.""" buf = io.StringIO() writer = csv.writer(buf) writer.writerow(["type", "address", "value", "extra"]) for bm in data.get("bookmarks", []): writer.writerow(["bookmark", bm.get("address", ""), bm.get("label", ""), ""]) for rn in data.get("renames", []): writer.writerow(["rename", rn.get("address", ""), rn.get("name", ""), ""]) for cm in data.get("comments", []): writer.writerow(["comment", cm.get("address", ""), cm.get("text", ""), ""]) for fn in data.get("functions", []): writer.writerow([ "function", fn.get("address", ""), fn.get("name", ""), f"size={fn.get('size', '?')}", ]) for p in data.get("hexPatches", []): writer.writerow([ "hex_patch", p.get("address", ""), p.get("patched", ""), f"original={p.get('original', '')}", ]) return buf.getvalue() # --------------------------------------------------------------------------- # Dispatch # --------------------------------------------------------------------------- FORMATTERS = { "json": lambda data: json.dumps(data, indent=2), "markdown": to_markdown, "csv": to_csv, } def convert(input_path: Path, output_format: str) -> str: """Load an export file and convert it to the requested format.""" data = load_export(input_path) formatter = FORMATTERS[output_format] return formatter(data) # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser( description="Convert peek-a-bin ExportSchemaV1 JSON to other formats." ) parser.add_argument( "--input", type=Path, required=True, help="Path to ExportSchemaV1 JSON file", ) parser.add_argument( "--output", type=Path, default=None, help="Path to write converted output (stdout if omitted)", ) parser.add_argument( "--format", default="markdown", choices=["json", "markdown", "csv"], help="Output format (default: markdown)", ) args = parser.parse_args() if not args.input.exists(): print(f"Error: input file not found: {args.input}", file=sys.stderr) sys.exit(1) result = convert(args.input, args.format) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(result, encoding="utf-8") print(f"Output written to {args.output}", file=sys.stderr) else: print(result) if __name__ == "__main__": main()