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