Fix WEBRUNNER: attack_box format, self-contained CIDR, tools submenu, Tor, teardown
- deploy_webrunner.py: full rewrite following attack_box pattern exactly — auto-generate engagement name when blank, select_provider+gather_provider_config per provider (handles region selection), separate teardown_after_scan (default=yes) from enhanced_opsec, use_tor prompt, bundled YAML defaults, setup_logging+confirm_action+show_naming_relationship - utils/cidr_resolver.py: self-contained RIR delegation file downloader/parser — downloads APNIC/ARIN/RIPE/LACNIC/AFRINIC, caches 24h in ~/.cache/c2itall/cidr/, no geo-scout dep - utils/chunk_utils.py: remove geo-scout _try_load_geo_scout() dependency, use cidr_resolver - modules/webrunner/inputs/countries.yaml: bundled default country list (no external dep) - modules/webrunner/inputs/targets.yaml: bundled default scan targets and ports - deploy.py: move WEBRUNNER from main menu (item 14) to tools submenu (item 10) - configure_node.yml: install tor+proxychains4, start tor service, configure proxychains, wait for circuit establishment when use_tor=true - run_scan.yml: prefix command with proxychains4 when use_tor=true - providers/webrunner.yml: add Play 4 teardown — Linode DELETE + AWS terminate-instances using hostvars instance IDs stored during provisioning, conditional on teardown_after_scan
This commit is contained in:
@@ -47,7 +47,6 @@ def main_menu():
|
|||||||
print(f"11) Deploy Privacy Server {COLORS['GREEN']}(Phantom — VPN, DNS, Matrix, etc.){COLORS['RESET']}")
|
print(f"11) Deploy Privacy Server {COLORS['GREEN']}(Phantom — VPN, DNS, Matrix, etc.){COLORS['RESET']}")
|
||||||
print(f"12) Tools & Utilities")
|
print(f"12) Tools & Utilities")
|
||||||
print(f"13) Cleanup & Teardown")
|
print(f"13) Cleanup & Teardown")
|
||||||
print(f"14) WEBRUNNER {COLORS['CYAN']}(Distributed Geo-Targeted Recon){COLORS['RESET']}")
|
|
||||||
print(f"\n99) Exit")
|
print(f"\n99) Exit")
|
||||||
|
|
||||||
choice = input(f"\nSelect an option: ")
|
choice = input(f"\nSelect an option: ")
|
||||||
@@ -75,8 +74,6 @@ def main_menu():
|
|||||||
tools_menu()
|
tools_menu()
|
||||||
elif choice == "13":
|
elif choice == "13":
|
||||||
cleanup_menu()
|
cleanup_menu()
|
||||||
elif choice == "14":
|
|
||||||
deploy_webrunner()
|
|
||||||
elif choice == "99":
|
elif choice == "99":
|
||||||
print(f"\n{COLORS['GREEN']}Exiting C2ingRed. Goodbye!{COLORS['RESET']}")
|
print(f"\n{COLORS['GREEN']}Exiting C2ingRed. Goodbye!{COLORS['RESET']}")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -247,6 +244,7 @@ def tools_menu():
|
|||||||
print(f"7) Ops Dashboard {COLORS['CYAN']}(Real-Time Engagement Monitor){COLORS['RESET']}")
|
print(f"7) Ops Dashboard {COLORS['CYAN']}(Real-Time Engagement Monitor){COLORS['RESET']}")
|
||||||
print(f"8) Claude Bot {COLORS['CYAN']}(Matrix-Claude Code Bridge){COLORS['RESET']}")
|
print(f"8) Claude Bot {COLORS['CYAN']}(Matrix-Claude Code Bridge){COLORS['RESET']}")
|
||||||
print(f"9) Chaos C2 {COLORS['CYAN']}(Deploy / Manage Chaos teamserver){COLORS['RESET']}")
|
print(f"9) Chaos C2 {COLORS['CYAN']}(Deploy / Manage Chaos teamserver){COLORS['RESET']}")
|
||||||
|
print(f"10) WEBRUNNER {COLORS['CYAN']}(Distributed Geo-Targeted Recon){COLORS['RESET']}")
|
||||||
print(f"99) Return to Main Menu")
|
print(f"99) Return to Main Menu")
|
||||||
|
|
||||||
choice = input(f"\nSelect an option: ")
|
choice = input(f"\nSelect an option: ")
|
||||||
@@ -279,6 +277,8 @@ def tools_menu():
|
|||||||
chaos_module = import_module_from_path('deploy_chaos', chaos_module_path)
|
chaos_module = import_module_from_path('deploy_chaos', chaos_module_path)
|
||||||
if chaos_module:
|
if chaos_module:
|
||||||
chaos_module.chaos_menu()
|
chaos_module.chaos_menu()
|
||||||
|
elif choice == "10":
|
||||||
|
deploy_webrunner()
|
||||||
elif choice == "99":
|
elif choice == "99":
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ if _base not in sys.path:
|
|||||||
sys.path.insert(0, _base)
|
sys.path.insert(0, _base)
|
||||||
|
|
||||||
from utils.common import (
|
from utils.common import (
|
||||||
COLORS, clear_screen, print_banner, wait_for_input,
|
COLORS, clear_screen, print_banner, generate_deployment_id,
|
||||||
get_public_ip, load_vars_file,
|
setup_logging, get_public_ip, confirm_action, wait_for_input,
|
||||||
|
load_vars_file,
|
||||||
)
|
)
|
||||||
|
from utils.provider_utils import select_provider, gather_provider_config
|
||||||
from utils.ssh_utils import generate_ssh_key
|
from utils.ssh_utils import generate_ssh_key
|
||||||
from utils.name_generator import generate_deployment_id
|
from utils.naming_utils import get_deployment_name_with_options, show_naming_relationship
|
||||||
from utils.naming_utils import get_deployment_name_with_options
|
|
||||||
from utils.deployment_engine import execute_playbook, set_provider_environment
|
from utils.deployment_engine import execute_playbook, set_provider_environment
|
||||||
from utils.chunk_utils import get_country_cidrs, chunk_cidrs, ip_count
|
from utils.chunk_utils import get_country_cidrs, chunk_cidrs, ip_count
|
||||||
from utils.provider_rates import (
|
from utils.provider_rates import (
|
||||||
@@ -28,7 +29,7 @@ from utils.provider_rates import (
|
|||||||
build_estimate_table, fmt_hours, fmt_ip_count,
|
build_estimate_table, fmt_hours, fmt_ip_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
GEO_SCOUT_INPUTS = Path(__file__).parents[3] / 'geo-scout' / 'inputs'
|
WEBRUNNER_INPUTS = Path(__file__).parent / 'inputs'
|
||||||
|
|
||||||
SUPPORTED_PROVIDERS = ['linode', 'aws', 'flokinet']
|
SUPPORTED_PROVIDERS = ['linode', 'aws', 'flokinet']
|
||||||
PROVIDER_LABELS = {'linode': 'Linode', 'aws': 'AWS', 'flokinet': 'FlokiNET'}
|
PROVIDER_LABELS = {'linode': 'Linode', 'aws': 'AWS', 'flokinet': 'FlokiNET'}
|
||||||
@@ -50,7 +51,6 @@ def webrunner_menu():
|
|||||||
config = gather_webrunner_parameters()
|
config = gather_webrunner_parameters()
|
||||||
if config:
|
if config:
|
||||||
execute_webrunner_deployment(config)
|
execute_webrunner_deployment(config)
|
||||||
wait_for_input()
|
|
||||||
elif choice == "99":
|
elif choice == "99":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -58,56 +58,7 @@ def webrunner_menu():
|
|||||||
wait_for_input()
|
wait_for_input()
|
||||||
|
|
||||||
|
|
||||||
# ── parameter gathering ───────────────────────────────────────────────────────
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _select_countries() -> tuple[list[str], dict[str, list], str]:
|
|
||||||
print(f"\n{COLORS['BLUE']}Country Selection:{COLORS['RESET']}")
|
|
||||||
print(f"1) Use geo-scout countries.yaml")
|
|
||||||
print(f"2) Enter country codes manually")
|
|
||||||
|
|
||||||
choice = input("Select [1]: ").strip() or "1"
|
|
||||||
exclude_map: dict[str, list] = {}
|
|
||||||
|
|
||||||
if choice == "1":
|
|
||||||
default_path = str(GEO_SCOUT_INPUTS / 'countries.yaml')
|
|
||||||
raw = input(f"Path [{default_path}]: ").strip()
|
|
||||||
path = Path(raw) if raw else GEO_SCOUT_INPUTS / 'countries.yaml'
|
|
||||||
|
|
||||||
if not path.exists():
|
|
||||||
print(f"{COLORS['RED']}File not found: {path}{COLORS['RESET']}")
|
|
||||||
return [], {}, ""
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
with open(path) as f:
|
|
||||||
data = yaml.safe_load(f)
|
|
||||||
|
|
||||||
countries = data.get('countries', [])
|
|
||||||
codes = [c['code'].upper() for c in countries]
|
|
||||||
for c in countries:
|
|
||||||
if c.get('exclude_cidrs'):
|
|
||||||
exclude_map[c['code'].upper()] = c['exclude_cidrs']
|
|
||||||
return codes, exclude_map, str(path)
|
|
||||||
|
|
||||||
raw = input("Country codes (comma-separated, e.g. RU,IR,CN): ").strip()
|
|
||||||
codes = [c.strip().upper() for c in raw.split(',') if c.strip()]
|
|
||||||
return codes, exclude_map, "manual"
|
|
||||||
|
|
||||||
|
|
||||||
def _select_scan_mode() -> str:
|
|
||||||
print(f"\n{COLORS['BLUE']}Scan Mode:{COLORS['RESET']}")
|
|
||||||
modes = list(SCAN_MODES.items())
|
|
||||||
for i, (key, val) in enumerate(modes, 1):
|
|
||||||
print(f"{i}) {key:<20} {val['desc']}")
|
|
||||||
|
|
||||||
choice = input("Select [1]: ").strip() or "1"
|
|
||||||
try:
|
|
||||||
idx = int(choice) - 1
|
|
||||||
if 0 <= idx < len(modes):
|
|
||||||
return modes[idx][0]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return 'geo-scout'
|
|
||||||
|
|
||||||
|
|
||||||
def _select_providers() -> list[str]:
|
def _select_providers() -> list[str]:
|
||||||
print(f"\n{COLORS['BLUE']}Provider Selection (multi-select):{COLORS['RESET']}")
|
print(f"\n{COLORS['BLUE']}Provider Selection (multi-select):{COLORS['RESET']}")
|
||||||
@@ -128,9 +79,58 @@ def _select_providers() -> list[str]:
|
|||||||
return selected or ['linode']
|
return selected or ['linode']
|
||||||
|
|
||||||
|
|
||||||
|
def _select_countries() -> tuple[list[str], dict[str, list]]:
|
||||||
|
print(f"\n{COLORS['BLUE']}Country Selection:{COLORS['RESET']}")
|
||||||
|
print(f"1) Use bundled countries.yaml")
|
||||||
|
print(f"2) Enter country codes manually")
|
||||||
|
|
||||||
|
choice = input("Select [1]: ").strip() or "1"
|
||||||
|
exclude_map: dict[str, list] = {}
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
default_path = str(WEBRUNNER_INPUTS / 'countries.yaml')
|
||||||
|
raw = input(f"Path [{default_path}]: ").strip()
|
||||||
|
path = Path(raw) if raw else WEBRUNNER_INPUTS / 'countries.yaml'
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
print(f"{COLORS['RED']}File not found: {path}{COLORS['RESET']}")
|
||||||
|
return [], {}
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
with open(path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
countries = data.get('countries', [])
|
||||||
|
codes = [c['code'].upper() for c in countries]
|
||||||
|
for c in countries:
|
||||||
|
if c.get('exclude_cidrs'):
|
||||||
|
exclude_map[c['code'].upper()] = c['exclude_cidrs']
|
||||||
|
return codes, exclude_map
|
||||||
|
|
||||||
|
raw = input("Country codes (comma-separated, e.g. RU,IR,CN): ").strip()
|
||||||
|
codes = [c.strip().upper() for c in raw.split(',') if c.strip()]
|
||||||
|
return codes, exclude_map
|
||||||
|
|
||||||
|
|
||||||
|
def _select_scan_mode() -> str:
|
||||||
|
print(f"\n{COLORS['BLUE']}Scan Mode:{COLORS['RESET']}")
|
||||||
|
modes = list(SCAN_MODES.items())
|
||||||
|
for i, (key, val) in enumerate(modes, 1):
|
||||||
|
print(f" {i}) {key:<20} {val['desc']}")
|
||||||
|
|
||||||
|
choice = input("Select [1]: ").strip() or "1"
|
||||||
|
try:
|
||||||
|
idx = int(choice) - 1
|
||||||
|
if 0 <= idx < len(modes):
|
||||||
|
return modes[idx][0]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return 'geo-scout'
|
||||||
|
|
||||||
|
|
||||||
def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]:
|
def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]:
|
||||||
if scan_mode == 'geo-scout':
|
if scan_mode == 'geo-scout':
|
||||||
default = str(GEO_SCOUT_INPUTS / 'targets.yaml')
|
default = str(WEBRUNNER_INPUTS / 'targets.yaml')
|
||||||
raw = input(f"Path to targets.yaml [{default}]: ").strip()
|
raw = input(f"Path to targets.yaml [{default}]: ").strip()
|
||||||
path = raw or default
|
path = raw or default
|
||||||
|
|
||||||
@@ -160,8 +160,8 @@ def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]:
|
|||||||
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str):
|
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str):
|
||||||
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode)
|
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode)
|
||||||
|
|
||||||
W = COLORS['WHITE']
|
|
||||||
C = COLORS['CYAN']
|
C = COLORS['CYAN']
|
||||||
|
W = COLORS['WHITE']
|
||||||
Y = COLORS['YELLOW']
|
Y = COLORS['YELLOW']
|
||||||
G = COLORS['GREEN']
|
G = COLORS['GREEN']
|
||||||
R = COLORS['RESET']
|
R = COLORS['RESET']
|
||||||
@@ -189,48 +189,91 @@ def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], sca
|
|||||||
print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n")
|
print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ── parameter gathering ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def gather_webrunner_parameters() -> dict | None:
|
def gather_webrunner_parameters() -> dict | None:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_banner()
|
print_banner()
|
||||||
print(f"{COLORS['CYAN']}WEBRUNNER — New Scan Deployment{COLORS['RESET']}")
|
print(f"{COLORS['WHITE']}WEBRUNNER SETUP{COLORS['RESET']}")
|
||||||
print(f"{COLORS['CYAN']}================================{COLORS['RESET']}\n")
|
print(f"{COLORS['WHITE']}==============={COLORS['RESET']}")
|
||||||
|
|
||||||
engagement = input("Engagement name: ").strip()
|
config: dict = {}
|
||||||
if not engagement:
|
|
||||||
print(f"{COLORS['RED']}Engagement name required.{COLORS['RESET']}")
|
|
||||||
wait_for_input()
|
|
||||||
return None
|
|
||||||
|
|
||||||
country_codes, exclude_map, _ = _select_countries()
|
# Deployment ID
|
||||||
if not country_codes:
|
config['deployment_id'] = generate_deployment_id()
|
||||||
print(f"{COLORS['RED']}No countries selected.{COLORS['RESET']}")
|
print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}")
|
||||||
wait_for_input()
|
|
||||||
return None
|
|
||||||
|
|
||||||
print(f"{COLORS['GREEN']}Countries: {', '.join(country_codes)}{COLORS['RESET']}")
|
# Engagement name — auto if blank
|
||||||
|
raw_eng = input(f"Engagement name [{config['deployment_id']}]: ").strip()
|
||||||
|
config['engagement'] = raw_eng or config['deployment_id']
|
||||||
|
|
||||||
scan_mode = _select_scan_mode()
|
# Provider selection — multi-select, each provider prompts for creds + region
|
||||||
providers = _select_providers()
|
providers = _select_providers()
|
||||||
|
config['providers'] = providers
|
||||||
|
|
||||||
|
for provider in providers:
|
||||||
|
provider_config = gather_provider_config(provider)
|
||||||
|
if not provider_config:
|
||||||
|
return None
|
||||||
|
config.update(provider_config)
|
||||||
|
|
||||||
print(f"{COLORS['GREEN']}Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{COLORS['RESET']}")
|
print(f"{COLORS['GREEN']}Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{COLORS['RESET']}")
|
||||||
|
|
||||||
targets_file, ports = _get_targets_info(scan_mode)
|
# Deployment type
|
||||||
|
config['deployment_type'] = 'webrunner'
|
||||||
|
config['webrunner_deployment'] = True
|
||||||
|
|
||||||
print(f"\n{COLORS['CYAN']}[*] Resolving CIDR data for {len(country_codes)} countries...{COLORS['RESET']}")
|
# Naming
|
||||||
cc_cidrs = get_country_cidrs(country_codes, exclude_map)
|
config['webrunner_name'] = get_deployment_name_with_options(
|
||||||
total_ips = sum(
|
deployment_type='webrunner',
|
||||||
sum(ip_count(c) for c in cidrs)
|
deployment_id=config['deployment_id'],
|
||||||
for cidrs in cc_cidrs.values()
|
prefix='wr-',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# SSH key
|
||||||
|
ssh_key_path = generate_ssh_key(config['webrunner_name'])
|
||||||
|
if not ssh_key_path:
|
||||||
|
print(f"{COLORS['RED']}Failed to generate SSH key.{COLORS['RESET']}")
|
||||||
|
return None
|
||||||
|
config['ssh_key_path'] = f"{ssh_key_path}.pub"
|
||||||
|
config['ssh_key_name'] = os.path.basename(ssh_key_path)
|
||||||
|
print(f"{COLORS['GREEN']}SSH key generated: {ssh_key_path}{COLORS['RESET']}")
|
||||||
|
|
||||||
|
# Country / CIDR selection
|
||||||
|
country_codes, exclude_map = _select_countries()
|
||||||
|
if not country_codes:
|
||||||
|
print(f"{COLORS['RED']}No countries selected.{COLORS['RESET']}")
|
||||||
|
return None
|
||||||
|
config['country_codes'] = country_codes
|
||||||
|
print(f"{COLORS['GREEN']}Countries: {', '.join(country_codes)}{COLORS['RESET']}")
|
||||||
|
|
||||||
|
# Scan mode
|
||||||
|
scan_mode = _select_scan_mode()
|
||||||
|
config['scan_mode'] = scan_mode
|
||||||
|
|
||||||
|
# Targets / ports
|
||||||
|
targets_file, ports = _get_targets_info(scan_mode)
|
||||||
|
config['targets_file'] = targets_file
|
||||||
|
config['ports'] = ports
|
||||||
|
config['ports_str'] = ','.join(str(p) for p in ports)
|
||||||
|
config['masscan_rate'] = SCAN_MODES.get(scan_mode, {}).get('rate', 3000)
|
||||||
|
|
||||||
|
# Resolve CIDR data
|
||||||
|
print(f"\n{COLORS['CYAN']}[*] Resolving CIDR data for {len(country_codes)} countries...{COLORS['RESET']}")
|
||||||
|
cc_cidrs = get_country_cidrs(country_codes, exclude_map)
|
||||||
|
total_ips = sum(sum(ip_count(c) for c in cidrs) for cidrs in cc_cidrs.values())
|
||||||
|
|
||||||
if total_ips == 0:
|
if total_ips == 0:
|
||||||
print(f"{COLORS['RED']}No IPs resolved. Ensure geo-scout RIR data is available.{COLORS['RESET']}")
|
print(f"{COLORS['RED']}No IPs resolved. Check your country codes or network connectivity.{COLORS['RESET']}")
|
||||||
wait_for_input()
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
print(f"{COLORS['GREEN']}Total: {fmt_ip_count(total_ips)} IPs across {len(country_codes)} countries{COLORS['RESET']}")
|
print(f"{COLORS['GREEN']}Total: {fmt_ip_count(total_ips)} IPs across {len(country_codes)} countries{COLORS['RESET']}")
|
||||||
|
config['total_ips'] = total_ips
|
||||||
|
|
||||||
|
# Cost / time estimate
|
||||||
_show_estimate_table(total_ips, len(ports), providers, scan_mode)
|
_show_estimate_table(total_ips, len(ports), providers, scan_mode)
|
||||||
|
|
||||||
|
# Preset selection
|
||||||
print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}")
|
print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}")
|
||||||
preset_list = list(PRESETS.items())
|
preset_list = list(PRESETS.items())
|
||||||
for i, (key, val) in enumerate(preset_list, 1):
|
for i, (key, val) in enumerate(preset_list, 1):
|
||||||
@@ -261,54 +304,10 @@ def gather_webrunner_parameters() -> dict | None:
|
|||||||
preset_key = 'balanced'
|
preset_key = 'balanced'
|
||||||
chunk_size = PRESETS['balanced']['chunk_size']
|
chunk_size = PRESETS['balanced']['chunk_size']
|
||||||
|
|
||||||
opsec_raw = input(f"\nEnhanced OPSEC mode? (randomize names, auto-teardown) [y/N]: ").strip().lower()
|
config['preset'] = preset_key
|
||||||
enhanced_opsec = opsec_raw in ['y', 'yes']
|
config['chunk_size'] = chunk_size
|
||||||
|
|
||||||
print(f"\n{COLORS['CYAN']}[*] Generating deployment assets...{COLORS['RESET']}")
|
# Distribute CIDRs into node chunks
|
||||||
deployment_id = generate_deployment_id()
|
|
||||||
webrunner_name = get_deployment_name_with_options('webrunner', deployment_id, prefix='wr-')
|
|
||||||
|
|
||||||
raw_key_path = generate_ssh_key(webrunner_name)
|
|
||||||
if not raw_key_path:
|
|
||||||
print(f"{COLORS['RED']}Failed to generate SSH key.{COLORS['RESET']}")
|
|
||||||
wait_for_input()
|
|
||||||
return None
|
|
||||||
|
|
||||||
config: dict = {
|
|
||||||
'engagement': engagement,
|
|
||||||
'deployment_id': deployment_id,
|
|
||||||
'deployment_type': 'webrunner',
|
|
||||||
'webrunner_deployment': True,
|
|
||||||
'webrunner_name': webrunner_name,
|
|
||||||
'ssh_key_path': f"{raw_key_path}.pub",
|
|
||||||
'ssh_key_name': os.path.basename(raw_key_path),
|
|
||||||
'providers': providers,
|
|
||||||
'scan_mode': scan_mode,
|
|
||||||
'preset': preset_key,
|
|
||||||
'chunk_size': chunk_size,
|
|
||||||
'country_codes': country_codes,
|
|
||||||
'ports': ports,
|
|
||||||
'ports_str': ','.join(str(p) for p in ports),
|
|
||||||
'targets_file': targets_file,
|
|
||||||
'masscan_rate': SCAN_MODES.get(scan_mode, {}).get('rate', 3000),
|
|
||||||
'enhanced_opsec': enhanced_opsec,
|
|
||||||
'linode_instance_type': DEFAULT_INSTANCE.get('linode', 'g6-standard-2'),
|
|
||||||
'linode_region': 'us-east',
|
|
||||||
'aws_instance_type': DEFAULT_INSTANCE.get('aws', 't3.small'),
|
|
||||||
'aws_region': 'us-east-1',
|
|
||||||
}
|
|
||||||
|
|
||||||
operator_ip = get_public_ip()
|
|
||||||
if operator_ip:
|
|
||||||
config['operator_ip'] = operator_ip
|
|
||||||
print(f"{COLORS['GREEN']}Operator IP: {operator_ip}{COLORS['RESET']}")
|
|
||||||
|
|
||||||
for provider in providers:
|
|
||||||
vars_data = load_vars_file(provider)
|
|
||||||
if vars_data:
|
|
||||||
config.update({k: v for k, v in vars_data.items() if k not in config})
|
|
||||||
|
|
||||||
print(f"{COLORS['CYAN']}[*] Distributing {fmt_ip_count(total_ips)} IPs across nodes...{COLORS['RESET']}")
|
|
||||||
all_cidrs: list[str] = []
|
all_cidrs: list[str] = []
|
||||||
for cc in country_codes:
|
for cc in country_codes:
|
||||||
all_cidrs.extend(cc_cidrs.get(cc.upper(), []))
|
all_cidrs.extend(cc_cidrs.get(cc.upper(), []))
|
||||||
@@ -317,54 +316,109 @@ def gather_webrunner_parameters() -> dict | None:
|
|||||||
node_chunks = [
|
node_chunks = [
|
||||||
{
|
{
|
||||||
'idx': i,
|
'idx': i,
|
||||||
'node_name': f"{webrunner_name}-{i + 1:02d}",
|
'node_name': f"{config['webrunner_name']}-{i + 1:02d}",
|
||||||
'provider': providers[i % len(providers)],
|
'provider': providers[i % len(providers)],
|
||||||
'cidrs': chunk['cidrs'],
|
'cidrs': chunk['cidrs'],
|
||||||
'ip_count': chunk['ip_count'],
|
'ip_count': chunk['ip_count'],
|
||||||
}
|
}
|
||||||
for i, chunk in enumerate(chunks)
|
for i, chunk in enumerate(chunks)
|
||||||
]
|
]
|
||||||
|
config['node_chunks'] = node_chunks
|
||||||
print(f"{COLORS['GREEN']}Nodes: {len(node_chunks)} ({preset_key}, {fmt_ip_count(chunk_size)}/node){COLORS['RESET']}")
|
print(f"{COLORS['GREEN']}Nodes: {len(node_chunks)} ({preset_key}, {fmt_ip_count(chunk_size)}/node){COLORS['RESET']}")
|
||||||
|
|
||||||
print(f"\n{COLORS['YELLOW']}{'─' * 50}{COLORS['RESET']}")
|
# Tor routing
|
||||||
print(f" Deployment: {webrunner_name}")
|
tor_raw = input(f"\nRoute scans through Tor? [y/N]: ").strip().lower()
|
||||||
print(f" Nodes: {len(node_chunks)}")
|
config['use_tor'] = tor_raw in ['y', 'yes']
|
||||||
print(f" Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}")
|
if config['use_tor']:
|
||||||
print(f" Scan mode: {scan_mode}")
|
print(f"{COLORS['YELLOW']} Tor routing enabled — masscan/nmap will use proxychains{COLORS['RESET']}")
|
||||||
print(f" Ports: {', '.join(str(p) for p in ports[:8])}{'...' if len(ports) > 8 else ''}")
|
|
||||||
print(f" Total IPs: {fmt_ip_count(total_ips)}")
|
|
||||||
print(f"{COLORS['YELLOW']}{'─' * 50}{COLORS['RESET']}")
|
|
||||||
|
|
||||||
confirm = input(f"\nDeploy {len(node_chunks)} nodes? [y/N]: ").strip().lower()
|
# Operator IP
|
||||||
if confirm not in ['y', 'yes']:
|
config['operator_ip'] = get_public_ip()
|
||||||
print(f"{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}")
|
if config['operator_ip']:
|
||||||
return None
|
print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}")
|
||||||
|
|
||||||
|
# OPSEC / teardown options
|
||||||
|
print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}")
|
||||||
|
|
||||||
|
opsec_raw = input(f"Enhanced OPSEC mode? (randomize node names, minimal logging) [y/N]: ").strip().lower()
|
||||||
|
config['enhanced_opsec'] = opsec_raw in ['y', 'yes']
|
||||||
|
|
||||||
|
teardown_raw = input(f"Teardown nodes after scan completes? [Y/n]: ").strip().lower()
|
||||||
|
config['teardown_after_scan'] = teardown_raw not in ['n', 'no']
|
||||||
|
|
||||||
|
if config['teardown_after_scan']:
|
||||||
|
print(f"{COLORS['YELLOW']} Nodes will be destroyed automatically when scan completes.{COLORS['RESET']}")
|
||||||
|
else:
|
||||||
|
print(f"{COLORS['CYAN']} Nodes will remain running after scan — remember to teardown manually.{COLORS['RESET']}")
|
||||||
|
|
||||||
|
# Merge any provider vars not already set
|
||||||
|
for provider in providers:
|
||||||
|
vars_data = load_vars_file(provider)
|
||||||
|
if vars_data:
|
||||||
|
config.update({k: v for k, v in vars_data.items() if k not in config})
|
||||||
|
|
||||||
|
# Apply per-provider instance defaults if not set by gather_provider_config
|
||||||
|
if 'linode_instance_type' not in config:
|
||||||
|
config['linode_instance_type'] = DEFAULT_INSTANCE.get('linode', 'g6-standard-2')
|
||||||
|
if 'aws_instance_type' not in config:
|
||||||
|
config['aws_instance_type'] = DEFAULT_INSTANCE.get('aws', 't3.small')
|
||||||
|
|
||||||
config['node_chunks'] = node_chunks
|
|
||||||
config['total_ips'] = total_ips
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
# ── execution ─────────────────────────────────────────────────────────────────
|
# ── execution ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def execute_webrunner_deployment(config: dict):
|
def execute_webrunner_deployment(config: dict):
|
||||||
deployment_id = config['deployment_id']
|
clear_screen()
|
||||||
node_chunks = config.pop('node_chunks')
|
print_banner()
|
||||||
|
print(f"\n{COLORS['GREEN']}Starting WEBRUNNER deployment...{COLORS['RESET']}")
|
||||||
|
|
||||||
|
log_file = setup_logging(config['deployment_id'], "webrunner_deployment")
|
||||||
|
|
||||||
|
node_chunks = config.pop('node_chunks')
|
||||||
|
n_nodes = len(node_chunks)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}")
|
||||||
|
print(f" Deployment ID: {config['deployment_id']}")
|
||||||
|
print(f" Name: {config['webrunner_name']}")
|
||||||
|
|
||||||
|
naming_info = show_naming_relationship(config['webrunner_name'], config['deployment_id'], 'webrunner')
|
||||||
|
if naming_info:
|
||||||
|
print(f" └─ {naming_info['relationship_text']}")
|
||||||
|
|
||||||
|
print(f" Engagement: {config['engagement']}")
|
||||||
|
print(f" Providers: {', '.join(PROVIDER_LABELS[p] for p in config['providers'])}")
|
||||||
|
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" 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'}")
|
||||||
|
print(f" Teardown after: {'Yes' if config['teardown_after_scan'] else 'No'}")
|
||||||
|
|
||||||
|
if config.get('ssh_key_path'):
|
||||||
|
key_name = os.path.basename(config['ssh_key_path']).replace('.pub', '')
|
||||||
|
print(f" SSH Key: {key_name}")
|
||||||
|
|
||||||
|
if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with WEBRUNNER deployment?{COLORS['RESET']}", default=True):
|
||||||
|
print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write node chunks file
|
||||||
os.makedirs('logs', exist_ok=True)
|
os.makedirs('logs', exist_ok=True)
|
||||||
chunks_file = f"logs/node_chunks_{deployment_id}.json"
|
chunks_file = f"logs/node_chunks_{config['deployment_id']}.json"
|
||||||
with open(chunks_file, 'w') as f:
|
with open(chunks_file, 'w') as f:
|
||||||
json.dump(node_chunks, f)
|
json.dump(node_chunks, f)
|
||||||
|
|
||||||
config['node_chunks_file'] = chunks_file
|
config['node_chunks_file'] = chunks_file
|
||||||
config['scanner_ip_log'] = f"logs/scanner_ips_{config['webrunner_name']}.txt"
|
config['scanner_ip_log'] = f"logs/scanner_ips_{config['webrunner_name']}.txt"
|
||||||
config['results_dir'] = f"logs/webrunner_{deployment_id}"
|
config['results_dir'] = f"logs/webrunner_{config['deployment_id']}"
|
||||||
|
|
||||||
for provider in config['providers']:
|
for provider in config['providers']:
|
||||||
set_provider_environment({**config, 'provider': provider})
|
set_provider_environment({**config, 'provider': provider})
|
||||||
|
|
||||||
playbook = 'providers/webrunner.yml'
|
playbook = 'providers/webrunner.yml'
|
||||||
n_nodes = len(node_chunks)
|
|
||||||
|
|
||||||
print(f"\n{COLORS['CYAN']}[*] Launching WEBRUNNER — {n_nodes} nodes across "
|
print(f"\n{COLORS['CYAN']}[*] Launching WEBRUNNER — {n_nodes} nodes across "
|
||||||
f"{', '.join(PROVIDER_LABELS[p] for p in config['providers'])}{COLORS['RESET']}")
|
f"{', '.join(PROVIDER_LABELS[p] for p in config['providers'])}{COLORS['RESET']}")
|
||||||
@@ -374,8 +428,10 @@ def execute_webrunner_deployment(config: dict):
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
print(f"\n{COLORS['GREEN']}[+] WEBRUNNER complete.{COLORS['RESET']}")
|
print(f"\n{COLORS['GREEN']}[+] WEBRUNNER complete.{COLORS['RESET']}")
|
||||||
print(f" Results: logs/webrunner_{deployment_id}/")
|
print(f" Results: logs/webrunner_{config['deployment_id']}/")
|
||||||
print(f" Scanner IPs: logs/scanner_ips_{config['webrunner_name']}.txt")
|
print(f" Scanner IPs: logs/scanner_ips_{config['webrunner_name']}.txt")
|
||||||
print(f" Log: logs/deployment_{deployment_id}.log")
|
print(f" Log: logs/deployment_{config['deployment_id']}.log")
|
||||||
else:
|
else:
|
||||||
print(f"\n{COLORS['RED']}[-] WEBRUNNER failed. Check: logs/deployment_{deployment_id}.log{COLORS['RESET']}")
|
print(f"\n{COLORS['RED']}[-] WEBRUNNER failed. Check: logs/deployment_{config['deployment_id']}.log{COLORS['RESET']}")
|
||||||
|
|
||||||
|
wait_for_input()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
# WEBRUNNER — default country target list
|
||||||
|
# Adjust to match your engagement scope
|
||||||
|
countries:
|
||||||
|
- code: RU
|
||||||
|
name: Russia
|
||||||
|
- code: CN
|
||||||
|
name: China
|
||||||
|
- code: IR
|
||||||
|
name: Iran
|
||||||
|
- code: KP
|
||||||
|
name: North Korea
|
||||||
|
- code: BY
|
||||||
|
name: Belarus
|
||||||
|
- code: SY
|
||||||
|
name: Syria
|
||||||
|
- code: CU
|
||||||
|
name: Cuba
|
||||||
|
- code: VE
|
||||||
|
name: Venezuela
|
||||||
|
- code: MM
|
||||||
|
name: Myanmar
|
||||||
|
- code: SD
|
||||||
|
name: Sudan
|
||||||
|
- code: IQ
|
||||||
|
name: Iraq
|
||||||
|
- code: UA
|
||||||
|
name: Ukraine
|
||||||
|
- code: AZ
|
||||||
|
name: Azerbaijan
|
||||||
|
- code: GE
|
||||||
|
name: Georgia
|
||||||
|
- code: AM
|
||||||
|
name: Armenia
|
||||||
|
- code: MD
|
||||||
|
name: Moldova
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
# WEBRUNNER — default scan targets
|
||||||
|
# Define services of interest with ports and match criteria
|
||||||
|
targets:
|
||||||
|
- name: SSH
|
||||||
|
description: OpenSSH / SSH servers
|
||||||
|
ports: [22]
|
||||||
|
|
||||||
|
- name: HTTP
|
||||||
|
description: Web servers (plain)
|
||||||
|
ports: [80, 8080, 8000, 8888]
|
||||||
|
|
||||||
|
- name: HTTPS
|
||||||
|
description: Web servers (TLS)
|
||||||
|
ports: [443, 8443, 4443]
|
||||||
|
|
||||||
|
- name: RDP
|
||||||
|
description: Remote Desktop Protocol
|
||||||
|
ports: [3389]
|
||||||
|
|
||||||
|
- name: SMB
|
||||||
|
description: Windows file sharing
|
||||||
|
ports: [445, 139]
|
||||||
|
|
||||||
|
- name: FTP
|
||||||
|
description: File Transfer Protocol
|
||||||
|
ports: [21]
|
||||||
|
|
||||||
|
- name: Telnet
|
||||||
|
description: Legacy telnet
|
||||||
|
ports: [23]
|
||||||
|
|
||||||
|
- name: VNC
|
||||||
|
description: VNC remote desktop
|
||||||
|
ports: [5900, 5901, 5902]
|
||||||
|
|
||||||
|
- name: Proxy
|
||||||
|
description: HTTP/SOCKS proxies
|
||||||
|
ports: [3128, 8888, 1080, 1081]
|
||||||
|
|
||||||
|
- name: Database
|
||||||
|
description: Common database ports
|
||||||
|
ports: [3306, 5432, 1433, 27017, 6379]
|
||||||
|
|
||||||
|
- name: Kubernetes
|
||||||
|
description: K8s API server
|
||||||
|
ports: [6443, 8001]
|
||||||
|
|
||||||
|
- name: Docker
|
||||||
|
description: Docker daemon API
|
||||||
|
ports: [2375, 2376]
|
||||||
|
|
||||||
|
- name: SCADA
|
||||||
|
description: Industrial control systems
|
||||||
|
ports: [102, 502, 44818, 47808]
|
||||||
@@ -19,6 +19,44 @@
|
|||||||
retries: 3
|
retries: 3
|
||||||
delay: 10
|
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
|
- name: Create webrunner workspace
|
||||||
file:
|
file:
|
||||||
path: /root/webrunner
|
path: /root/webrunner
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
# Run the assigned scan on this WEBRUNNER node
|
# Run the assigned scan on this WEBRUNNER node
|
||||||
# Per-node vars: node_name, node_cidrs, node_ip_count, node_idx
|
# 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
|
# Global vars (from extra-vars): scan_mode, ports_str, masscan_rate, webrunner_name, deployment_id, use_tor
|
||||||
|
|
||||||
- name: Write CIDR list for {{ node_name }}
|
- name: Write CIDR list for {{ node_name }}
|
||||||
copy:
|
copy:
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
- 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: >
|
||||||
|
{{ 'proxychains4 -f /etc/proxychains4.conf' if (use_tor | default(false) | bool) else '' }}
|
||||||
python3 /root/webrunner/node_scanner.py
|
python3 /root/webrunner/node_scanner.py
|
||||||
--mode {{ scan_mode }}
|
--mode {{ scan_mode }}
|
||||||
--ports {{ ports_str }}
|
--ports {{ ports_str }}
|
||||||
|
|||||||
@@ -90,3 +90,43 @@
|
|||||||
- "Results: {{ results_dir }}/merged_results.json"
|
- "Results: {{ results_dir }}/merged_results.json"
|
||||||
- "Scanner IPs: {{ scanner_ip_log }}"
|
- "Scanner IPs: {{ scanner_ip_log }}"
|
||||||
- "Log: logs/deployment_{{ deployment_id }}.log"
|
- "Log: logs/deployment_{{ deployment_id }}.log"
|
||||||
|
|
||||||
|
|
||||||
|
- name: WEBRUNNER — Teardown scan nodes
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
when: teardown_after_scan | default(true) | bool
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Teardown Linode nodes
|
||||||
|
uri:
|
||||||
|
url: "https://api.linode.com/v4/linode/instances/{{ hostvars[item]['linode_instance_id'] }}"
|
||||||
|
method: DELETE
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{ linode_token }}"
|
||||||
|
status_code: [200, 204]
|
||||||
|
loop: "{{ groups['webrunner_nodes'] | default([]) }}"
|
||||||
|
when:
|
||||||
|
- hostvars[item]['provider'] == 'linode'
|
||||||
|
- hostvars[item]['linode_instance_id'] is defined
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Teardown AWS nodes
|
||||||
|
amazon.aws.ec2_instance:
|
||||||
|
instance_ids:
|
||||||
|
- "{{ hostvars[item]['ec2_instance_id'] }}"
|
||||||
|
region: "{{ aws_region | default('us-east-1') }}"
|
||||||
|
aws_access_key: "{{ aws_access_key | default('') }}"
|
||||||
|
aws_secret_key: "{{ aws_secret_key | default('') }}"
|
||||||
|
state: terminated
|
||||||
|
loop: "{{ groups['webrunner_nodes'] | default([]) }}"
|
||||||
|
when:
|
||||||
|
- hostvars[item]['provider'] == 'aws'
|
||||||
|
- hostvars[item]['ec2_instance_id'] is defined
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Teardown complete
|
||||||
|
debug:
|
||||||
|
msg: "All WEBRUNNER scan nodes destroyed."
|
||||||
|
when: groups['webrunner_nodes'] is defined and groups['webrunner_nodes'] | length > 0
|
||||||
|
|||||||
+4
-36
@@ -1,20 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import sys
|
from utils.cidr_resolver import get_country_cidrs
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _try_load_geo_scout() -> bool:
|
|
||||||
candidates = [
|
|
||||||
Path(__file__).parents[2] / 'geo-scout',
|
|
||||||
Path.home() / 'tools' / 'geo-scout',
|
|
||||||
]
|
|
||||||
for p in candidates:
|
|
||||||
if (p / 'lib' / 'cidr.py').exists():
|
|
||||||
if str(p) not in sys.path:
|
|
||||||
sys.path.insert(0, str(p))
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def ip_count(cidr: str) -> int:
|
def ip_count(cidr: str) -> int:
|
||||||
@@ -24,27 +10,6 @@ def ip_count(cidr: str) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def get_country_cidrs(country_codes: list[str], exclude_map: dict[str, list] | None = None) -> dict[str, list[str]]:
|
|
||||||
if exclude_map is None:
|
|
||||||
exclude_map = {}
|
|
||||||
|
|
||||||
_try_load_geo_scout()
|
|
||||||
try:
|
|
||||||
from lib.cidr import fetch_rir_data, load_index, get_cidrs, merge_cidrs
|
|
||||||
except ImportError:
|
|
||||||
return {cc.upper(): [] for cc in country_codes}
|
|
||||||
|
|
||||||
fetch_rir_data()
|
|
||||||
index = load_index()
|
|
||||||
result = {}
|
|
||||||
for cc in country_codes:
|
|
||||||
cc = cc.upper()
|
|
||||||
cidrs = get_cidrs(cc, index)
|
|
||||||
cidrs = merge_cidrs(cidrs, exclude_map.get(cc, []))
|
|
||||||
result[cc] = cidrs
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]:
|
def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]:
|
||||||
chunks = []
|
chunks = []
|
||||||
current_cidrs: list[str] = []
|
current_cidrs: list[str] = []
|
||||||
@@ -69,3 +34,6 @@ def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]:
|
|||||||
chunks.append({'cidrs': current_cidrs, 'ip_count': current_count})
|
chunks.append({'cidrs': current_cidrs, 'ip_count': current_count})
|
||||||
|
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['get_country_cidrs', 'chunk_cidrs', 'ip_count']
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import ipaddress
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.request import urlretrieve
|
||||||
|
from urllib.error import URLError
|
||||||
|
|
||||||
|
CACHE_DIR = Path.home() / '.cache' / 'c2itall' / 'cidr'
|
||||||
|
CACHE_TTL = 86400 # 24 hours
|
||||||
|
|
||||||
|
RIR_URLS = {
|
||||||
|
'arin': 'https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest',
|
||||||
|
'ripe': 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest',
|
||||||
|
'apnic': 'https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest',
|
||||||
|
'lacnic': 'https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest',
|
||||||
|
'afrinic': 'https://ftp.afrinic.net/stats/delegated-afrinic-extended-latest',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_path(rir: str) -> Path:
|
||||||
|
return CACHE_DIR / f'{rir}.txt'
|
||||||
|
|
||||||
|
|
||||||
|
def _is_stale(path: Path) -> bool:
|
||||||
|
if not path.exists():
|
||||||
|
return True
|
||||||
|
return (time.time() - path.stat().st_mtime) > CACHE_TTL
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_rir(rir: str, url: str) -> Path:
|
||||||
|
path = _cache_path(rir)
|
||||||
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
if _is_stale(path):
|
||||||
|
try:
|
||||||
|
urlretrieve(url, path)
|
||||||
|
except (URLError, OSError):
|
||||||
|
pass # use stale cache if download fails
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _count_to_cidrs(start: str, count: int) -> list[str]:
|
||||||
|
try:
|
||||||
|
start_addr = ipaddress.IPv4Address(start)
|
||||||
|
end_addr = ipaddress.IPv4Address(int(start_addr) + count - 1)
|
||||||
|
return [str(n) for n in ipaddress.summarize_address_range(start_addr, end_addr)]
|
||||||
|
except (ipaddress.AddressValueError, ValueError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rir_file(path: Path, target_ccs: set[str]) -> dict[str, list[str]]:
|
||||||
|
result: dict[str, list[str]] = {cc: [] for cc in target_ccs}
|
||||||
|
try:
|
||||||
|
with open(path, encoding='latin-1') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
parts = line.split('|')
|
||||||
|
if len(parts) < 7:
|
||||||
|
continue
|
||||||
|
_, cc, type_, start, count_str, _, status = parts[:7]
|
||||||
|
if type_ != 'ipv4':
|
||||||
|
continue
|
||||||
|
if status not in ('allocated', 'assigned'):
|
||||||
|
continue
|
||||||
|
cc = cc.upper()
|
||||||
|
if cc not in target_ccs:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
count = int(count_str)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
result[cc].extend(_count_to_cidrs(start, count))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_country_cidrs(country_codes: list[str], exclude_map: dict[str, list] | None = None) -> dict[str, list[str]]:
|
||||||
|
if exclude_map is None:
|
||||||
|
exclude_map = {}
|
||||||
|
target_ccs = {cc.upper() for cc in country_codes}
|
||||||
|
combined: dict[str, list[str]] = {cc: [] for cc in target_ccs}
|
||||||
|
|
||||||
|
for rir, url in RIR_URLS.items():
|
||||||
|
path = _fetch_rir(rir, url)
|
||||||
|
partial = _parse_rir_file(path, target_ccs)
|
||||||
|
for cc, cidrs in partial.items():
|
||||||
|
combined[cc].extend(cidrs)
|
||||||
|
|
||||||
|
for cc, excludes in exclude_map.items():
|
||||||
|
cc = cc.upper()
|
||||||
|
if cc not in combined or not excludes:
|
||||||
|
continue
|
||||||
|
exclude_nets = []
|
||||||
|
for ex in excludes:
|
||||||
|
try:
|
||||||
|
exclude_nets.append(ipaddress.ip_network(ex, strict=False))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if not exclude_nets:
|
||||||
|
continue
|
||||||
|
filtered = []
|
||||||
|
for cidr in combined[cc]:
|
||||||
|
try:
|
||||||
|
net = ipaddress.ip_network(cidr, strict=False)
|
||||||
|
if not any(net.overlaps(ex) for ex in exclude_nets):
|
||||||
|
filtered.append(cidr)
|
||||||
|
except ValueError:
|
||||||
|
filtered.append(cidr)
|
||||||
|
combined[cc] = filtered
|
||||||
|
|
||||||
|
return combined
|
||||||
Reference in New Issue
Block a user