Add WEBRUNNER distributed geo-targeted recon module

Multi-provider (Linode/AWS/FlokiNET) scan orchestration:
- Flatten all country CIDRs, chunk by IP count (sprint/balanced/economy presets)
- Assign chunks round-robin across providers
- Cost+time estimate table before deploy
- Ansible provisions N nodes in parallel, runs masscan/nmap/probe per node
- Results fetched back and merged into unified JSON report
- Scanner IPs logged per engagement for client IR reporting
- Operator IP whitelisting, enhanced OPSEC mode, YAML targets.yaml support
This commit is contained in:
n0mad1k
2026-04-30 16:26:45 -04:00
parent 34ce52650f
commit 2c6cbc4cb7
15 changed files with 1314 additions and 3 deletions
@@ -0,0 +1,98 @@
---
# AWS provision tasks for one WEBRUNNER node
# Called in a loop — loop_var: node_chunk
# Requires: aws_access_key, aws_secret_key, aws_region, aws_instance_type,
# ssh_key_name, webrunner_name, deployment_id, operator_ip,
# scanner_ip_log, results_dir
- name: Read public key for {{ node_chunk.node_name }}
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey_aws
- name: Import SSH key to AWS ({{ node_chunk.node_name }})
amazon.aws.ec2_key:
name: "{{ webrunner_name }}"
key_material: "{{ wr_pubkey_aws.content | b64decode | trim }}"
region: "{{ aws_region | default('us-east-1') }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
state: present
ignore_errors: true
- name: Create security group for {{ node_chunk.node_name }}
amazon.aws.ec2_security_group:
name: "wr-{{ deployment_id }}-sg"
description: "WEBRUNNER {{ deployment_id }} scanner nodes"
region: "{{ aws_region | default('us-east-1') }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
rules:
- proto: tcp
ports: [22]
cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32"
rules_egress:
- proto: all
cidr_ip: "0.0.0.0/0"
tags:
Name: "wr-{{ deployment_id }}-sg"
DeploymentID: "{{ deployment_id }}"
state: present
register: wr_sg
ignore_errors: true
- name: Launch EC2 instance {{ node_chunk.node_name }}
amazon.aws.ec2_instance:
name: "{{ node_chunk.node_name }}"
key_name: "{{ webrunner_name }}"
instance_type: "{{ aws_instance_type | default('t3.small') }}"
image_id: "{{ aws_ami | default('ami-0c55b159cbfafe1f0') }}"
region: "{{ aws_region | default('us-east-1') }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
security_group: "wr-{{ deployment_id }}-sg"
network:
assign_public_ip: true
tags:
Name: "{{ node_chunk.node_name }}"
DeploymentID: "{{ deployment_id }}"
webrunner: "{{ webrunner_name }}"
wait: true
state: running
register: wr_ec2
- name: Extract EC2 public IP
set_fact:
wr_node_ip: "{{ wr_ec2.instances[0].public_ip_address }}"
- name: Log scanner IP
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
create: true
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
wait_for:
host: "{{ wr_node_ip }}"
port: 22
delay: 30
timeout: 300
- name: Add {{ node_chunk.node_name }} to inventory
add_host:
name: "{{ wr_node_ip }}"
groups: webrunner_nodes
ansible_host: "{{ wr_node_ip }}"
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ node_chunk.node_name }}"
node_cidrs: "{{ node_chunk.cidrs }}"
node_ip_count: "{{ node_chunk.ip_count }}"
node_idx: "{{ node_chunk.idx }}"
ec2_instance_id: "{{ wr_ec2.instances[0].instance_id }}"
provider: aws
- name: Show {{ node_chunk.node_name }} ready
debug:
msg: "AWS node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
@@ -0,0 +1,12 @@
---
# FlokiNET provision tasks for WEBRUNNER nodes
# FlokiNET does not expose a public API — manual provisioning required.
- name: FlokiNET not supported for automated WEBRUNNER deployment
fail:
msg: >
FlokiNET does not provide a public provisioning API.
To use FlokiNET nodes with WEBRUNNER, provision VPS instances manually,
add their IPs to the [webrunner_nodes] inventory group, and re-run
the scan plays directly. Node chunk {{ node_chunk.node_name }} assigned
{{ node_chunk.ip_count }} IPs.
@@ -0,0 +1,90 @@
---
# Linode provision tasks for one WEBRUNNER node
# Called in a loop — loop_var: node_chunk
# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id,
# linode_instance_type, linode_region, scanner_ip_log, results_dir
- name: Generate root password for {{ node_chunk.node_name }}
set_fact:
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
- name: Read public key
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey
- name: Register SSH key in Linode ({{ node_chunk.node_name }})
uri:
url: "https://api.linode.com/v4/profile/sshkeys"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ webrunner_name }}"
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
status_code: [200, 201]
ignore_errors: true
- name: Create Linode instance {{ node_chunk.node_name }}
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ node_chunk.node_name }}"
type: "{{ linode_instance_type | default('g6-standard-2') }}"
region: "{{ linode_region | default('us-east') }}"
image: "linode/debian12"
root_pass: "{{ wr_root_pass }}"
authorized_keys:
- "{{ wr_pubkey.content | b64decode | trim }}"
booted: true
backups_enabled: false
private_ip: false
tags:
- "webrunner"
- "{{ webrunner_name }}"
- "{{ deployment_id }}"
status_code: [200, 201]
register: wr_linode
- name: Extract node IP
set_fact:
wr_node_ip: "{{ wr_linode.json.ipv4[0] }}"
- name: Log scanner IP
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
create: true
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
wait_for:
host: "{{ wr_node_ip }}"
port: 22
delay: 30
timeout: 300
- name: Add {{ node_chunk.node_name }} to inventory
add_host:
name: "{{ wr_node_ip }}"
groups: webrunner_nodes
ansible_host: "{{ wr_node_ip }}"
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ node_chunk.node_name }}"
node_cidrs: "{{ node_chunk.cidrs }}"
node_ip_count: "{{ node_chunk.ip_count }}"
node_idx: "{{ node_chunk.idx }}"
linode_instance_id: "{{ wr_linode.json.id }}"
provider: linode
- name: Show {{ node_chunk.node_name }} ready
debug:
msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
+92
View File
@@ -0,0 +1,92 @@
---
# WEBRUNNER — Distributed geo-targeted recon
# Multi-provider: Linode, AWS, FlokiNET
# Controller: ~/tools/c2itall/ (CWD when ansible-playbook runs)
- name: WEBRUNNER — Provision scan nodes
hosts: localhost
connection: local
gather_facts: false
vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}"
ssh_key_name: "{{ ssh_key_path | basename | regex_replace('\\.pub$', '') }}"
all_node_chunks: "{{ lookup('file', node_chunks_file) | from_json }}"
tasks:
- name: Ensure results directory
file:
path: "{{ results_dir }}"
state: directory
mode: '0755'
- name: Initialize scanner IP log
copy:
content: "# WEBRUNNER scanner IPs — {{ webrunner_name }}\n"
dest: "{{ scanner_ip_log }}"
mode: '0644'
force: false
- name: Provision Linode nodes
include_tasks: "Linode/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'linode') | list }}"
loop_var: node_chunk
- name: Provision AWS nodes
include_tasks: "AWS/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | list }}"
loop_var: node_chunk
- name: Provision FlokiNET nodes
include_tasks: "FlokiNET/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'flokinet') | list }}"
loop_var: node_chunk
- name: WEBRUNNER — Configure and scan
hosts: webrunner_nodes
gather_facts: false
become: true
tasks:
- name: Wait for SSH
wait_for_connection:
delay: 20
timeout: 300
- name: Configure node
include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/configure_node.yml"
- name: Run scan
include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/run_scan.yml"
- name: Collect results
include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/collect_results.yml"
- name: WEBRUNNER — Merge and report
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Merge per-node results
command: >
python3 {{ playbook_dir }}/../modules/webrunner/tasks/merge_results.py
--results-dir {{ results_dir }}
--output {{ results_dir }}/merged_results.json
--deployment-id {{ deployment_id }}
register: merge_out
ignore_errors: true
- name: Show merge output
debug:
var: merge_out.stdout_lines
when: merge_out.stdout_lines is defined and merge_out.stdout_lines | length > 0
- name: Summary
debug:
msg:
- "WEBRUNNER complete — {{ webrunner_name }}"
- "Results: {{ results_dir }}/merged_results.json"
- "Scanner IPs: {{ scanner_ip_log }}"
- "Log: logs/deployment_{{ deployment_id }}.log"