Files
Operator 0799bfbae8 Initial public portfolio release
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.
2026-06-23 16:12:14 -04:00

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