98103466d8
Sanitized version of red team infrastructure automation platform. Operational content (implant pipelines, lures, credential capture) replaced with documented stubs. Architecture and infrastructure automation code intact.
118 lines
3.6 KiB
Python
118 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
WEBRUNNER merge — combine per-node results.json files into a unified report.
|
|
Run by Ansible on the controller after all nodes complete.
|
|
"""
|
|
|
|
import argparse
|
|
import ipaddress
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def _build_cidr_map(cc_cidrs: dict) -> list[tuple]:
|
|
result = []
|
|
for cc, cidrs in cc_cidrs.items():
|
|
for cidr in cidrs:
|
|
try:
|
|
net = ipaddress.ip_network(cidr, strict=False)
|
|
result.append((net, cc.upper()))
|
|
except ValueError:
|
|
pass
|
|
result.sort(key=lambda x: x[0].prefixlen, reverse=True)
|
|
return result
|
|
|
|
|
|
def _lookup_country(ip: str, cidr_map: list[tuple]) -> str:
|
|
try:
|
|
addr = ipaddress.ip_address(ip)
|
|
except ValueError:
|
|
return ""
|
|
for net, cc in cidr_map:
|
|
if addr in net:
|
|
return cc
|
|
return ""
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--results-dir", required=True)
|
|
parser.add_argument("--output", required=True)
|
|
parser.add_argument("--deployment-id", required=True)
|
|
parser.add_argument("--cidr-map", default="", help="JSON file mapping country codes to CIDR lists")
|
|
args = parser.parse_args()
|
|
|
|
results_dir = Path(args.results_dir)
|
|
node_files = sorted(results_dir.glob("*_results.json"))
|
|
|
|
if not node_files:
|
|
print(f"No result files found in {results_dir}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
cidr_map: list[tuple] = []
|
|
if args.cidr_map:
|
|
try:
|
|
cc_cidrs = json.loads(Path(args.cidr_map).read_text())
|
|
cidr_map = _build_cidr_map(cc_cidrs)
|
|
except (OSError, json.JSONDecodeError):
|
|
pass
|
|
|
|
merged: dict = {
|
|
"deployment_id": args.deployment_id,
|
|
"nodes": [],
|
|
"total_results": 0,
|
|
"results": [],
|
|
}
|
|
|
|
seen: set[str] = set() # deduplicate by ip:port
|
|
|
|
for f in node_files:
|
|
try:
|
|
data = json.loads(f.read_text())
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f"Skipping {f.name}: {e}", file=sys.stderr)
|
|
continue
|
|
|
|
node_summary = {
|
|
"node_name": data.get("node_name", f.stem),
|
|
"scan_mode": data.get("scan_mode", "unknown"),
|
|
"result_count": data.get("result_count", 0),
|
|
}
|
|
merged["nodes"].append(node_summary)
|
|
|
|
for entry in data.get("results", []):
|
|
key = f"{entry.get('ip')}:{entry.get('port')}"
|
|
if key not in seen:
|
|
seen.add(key)
|
|
if cidr_map:
|
|
entry["country"] = _lookup_country(entry.get("ip", ""), cidr_map)
|
|
merged["results"].append(entry)
|
|
|
|
merged["total_results"] = len(merged["results"])
|
|
|
|
if cidr_map:
|
|
country_counts: dict[str, int] = {}
|
|
for entry in merged["results"]:
|
|
cc = entry.get("country", "")
|
|
country_counts[cc] = country_counts.get(cc, 0) + 1
|
|
merged["country_summary"] = dict(
|
|
sorted(country_counts.items(), key=lambda x: x[1], reverse=True)
|
|
)
|
|
|
|
Path(args.output).write_text(json.dumps(merged, indent=2))
|
|
|
|
print(f"Merged {len(node_files)} node(s) — {merged['total_results']} unique results")
|
|
for n in merged["nodes"]:
|
|
print(f" {n['node_name']}: {n['result_count']} results ({n['scan_mode']})")
|
|
|
|
if cidr_map and merged.get("country_summary"):
|
|
print("\nVulnerable hosts by country:")
|
|
for cc, count in list(merged["country_summary"].items())[:20]:
|
|
label = cc if cc else "unknown"
|
|
print(f" {label:<6} {count}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|