0799bfbae8
Sanitized version of red team infrastructure automation platform. Operational content (implant pipelines, lures, credential capture) replaced with documented stubs. Architecture and infrastructure automation code intact.
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
|