--- # 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