README / ghidra-re-loop /progress.py
Nekochu's picture
Add skill ghidra-re-loop
ae4930d verified
Raw
History Blame Contribute Delete
5.8 kB
#!/usr/bin/env python3
"""Session / progress tracker — adapted port of auto-re-agent's core/session.py,
reports/tracker.py, and reports/formatter.py. JSON-backed, append-only, atomic
writes. Mirrors the original's REPORT format so runs are resumable and progress
claims are auditable against recorded tool evidence (never the model's memory).
A "function" record = one analysis target (a gate/feature/function) the loop has
attempted, keyed by normalized address. Record after EACH attempt so a crash or
new session resumes where it left off.
Usage:
python progress.py record --file re-progress.json --address 0x401abc \
--name "CLicense::Validate" --component license \
--success true --rounds 2 --verdict PASS --parity GREEN \
--objective PASS --note "jz at 0x401ad0 gates the dialog"
python progress.py summary --file re-progress.json
python progress.py table --file re-progress.json [--component license]
python progress.py attempted --file re-progress.json --address 0x401abc # exit 0 if attempted
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
def norm_addr(a: str) -> str:
a = a.strip().lower()
if a.startswith("0x"):
try:
return hex(int(a, 16))
except ValueError:
return a
return a
class Session:
def __init__(self, path: str | Path):
self.path = Path(path)
self.data = {"functions": {}, "runs": []}
if self.path.exists():
try:
self.data = json.loads(self.path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
pass
self.data.setdefault("functions", {})
self.data.setdefault("runs", [])
def save(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp = self.path.with_suffix(".tmp")
tmp.write_text(json.dumps(self.data, indent=2), encoding="utf-8")
tmp.replace(self.path)
def record(self, entry: dict):
addr = norm_addr(entry["address"])
entry["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S")
self.data["functions"][addr] = entry
self.data["runs"].append(entry)
self.save()
def is_attempted(self, address: str) -> bool:
return norm_addr(address) in self.data["functions"]
def summary(self) -> dict:
funcs = self.data["functions"].values()
total = len(self.data["functions"])
passed = sum(1 for f in funcs if f.get("success"))
comps = {f.get("component", "") for f in funcs if f.get("component")}
return {"total": total, "passed": passed, "failed": total - passed,
"components": len(comps)}
def table(self, component: str | None):
rows = []
for f in self.data["functions"].values():
if component and f.get("component") != component:
continue
rows.append(f)
return rows
def _b(v: str | None) -> bool:
return str(v).lower() in {"1", "true", "yes", "pass", "y"}
def cmd_record(a):
s = Session(a.file)
s.record({
"address": a.address,
"function_name": a.name or "",
"component": a.component or "",
"success": _b(a.success),
"rounds_used": a.rounds,
"verdict": a.verdict,
"objective_verdict": a.objective,
"parity_status": a.parity,
"note": a.note or "",
})
print(f"recorded {norm_addr(a.address)} ({'PASS' if _b(a.success) else 'FAIL'})")
return 0
def cmd_summary(a):
s = Session(a.file)
su = s.summary()
print("RE Progress Summary")
print("=" * 40)
print(f"Total targets: {su['total']}")
print(f"Passed: {su['passed']}")
print(f"Failed: {su['failed']}")
print(f"Components: {su['components']}")
return 0
def cmd_table(a):
s = Session(a.file)
rows = s.table(a.component)
print("| Address | Function | Comp | Status | Rounds | Parity | Obj |")
print("|---------|----------|------|--------|--------|--------|-----|")
for f in rows:
st = "PASS" if f.get("success") else "FAIL"
print(f"| {f.get('address','')} | {f.get('function_name','')} | "
f"{f.get('component','')} | {st} | {f.get('rounds_used','')} | "
f"{f.get('parity_status') or '-'} | {f.get('objective_verdict') or '-'} |")
return 0
def cmd_attempted(a):
return 0 if Session(a.file).is_attempted(a.address) else 1
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
r = sub.add_parser("record")
r.add_argument("--file", required=True)
r.add_argument("--address", required=True)
r.add_argument("--name", default="")
r.add_argument("--component", default="")
r.add_argument("--success", default="false")
r.add_argument("--rounds", type=int, default=0)
r.add_argument("--verdict", default=None)
r.add_argument("--objective", default=None)
r.add_argument("--parity", default=None)
r.add_argument("--note", default="")
r.set_defaults(fn=cmd_record)
for name, fn in (("summary", cmd_summary), ("table", cmd_table)):
p = sub.add_parser(name)
p.add_argument("--file", required=True)
if name == "table":
p.add_argument("--component", default=None)
p.set_defaults(fn=fn)
at = sub.add_parser("attempted")
at.add_argument("--file", required=True)
at.add_argument("--address", required=True)
at.set_defaults(fn=cmd_attempted)
a = ap.parse_args()
return a.fn(a)
if __name__ == "__main__":
sys.exit(main())