78da72e9f6
Each node chunk now carries its own region field, assigned round-robin across the full provider region list unless the user pinned a specific region. AWS uses the same async/poll:0 pattern as Linode but with a serial per-unique-region setup phase (key import + SG) before firing all creates in parallel. Teardown uses hostvars ec2_region per node so multi-region AWS teardowns hit the right endpoint. Infisical replaces interactive credential prompts for both providers. Linode vars.yaml instance type corrected to g6-nanode-1.
345 lines
12 KiB
YAML
345 lines
12 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 }}"
|
|
aws_chunks: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | 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-nanode-1') }}"
|
|
region: "{{ item.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] }} ({{ linode_chunks[idx].region }})"
|
|
loop: "{{ wr_create_results.results }}"
|
|
loop_control:
|
|
index_var: idx
|
|
|
|
when: linode_chunks | length > 0
|
|
|
|
# ── AWS — per-unique-region setup then parallel creates ───────────────────
|
|
|
|
- block:
|
|
- name: Read SSH public key for AWS
|
|
slurp:
|
|
src: "~/.ssh/{{ ssh_key_name }}.pub"
|
|
register: aws_pubkey
|
|
|
|
- name: Get unique AWS regions
|
|
set_fact:
|
|
aws_unique_regions: "{{ aws_chunks | map(attribute='region') | unique | list }}"
|
|
|
|
- name: Import SSH key pair per region
|
|
amazon.aws.ec2_key:
|
|
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
|
|
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
|
|
name: "{{ webrunner_name }}"
|
|
key_material: "{{ aws_pubkey.content | b64decode | trim }}"
|
|
region: "{{ item }}"
|
|
state: present
|
|
loop: "{{ aws_unique_regions }}"
|
|
ignore_errors: true
|
|
|
|
- name: Create security group per region
|
|
amazon.aws.ec2_security_group:
|
|
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
|
|
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
|
|
name: "webrunner-{{ webrunner_name }}"
|
|
description: "WEBRUNNER scan node SG"
|
|
region: "{{ item }}"
|
|
rules:
|
|
- proto: tcp
|
|
ports: [22]
|
|
cidr_ip: "{{ (operator_ip ~ '/32') if operator_ip else '0.0.0.0/0' }}"
|
|
rules_egress:
|
|
- proto: all
|
|
cidr_ip: "0.0.0.0/0"
|
|
state: present
|
|
loop: "{{ aws_unique_regions }}"
|
|
ignore_errors: true
|
|
|
|
- name: Fire all AWS instance creates
|
|
amazon.aws.ec2_instance:
|
|
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
|
|
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
|
|
name: "{{ item.node_name }}"
|
|
instance_type: "{{ aws_instance_type | default('t3.micro') }}"
|
|
region: "{{ item.region }}"
|
|
image_id: "{{ ami_map[item.region] }}"
|
|
key_name: "{{ webrunner_name }}"
|
|
security_groups:
|
|
- "webrunner-{{ webrunner_name }}"
|
|
network:
|
|
assign_public_ip: true
|
|
tags:
|
|
Name: "{{ item.node_name }}"
|
|
webrunner: "{{ webrunner_name }}"
|
|
deployment_id: "{{ deployment_id }}"
|
|
state: running
|
|
wait: true
|
|
wait_timeout: 300
|
|
loop: "{{ aws_chunks }}"
|
|
async: 600
|
|
poll: 0
|
|
register: aws_create_jobs
|
|
|
|
- name: Wait for all AWS creates to confirm
|
|
async_status:
|
|
jid: "{{ item.ansible_job_id }}"
|
|
loop: "{{ aws_create_jobs.results }}"
|
|
register: aws_create_results
|
|
until: aws_create_results.finished
|
|
retries: 60
|
|
delay: 10
|
|
|
|
- name: Add all AWS nodes to inventory
|
|
add_host:
|
|
name: "{{ item.instances[0].public_ip_address }}"
|
|
groups: webrunner_nodes
|
|
ansible_host: "{{ item.instances[0].public_ip_address }}"
|
|
ansible_user: admin
|
|
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
|
|
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
|
|
node_name: "{{ aws_chunks[idx].node_name }}"
|
|
node_cidrs: "{{ aws_chunks[idx].cidrs }}"
|
|
node_ip_count: "{{ aws_chunks[idx].ip_count }}"
|
|
node_idx: "{{ aws_chunks[idx].idx }}"
|
|
ec2_instance_id: "{{ item.instances[0].instance_id }}"
|
|
ec2_region: "{{ aws_chunks[idx].region }}"
|
|
provider: aws
|
|
loop: "{{ aws_create_results.results }}"
|
|
loop_control:
|
|
index_var: idx
|
|
|
|
- name: Log AWS scanner IPs
|
|
lineinfile:
|
|
path: "{{ scanner_ip_log }}"
|
|
line: "{{ aws_chunks[idx].node_name }}: {{ item.instances[0].public_ip_address }}"
|
|
create: true
|
|
loop: "{{ aws_create_results.results }}"
|
|
loop_control:
|
|
index_var: idx
|
|
|
|
- name: Show provisioned AWS nodes
|
|
debug:
|
|
msg: "Node ready: {{ aws_chunks[idx].node_name }} @ {{ item.instances[0].public_ip_address }} ({{ aws_chunks[idx].region }})"
|
|
loop: "{{ aws_create_results.results }}"
|
|
loop_control:
|
|
index_var: idx
|
|
|
|
when: aws_chunks | length > 0
|
|
|
|
# ── FlokiNET ──────────────────────────────────────────────────────────────
|
|
|
|
- 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:
|
|
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
|
|
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
|
|
instance_ids:
|
|
- "{{ hostvars[item]['ec2_instance_id'] }}"
|
|
region: "{{ hostvars[item]['ec2_region'] }}"
|
|
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
|