Files
CoM-c2itall/providers/webrunner.yml
T
n0mad1k 4c60ba114d Parallelize WEBRUNNER node provisioning
All Linode creates fire simultaneously via async/poll:0 loop instead of
serial include_tasks. One shared root pass + SSH key generated before
the loop eliminates per-node credential races. async_status collects
all create confirmations before add_host. Play 2 runs with strategy:free
and -f 50 so all nodes configure, scan, and collect results concurrently.

Per-node serial provision file retired — logic is now inline.
2026-04-30 23:21:23 -04:00

236 lines
7.9 KiB
YAML

---
# 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 }}"
linode_chunks: "{{ all_node_chunks | selectattr('provider', 'equalto', 'linode') | list }}"
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
# ── Linode — all nodes created in parallel ────────────────────────────────
- block:
- name: Generate shared root password
set_fact:
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
- name: Read SSH public key
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey
- name: Register SSH key with Linode
uri:
url: "https://api.linode.com/v4/profile/sshkeys"
method: POST
headers:
Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ webrunner_name }}"
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
status_code: [200, 201, 422]
ignore_errors: true
- name: Fire all Linode instance creates
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ item.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]
loop: "{{ linode_chunks }}"
async: 300
poll: 0
register: wr_create_jobs
- name: Wait for all creates to confirm
async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ wr_create_jobs.results }}"
register: wr_create_results
until: wr_create_results.finished
retries: 30
delay: 10
- name: Add all Linode nodes to inventory
add_host:
name: "{{ item.json.ipv4[0] }}"
groups: webrunner_nodes
ansible_host: "{{ item.json.ipv4[0] }}"
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ linode_chunks[idx].node_name }}"
node_cidrs: "{{ linode_chunks[idx].cidrs }}"
node_ip_count: "{{ linode_chunks[idx].ip_count }}"
node_idx: "{{ linode_chunks[idx].idx }}"
linode_instance_id: "{{ item.json.id }}"
provider: linode
loop: "{{ wr_create_results.results }}"
loop_control:
index_var: idx
- name: Log scanner IPs
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ linode_chunks[idx].node_name }}: {{ item.json.ipv4[0] }}"
create: true
loop: "{{ wr_create_results.results }}"
loop_control:
index_var: idx
- name: Show provisioned nodes
debug:
msg: "Node ready: {{ linode_chunks[idx].node_name }} @ {{ item.json.ipv4[0] }}"
loop: "{{ wr_create_results.results }}"
loop_control:
index_var: idx
when: linode_chunks | length > 0
# ── AWS / FlokiNET remain serial until those providers are implemented ─────
- name: Provision AWS nodes
include_tasks: "AWS/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | list }}"
loop_control:
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_control:
loop_var: node_chunk
- name: WEBRUNNER — Configure and scan
hosts: webrunner_nodes
gather_facts: false
become: true
strategy: free
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"
- name: WEBRUNNER — Teardown scan nodes
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Teardown Linode nodes
uri:
url: "https://api.linode.com/v4/linode/instances/{{ hostvars[item]['linode_instance_id'] }}"
method: DELETE
headers:
Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}"
status_code: [200, 204]
loop: "{{ groups['webrunner_nodes'] | default([]) }}"
when:
- teardown_after_scan | default(true) | bool
- 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') }}"
state: terminated
loop: "{{ groups['webrunner_nodes'] | default([]) }}"
when:
- teardown_after_scan | default(true) | bool
- 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:
- teardown_after_scan | default(true) | bool
- groups['webrunner_nodes'] is defined
- groups['webrunner_nodes'] | length > 0