Files
reverse-skills-max/vendor-new/js-reverse-automation--skill/js-reverse-automation/scripts/build_evidence_graph.py

132 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Build a lightweight JSRA evidence graph from probe and module dumps.
Creates nodes (events) and edges (parent-child, fingerprint_flow, temporal)
to correlate producers and consumers via matching SHA-256 fingerprints.
Usage:
python3 scripts/build_evidence_graph.py \
--probe artifacts/probe_dump.json \
--modules artifacts/module_dump.json \
--output artifacts/evidence_graph.json
"""
from __future__ import annotations
import argparse
from collections import defaultdict
from common import dump_json, flatten_events, load_json
def main() -> int:
parser = argparse.ArgumentParser(description="Build a lightweight JSRA evidence graph.")
parser.add_argument("--probe", required=True, help="Path to probe_dump.json.")
parser.add_argument("--modules", required=False, help="Path to module_dump.json.")
parser.add_argument("--output", required=True, help="Path to output evidence_graph.json.")
args = parser.parse_args()
probe = load_json(args.probe, {})
modules = load_json(args.modules, {}) if args.modules else {}
events = flatten_events(probe)
nodes = []
edges = []
by_trace = defaultdict(list)
producers = defaultdict(list)
consumers = defaultdict(list)
for event in events:
event_id = str(event.get("event_id"))
trace_id = str(event.get("trace_id") or "unscoped")
nodes.append({"id": event_id, "kind": "event", "data": event})
by_trace[trace_id].append(event)
parent = event.get("parent_event_id")
if parent:
edges.append({"from": str(parent), "to": event_id, "type": "parent"})
if event.get("output_fingerprint"):
producers[event["output_fingerprint"]].append(event_id)
if event.get("input_fingerprint"):
consumers[event["input_fingerprint"]].append(event_id)
# Fingerprint flow edges (producer -> consumer). A runtime hook often
# creates a new trace id for the network call after the crypto call has
# completed, so requiring equal trace ids loses the most useful edges.
# Pair each consumer with the nearest preceding producer instead. Exact
# fingerprints remain the proof signal; the time relation only disambiguates
# repeated values and prevents future-to-past edges.
event_ids = {str(event.get("event_id")) for event in events}
for fp, source_ids in producers.items():
source_events = [
next((event for event in events if str(event.get("event_id")) == source_id), {})
for source_id in source_ids
if source_id in event_ids
]
for target_id in consumers.get(fp, []):
if target_id not in event_ids:
continue
target_event = next((e for e in events if str(e.get("event_id")) == target_id), {})
target_time = target_event.get("timestamp")
preceding = [
event for event in source_events
if str(event.get("event_id")) != target_id
and (
not isinstance(target_time, (int, float))
or not isinstance(event.get("timestamp"), (int, float))
or event.get("timestamp") <= target_time
)
]
if not preceding:
continue
source_event = max(
preceding,
key=lambda event: event.get("timestamp", 0)
if isinstance(event.get("timestamp"), (int, float)) else 0,
)
source_id = str(source_event.get("event_id"))
source_time = source_event.get("timestamp")
delta = None
if isinstance(source_time, (int, float)) and isinstance(target_time, (int, float)):
delta = max(0, target_time - source_time)
edges.append({
"from": source_id, "to": target_id,
"type": "fingerprint_flow",
"fingerprint": fp,
"trace_id": source_event.get("trace_id"),
"target_trace_id": target_event.get("trace_id"),
"same_trace": source_event.get("trace_id") == target_event.get("trace_id"),
"time_delta_ms": delta,
})
# Temporal edges (within same trace)
for trace_id, trace_events in by_trace.items():
ordered = sorted(trace_events, key=lambda e: e.get("timestamp", 0))
for left, right in zip(ordered, ordered[1:]):
edges.append({
"from": str(left.get("event_id")),
"to": str(right.get("event_id")),
"type": "temporal",
"trace_id": trace_id
})
# Module/globals as candidate source nodes
for item in (modules.get("modules") or []) + (modules.get("globals") or []):
node_id = str(item.get("id") or item.get("path") or item.get("name"))
nodes.append({"id": node_id, "kind": "candidate_source", "data": item})
result = {
"nodes": nodes,
"edges": edges,
"stats": {
"events": len(events),
"traces": len(by_trace),
"fingerprint_flows": sum(1 for e in edges if e["type"] == "fingerprint_flow")
},
}
dump_json(args.output, result)
print(f"[OK] evidence graph: {args.output} ({result['stats']})")
return 0
if __name__ == "__main__":
raise SystemExit(main())