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

260 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""Evidence-driven encryption/signature candidate discovery.
Consumes runtime events, Webpack exports, globals, optional static AST results,
and manual hints. Scores candidates across 8 dimensions and tracks verification
status.
Usage:
python3 scripts/detect_encryption.py \
--probe-artifacts artifacts/probe_dump.json \
--module-artifacts artifacts/module_dump.json \
--evidence-graph artifacts/evidence_graph.json \
--analysis analysis_result.json \
--output artifacts/encryption_candidates.json
"""
from __future__ import annotations
import argparse
import re
from collections import defaultdict
from typing import Any
from common import dump_json, flatten_events, iter_stack_lines, keyword_score, load_json
STACK_FN = re.compile(r"(?:at\s+)?([\w$.[\]<>-]+)\s*(?:\(|@)")
def canonical_id(source: str, name: str, module_id: str = "") -> str:
clean = re.sub(r"[^a-zA-Z0-9_.:$\-]+", "_", name or "anonymous")
return f"{source}:{module_id}:{clean}" if module_id else f"{source}:{clean}"
def ensure_candidate(store: dict[str, dict], cid: str, name: str, source: str, **extra: Any) -> dict:
candidate = store.setdefault(cid, {
"id": cid, "name": name or "anonymous", "source": source, "type": extra.get("type", "function"),
"module_id": extra.get("module_id"), "path": extra.get("path") or name,
"source_snippet": extra.get("source_snippet", ""), "evidence": [], "scores": {},
"verified": False, "verification": [],
})
for key, value in extra.items():
if value not in (None, "", []):
candidate[key] = value
return candidate
def build_candidates(probe: dict, modules: dict, graph: dict, analysis: dict) -> list[dict]:
store: dict[str, dict] = {}
events = flatten_events(probe)
event_by_id = {str(e.get("event_id")): e for e in events}
fingerprint_edges = [e for e in graph.get("edges", []) if e.get("type") == "fingerprint_flow"]
# === Runtime functions and stack frames ===
for event in events:
function = event.get("function") or event.get("name")
event_type = str(event.get("type", ""))
if function and not event_type.startswith("network."):
cid = canonical_id("runtime", str(function))
role = "primitive" if event_type.startswith(("crypto.", "encoding.", "serializer.")) else "entrypoint"
c = ensure_candidate(store, cid, str(function), "runtime", role=role)
c["evidence"].append({
"type": "runtime_event",
"event_id": event.get("event_id"),
"event_type": event.get("type"),
"trace_id": event.get("trace_id")
})
if event.get("output_fingerprint"):
c.setdefault("output_fingerprints", []).append(event["output_fingerprint"])
# Stack frame extraction
for line in iter_stack_lines(event):
match = STACK_FN.search(line)
if match:
name = match.group(1)
if name in ("Error", "record", "wrapped"):
continue
cid = canonical_id("stack", name)
c = ensure_candidate(store, cid, name, "stack")
c["evidence"].append({
"type": "runtime_stack",
"event_id": event.get("event_id"),
"line": line[:300]
})
# === Real Webpack exports ===
for item in modules.get("modules", []) or []:
name = item.get("export_path") or item.get("name") or "anonymous"
cid = canonical_id("webpack", name, str(item.get("module_id", "unknown")))
module_id = str(item.get("module_id", ""))
export_path = item.get("export_path") or name
webpack_source = str(item.get("source") or "default")
runtime_path = f"webpack:{webpack_source}:{module_id}:{export_path}"
ensure_candidate(
store, cid, name, "webpack",
type="webpack_export", module_id=module_id,
path=runtime_path, export_path=export_path,
source_snippet=item.get("source_snippet", "")
)["evidence"].append({"type": "module_export", "source": item.get("source")})
# === Global functions ===
for item in modules.get("globals", []) or []:
name = item.get("name") or item.get("path") or "anonymous"
cid = canonical_id("global", name)
ensure_candidate(
store, cid, name, "global",
path=item.get("path"), source_snippet=item.get("source_snippet", "")
)["evidence"].append({"type": "global_function"})
# === Optional static AST candidates ===
for item in modules.get("static", []) or []:
name = item.get("name") or item.get("path") or "anonymous"
cid = canonical_id("static", name)
ensure_candidate(
store, cid, name, "static",
path=item.get("path") or name,
source_snippet=item.get("source_snippet", ""),
location=item.get("location")
)["evidence"].append({"type": "static_ast", "signals": item.get("signals", [])})
# === Manual hints (backward-compatible, evidence only) ===
for param_name, config in (analysis.get("parameters") or {}).items():
entry = config.get("entrypoint") or {}
name = entry.get("path")
if name:
cid = canonical_id("hint", name)
c = ensure_candidate(
store, cid, name, "hint",
type=entry.get("type", "hint"), path=name,
source_snippet=entry.get("source_hint", "")
)
c["parameter"] = param_name
c["evidence"].append({"type": "manual_hint", "details": entry.get("evidence", [])})
# === Cross-source name correlation ===
normalized = defaultdict(list)
for c in store.values():
normalized[re.sub(r"[^a-z0-9]", "", c["name"].lower())].append(c)
for group in normalized.values():
if len(group) > 1:
sources = sorted({c["source"] for c in group})
for c in group:
c["evidence"].append({"type": "cross_source_name", "sources": sources})
# === Fingerprint flow correlation ===
# The graph builder may correlate across trace ids when the crypto output
# is later serialized into a request field. Promote that relationship to
# candidate evidence so request_correlation is not silently stuck at zero.
graph_flows_by_source: defaultdict[str, list[dict]] = defaultdict(list)
for edge in fingerprint_edges:
source_id = str(edge.get("from") or "")
target_id = str(edge.get("to") or "")
source_event = event_by_id.get(source_id, {})
target_event = event_by_id.get(target_id, {})
target_type = str(target_event.get("type") or "")
if source_id and target_id and target_type.startswith(("network.", "serializer.")):
graph_flows_by_source[source_id].append(edge)
for candidate in store.values():
source_event_ids = {
str(item.get("event_id"))
for item in candidate.get("evidence", [])
if item.get("type") == "runtime_event" and item.get("event_id")
}
flow_edges = [
edge for event_id in source_event_ids
for edge in graph_flows_by_source.get(event_id, [])
]
if flow_edges:
candidate["evidence"].append({
"type": "fingerprint_flow",
"flows": [
{
"from": edge.get("from"),
"to": edge.get("to"),
"fingerprint": edge.get("fingerprint"),
"same_trace": edge.get("same_trace"),
"time_delta_ms": edge.get("time_delta_ms"),
}
for edge in flow_edges[:20]
],
})
candidate["_has_fingerprint_flow"] = bool(flow_edges)
# === Score each candidate ===
for c in store.values():
evidence_types = [e.get("type") for e in c["evidence"]]
runtime_count = evidence_types.count("runtime_event") + evidence_types.count("runtime_stack")
network_correlation = (
1.0 if c["verified"]
else (0.8 if c.get("_has_fingerprint_flow") else (0.5 if runtime_count and any(
str(e.get("event_type", "")).startswith("network") for e in c["evidence"]
) else 0.0))
)
scores = {
"name": keyword_score(c["name"]),
"source_keyword": keyword_score(c.get("source_snippet", "")),
"runtime_stack": min(1.0, runtime_count / 3),
"request_correlation": network_correlation,
"input_output_flow": 1.0 if c["verified"] else (
0.6 if c.get("_has_fingerprint_flow") else (0.3 if c.get("output_fingerprints") else 0.0)
),
"module_export": 1.0 if c["source"] == "webpack" else (0.6 if c["source"] == "global" else 0.0),
"cross_source": 1.0 if "cross_source_name" in evidence_types else 0.0,
"verification": 1.0 if c["verified"] else 0.0,
}
weights = {
"name": .12, "source_keyword": .10, "runtime_stack": .17,
"request_correlation": .16, "input_output_flow": .18,
"module_export": .08, "cross_source": .07, "verification": .12
}
total = round(sum(scores[k] * weights[k] for k in weights), 3)
c["scores"] = scores
c["total_score"] = total
c["confidence"] = (
"high" if c["verified"] and total >= .62
else ("medium" if total >= .38 else "low")
)
c["evidence"] = c["evidence"][:100]
c.pop("_has_fingerprint_flow", None)
return sorted(store.values(), key=lambda x: (x["verified"], x["total_score"]), reverse=True)
def main() -> int:
parser = argparse.ArgumentParser(description="Evidence-driven encryption/signature candidate discovery.")
parser.add_argument("--probe-artifacts", required=True, help="Path to probe_dump.json.")
parser.add_argument("--module-artifacts", help="Path to module_dump.json.")
parser.add_argument("--evidence-graph", help="Path to evidence_graph.json.")
parser.add_argument("--static-artifacts", help="Path to static_candidates.json.")
parser.add_argument("--analysis", help="Path to analysis_result.json.")
parser.add_argument("--output", required=True, help="Path to output candidates JSON.")
args = parser.parse_args()
probe = load_json(args.probe_artifacts, {})
modules = load_json(args.module_artifacts, {}) if args.module_artifacts else {}
graph = load_json(args.evidence_graph, {}) if args.evidence_graph else {}
static_artifacts = load_json(args.static_artifacts, {}) if args.static_artifacts else {}
if static_artifacts:
modules = dict(modules)
modules.setdefault("static", []).extend(static_artifacts.get("candidates", []))
analysis = load_json(args.analysis, {}) if args.analysis else {}
candidates = build_candidates(probe, modules, graph, analysis)
result = {
"candidates": candidates,
"stats": {
"total": len(candidates),
"high": sum(c["confidence"] == "high" for c in candidates),
"medium": sum(c["confidence"] == "medium" for c in candidates),
"low": sum(c["confidence"] == "low" for c in candidates),
"verified": sum(c["verified"] for c in candidates)
}
}
dump_json(args.output, result)
print(f"[OK] candidate discovery: {args.output} {result['stats']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())