Parallelize nmap phase in webrunner node scanner

scan_masscan_nmap() and scan_geo_scout() were running nmap serially —
one host at a time with a 60s timeout, turning a 1h estimate into 24h+
for large scans.

Both modes now use ThreadPoolExecutor with 10 workers (configurable via
WEBRUNNER_NMAP_WORKERS env var). geo-scout uses an inner scan_one()
helper to batch nmap + probe_service() per host within the same future.

Also fixed probe_service() bytes format antipattern and removed dead
targets_arg variable from scan_nmap_only().

run_scan.yml converted from command: > folded scalar to command: argv:
list form to prevent argument splitting on space-containing values.
This commit is contained in:
n0mad1k
2026-05-01 11:47:12 -04:00
parent 78da72e9f6
commit 6da2779f5c
2 changed files with 60 additions and 32 deletions
+33 -19
View File
@@ -6,13 +6,16 @@ Reads cidrs.txt, runs scan pipeline, writes results.json.
import argparse import argparse
import json import json
import os
import subprocess import subprocess
import sys import sys
import time import time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
WORKDIR = Path("/root/webrunner") WORKDIR = Path("/root/webrunner")
NMAP_WORKERS = int(os.environ.get("WEBRUNNER_NMAP_WORKERS", "10"))
def log(msg: str): def log(msg: str):
@@ -120,7 +123,7 @@ def probe_service(ip: str, port: int) -> str:
with socket.create_connection((ip, port), timeout=3) as s: with socket.create_connection((ip, port), timeout=3) as s:
s.settimeout(3) s.settimeout(3)
try: try:
s.send(b"HEAD / HTTP/1.0\r\nHost: %b\r\n\r\n" % ip.encode()) s.send(b"HEAD / HTTP/1.0\r\nHost: " + ip.encode() + b"\r\n\r\n")
banner = s.recv(512).decode(errors="replace").strip() banner = s.recv(512).decode(errors="replace").strip()
return banner[:200] return banner[:200]
except Exception: except Exception:
@@ -145,7 +148,6 @@ def scan_nmap_only(ports: str, node_name: str) -> list[dict]:
port_str = ports port_str = ports
out_file = WORKDIR / "nmap_sweep.xml" out_file = WORKDIR / "nmap_sweep.xml"
targets_arg = " ".join(cidrs[:50]) # nmap handles CIDRs natively
cmd = [ cmd = [
"nmap", "-sV", "--version-intensity", "3", "nmap", "-sV", "--version-intensity", "3",
"-p", port_str, "-T4", "--open", "-p", port_str, "-T4", "--open",
@@ -196,17 +198,22 @@ def scan_masscan_nmap(ports: str, rate: int, node_name: str) -> list[dict]:
return [] return []
by_ip = group_hits_by_ip(hits) by_ip = group_hits_by_ip(hits)
log(f"nmap fingerprinting {len(by_ip)} hosts...") log(f"nmap fingerprinting {len(by_ip)} hosts (workers={NMAP_WORKERS})...")
results = [] results = []
for idx, (ip, ip_ports) in enumerate(by_ip.items()): done = 0
nmap_info = run_nmap(ip, ip_ports) with ThreadPoolExecutor(max_workers=NMAP_WORKERS) as pool:
for port in ip_ports: futures = {pool.submit(run_nmap, ip, ip_ports): (ip, ip_ports) for ip, ip_ports in by_ip.items()}
entry: dict = {"ip": ip, "port": port} for future in as_completed(futures):
if port in nmap_info: ip, ip_ports = futures[future]
entry.update(nmap_info[port]) nmap_info = future.result()
results.append(entry) for port in ip_ports:
if (idx + 1) % 50 == 0: entry: dict = {"ip": ip, "port": port}
log(f" nmap: {idx + 1}/{len(by_ip)} hosts done") 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 return results
@@ -231,25 +238,32 @@ def scan_geo_scout(ports: str, rate: int, node_name: str) -> list[dict]:
except Exception: except Exception:
pass pass
results = [] def scan_one(ip: str, ip_ports: list[int]) -> list[dict]:
for idx, (ip, ip_ports) in enumerate(by_ip.items()):
nmap_info = run_nmap(ip, ip_ports) nmap_info = run_nmap(ip, ip_ports)
entries = []
for port in ip_ports: for port in ip_ports:
entry: dict = {"ip": ip, "port": port} entry: dict = {"ip": ip, "port": port}
if port in nmap_info: if port in nmap_info:
entry.update(nmap_info[port]) entry.update(nmap_info[port])
# Banner grab / probe
banner = probe_service(ip, port) banner = probe_service(ip, port)
if banner: if banner:
entry["probe_banner"] = banner entry["probe_banner"] = banner
# Match against target profile
if port in target_ports: if port in target_ports:
t = target_ports[port] t = target_ports[port]
entry["target_name"] = t.get("name", "") entry["target_name"] = t.get("name", "")
entry["target_desc"] = t.get("description", "") entry["target_desc"] = t.get("description", "")
results.append(entry) entries.append(entry)
if (idx + 1) % 50 == 0: return entries
log(f" geo-scout: {idx + 1}/{len(by_ip)} hosts done")
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 return results
+27 -13
View File
@@ -10,12 +10,18 @@
mode: '0644' mode: '0644'
- name: Run scan on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }}) - name: Run scan on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }})
command: > command:
python3 /root/webrunner/node_scanner.py argv:
--mode {{ scan_mode }} - python3
--ports {{ ports_str }} - /root/webrunner/node_scanner.py
--rate {{ masscan_rate }} - --mode
--node-name {{ node_name }} - "{{ scan_mode }}"
- --ports
- "{{ ports_str }}"
- --rate
- "{{ masscan_rate }}"
- --node-name
- "{{ node_name }}"
args: args:
chdir: /root/webrunner chdir: /root/webrunner
register: scan_output register: scan_output
@@ -25,13 +31,21 @@
when: not (use_tor | default(false) | bool) when: not (use_tor | default(false) | bool)
- name: Run scan via Tor on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }}) - name: Run scan via Tor on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }})
command: > command:
proxychains4 -f /etc/proxychains4.conf argv:
python3 /root/webrunner/node_scanner.py - proxychains4
--mode {{ scan_mode }} - -f
--ports {{ ports_str }} - /etc/proxychains4.conf
--rate {{ masscan_rate }} - python3
--node-name {{ node_name }} - /root/webrunner/node_scanner.py
- --mode
- "{{ scan_mode }}"
- --ports
- "{{ ports_str }}"
- --rate
- "{{ masscan_rate }}"
- --node-name
- "{{ node_name }}"
args: args:
chdir: /root/webrunner chdir: /root/webrunner
register: scan_output register: scan_output