Initial public portfolio release

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.
This commit is contained in:
Operator
2026-06-23 16:12:14 -04:00
commit 0799bfbae8
239 changed files with 40012 additions and 0 deletions
@@ -0,0 +1,19 @@
---
# Fetch scan results from node back to controller
- name: Check results exist on {{ node_name }}
stat:
path: /root/webrunner/results.json
register: results_stat
- name: Fetch results from {{ node_name }}
fetch:
src: /root/webrunner/results.json
dest: "{{ results_dir }}/{{ node_name }}_results.json"
flat: true
when: results_stat.stat.exists
- name: Warn if no results from {{ node_name }}
debug:
msg: "No results.json found on {{ node_name }} — scan may have failed"
when: not results_stat.stat.exists
@@ -0,0 +1,98 @@
---
# Configure a WEBRUNNER scan node — install deps, create workspace
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 3600
retries: 3
delay: 10
- name: Install scan tools
apt:
name:
- masscan
- nmap
- python3
- python3-pip
- curl
state: present
retries: 3
delay: 10
- name: Install Tor and proxychains
apt:
name:
- tor
- proxychains4
state: present
retries: 3
delay: 10
when: use_tor | default(false) | bool
- name: Start and enable Tor service
systemd:
name: tor
state: started
enabled: true
when: use_tor | default(false) | bool
- name: Configure proxychains for Tor SOCKS5
copy:
content: |
strict_chain
proxy_dns
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
socks5 127.0.0.1 9050
dest: /etc/proxychains4.conf
mode: '0644'
when: use_tor | default(false) | bool
- name: Wait for Tor to establish circuit
wait_for:
port: 9050
host: 127.0.0.1
timeout: 60
delay: 5
when: use_tor | default(false) | bool
- name: Create webrunner workspace
file:
path: /root/webrunner
state: directory
mode: '0700'
- name: Copy node scanner script
copy:
src: "{{ playbook_dir }}/../modules/webrunner/tasks/node_scanner.py"
dest: /root/webrunner/node_scanner.py
mode: '0755'
- name: Copy targets.yaml for geo-scout mode
copy:
src: "{{ targets_file }}"
dest: /root/webrunner/targets.yaml
mode: '0644'
when: scan_mode == 'geo-scout' and targets_file != ""
ignore_errors: true
- name: Install nuclei vulnerability scanner
shell: |
if ! command -v nuclei &>/dev/null; then
curl -sL "https://github.com/projectdiscovery/nuclei/releases/download/v3.3.9/nuclei_3.3.9_linux_amd64.tar.gz" | tar -xz -C /usr/local/bin nuclei
chmod +x /usr/local/bin/nuclei
fi
args:
executable: /bin/bash
when: scan_mode == 'masscan+nuclei'
retries: 2
delay: 5
- name: Upload nuclei template
copy:
src: "{{ nuclei_template_local }}"
dest: /root/webrunner/nuclei_template.yaml
mode: '0644'
when: scan_mode == 'masscan+nuclei' and nuclei_template_local | default('') != ''
+117
View File
@@ -0,0 +1,117 @@
#!/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()
+416
View File
@@ -0,0 +1,416 @@
#!/usr/bin/env python3
"""
WEBRUNNER node scanner — runs on each cloud node.
Reads cidrs.txt, runs scan pipeline, writes results.json.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
WORKDIR = Path("/root/webrunner")
NMAP_WORKERS = int(os.environ.get("WEBRUNNER_NMAP_WORKERS", "10"))
NMAP_TIMING = int(os.environ.get("WEBRUNNER_NMAP_TIMING", "4"))
NMAP_TIMEOUT = int(os.environ.get("WEBRUNNER_NMAP_TIMEOUT", "60"))
def log(msg: str):
ts = time.strftime("%H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def run_masscan(ports: str, rate: int) -> list[dict]:
cidr_file = WORKDIR / "cidrs.txt"
out_file = WORKDIR / "masscan.json"
out_file.unlink(missing_ok=True)
cmd = [
"masscan",
f"--rate={rate}",
f"--ports={ports}",
"-iL", str(cidr_file),
"-oJ", str(out_file),
"--wait", "3",
]
log(f"masscan starting: rate={rate} ports={ports}")
try:
subprocess.run(cmd, timeout=36000, check=False)
except subprocess.TimeoutExpired:
log("masscan timed out after 10h")
if not out_file.exists():
return []
raw = out_file.read_text(errors="replace").strip()
if not raw or raw == "[]":
return []
raw = raw.rstrip(",\n")
if not raw.endswith("]"):
raw += "]"
if not raw.startswith("["):
raw = "[" + raw
try:
data = json.loads(raw)
except json.JSONDecodeError:
log("masscan JSON parse error")
return []
hits = []
for entry in data:
ip = entry.get("ip")
for pe in entry.get("ports", []):
port = pe.get("port")
if ip and port:
hits.append({"ip": ip, "port": port})
log(f"masscan found {len(hits)} open port/host pairs")
return hits
def group_hits_by_ip(hits: list[dict]) -> dict[str, list[int]]:
result: dict[str, list[int]] = {}
for h in hits:
result.setdefault(h["ip"], []).append(h["port"])
return result
def run_nmap(ip: str, ports: list[int]) -> dict:
port_str = ",".join(str(p) for p in sorted(set(ports)))
out_file = WORKDIR / f"nmap_{ip.replace('.', '_')}.xml"
cmd = [
"nmap", "-sV", "--version-intensity", "5",
"-p", port_str, f"-T{NMAP_TIMING}", "--open",
"-oX", str(out_file), ip,
]
try:
subprocess.run(cmd, capture_output=True, timeout=NMAP_TIMEOUT, check=False)
except (subprocess.TimeoutExpired, FileNotFoundError):
return {}
if not out_file.exists():
return {}
try:
tree = ET.parse(out_file)
except ET.ParseError:
return {}
result = {}
for port_el in tree.findall(".//port"):
portid = int(port_el.get("portid", 0))
state = port_el.find("state")
if state is None or state.get("state") != "open":
continue
svc = port_el.find("service")
info: dict = {}
if svc is not None:
info["service"] = svc.get("name", "")
info["product"] = svc.get("product", "")
info["version"] = svc.get("version", "")
info["banner"] = " ".join(filter(None, [
svc.get("product", ""), svc.get("version", ""), svc.get("extrainfo", "")
]))
result[portid] = info
return result
def probe_service(ip: str, port: int) -> str:
import socket
try:
with socket.create_connection((ip, port), timeout=3) as s:
s.settimeout(3)
try:
s.send(b"HEAD / HTTP/1.0\r\nHost: " + ip.encode() + b"\r\n\r\n")
banner = s.recv(512).decode(errors="replace").strip()
return banner[:200]
except Exception:
try:
banner = s.recv(512).decode(errors="replace").strip()
return banner[:200]
except Exception:
return ""
except Exception:
return ""
def scan_masscan_only(ports: str, rate: int, node_name: str) -> list[dict]:
hits = run_masscan(ports, rate)
results = [{"ip": h["ip"], "port": h["port"]} for h in hits]
return results
def scan_nmap_only(ports: str, node_name: str) -> list[dict]:
cidr_file = WORKDIR / "cidrs.txt"
cidrs = [l.strip() for l in cidr_file.read_text().splitlines() if l.strip()]
port_str = ports
out_file = WORKDIR / "nmap_sweep.xml"
cmd = [
"nmap", "-sV", "--version-intensity", "3",
"-p", port_str, "-T4", "--open",
"-oX", str(out_file),
] + cidrs
log(f"nmap starting across {len(cidrs)} CIDRs")
try:
subprocess.run(cmd, timeout=36000, check=False)
except subprocess.TimeoutExpired:
log("nmap timed out")
if not out_file.exists():
return []
results = []
try:
tree = ET.parse(out_file)
except ET.ParseError:
return []
for host_el in tree.findall(".//host"):
addr_el = host_el.find("address[@addrtype='ipv4']")
if addr_el is None:
continue
ip = addr_el.get("addr", "")
for port_el in host_el.findall(".//port"):
state = port_el.find("state")
if state is None or state.get("state") != "open":
continue
portid = int(port_el.get("portid", 0))
svc = port_el.find("service")
entry: dict = {"ip": ip, "port": portid}
if svc is not None:
entry["service"] = svc.get("name", "")
entry["banner"] = " ".join(filter(None, [
svc.get("product", ""), svc.get("version", ""), svc.get("extrainfo", "")
]))
results.append(entry)
log(f"nmap found {len(results)} open ports")
return results
def scan_masscan_nmap(ports: str, rate: int, node_name: str) -> list[dict]:
hits = run_masscan(ports, rate)
if not hits:
return []
by_ip = group_hits_by_ip(hits)
log(f"nmap fingerprinting {len(by_ip)} hosts (workers={NMAP_WORKERS})...")
results = []
done = 0
with ThreadPoolExecutor(max_workers=NMAP_WORKERS) as pool:
futures = {pool.submit(run_nmap, ip, ip_ports): (ip, ip_ports) for ip, ip_ports in by_ip.items()}
for future in as_completed(futures):
ip, ip_ports = futures[future]
nmap_info = future.result()
for port in ip_ports:
entry: dict = {"ip": ip, "port": port}
if port in nmap_info:
entry.update(nmap_info[port])
results.append(entry)
done += 1
if done % 50 == 0:
log(f" nmap: {done}/{len(by_ip)} hosts done")
return results
def scan_geo_scout(ports: str, rate: int, node_name: str) -> list[dict]:
hits = run_masscan(ports, rate)
if not hits:
return []
by_ip = group_hits_by_ip(hits)
log(f"nmap + probe fingerprinting {len(by_ip)} hosts...")
targets_file = WORKDIR / "targets.yaml"
target_ports: dict[int, dict] = {}
if targets_file.exists():
try:
import yaml
with open(targets_file) as f:
tdata = yaml.safe_load(f)
for t in tdata.get("targets", []):
for p in t.get("ports", []):
target_ports[p] = t
except Exception:
pass
def scan_one(ip: str, ip_ports: list[int]) -> list[dict]:
nmap_info = run_nmap(ip, ip_ports)
entries = []
for port in ip_ports:
entry: dict = {"ip": ip, "port": port}
if port in nmap_info:
entry.update(nmap_info[port])
banner = probe_service(ip, port)
if banner:
entry["probe_banner"] = banner
if port in target_ports:
t = target_ports[port]
entry["target_name"] = t.get("name", "")
entry["target_desc"] = t.get("description", "")
entries.append(entry)
return entries
results = []
done = 0
with ThreadPoolExecutor(max_workers=NMAP_WORKERS) as pool:
futures = {pool.submit(scan_one, ip, ip_ports): ip for ip, ip_ports in by_ip.items()}
for future in as_completed(futures):
results.extend(future.result())
done += 1
if done % 50 == 0:
log(f" geo-scout: {done}/{len(by_ip)} hosts done")
return results
def run_nuclei(targets: list[str], template: str, rate: int, concurrency: int, timeout: int) -> list[dict]:
targets_file = WORKDIR / "nuclei_targets.txt"
targets_file.write_text("\n".join(targets) + "\n")
out_file = WORKDIR / "nuclei_results.jsonl"
out_file.unlink(missing_ok=True)
cmd = [
"nuclei",
"-t", template,
"-l", str(targets_file),
"-rl", str(rate),
"-c", str(concurrency),
"-timeout", str(timeout),
"-j",
"-o", str(out_file),
"-silent",
"-no-color",
"-disable-update-check",
]
log(f"nuclei starting: template={template} targets={len(targets)} rate={rate} concurrency={concurrency}")
try:
subprocess.run(cmd, timeout=43200, check=False)
except subprocess.TimeoutExpired:
log("nuclei timed out after 12h")
if not out_file.exists():
return []
results = []
for line in out_file.read_text(errors="replace").splitlines():
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
host = entry.get("host", "")
ip = entry.get("ip", "")
if not ip and host:
raw = host.split("//")[-1].split("/")[0]
ip_part = raw.rsplit(":", 1)[0] if ":" in raw else raw
m = re.match(r"^(\d+\.\d+\.\d+\.\d+)$", ip_part)
ip = m.group(1) if m else ip_part
port = 0
matched_at = entry.get("matched-at", host)
raw_host = matched_at.split("//")[-1].split("/")[0]
if ":" in raw_host:
try:
port = int(raw_host.rsplit(":", 1)[1])
except (ValueError, IndexError):
pass
results.append({
"ip": ip,
"port": port,
"template_id": entry.get("template-id", ""),
"vuln_name": entry.get("info", {}).get("name", ""),
"severity": entry.get("info", {}).get("severity", ""),
"matched_at": entry.get("matched-at", ""),
"vuln": True,
})
log(f"nuclei found {len(results)} vulnerable hosts/services")
return results
def scan_masscan_nuclei(ports: str, rate: int, template: str, nuclei_rate: int, nuclei_concurrency: int, nuclei_timeout: int, node_name: str) -> list[dict]:
if not template:
log("ERROR: --template required for masscan+nuclei mode")
return []
hits = run_masscan(ports, rate)
if not hits:
return []
targets = [f"{h['ip']}:{h['port']}" for h in hits]
log(f"nuclei scanning {len(targets)} ip:port targets from masscan...")
return run_nuclei(targets, template, nuclei_rate, nuclei_concurrency, nuclei_timeout)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", required=True,
choices=["masscan-only", "nmap-only", "masscan+nmap", "geo-scout", "masscan+nuclei"])
parser.add_argument("--ports", required=True)
parser.add_argument("--rate", type=int, default=3000)
parser.add_argument("--node-name", default="node")
parser.add_argument("--template", default="")
parser.add_argument("--nmap-timing", type=int, default=None, choices=[1, 2, 3, 4])
parser.add_argument("--nmap-timeout", type=int, default=None)
parser.add_argument("--nmap-workers", type=int, default=None)
parser.add_argument("--nuclei-rate", type=int, default=150)
parser.add_argument("--nuclei-concurrency", type=int, default=25)
parser.add_argument("--nuclei-timeout", type=int, default=10)
args = parser.parse_args()
global NMAP_WORKERS, NMAP_TIMING, NMAP_TIMEOUT
if args.nmap_workers is not None:
NMAP_WORKERS = args.nmap_workers
if args.nmap_timing is not None:
NMAP_TIMING = args.nmap_timing
if args.nmap_timeout is not None:
NMAP_TIMEOUT = args.nmap_timeout
log(f"WEBRUNNER node scanner starting — mode={args.mode} node={args.node_name}")
if args.mode == "masscan-only":
results = scan_masscan_only(args.ports, args.rate, args.node_name)
elif args.mode == "nmap-only":
results = scan_nmap_only(args.ports, args.node_name)
elif args.mode == "masscan+nmap":
results = scan_masscan_nmap(args.ports, args.rate, args.node_name)
elif args.mode == "geo-scout":
results = scan_geo_scout(args.ports, args.rate, args.node_name)
elif args.mode == "masscan+nuclei":
results = scan_masscan_nuclei(
args.ports, args.rate, args.template,
args.nuclei_rate, args.nuclei_concurrency, args.nuclei_timeout,
args.node_name,
)
else:
log(f"Unknown mode: {args.mode}")
sys.exit(1)
out = {
"node_name": args.node_name,
"scan_mode": args.mode,
"result_count": len(results),
"results": results,
}
out_file = WORKDIR / "results.json"
out_file.write_text(json.dumps(out, indent=2))
log(f"Done — {len(results)} results written to {out_file}")
if __name__ == "__main__":
main()
+88
View File
@@ -0,0 +1,88 @@
---
# Run the assigned scan on this WEBRUNNER node
# Per-node vars: node_name, node_cidrs, node_ip_count, node_idx
# Global vars (from extra-vars): scan_mode, ports_str, masscan_rate, webrunner_name, deployment_id, use_tor
- name: Write CIDR list for {{ node_name }}
copy:
content: "{{ node_cidrs | join('\n') }}\n"
dest: /root/webrunner/cidrs.txt
mode: '0644'
- name: Run scan on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }})
command:
argv:
- python3
- /root/webrunner/node_scanner.py
- --mode
- "{{ scan_mode }}"
- --ports
- "{{ ports_str }}"
- --rate
- "{{ masscan_rate }}"
- --node-name
- "{{ node_name }}"
- --nmap-timing
- "{{ nmap_timing | default(4) }}"
- --nmap-timeout
- "{{ nmap_timeout | default(60) }}"
- --nmap-workers
- "{{ nmap_workers | default(10) }}"
- --nuclei-rate
- "{{ nuclei_rate | default(150) }}"
- --nuclei-concurrency
- "{{ nuclei_concurrency | default(25) }}"
- --nuclei-timeout
- "{{ nuclei_timeout | default(10) }}"
- --template
- "{{ nuclei_template_remote | default('') }}"
args:
chdir: /root/webrunner
register: scan_output
async: 43200
poll: 60
ignore_errors: true
when: not (use_tor | default(false) | bool)
- name: Run scan via Tor on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }})
command:
argv:
- proxychains4
- -f
- /etc/proxychains4.conf
- python3
- /root/webrunner/node_scanner.py
- --mode
- "{{ scan_mode }}"
- --ports
- "{{ ports_str }}"
- --rate
- "{{ masscan_rate }}"
- --node-name
- "{{ node_name }}"
- --nmap-timing
- "{{ nmap_timing | default(4) }}"
- --nmap-timeout
- "{{ nmap_timeout | default(60) }}"
- --nmap-workers
- "{{ nmap_workers | default(10) }}"
- --nuclei-rate
- "{{ nuclei_rate | default(150) }}"
- --nuclei-concurrency
- "{{ nuclei_concurrency | default(25) }}"
- --nuclei-timeout
- "{{ nuclei_timeout | default(10) }}"
- --template
- "{{ nuclei_template_remote | default('') }}"
args:
chdir: /root/webrunner
register: scan_output
async: 43200
poll: 60
ignore_errors: true
when: use_tor | default(false) | bool
- name: Show scan output for {{ node_name }}
debug:
var: scan_output.stdout_lines
when: scan_output.stdout_lines is defined