Add masscan+nuclei mode, operator tuning controls, and vars file support to webrunner

- New scan mode: masscan+nuclei — masscan finds open ip:port pairs,
  nuclei runs operator-supplied CVE template against discovered hosts
- Per-country vulnerable host count in merge output via CIDR→country lookup
- Tuning args on every scan: --nmap-timing, --nmap-timeout, --nmap-workers,
  --nuclei-rate, --nuclei-concurrency, --nuclei-timeout, --masscan-rate
- Vars file support: operator provides scan_vars.yaml to pre-fill all
  tuning settings without interactive prompts
- Templates copied to nodes at provision time — no live fetches (OPSEC)
- run_scan.yml passes all tuning args with Ansible defaults as fallback
- configure_node.yml installs nuclei binary + uploads template on demand
- Updated deployment summary shows mode-specific tuning params
- countries-format.md and targets-format.md updated for new capabilities
- scan_vars.yaml.example documents all configurable settings with comments
This commit is contained in:
n0mad1k
2026-05-02 14:49:24 -04:00
parent c34f9f0401
commit acb32ab3c6
9 changed files with 406 additions and 13 deletions
+104
View File
@@ -112,6 +112,24 @@ def _select_scan_mode() -> str:
return 'geo-scout'
def _load_vars_file(path: str) -> dict:
if not path:
return {}
p = Path(path)
if not p.exists():
print(f"{COLORS['YELLOW']} Vars file not found: {path} — continuing interactively{COLORS['RESET']}")
return {}
import yaml
try:
with open(p) as f:
data = yaml.safe_load(f) or {}
print(f"{COLORS['GREEN']} Loaded vars: {p.name}{COLORS['RESET']}")
return data
except Exception as e:
print(f"{COLORS['YELLOW']} Could not parse vars file ({e}) — continuing interactively{COLORS['RESET']}")
return {}
def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]:
if scan_mode == 'geo-scout':
default = str(WEBRUNNER_INPUTS / 'targets.yaml')
@@ -141,6 +159,55 @@ def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]:
return "", sorted(set(ports_list)) or [80, 443, 22]
def _get_nuclei_info(vars_overrides: dict) -> tuple[str, str]:
"""Returns (local_template_path, remote_template_path)."""
print(f"\n{COLORS['BLUE']}Nuclei Template:{COLORS['RESET']}")
if 'nuclei_template' in vars_overrides:
local_path = vars_overrides['nuclei_template']
print(f" Template: {local_path} (from vars file)")
else:
local_path = input(" Path to nuclei template (.yaml): ").strip()
if not local_path or not Path(local_path).exists():
print(f"{COLORS['RED']} Template not found: {local_path}{COLORS['RESET']}")
return "", ""
return local_path, "/root/webrunner/nuclei_template.yaml"
def _get_tuning_params(scan_mode: str, default_rate: int, vars_overrides: dict) -> dict:
"""Gather advanced tuning params. Returns dict of tuning config keys."""
tuning: dict = {}
show_header = True
def _prompt(label: str, key: str, default, cast=int) -> None:
nonlocal show_header
if show_header:
print(f"\n{COLORS['BLUE']}Advanced Tuning (Enter = use default):{COLORS['RESET']}")
show_header = False
if key in vars_overrides:
tuning[key] = cast(vars_overrides[key])
print(f" {label}: {tuning[key]} (vars file)")
else:
raw = input(f" {label} [{default}]: ").strip()
tuning[key] = cast(raw) if raw else default
_prompt("masscan rate (pkt/s)", "masscan_rate", default_rate)
if scan_mode in ('masscan+nmap', 'geo-scout', 'masscan+nuclei'):
pass # masscan_rate already handled
if scan_mode in ('masscan+nmap', 'geo-scout'):
_prompt("nmap timing T1-T4", "nmap_timing", 4)
_prompt("nmap per-host timeout (s)", "nmap_timeout", 60)
_prompt("nmap parallel workers", "nmap_workers", 10)
if scan_mode == 'masscan+nuclei':
_prompt("nuclei rate limit (req/s)", "nuclei_rate", 150)
_prompt("nuclei concurrency", "nuclei_concurrency", 25)
_prompt("nuclei timeout (s)", "nuclei_timeout", 10)
return tuning
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str, use_tor: bool = False):
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode, use_tor=use_tor)
@@ -188,6 +255,10 @@ def gather_webrunner_parameters() -> dict | None:
config['deployment_id'] = generate_deployment_id()
print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}")
# Optional vars file — pre-fills tuning defaults
vars_raw = input("Vars file (optional, skip to configure interactively): ").strip()
vars_overrides = _load_vars_file(vars_raw)
# Engagement name — auto if blank
raw_eng = input(f"Engagement name [{config['deployment_id']}]: ").strip()
config['engagement'] = raw_eng or config['deployment_id']
@@ -232,6 +303,16 @@ def gather_webrunner_parameters() -> dict | None:
scan_mode = _select_scan_mode()
config['scan_mode'] = scan_mode
# Nuclei template (masscan+nuclei only)
config['nuclei_template_local'] = ''
config['nuclei_template_remote'] = ''
if scan_mode == 'masscan+nuclei':
local_tmpl, remote_tmpl = _get_nuclei_info(vars_overrides)
if not local_tmpl:
return None
config['nuclei_template_local'] = local_tmpl
config['nuclei_template_remote'] = remote_tmpl
# Targets / ports
targets_file, ports = _get_targets_info(scan_mode)
config['targets_file'] = targets_file
@@ -299,6 +380,8 @@ def gather_webrunner_parameters() -> dict | None:
config['preset'] = preset_key
config['chunk_size'] = chunk_size
config['cidr_country_map_file'] = '' # filled in execute_webrunner_deployment
# Distribute CIDRs into node chunks
all_cidrs: list[str] = []
for cc in country_codes:
@@ -355,6 +438,11 @@ def gather_webrunner_parameters() -> dict | None:
if config['operator_ip']:
print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}")
# Advanced tuning
default_rate = config['masscan_rate']
tuning = _get_tuning_params(scan_mode, default_rate, vars_overrides)
config.update(tuning)
# OPSEC / teardown options
print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}")
@@ -404,6 +492,12 @@ def execute_webrunner_deployment(config: dict):
print(f" Nodes: {n_nodes} ({config['preset']}, {fmt_ip_count(config['chunk_size'])}/node)")
print(f" Scan mode: {config['scan_mode']}")
print(f" Ports: {', '.join(str(p) for p in config['ports'][:8])}{'...' if len(config['ports']) > 8 else ''}")
print(f" masscan rate: {config.get('masscan_rate', '')} pkt/s")
if config['scan_mode'] in ('masscan+nmap', 'geo-scout'):
print(f" nmap: T{config.get('nmap_timing', 4)} timeout={config.get('nmap_timeout', 60)}s workers={config.get('nmap_workers', 10)}")
if config['scan_mode'] == 'masscan+nuclei':
tmpl_name = os.path.basename(config.get('nuclei_template_local', ''))
print(f" nuclei: {tmpl_name} rate={config.get('nuclei_rate', 150)} concurrency={config.get('nuclei_concurrency', 25)} timeout={config.get('nuclei_timeout', 10)}s")
print(f" Total IPs: {fmt_ip_count(config['total_ips'])}")
print(f" Tor routing: {'Yes' if config['use_tor'] else 'No'}")
print(f" Enhanced OPSEC: {'Yes' if config['enhanced_opsec'] else 'No'}")
@@ -421,6 +515,16 @@ def execute_webrunner_deployment(config: dict):
# regardless of playbook-relative CWD
logs_dir = os.path.abspath(os.path.join(_base, 'logs'))
os.makedirs(logs_dir, exist_ok=True)
# Save CIDR→country map for merge_results per-country attribution
cc_map_file = os.path.join(logs_dir, f"cidr_country_map_{config['deployment_id']}.json")
if config.get('country_codes'):
from utils.chunk_utils import get_country_cidrs
cc_cidrs_saved = get_country_cidrs(config['country_codes'], {})
with open(cc_map_file, 'w') as f:
json.dump(cc_cidrs_saved, f)
config['cidr_country_map_file'] = cc_map_file
chunks_file = os.path.join(logs_dir, f"node_chunks_{config['deployment_id']}.json")
with open(chunks_file, 'w') as f:
json.dump(node_chunks, f)