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:
@@ -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)"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user