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.
This commit is contained in:
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Default configurations
|
||||
DEFAULT_IMAGE_FILTER="*kali-last-snapshot*"
|
||||
DEFAULT_OWNER_ID="679593333241"
|
||||
DEFAULT_REGION="us-east-1"
|
||||
|
||||
# Debugging flag
|
||||
DEBUG=false
|
||||
|
||||
# Usage function
|
||||
function usage() {
|
||||
echo "Usage: $0 [--image-filter <filter>] [--owner-id <owner-id>] [--debug] [--help]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --image-filter <filter> Filter for AMI names (default: '${DEFAULT_IMAGE_FILTER}')."
|
||||
echo " --owner-id <owner-id> Owner ID for filtering AMIs (default: '${DEFAULT_OWNER_ID}')."
|
||||
echo " --debug Enable verbose debugging."
|
||||
echo " --help Display this help message."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
IMAGE_FILTER="$DEFAULT_IMAGE_FILTER"
|
||||
OWNER_ID="$DEFAULT_OWNER_ID"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--image-filter)
|
||||
IMAGE_FILTER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--owner-id)
|
||||
OWNER_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--debug)
|
||||
DEBUG=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if $DEBUG; then
|
||||
echo "DEBUG: Using image filter: $IMAGE_FILTER"
|
||||
echo "DEBUG: Using owner ID: $OWNER_ID"
|
||||
fi
|
||||
|
||||
# Step 1: Fetch all AMIs in the default region to identify the latest version
|
||||
LATEST_AMI=""
|
||||
LATEST_YEAR=0
|
||||
LATEST_VERSION=0
|
||||
|
||||
AMI_LIST=$(aws ec2 describe-images \
|
||||
--region "$DEFAULT_REGION" \
|
||||
--filters "Name=name,Values=$IMAGE_FILTER" "Name=owner-id,Values=$OWNER_ID" \
|
||||
--query "Images[].[Name]" \
|
||||
--output text)
|
||||
|
||||
if $DEBUG; then
|
||||
echo "DEBUG: AMI list in $DEFAULT_REGION: $AMI_LIST"
|
||||
fi
|
||||
|
||||
for AMI_NAME in $AMI_LIST; do
|
||||
if [[ $AMI_NAME != *"-prod-"* ]]; then
|
||||
# Extract year and version using regex
|
||||
if [[ $AMI_NAME =~ ([0-9]{4})\.([0-9]+)\.([0-9]+) ]]; then
|
||||
YEAR=${BASH_REMATCH[1]}
|
||||
VERSION=${BASH_REMATCH[2]}
|
||||
|
||||
if $DEBUG; then
|
||||
echo "DEBUG: Checking AMI: $AMI_NAME (Year: $YEAR, Version: $VERSION)"
|
||||
fi
|
||||
|
||||
if (( YEAR > LATEST_YEAR )) || (( YEAR == LATEST_YEAR && VERSION > LATEST_VERSION )); then
|
||||
LATEST_AMI="$AMI_NAME"
|
||||
LATEST_YEAR=$YEAR
|
||||
LATEST_VERSION=$VERSION
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if $DEBUG; then
|
||||
echo "DEBUG: Latest AMI determined: $LATEST_AMI"
|
||||
fi
|
||||
|
||||
# Step 2: Use the latest AMI name to filter across all regions
|
||||
if [[ -z "$LATEST_AMI" ]]; then
|
||||
echo "No valid AMIs found matching the criteria."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_FILTER_LATEST="${LATEST_AMI%-*}*" # Strip the region-specific suffix and add a wildcard
|
||||
|
||||
if $DEBUG; then
|
||||
echo "DEBUG: Using refined image filter: $IMAGE_FILTER_LATEST"
|
||||
fi
|
||||
|
||||
# Step 3: Fetch AMIs across all regions
|
||||
REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
|
||||
|
||||
echo "Fetching AMIs with filter '$IMAGE_FILTER_LATEST' and owner ID '$OWNER_ID'..."
|
||||
|
||||
AMI_MAP=""
|
||||
REGION_LIST=()
|
||||
|
||||
for REGION in $REGIONS; do
|
||||
if $DEBUG; then
|
||||
echo "DEBUG: Querying region: $REGION"
|
||||
fi
|
||||
|
||||
AMI_INFO=$(aws ec2 describe-images \
|
||||
--region "$REGION" \
|
||||
--filters "Name=name,Values=$IMAGE_FILTER_LATEST" "Name=owner-id,Values=$OWNER_ID" \
|
||||
--query "Images[].[Name,ImageId]" \
|
||||
--output text)
|
||||
|
||||
if [[ -n "$AMI_INFO" ]]; then
|
||||
while read -r NAME AMI_ID; do
|
||||
echo "$NAME"
|
||||
echo " $REGION: $AMI_ID"
|
||||
REGION_LIST+=("$REGION")
|
||||
AMI_MAP+="$REGION: $AMI_ID"$'\n'
|
||||
done <<< "$AMI_INFO"
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 4: Generate YAML
|
||||
YAML_OUTPUT="aws_region_choices:\n"
|
||||
for REGION in "${REGION_LIST[@]}"; do
|
||||
YAML_OUTPUT+=" - $REGION\n"
|
||||
done
|
||||
YAML_OUTPUT+="ami_map:\n$AMI_MAP"
|
||||
|
||||
echo -e "\nGenerated YAML:\n$YAML_OUTPUT"
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
# AWS Attack Box Deployment Playbook
|
||||
# Based on attk-box-setup but optimized for AWS deployment
|
||||
|
||||
- name: Deploy Attack Box on AWS
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
deployment_type: "attack_box"
|
||||
attack_box_name: "{{ attack_box_name | default('a-' + deployment_id) }}"
|
||||
attack_box_type: "{{ attack_box_type | default('kali') }}"
|
||||
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
|
||||
instance_type: "{{ aws_instance_type | default('t2.medium') }}"
|
||||
ssh_key_name: "attack-box-{{ deployment_id }}"
|
||||
|
||||
# Attack box AMI mapping (use Kali Linux AMIs)
|
||||
attack_box_ami_map:
|
||||
us-east-1: "{{ kali_ami_map['us-east-1'] | default('ami-061b17d332829ab1c') }}"
|
||||
us-east-2: "{{ kali_ami_map['us-east-2'] | default('ami-061b17d332829ab1c') }}"
|
||||
us-west-1: "{{ kali_ami_map['us-west-1'] | default('ami-061b17d332829ab1c') }}"
|
||||
us-west-2: "{{ kali_ami_map['us-west-2'] | default('ami-061b17d332829ab1c') }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate AWS credentials
|
||||
assert:
|
||||
that:
|
||||
- aws_access_key is defined and aws_access_key != ""
|
||||
- aws_secret_key is defined and aws_secret_key != ""
|
||||
fail_msg: "AWS credentials are required"
|
||||
|
||||
- name: Debug attack box deployment
|
||||
debug:
|
||||
msg: "Deploying {{ attack_box_type }} attack box on AWS in {{ aws_region }}"
|
||||
|
||||
- name: Set attack box AMI
|
||||
set_fact:
|
||||
attack_box_ami: "{{ attack_box_ami_map[aws_region] | default(attack_box_ami_map['us-east-1']) }}"
|
||||
|
||||
- name: Generate SSH key pair for attack box
|
||||
ec2_key:
|
||||
name: "{{ ssh_key_name }}"
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
state: present
|
||||
register: ec2_key_result
|
||||
|
||||
- name: Save private key locally
|
||||
copy:
|
||||
content: "{{ ec2_key_result.key.private_key }}"
|
||||
dest: "~/.ssh/{{ ssh_key_name }}"
|
||||
mode: '0600'
|
||||
when: ec2_key_result.key.private_key is defined
|
||||
|
||||
- name: Create security group for attack box
|
||||
ec2_group:
|
||||
name: "attack-box-sg-{{ deployment_id }}"
|
||||
description: "Security group for attack box {{ deployment_id }}"
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
rules:
|
||||
- proto: tcp
|
||||
ports:
|
||||
- 22
|
||||
cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32"
|
||||
rule_desc: "SSH access from operator IP"
|
||||
- proto: tcp
|
||||
ports:
|
||||
- 80
|
||||
- 443
|
||||
cidr_ip: 0.0.0.0/0
|
||||
rule_desc: "HTTP/HTTPS for tools"
|
||||
rules_egress:
|
||||
- proto: all
|
||||
cidr_ip: 0.0.0.0/0
|
||||
tags:
|
||||
Name: "attack-box-sg-{{ deployment_id }}"
|
||||
DeploymentID: "{{ deployment_id }}"
|
||||
Type: "attack-box"
|
||||
register: security_group
|
||||
|
||||
- name: Launch attack box EC2 instance
|
||||
ec2:
|
||||
key_name: "{{ ssh_key_name }}"
|
||||
group: "{{ security_group.group_name }}"
|
||||
instance_type: "{{ instance_type }}"
|
||||
image: "{{ attack_box_ami }}"
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
wait: true
|
||||
count: 1
|
||||
instance_tags:
|
||||
Name: "{{ attack_box_name }}"
|
||||
DeploymentID: "{{ deployment_id }}"
|
||||
Type: "attack-box"
|
||||
Environment: "{{ attack_box_type }}"
|
||||
user_data: |
|
||||
#!/bin/bash
|
||||
# Update system
|
||||
apt-get update
|
||||
apt-get upgrade -y
|
||||
|
||||
# Install additional tools if needed
|
||||
{% if attack_box_type == 'kali' %}
|
||||
# Kali already has most tools
|
||||
apt-get install -y nmap curl wget git
|
||||
{% else %}
|
||||
# Install basic pentest tools for other distros
|
||||
apt-get install -y nmap curl wget git python3-pip
|
||||
{% endif %}
|
||||
|
||||
# Set hostname
|
||||
echo "{{ attack_box_name }}" > /etc/hostname
|
||||
hostname "{{ attack_box_name }}"
|
||||
|
||||
# Create deployment info
|
||||
mkdir -p /root/deployment-info
|
||||
cat > /root/deployment-info/info.txt << EOF
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Attack Box Name: {{ attack_box_name }}
|
||||
Attack Box Type: {{ attack_box_type }}
|
||||
Region: {{ aws_region }}
|
||||
Instance Type: {{ instance_type }}
|
||||
SSH Key: {{ ssh_key_name }}
|
||||
EOF
|
||||
|
||||
register: ec2_result
|
||||
|
||||
- name: Wait for SSH to be available
|
||||
wait_for:
|
||||
host: "{{ ec2_result.instances[0].public_ip }}"
|
||||
port: 22
|
||||
delay: 60
|
||||
timeout: 300
|
||||
|
||||
- name: Display attack box information
|
||||
debug:
|
||||
msg:
|
||||
- "Attack box deployed successfully!"
|
||||
- "Instance ID: {{ ec2_result.instances[0].id }}"
|
||||
- "Public IP: {{ ec2_result.instances[0].public_ip }}"
|
||||
- "Private IP: {{ ec2_result.instances[0].private_ip }}"
|
||||
- "SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ ec2_result.instances[0].public_ip }}"
|
||||
|
||||
- name: Save deployment information
|
||||
copy:
|
||||
content: |
|
||||
# Attack Box Deployment Information
|
||||
Instance ID: {{ ec2_result.instances[0].id }}
|
||||
Public IP: {{ ec2_result.instances[0].public_ip }}
|
||||
Private IP: {{ ec2_result.instances[0].private_ip }}
|
||||
SSH Key: ~/.ssh/{{ ssh_key_name }}
|
||||
SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ ec2_result.instances[0].public_ip }}
|
||||
Region: {{ aws_region }}
|
||||
Instance Type: {{ instance_type }}
|
||||
AMI: {{ attack_box_ami }}
|
||||
Security Group: {{ security_group.group_name }}
|
||||
dest: "logs/attack_box_{{ deployment_id }}_info.txt"
|
||||
|
||||
- name: Set attack box facts for other playbooks
|
||||
set_fact:
|
||||
attack_box_instance_id: "{{ ec2_result.instances[0].id }}"
|
||||
attack_box_public_ip: "{{ ec2_result.instances[0].public_ip }}"
|
||||
attack_box_private_ip: "{{ ec2_result.instances[0].private_ip }}"
|
||||
attack_box_security_group: "{{ security_group.group_name }}"
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
# AWS-specific phishing infrastructure tasks
|
||||
|
||||
- name: Set AWS-specific variables
|
||||
set_fact:
|
||||
region: "{{ aws_region | default('us-east-1') }}"
|
||||
instance_type_map:
|
||||
gophish: "{{ gophish_instance_type | default('t3.large') }}"
|
||||
mta_front: "{{ mta_instance_type | default('t3.medium') }}"
|
||||
redirector: "{{ redirector_instance_type | default('t3.small') }}"
|
||||
webserver: "{{ webserver_instance_type | default('t3.medium') }}"
|
||||
|
||||
- name: Create security group for phishing infrastructure
|
||||
debug:
|
||||
msg: "Would create security group: phishing-{{ deployment_id }}"
|
||||
|
||||
- name: Deploy Gophish server
|
||||
debug:
|
||||
msg: |
|
||||
Would deploy Gophish server:
|
||||
- Instance type: {{ instance_type_map.gophish }}
|
||||
- Region: {{ region }}
|
||||
- Name: gophish-{{ deployment_id }}
|
||||
- Framework: gophish
|
||||
when: "'gophish' in deployment_components"
|
||||
|
||||
- name: Deploy MTA Front server
|
||||
debug:
|
||||
msg: |
|
||||
Would deploy MTA Front server:
|
||||
- Instance type: {{ instance_type_map.mta_front }}
|
||||
- Region: {{ region }}
|
||||
- Name: mta-{{ deployment_id }}
|
||||
- Hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }}
|
||||
when: "'mta_front' in deployment_components"
|
||||
|
||||
- name: Deploy Phishing Redirector
|
||||
debug:
|
||||
msg: |
|
||||
Would deploy Phishing Redirector:
|
||||
- Instance type: {{ instance_type_map.redirector }}
|
||||
- Region: {{ region }}
|
||||
- Name: redirector-{{ deployment_id }}
|
||||
when: "'redirector' in deployment_components"
|
||||
|
||||
- name: Deploy Phishing Webserver
|
||||
debug:
|
||||
msg: |
|
||||
Would deploy Phishing Webserver:
|
||||
- Instance type: {{ instance_type_map.webserver }}
|
||||
- Region: {{ region }}
|
||||
- Name: web-{{ deployment_id }}
|
||||
when: "'webserver' in deployment_components"
|
||||
|
||||
- name: Set deployment results
|
||||
set_fact:
|
||||
phishing_deployment_results:
|
||||
gophish_ip: "{{ '10.0.0.10' if 'gophish' in deployment_components else '' }}"
|
||||
mta_ip: "{{ '10.0.0.11' if 'mta_front' in deployment_components else '' }}"
|
||||
redirector_ip: "{{ '10.0.0.12' if 'redirector' in deployment_components else '' }}"
|
||||
webserver_ip: "{{ '10.0.0.13' if 'webserver' in deployment_components else '' }}"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
domain: "{{ phishing_domain | default(domain) }}"
|
||||
@@ -0,0 +1,45 @@
|
||||
aws_access_key: "YOUR_AWS_ACCESS_KEY" # Your AWS access key
|
||||
aws_secret_key: "YOUR_AWS_SECRET_KEY" # Your AWS secret key
|
||||
aws_region_choices:
|
||||
- ap-south-1
|
||||
- eu-north-1
|
||||
- eu-west-3
|
||||
- eu-west-2
|
||||
- eu-west-1
|
||||
- ap-northeast-3
|
||||
- ap-northeast-2
|
||||
- ap-northeast-1
|
||||
- ca-central-1
|
||||
- sa-east-1
|
||||
- ap-southeast-1
|
||||
- ap-southeast-2
|
||||
- eu-central-1
|
||||
- us-east-1
|
||||
- us-east-2
|
||||
- us-west-1
|
||||
- us-west-2
|
||||
ami_map:
|
||||
ap-south-1: ami-0eeeb93aa51c48595
|
||||
eu-north-1: ami-05bb943edc7d12d2f
|
||||
eu-west-3: ami-01c1cbe631d766dcd
|
||||
eu-west-2: ami-0a9aba19a0b8e81da
|
||||
eu-west-1: ami-05b908c468c3a5373
|
||||
ap-northeast-3: ami-03809b00a4487dc46
|
||||
ap-northeast-2: ami-048f3574b7d304c04
|
||||
ap-northeast-1: ami-0b74305a62f8299e1
|
||||
ca-central-1: ami-0415ef7b9c3019285
|
||||
sa-east-1: ami-0ab7401488d50bf51
|
||||
ap-southeast-1: ami-0d2d12d390e9c0a34
|
||||
ap-southeast-2: ami-0bd344ea1f492feab
|
||||
eu-central-1: ami-093d1ceb3279619b0
|
||||
us-east-1: ami-061b17d332829ab1c
|
||||
us-east-2: ami-0327cf1c5e479e093
|
||||
us-west-1: ami-0fbe3a8e1dcd86f23
|
||||
us-west-2: ami-030d7e8d6fbca8332
|
||||
aws_instance_type: "t2.medium" # EC2 instance type
|
||||
domain: "example.com"
|
||||
mail_hostname: "mail.example.com"
|
||||
letsencrypt_email: "admin@example.com"
|
||||
smtp_auth_user: "phishuser"
|
||||
smtp_auth_pass: "SuperSecretPass123!"
|
||||
gophish_admin_port: "2222"
|
||||
@@ -0,0 +1,518 @@
|
||||
---
|
||||
# AWS C2 Server Deployment Playbook
|
||||
|
||||
- name: Deploy AWS C2 server
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Default values
|
||||
ssh_user: "{{ ssh_user | default('kali') }}"
|
||||
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
|
||||
instance_type: "{{ aws_instance_type | default('t2.medium') }}"
|
||||
deployment_id: "{{ deployment_id | default('') }}"
|
||||
c2_name: "{{ c2_name | default('s-' + deployment_id) }}"
|
||||
# Define split_regions - only true when regions are explicitly different
|
||||
split_regions: "{{ c2_region is defined and redirector_region is defined and c2_region != redirector_region }}"
|
||||
# Only use shared infra when NOT doing split-region deployment
|
||||
use_shared_infra: "{{ not split_regions and not c2_only | default(false) | bool and not redirector_only | default(false) | bool }}"
|
||||
# Set correct region variable
|
||||
aws_c2_region: "{{ c2_region | default(aws_region) }}"
|
||||
# AMI map comes from vars.yaml - add fallback for safety
|
||||
kali_ami_map_fallback:
|
||||
us-east-1: "ami-061b17d332829ab1c"
|
||||
us-east-2: "ami-061b17d332829ab1c" # Fallback to us-east-1 AMI
|
||||
|
||||
tasks:
|
||||
- name: Validate AWS credentials
|
||||
assert:
|
||||
that:
|
||||
- aws_access_key is defined and aws_access_key != ""
|
||||
- aws_secret_key is defined and aws_secret_key != ""
|
||||
fail_msg: "AWS credentials are required"
|
||||
|
||||
# Load shared infrastructure state if available
|
||||
- name: Check for shared infrastructure state
|
||||
stat:
|
||||
path: "infrastructure_state_{{ deployment_id }}.json"
|
||||
register: infra_state_file
|
||||
when: use_shared_infra | bool
|
||||
|
||||
- name: Load shared infrastructure state
|
||||
include_vars:
|
||||
file: "infrastructure_state_{{ deployment_id }}.json"
|
||||
name: shared_infra
|
||||
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
|
||||
|
||||
- name: Set region for C2
|
||||
set_fact:
|
||||
aws_c2_region: "{{ shared_infra.region | default(aws_region) }}"
|
||||
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
|
||||
|
||||
- name: Set default region for C2
|
||||
set_fact:
|
||||
aws_c2_region: "{{ c2_region | default(aws_region) }}"
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Check if ami_map is provided in vars.yaml
|
||||
debug:
|
||||
msg: "ami_map is {{ 'defined' if ami_map is defined else 'NOT defined' }} in vars.yaml"
|
||||
|
||||
- name: Set AMI ID for selected region (from vars.yaml)
|
||||
set_fact:
|
||||
ami_id: "{{ ami_map[aws_c2_region] | default(ami_map.us-east-1) }}"
|
||||
when: ami_map is defined and ami_map
|
||||
|
||||
- name: Set AMI ID for selected region (fallback)
|
||||
set_fact:
|
||||
ami_id: "{{ kali_ami_map_fallback[aws_c2_region] | default(kali_ami_map_fallback['us-east-1']) }}"
|
||||
when: ami_id is not defined or ami_id == ""
|
||||
|
||||
- name: Ensure we have a valid AMI ID
|
||||
assert:
|
||||
that:
|
||||
- ami_id is defined and ami_id != ""
|
||||
fail_msg: "Could not determine a valid AMI ID for region {{ aws_c2_region }}. Please add it to ami_map in vars.yaml."
|
||||
|
||||
# Add AMI username mapping - improved with better detection
|
||||
- name: Determine correct SSH user for the AMI
|
||||
set_fact:
|
||||
ami_ssh_user: "{{ 'kali' if (ami_id is defined and ami_id is search('-kali-')) or (ami_id is defined and ami_id == 'ami-061b17d332829ab1c') else 'ubuntu' }}"
|
||||
|
||||
- name: Display AMI and user information for debugging
|
||||
debug:
|
||||
msg:
|
||||
- "Using AMI ID: {{ ami_id | default('AMI not defined') }}"
|
||||
- "Detected SSH user: {{ ami_ssh_user }}"
|
||||
|
||||
# Create new infrastructure only if not using shared
|
||||
- name: Create VPC
|
||||
amazon.aws.ec2_vpc_net:
|
||||
name: "{{ c2_name }}-vpc"
|
||||
cidr_block: "10.0.0.0/16"
|
||||
region: "{{ aws_c2_region }}"
|
||||
tags:
|
||||
Name: "{{ c2_name }}-vpc"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
state: present
|
||||
register: vpc_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Create internet gateway for VPC
|
||||
amazon.aws.ec2_vpc_igw:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
region: "{{ aws_c2_region }}"
|
||||
state: present
|
||||
tags:
|
||||
Name: "{{ c2_name }}-igw"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: igw_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Create subnet in VPC
|
||||
amazon.aws.ec2_vpc_subnet:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
cidr: "10.0.1.0/24"
|
||||
region: "{{ aws_c2_region }}"
|
||||
az: "{{ aws_c2_region }}a"
|
||||
map_public: yes
|
||||
tags:
|
||||
Name: "{{ c2_name }}-subnet"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: subnet_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Create routing table for internet access
|
||||
amazon.aws.ec2_vpc_route_table:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
region: "{{ aws_c2_region }}"
|
||||
tags:
|
||||
Name: "{{ c2_name }}-rtb"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
routes:
|
||||
- dest: "0.0.0.0/0"
|
||||
gateway_id: "{{ igw_result.gateway_id }}"
|
||||
subnets:
|
||||
- "{{ subnet_result.subnet.id }}"
|
||||
register: route_table_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
# Set VPC ID based on shared or created
|
||||
- name: Set VPC ID from shared infrastructure
|
||||
set_fact:
|
||||
vpc_id: "{{ shared_infra.vpc_id }}"
|
||||
subnet_id: "{{ shared_infra.subnet_id }}"
|
||||
c2_vpc_id: "{{ shared_infra.vpc_id }}" # Store for cleanup reference
|
||||
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
|
||||
|
||||
- name: Set VPC ID from created infrastructure
|
||||
set_fact:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
subnet_id: "{{ subnet_result.subnet.id }}"
|
||||
c2_vpc_id: "{{ vpc_result.vpc.id }}" # Store for cleanup reference
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Set default redirector_ip for C2-only deployments
|
||||
set_fact:
|
||||
redirector_ip: "{{ operator_ip }}"
|
||||
when: c2_only | default(false) | bool and redirector_ip is not defined
|
||||
|
||||
- name: Load redirector_ip from infrastructure state
|
||||
block:
|
||||
- name: Check if infrastructure state file exists
|
||||
stat:
|
||||
path: "infrastructure_state_{{ deployment_id }}.json"
|
||||
register: infra_state_check
|
||||
|
||||
- name: Load infrastructure state
|
||||
include_vars:
|
||||
file: "infrastructure_state_{{ deployment_id }}.json"
|
||||
name: infra_state
|
||||
when: infra_state_check.stat.exists
|
||||
|
||||
- name: Set redirector IP from infrastructure state
|
||||
set_fact:
|
||||
redirector_ip: "{{ infra_state.redirector_ip | default(operator_ip) }}"
|
||||
when: infra_state_check.stat.exists and infra_state.redirector_ip is defined
|
||||
|
||||
- name: Debug redirector IP
|
||||
debug:
|
||||
msg: "Using redirector IP: {{ redirector_ip | default('undefined') }}"
|
||||
when: redirector_ip is undefined
|
||||
|
||||
- name: Default to operator IP if redirector IP is still undefined
|
||||
set_fact:
|
||||
redirector_ip: "{{ operator_ip }}"
|
||||
when: redirector_ip is undefined
|
||||
|
||||
- name: Create security group for C2 server
|
||||
amazon.aws.ec2_security_group:
|
||||
name: "{{ c2_name }}-sg"
|
||||
description: "Secured C2 server {{ c2_name }}"
|
||||
vpc_id: "{{ vpc_id }}"
|
||||
region: "{{ aws_c2_region }}"
|
||||
rules:
|
||||
# Management access only from operator IP
|
||||
- proto: tcp
|
||||
ports: 22
|
||||
cidr_ip: "{{ operator_ip }}/32"
|
||||
- proto: tcp
|
||||
ports: "{{ havoc_teamserver_port | default(40056) }}"
|
||||
cidr_ip: "{{ operator_ip }}/32"
|
||||
# Allow traffic only from redirector
|
||||
- proto: tcp
|
||||
ports:
|
||||
- 80
|
||||
- 443
|
||||
- "{{ havoc_http_port | default(8080) }}"
|
||||
- "{{ havoc_https_port | default(9443) }}" # Updated from 443
|
||||
- "{{ havoc_payload_port | default(8443) }}"
|
||||
- "{{ gophish_admin_port | default(2222) }}"
|
||||
- "{{ gophish_phish_port | default(8081) }}"
|
||||
- "{{ tracker_port | default(5000) }}"
|
||||
cidr_ip: "{{ redirector_ip }}/32"
|
||||
rules_egress:
|
||||
- proto: -1
|
||||
cidr_ip: 0.0.0.0/0
|
||||
state: present
|
||||
register: security_group
|
||||
|
||||
# Generate or import SSH key for the deployment - FIXED KEY HANDLING
|
||||
- name: Check if deployment SSH key already exists locally
|
||||
stat:
|
||||
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
register: ssh_key_file
|
||||
|
||||
- name: Generate key pair if it doesn't exist
|
||||
block:
|
||||
- name: Create SSH key pair
|
||||
command: ssh-keygen -t rsa -b 2048 -f ~/.ssh/c2deploy_{{ deployment_id }} -N ""
|
||||
args:
|
||||
creates: "~/.ssh/c2deploy_{{ deployment_id }}"
|
||||
|
||||
- name: Rename private key to .pem format
|
||||
command: mv ~/.ssh/c2deploy_{{ deployment_id }} ~/.ssh/c2deploy_{{ deployment_id }}.pem
|
||||
args:
|
||||
creates: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
removes: "~/.ssh/c2deploy_{{ deployment_id }}"
|
||||
when: not ssh_key_file.stat.exists
|
||||
|
||||
- name: Ensure proper permissions on SSH key
|
||||
file:
|
||||
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
mode: '0600'
|
||||
state: file
|
||||
|
||||
- name: Check if key pair exists in AWS
|
||||
amazon.aws.ec2_key_info:
|
||||
region: "{{ aws_c2_region }}"
|
||||
filters:
|
||||
key-name: "c2deploy_{{ deployment_id }}"
|
||||
register: existing_key_pair
|
||||
|
||||
- name: Import SSH key to AWS
|
||||
amazon.aws.ec2_key:
|
||||
name: "c2deploy_{{ deployment_id }}"
|
||||
key_material: "{{ lookup('file', '~/.ssh/c2deploy_{{ deployment_id }}.pub') }}"
|
||||
region: "{{ aws_c2_region }}"
|
||||
state: present
|
||||
when: existing_key_pair.keypairs | length == 0
|
||||
|
||||
# Launch the C2 server - use the consistent key pair name
|
||||
- name: Launch C2 instance
|
||||
amazon.aws.ec2_instance:
|
||||
name: "{{ c2_name }}"
|
||||
key_name: "c2deploy_{{ deployment_id }}" # Use the imported key
|
||||
instance_type: "{{ instance_type | default('t2.medium') }}"
|
||||
vpc_subnet_id: "{{ subnet_id }}"
|
||||
security_groups:
|
||||
- "{{ security_group.group_id }}"
|
||||
image_id: "{{ ami_id }}"
|
||||
region: "{{ aws_c2_region }}"
|
||||
state: present
|
||||
wait: yes
|
||||
volumes:
|
||||
- device_name: "/dev/xvda"
|
||||
ebs:
|
||||
volume_size: 100
|
||||
delete_on_termination: true
|
||||
tags:
|
||||
Name: "{{ c2_name }}"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: c2_instance
|
||||
|
||||
- name: Set c2_ip for later use
|
||||
set_fact:
|
||||
c2_ip: "{{ c2_instance.instances[0].public_ip_address }}"
|
||||
c2_instance_id: "{{ c2_instance.instances[0].instance_id }}"
|
||||
|
||||
- name: Display C2 instance details for debugging
|
||||
debug:
|
||||
msg:
|
||||
- "C2 IP: {{ c2_ip }}"
|
||||
- "C2 Instance ID: {{ c2_instance_id }}"
|
||||
- "SSH User to use: {{ ami_ssh_user }}"
|
||||
- "SSH Key path: ~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
|
||||
- name: Wait for C2 instance initialization
|
||||
pause:
|
||||
seconds: 180
|
||||
when: c2_instance.changed
|
||||
|
||||
- name: Set correct permissions on SSH key
|
||||
file:
|
||||
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
mode: "0600"
|
||||
|
||||
- name: Wait for C2 SSH to be available
|
||||
wait_for:
|
||||
host: "{{ c2_ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
state: started
|
||||
|
||||
- name: Test SSH and prepare remote environment
|
||||
block:
|
||||
- name: Ensure .ansible directory exists with proper permissions
|
||||
shell: |
|
||||
ssh -i ~/.ssh/c2deploy_{{ deployment_id }}.pem -o StrictHostKeyChecking=no {{ ami_ssh_user }}@{{ c2_ip }} "sudo mkdir -p /root/.ansible/tmp && sudo chmod 0700 /root/.ansible/tmp && sudo chown {{ ami_ssh_user }}:{{ ami_ssh_user }} /root/.ansible/tmp"
|
||||
register: ssh_prep
|
||||
until: ssh_prep is success
|
||||
retries: 5
|
||||
delay: 15
|
||||
ignore_errors: yes
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Display SSH preparation results
|
||||
debug:
|
||||
msg: "SSH preparation completed: {{ ssh_prep.stdout | default('No output') }}"
|
||||
|
||||
# Test SSH connection directly to verify key is working
|
||||
- name: Test SSH connection to verify key
|
||||
shell: "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i ~/.ssh/c2deploy_{{ deployment_id }}.pem {{ ami_ssh_user }}@{{ c2_ip }} 'echo SSH CONNECTION SUCCESSFUL'"
|
||||
register: ssh_test
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display SSH test results
|
||||
debug:
|
||||
msg: "{{ ssh_test.stdout | default('SSH Connection failed!') }}"
|
||||
|
||||
- name: Add C2 to inventory with updated SSH key path
|
||||
add_host:
|
||||
name: "c2"
|
||||
groups: "c2servers"
|
||||
ansible_host: "{{ c2_ip }}"
|
||||
ansible_user: "{{ ami_ssh_user }}"
|
||||
ansible_ssh_private_key_file: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes"
|
||||
ansible_python_interpreter: "/usr/bin/python3"
|
||||
# Add these lines to pass all required variables:
|
||||
smtp_auth_user: "{{ smtp_auth_user }}"
|
||||
smtp_auth_pass: "{{ smtp_auth_pass }}"
|
||||
gophish_admin_port: "{{ gophish_admin_port }}"
|
||||
domain: "{{ domain }}"
|
||||
redirector_subdomain: "{{ redirector_subdomain }}"
|
||||
letsencrypt_email: "{{ letsencrypt_email }}"
|
||||
havoc_teamserver_port: "{{ havoc_teamserver_port | default(40056) }}"
|
||||
havoc_http_port: "{{ havoc_http_port | default(8080) }}"
|
||||
havoc_https_port: "{{ havoc_https_port | default(9443) }}"
|
||||
zero_logs: "{{ zero_logs | default(true) }}"
|
||||
secure_memory: "{{ secure_memory | default(true) }}"
|
||||
disable_history: "{{ disable_history | default(true) }}"
|
||||
setup_integrated_tracker: "{{ setup_integrated_tracker | default(false) }}"
|
||||
tracker_domain: "{{ tracker_domain | default('track.' + domain) | default('') }}"
|
||||
|
||||
# Configure C2 server with a proper structure
|
||||
- name: Configure C2 server
|
||||
hosts: c2servers
|
||||
become: yes
|
||||
become_user: root
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml # Add this line to load the variables
|
||||
vars:
|
||||
redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}"
|
||||
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
|
||||
tasks:
|
||||
- name: Install python3 if it doesn't exist on target
|
||||
raw: test -e /usr/bin/python3 || (apt-get update && apt-get install -y python3)
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: python_install
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Debug connection information
|
||||
debug:
|
||||
msg:
|
||||
- "Connected to C2 server successfully"
|
||||
- "Host: {{ ansible_host }}"
|
||||
- "User: {{ ansible_user }}"
|
||||
- "Python version: {{ ansible_python_version | default('unknown') }}"
|
||||
|
||||
- name: Download Kali archive keyring to temporary location
|
||||
get_url:
|
||||
url: https://archive.kali.org/archive-keyring.gpg
|
||||
dest: /tmp/kali-archive-keyring.gpg
|
||||
mode: "0644"
|
||||
force: yes
|
||||
register: keyring_download
|
||||
|
||||
- name: Install Kali archive keyring
|
||||
command: install -m 0644 /tmp/kali-archive-keyring.gpg /usr/share/keyrings/kali-archive-keyring.gpg
|
||||
when: keyring_download is changed
|
||||
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 10
|
||||
delay: 10
|
||||
|
||||
- name: Install Rust compiler
|
||||
apt:
|
||||
name:
|
||||
- cargo
|
||||
- rustc
|
||||
- libssl-dev
|
||||
- pkg-config
|
||||
state: present
|
||||
register: rust_install
|
||||
until: rust_install is success
|
||||
retries: 3
|
||||
delay: 5
|
||||
|
||||
- name: Clean up any failed pipx installations
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
with_items:
|
||||
- "/root/.local/state/pipx/venvs/netexec"
|
||||
- "/root/.local/state/pipx/venvs/trevorspray"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Include common tool installation tasks
|
||||
include_tasks: "../../common/tasks/install_tools.yml"
|
||||
|
||||
- name: Include common C2 configuration tasks
|
||||
include_tasks: "../../modules/c2/tasks/configure_c2.yml"
|
||||
|
||||
- name: Include common security hardening tasks
|
||||
include_tasks: "../../common/tasks/security_hardening.yml"
|
||||
|
||||
- name: Include common mail server configuration tasks
|
||||
include_tasks: "../../common/tasks/configure_mail.yml"
|
||||
|
||||
- name: Set up SSH access to redirector
|
||||
block:
|
||||
- name: Ensure /root/.ssh directory exists on C2 server
|
||||
file:
|
||||
path: /root/.ssh
|
||||
state: directory
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Copy private SSH key to C2 server for redirector access
|
||||
copy:
|
||||
src: "{{ playbook_dir }}/../.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
|
||||
dest: "/root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
|
||||
mode: '0600'
|
||||
owner: root
|
||||
group: root
|
||||
register: key_copy
|
||||
ignore_errors: yes
|
||||
|
||||
- name: If direct path fails, try home directory location
|
||||
copy:
|
||||
src: "~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
|
||||
dest: "/root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
|
||||
mode: '0600'
|
||||
owner: root
|
||||
group: root
|
||||
when: key_copy is failed
|
||||
|
||||
- name: Create SSH config file to use key automatically
|
||||
copy:
|
||||
dest: "/root/.ssh/config"
|
||||
content: |
|
||||
Host redirector
|
||||
HostName {{ hostvars['localhost']['redirector_ip'] }}
|
||||
User ubuntu
|
||||
IdentityFile /root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
mode: '0600'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Create alias for easy redirector access
|
||||
lineinfile:
|
||||
path: /root/.bashrc
|
||||
line: 'alias redirector="ssh -i /root/.ssh/c2deploy_{{ hostvars["localhost"]["deployment_id"] }}.pem ubuntu@{{ hostvars["localhost"]["redirector_ip"] }}"'
|
||||
state: present
|
||||
|
||||
- name: Test SSH from C2 to redirector
|
||||
shell: |
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem ubuntu@{{ hostvars['localhost']['redirector_ip'] }} "echo 'SSH CONNECTION SUCCESSFUL FROM C2'"
|
||||
register: ssh_test_result
|
||||
changed_when: false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display SSH test result
|
||||
debug:
|
||||
msg: "{{ ssh_test_result.stdout if ssh_test_result.rc == 0 else 'SSH connection failed: ' + ssh_test_result.stderr }}"
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "C2 Server Deployment Complete!"
|
||||
- "-----------------------------"
|
||||
- "C2 Server IP: {{ ansible_host }}"
|
||||
- "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
|
||||
- "GoPhish Admin Port: {{ gophish_admin_port }}"
|
||||
- "SSH Key: ~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
|
||||
when: not disable_summary | default(false)
|
||||
@@ -0,0 +1,465 @@
|
||||
---
|
||||
# AWS Cleanup Playbook - Comprehensive version with robust VPC removal
|
||||
- name: Clean up AWS resources
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
|
||||
confirm_cleanup: false # Skip confirmation in automated teardown
|
||||
deployment_id: "{{ deployment_id | default('') }}"
|
||||
redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}"
|
||||
c2_name: "{{ c2_name | default('s-' + deployment_id) }}"
|
||||
tracker_name: "{{ tracker_name | default('t-' + deployment_id) }}"
|
||||
cleanup_summary: {}
|
||||
|
||||
tasks:
|
||||
# Confirmation step (if enabled)
|
||||
- name: Confirm cleanup
|
||||
pause:
|
||||
prompt: "Are you sure you want to delete all AWS resources for deployment ID {{ deployment_id }}? This action cannot be undone. Type 'yes' to confirm"
|
||||
register: cleanup_confirmation
|
||||
when: confirm_cleanup | bool
|
||||
|
||||
- name: Check confirmation
|
||||
assert:
|
||||
that:
|
||||
- cleanup_confirmation.user_input | default('yes') == 'yes'
|
||||
fail_msg: "Cleanup cancelled by user"
|
||||
when: confirm_cleanup | bool
|
||||
|
||||
# STEP 1: Find all instances by deployment ID
|
||||
- name: Find all EC2 instances for this deployment
|
||||
amazon.aws.ec2_instance_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
"tag:deployment_id": "{{ deployment_id }}"
|
||||
register: deployment_instances
|
||||
|
||||
- name: Set fact for instances found
|
||||
set_fact:
|
||||
cleanup_summary: "{{ cleanup_summary | combine({'instances_found': deployment_instances.instances | length}) }}"
|
||||
|
||||
# STEP 2: Terminate all instances with proper tagging
|
||||
- name: Terminate all instances for this deployment
|
||||
amazon.aws.ec2_instance:
|
||||
instance_ids: "{{ item.instance_id }}"
|
||||
region: "{{ aws_region }}"
|
||||
state: absent
|
||||
loop: "{{ deployment_instances.instances }}"
|
||||
register: terminated_instances
|
||||
when: deployment_instances.instances | length > 0
|
||||
|
||||
- name: Wait for instances to be terminated
|
||||
pause:
|
||||
seconds: 30
|
||||
when: deployment_instances.instances | length > 0
|
||||
|
||||
# STEP 3: Find all security groups by deployment ID
|
||||
- name: Find all security groups for this deployment
|
||||
amazon.aws.ec2_security_group_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
"tag:deployment_id": "{{ deployment_id }}"
|
||||
register: deployment_sgs
|
||||
|
||||
# Also find SGs by name pattern
|
||||
- name: Find security groups by name pattern
|
||||
amazon.aws.ec2_security_group_info:
|
||||
region: "{{ aws_region }}"
|
||||
register: all_sgs
|
||||
|
||||
- name: Filter SGs by name pattern
|
||||
set_fact:
|
||||
named_sgs: "{{ all_sgs.security_groups | selectattr('group_name', 'search', redirector_name + '-sg|' + c2_name + '-sg') | list }}"
|
||||
|
||||
- name: Combine all security groups to delete
|
||||
set_fact:
|
||||
all_sgs_to_delete: "{{ deployment_sgs.security_groups + named_sgs }}"
|
||||
cleanup_summary: "{{ cleanup_summary | combine({'security_groups_found': (deployment_sgs.security_groups + named_sgs) | length}) }}"
|
||||
|
||||
# STEP 4: Delete all security groups
|
||||
- name: Delete security groups
|
||||
amazon.aws.ec2_security_group:
|
||||
group_id: "{{ item.group_id }}"
|
||||
region: "{{ aws_region }}"
|
||||
state: absent
|
||||
loop: "{{ all_sgs_to_delete }}"
|
||||
when: all_sgs_to_delete | length > 0
|
||||
ignore_errors: yes
|
||||
register: deleted_sgs
|
||||
|
||||
# STEP 5: Find and delete all ENIs
|
||||
- name: Find network interfaces by tag
|
||||
amazon.aws.ec2_eni_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
"tag:deployment_id": "{{ deployment_id }}"
|
||||
register: deployment_enis
|
||||
|
||||
- name: Delete ENIs
|
||||
amazon.aws.ec2_eni:
|
||||
region: "{{ aws_region }}"
|
||||
eni_id: "{{ item.id }}"
|
||||
state: absent
|
||||
force_detach: true
|
||||
loop: "{{ deployment_enis.network_interfaces }}"
|
||||
ignore_errors: yes
|
||||
register: deleted_enis
|
||||
when: deployment_enis.network_interfaces | length > 0
|
||||
|
||||
- name: Set ENIs count in summary
|
||||
set_fact:
|
||||
cleanup_summary: "{{ cleanup_summary | combine({'enis_found': deployment_enis.network_interfaces | length}) }}"
|
||||
|
||||
# STEP 6: Find all VPCs by deployment ID
|
||||
- name: Find all VPCs for this deployment
|
||||
amazon.aws.ec2_vpc_net_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
"tag:deployment_id": "{{ deployment_id }}"
|
||||
register: deployment_vpcs
|
||||
|
||||
# STEP 7: Find VPCs by name pattern as fallback
|
||||
- name: Find all VPCs by name pattern
|
||||
amazon.aws.ec2_vpc_net_info:
|
||||
region: "{{ aws_region }}"
|
||||
register: all_vpcs
|
||||
|
||||
- name: Filter VPCs by name pattern
|
||||
set_fact:
|
||||
named_vpcs: "{{ all_vpcs.vpcs | selectattr('tags', 'defined') | selectattr('tags.Name', 'defined') | selectattr('tags.Name', 'search', redirector_name + '-vpc|' + c2_name + '-vpc') | list }}"
|
||||
|
||||
- name: Combine all VPCs to delete
|
||||
set_fact:
|
||||
all_vpcs_to_delete: "{{ deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id') }}"
|
||||
cleanup_summary: "{{ cleanup_summary | combine({'vpcs_found': (deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id')) | length}) }}"
|
||||
|
||||
# STEP 8: Find and delete NAT Gateways for each VPC separately
|
||||
- name: Find NAT gateways in each VPC
|
||||
amazon.aws.ec2_vpc_nat_gateway_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
vpc-id: "{{ item.vpc_id }}"
|
||||
register: natgw_results
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
- name: Delete NAT gateways
|
||||
amazon.aws.ec2_vpc_nat_gateway:
|
||||
region: "{{ aws_region }}"
|
||||
nat_gateway_id: "{{ item.1.nat_gateway_id }}"
|
||||
state: absent
|
||||
release_eip: true
|
||||
loop: "{{ natgw_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('nat_gateways', 'defined') | subelements('nat_gateways') }}"
|
||||
ignore_errors: yes
|
||||
register: deleted_natgws
|
||||
when: natgw_results.results is defined
|
||||
|
||||
- name: Wait after NAT deletion
|
||||
pause:
|
||||
seconds: 15
|
||||
when: deleted_natgws.results is defined and deleted_natgws.results | length > 0
|
||||
|
||||
# STEP 9: Find and delete Internet Gateways
|
||||
- name: Find internet gateways for each VPC
|
||||
amazon.aws.ec2_vpc_igw_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
attachment.vpc-id: "{{ item.vpc_id }}"
|
||||
register: igw_results
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
- name: Detach and delete internet gateways
|
||||
amazon.aws.ec2_vpc_igw:
|
||||
internet_gateway_id: "{{ item.1.internet_gateway_id }}"
|
||||
state: absent
|
||||
region: "{{ aws_region }}"
|
||||
loop: "{{ igw_results.results | default([]) | subelements('internet_gateways') }}"
|
||||
ignore_errors: yes
|
||||
register: deleted_igws
|
||||
|
||||
- name: Wait after IGW deletion
|
||||
pause:
|
||||
seconds: 15
|
||||
when: deleted_igws.results is defined and deleted_igws.results | length > 0
|
||||
|
||||
# STEP 10: Find and delete Route Tables
|
||||
- name: Find route tables for each VPC
|
||||
amazon.aws.ec2_vpc_route_table_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
vpc-id: "{{ item.vpc_id }}"
|
||||
register: rtb_results
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
- name: Delete non-main route tables
|
||||
amazon.aws.ec2_vpc_route_table:
|
||||
region: "{{ aws_region }}"
|
||||
route_table_id: "{{ item.1.id }}"
|
||||
lookup: id
|
||||
state: absent
|
||||
loop: "{{ rtb_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('route_tables', 'defined') | subelements('route_tables') }}"
|
||||
when: not item.1.associations[0].main | default(false)
|
||||
ignore_errors: yes
|
||||
register: deleted_rtbs
|
||||
|
||||
# Add this after your existing route table deletion
|
||||
- name: Delete main route tables with AWS CLI
|
||||
shell: |
|
||||
for rtb in $(aws ec2 describe-route-tables --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --query 'RouteTables[?Associations[?Main==`true`]].RouteTableId' --output text); do
|
||||
aws ec2 delete-route --route-table-id $rtb --destination-cidr-block 0.0.0.0/0 --region {{ aws_region }} || true
|
||||
done
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
ignore_errors: yes
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
# STEP 11: Find and delete Subnets
|
||||
- name: Find subnets for each VPC
|
||||
amazon.aws.ec2_vpc_subnet_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
vpc-id: "{{ item.vpc_id }}"
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
register: subnet_results
|
||||
|
||||
- name: Delete subnets
|
||||
amazon.aws.ec2_vpc_subnet:
|
||||
region: "{{ aws_region }}"
|
||||
vpc_id: "{{ item.1.vpc_id }}"
|
||||
cidr: "{{ item.1.cidr_block }}"
|
||||
state: absent
|
||||
loop: "{{ subnet_results.results | default([]) | subelements('subnets') }}"
|
||||
ignore_errors: yes
|
||||
register: deleted_subnets
|
||||
when: subnet_results.results is defined
|
||||
|
||||
# STEP 12: Find and delete VPC Endpoints
|
||||
- name: Find VPC endpoints for each VPC
|
||||
amazon.aws.ec2_vpc_endpoint_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
vpc-id: "{{ item.vpc_id }}"
|
||||
register: endpoint_results
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
- name: Delete VPC endpoints
|
||||
amazon.aws.ec2_vpc_endpoint:
|
||||
region: "{{ aws_region }}"
|
||||
vpc_endpoint_id: "{{ item.1.vpc_endpoint_id }}"
|
||||
state: absent
|
||||
loop: "{{ endpoint_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('vpc_endpoints', 'defined') | subelements('vpc_endpoints') }}"
|
||||
ignore_errors: yes
|
||||
register: deleted_endpoints
|
||||
when: endpoint_results.results is defined
|
||||
|
||||
# Add before STEP 13
|
||||
- name: Check for remaining VPC dependencies
|
||||
shell: |
|
||||
aws ec2 describe-network-interfaces --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --output json
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
register: remaining_deps
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
- name: Display any remaining dependencies
|
||||
debug:
|
||||
msg: "VPC {{ item.item.vpc_id }} still has dependencies that need to be removed"
|
||||
loop: "{{ remaining_deps.results }}"
|
||||
when: item.stdout | from_json | json_query('NetworkInterfaces') | length > 0
|
||||
|
||||
# Add this before the force delete of network interfaces
|
||||
- name: Detach remaining network interfaces
|
||||
shell: |
|
||||
aws ec2 detach-network-interface --attachment-id $(aws ec2 describe-network-interfaces --network-interface-ids {{ item.1 }} --query 'NetworkInterfaces[0].Attachment.AttachmentId' --output text) --region {{ aws_region }} --force
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') |
|
||||
map('attr', 'stdout') | map('from_json') |
|
||||
map('json_query', 'NetworkInterfaces[?Status==`in-use`].NetworkInterfaceId') |
|
||||
zip(remaining_deps.results | map('attr', 'item')) | list }}"
|
||||
ignore_errors: yes
|
||||
when: item.0 | length > 0
|
||||
|
||||
- name: Force delete any remaining network interfaces
|
||||
shell: |
|
||||
aws ec2 delete-network-interface --network-interface-id {{ item.1 }} --region {{ aws_region }}
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') |
|
||||
map('attr', 'stdout') | map('from_json') |
|
||||
map('json_query', 'NetworkInterfaces[].NetworkInterfaceId') |
|
||||
zip(remaining_deps.results | map('attr', 'item')) | list }}"
|
||||
ignore_errors: yes
|
||||
when: item.0 | length > 0
|
||||
|
||||
# STEP 13: Final VPC deletion with multiple retries
|
||||
- name: Wait for all dependencies to clear
|
||||
pause:
|
||||
seconds: 20
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
# First attempt with normal module - with error display
|
||||
- name: Delete all VPCs (first attempt)
|
||||
amazon.aws.ec2_vpc_net:
|
||||
vpc_id: "{{ item.vpc_id }}"
|
||||
region: "{{ aws_region }}"
|
||||
state: absent
|
||||
loop: "{{ all_vpcs_to_delete }}"
|
||||
register: vpc_deletion
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display VPC deletion errors
|
||||
debug:
|
||||
msg: "Failed to delete VPC {{ item.item.vpc_id }}: {{ item.msg }}"
|
||||
loop: "{{ vpc_deletion.results | default([]) }}"
|
||||
when: item.failed is defined and item.failed
|
||||
|
||||
# Direct API call for any VPCs that failed
|
||||
- name: Find which VPCs still exist
|
||||
amazon.aws.ec2_vpc_net_info:
|
||||
region: "{{ aws_region }}"
|
||||
vpc_ids: "{{ all_vpcs_to_delete | map(attribute='vpc_id') | list }}"
|
||||
register: remaining_vpcs
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
# Forcibly delete with direct AWS CLI command
|
||||
- name: Force delete remaining VPCs with CLI
|
||||
shell: |
|
||||
aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }}
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
loop: "{{ remaining_vpcs.vpcs }}"
|
||||
ignore_errors: yes
|
||||
when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0
|
||||
register: force_vpc_delete
|
||||
|
||||
# Add after the VPC deletion attempts - more aggressive approach
|
||||
- name: Force delete remaining VPCs with AWS CLI and debug output
|
||||
shell: |
|
||||
aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} 2>&1 || echo "Failed with: $?"
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
loop: "{{ remaining_vpcs.vpcs }}"
|
||||
register: force_vpc_delete_debug
|
||||
when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0
|
||||
|
||||
- name: Display debug output from force delete
|
||||
debug:
|
||||
msg: "{{ item.stdout }}"
|
||||
loop: "{{ force_vpc_delete_debug.results | default([]) }}"
|
||||
when: item.stdout is defined and item.stdout | trim != ""
|
||||
|
||||
# Track deleted VPCs in summary
|
||||
- name: Set VPC deletion results in summary
|
||||
set_fact:
|
||||
cleanup_summary: "{{ cleanup_summary | combine({
|
||||
'vpcs_deleted': ((vpc_deletion.results | default([]) | selectattr('failed', 'undefined') | list | length) + (force_vpc_delete.results | default([]) | selectattr('failed', 'undefined') | list | length))}) }}"
|
||||
when: all_vpcs_to_delete | length > 0
|
||||
|
||||
# Add these tasks to confirm VPC deletion
|
||||
- name: Verify VPC deletion
|
||||
amazon.aws.ec2_vpc_net_info:
|
||||
region: "{{ aws_region }}"
|
||||
filters:
|
||||
"tag:deployment_id": "{{ deployment_id }}"
|
||||
register: vpc_check
|
||||
|
||||
- name: Display cleanup summary
|
||||
debug:
|
||||
msg:
|
||||
- "Cleanup Summary:"
|
||||
- "Redirector instance deleted: {{ redirector_deleted | default('N/A') }}"
|
||||
- "C2 instance deleted: {{ c2_deleted | default('N/A') }}"
|
||||
- "VPC resources deleted: {{ vpc_check.vpcs | length == 0 }}"
|
||||
when: not disable_summary | default(false)
|
||||
|
||||
# STEP 14: Delete key pairs - completely revised implementation
|
||||
- name: Check for deployment key in AWS
|
||||
shell: |
|
||||
aws ec2 describe-key-pairs --region {{ aws_region }} --filters "Name=key-name,Values=c2deploy_{{ deployment_id }}" --query "KeyPairs[*].KeyName" --output text
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
register: keypair_check
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Delete deployment key pair if it exists
|
||||
amazon.aws.ec2_key:
|
||||
name: "c2deploy_{{ deployment_id }}"
|
||||
region: "{{ aws_region }}"
|
||||
state: absent
|
||||
when: keypair_check.stdout | trim != ""
|
||||
register: deleted_keypair
|
||||
|
||||
- name: Delete SSH key files
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
with_items:
|
||||
- "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
- "~/.ssh/c2deploy_{{ deployment_id }}.pub"
|
||||
- "~/.ssh/{{ redirector_name }}.pem"
|
||||
- "~/.ssh/{{ c2_name }}.pem"
|
||||
- "~/.ssh/{{ tracker_name }}.pem"
|
||||
ignore_errors: yes
|
||||
register: deleted_ssh_files
|
||||
|
||||
- name: Count deleted SSH files
|
||||
set_fact:
|
||||
cleanup_summary: "{{ cleanup_summary | combine({
|
||||
'keypairs_deleted': (deleted_keypair.changed | default(false)) | ternary(1, 0),
|
||||
'ssh_files_deleted': (deleted_ssh_files.results | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}"
|
||||
|
||||
# Remove infrastructure state file - fix path to include deployment_id
|
||||
- name: Remove infrastructure state file
|
||||
file:
|
||||
path: "infrastructure_state_{{ deployment_id }}.json"
|
||||
state: absent
|
||||
ignore_errors: yes
|
||||
register: infra_file
|
||||
|
||||
- name: Clean up deployment state file
|
||||
file:
|
||||
path: "{{ playbook_dir }}/../deployment_state_{{ deployment_id }}.json"
|
||||
state: absent
|
||||
ignore_errors: yes
|
||||
register: state_file_deletion
|
||||
|
||||
- name: Report state file cleanup
|
||||
debug:
|
||||
msg: "Deployment state file {{ 'deleted' if state_file_deletion.changed else 'not found' }}"
|
||||
when: state_file_deletion is defined
|
||||
|
||||
# STEP 16: Enhanced and Accurate Cleanup Summary
|
||||
- name: Enhanced cleanup summary
|
||||
debug:
|
||||
msg:
|
||||
- "=========================================================="
|
||||
- " AWS CLEANUP SUMMARY: {{ deployment_id }} "
|
||||
- "=========================================================="
|
||||
- "EC2 Instances: {{ cleanup_summary.instances_found | default(0) }} found, {{ terminated_instances.results | default([]) | length }} terminated"
|
||||
- "Security Groups: {{ cleanup_summary.security_groups_found | default(0) }} found, {{ deleted_sgs.results | default([]) | length }} deleted"
|
||||
- "Network Interfaces: {{ cleanup_summary.enis_found | default(0) }} found, {{ deleted_enis.results | default([]) | length }} deleted"
|
||||
- "VPCs: {{ cleanup_summary.vpcs_found | default(0) }} found, {{ cleanup_summary.vpcs_deleted | default(0) }} deleted"
|
||||
- "Key Pairs: {{ deleted_key_pairs.results | default([]) | length }} deleted"
|
||||
- "SSH Key Files: {{ cleanup_summary.ssh_files_deleted | default(0) }} deleted"
|
||||
- "Infrastructure file: {{ 'Removed' if infra_file.changed else 'Not found' }}"
|
||||
- "=========================================================="
|
||||
- "CLEANUP {{ 'COMPLETED' if (cleanup_summary.vpcs_deleted | default(0) == cleanup_summary.vpcs_found | default(0)) else 'PARTIAL - SOME RESOURCES MAY REMAIN' }}"
|
||||
- "========================================================="
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
# AWS Shared Infrastructure Deployment Playbook
|
||||
|
||||
- name: Deploy shared AWS infrastructure
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Deployment identifiers
|
||||
deployment_id: "{{ deployment_id | default('') }}"
|
||||
infra_name: "infra-{{ deployment_id }}"
|
||||
# Region settings
|
||||
deployment_region: "{{ aws_region | default(aws_region_choices | random) }}"
|
||||
# Check if using split regions (C2 and redirector in different regions)
|
||||
split_regions: "{{ c2_region is defined and redirector_region is defined and c2_region != redirector_region }}"
|
||||
# Check if only deploying one component (C2 only or redirector only)
|
||||
single_component: "{{ c2_only | default(false) | bool or redirector_only | default(false) | bool }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate AWS credentials
|
||||
assert:
|
||||
that:
|
||||
- aws_access_key is defined and aws_access_key != ""
|
||||
- aws_secret_key is defined and aws_secret_key != ""
|
||||
fail_msg: "AWS credentials are required"
|
||||
|
||||
- name: Skip shared infrastructure if using split regions
|
||||
meta: end_play
|
||||
when: split_regions | bool
|
||||
|
||||
- name: Print infrastructure deployment info
|
||||
debug:
|
||||
msg: "Deploying shared infrastructure in {{ deployment_region }}"
|
||||
when: not split_regions | bool and not single_component | bool
|
||||
|
||||
- name: Check if deploying just C2 or just redirector
|
||||
debug:
|
||||
msg: "Skipping shared infrastructure for single component deployment ({{ 'C2 only' if c2_only | default(false) else 'Redirector only' }})"
|
||||
when: single_component | bool
|
||||
|
||||
- name: Skip shared infrastructure for single component deployment
|
||||
meta: end_play
|
||||
when: single_component | bool
|
||||
|
||||
- name: Create shared VPC
|
||||
amazon.aws.ec2_vpc_net:
|
||||
name: "{{ infra_name }}-vpc"
|
||||
cidr_block: "10.0.0.0/16"
|
||||
region: "{{ deployment_region }}"
|
||||
tags:
|
||||
Name: "{{ infra_name }}-vpc"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
state: present
|
||||
register: vpc_result
|
||||
when: not split_regions | bool and not single_component | bool
|
||||
|
||||
- name: Store shared VPC ID
|
||||
set_fact:
|
||||
shared_vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
when: not split_regions | bool and not single_component | bool and vpc_result is defined
|
||||
|
||||
- name: Create internet gateway
|
||||
amazon.aws.ec2_vpc_igw:
|
||||
vpc_id: "{{ shared_vpc_id }}"
|
||||
region: "{{ deployment_region }}"
|
||||
state: present
|
||||
tags:
|
||||
Name: "{{ infra_name }}-igw"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: igw_result
|
||||
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined
|
||||
|
||||
- name: Create subnet
|
||||
amazon.aws.ec2_vpc_subnet:
|
||||
vpc_id: "{{ shared_vpc_id }}"
|
||||
cidr: "10.0.1.0/24"
|
||||
region: "{{ deployment_region }}"
|
||||
az: "{{ deployment_region }}a"
|
||||
map_public: yes
|
||||
tags:
|
||||
Name: "{{ infra_name }}-subnet"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: subnet_result
|
||||
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined
|
||||
|
||||
- name: Create routing table
|
||||
amazon.aws.ec2_vpc_route_table:
|
||||
vpc_id: "{{ shared_vpc_id }}"
|
||||
region: "{{ deployment_region }}"
|
||||
tags:
|
||||
Name: "{{ infra_name }}-rtb"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
routes:
|
||||
- dest: "0.0.0.0/0"
|
||||
gateway_id: "{{ igw_result.gateway_id }}"
|
||||
subnets:
|
||||
- "{{ subnet_result.subnet.id }}"
|
||||
register: route_table_result
|
||||
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined and igw_result is defined and subnet_result is defined
|
||||
|
||||
- name: Write infrastructure info to state file
|
||||
copy:
|
||||
content: |
|
||||
{
|
||||
"vpc_id": "{{ shared_vpc_id }}",
|
||||
"subnet_id": "{{ subnet_result.subnet.id }}",
|
||||
"igw_id": "{{ igw_result.gateway_id }}",
|
||||
"region": "{{ deployment_region }}",
|
||||
"deployment_id": "{{ deployment_id }}"
|
||||
}
|
||||
dest: "infrastructure_state.json"
|
||||
mode: "0600"
|
||||
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined and subnet_result is defined and igw_result is defined
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
# Phishing infrastructure deployment playbook
|
||||
# This is a comprehensive playbook that handles all phishing components
|
||||
|
||||
- name: Deploy Phishing Infrastructure
|
||||
hosts: localhost
|
||||
gather_facts: true
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
|
||||
tasks:
|
||||
- name: Display deployment information
|
||||
debug:
|
||||
msg: |
|
||||
Deploying phishing infrastructure
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Provider: {{ provider }}
|
||||
Domain: {{ domain | default(phishing_domain) }}
|
||||
Components: {{ deployment_type }}
|
||||
|
||||
- name: Set deployment facts
|
||||
set_fact:
|
||||
deployment_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
phishing_deployment_results: {}
|
||||
deployment_components: >-
|
||||
{% set components = [] %}
|
||||
{% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %}
|
||||
{% set _ = components.append('gophish') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %}
|
||||
{% set _ = components.append('mta_front') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %}
|
||||
{% set _ = components.append('redirector') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %}
|
||||
{% set _ = components.append('webserver') %}
|
||||
{% endif %}
|
||||
{{ components }}
|
||||
|
||||
- name: Include provider-specific tasks
|
||||
include_tasks: "{{ provider }}_phishing.yml"
|
||||
when: deployment_components | length > 0
|
||||
|
||||
- name: Ensure logs directory exists
|
||||
file:
|
||||
path: "../../logs"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Save deployment information
|
||||
template:
|
||||
src: phishing_deployment_info.j2
|
||||
dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Display success message
|
||||
debug:
|
||||
msg: |
|
||||
✅ Phishing infrastructure deployment completed
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Components deployed: {{ deployment_components | join(', ') }}
|
||||
Check logs/phishing_deployment_{{ deployment_id }}.json for details
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
# VPC Cleanup Process with enhanced dependency handling
|
||||
# Removed detailed cleanup tasks for ENIs, RTs, IGWs, NATs, subnets, SGs to simplify deletion
|
||||
|
||||
# Step X: Delete VPC directly
|
||||
- name: Delete VPC {{ vpc_id }}
|
||||
amazon.aws.ec2_vpc_net:
|
||||
region: "{{ aws_region }}"
|
||||
vpc_id: "{{ vpc_id }}"
|
||||
state: absent
|
||||
retries: 5
|
||||
delay: 15
|
||||
register: vpc_delete_result
|
||||
until: vpc_delete_result is success
|
||||
@@ -0,0 +1,379 @@
|
||||
---
|
||||
# AWS Redirector Deployment Playbook
|
||||
|
||||
- name: Deploy AWS redirector
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Default values
|
||||
ssh_user: "{{ ssh_user | default('ubuntu') }}"
|
||||
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
|
||||
instance_type: "{{ aws_instance_type | default('t2.micro') }}"
|
||||
deployment_id: "{{ deployment_id | default('') }}"
|
||||
redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}"
|
||||
effective_listen_port: "{{ shell_handler_port | default(4488) }}"
|
||||
# Check for shared infrastructure
|
||||
# Only use shared when C2 and redirector are in the same region
|
||||
use_shared_infra: "{{ not (c2_region is defined and redirector_region is defined and c2_region != redirector_region) and not c2_only | default(false) | bool and not redirector_only | default(false) | bool }}"
|
||||
# Ubuntu AMI IDs for different regions (Ubuntu 22.04 LTS)
|
||||
ubuntu_ami_map:
|
||||
us-east-1: "ami-0aa2b7722dc1b5612"
|
||||
us-east-2: "ami-06c4532923d4ba1ec"
|
||||
us-west-1: "ami-0573b70afecda915d"
|
||||
us-west-2: "ami-0c79c59ac2c572b87"
|
||||
eu-west-1: "ami-0694d931cee176e7d"
|
||||
eu-west-2: "ami-0505148b3591e4c07"
|
||||
eu-central-1: "ami-06dd92ecc74fdfb36"
|
||||
ap-southeast-1: "ami-0df7a207adb9748c7"
|
||||
ap-southeast-2: "ami-0df4b2961410d4cff"
|
||||
ap-northeast-1: "ami-0014b5f031a76c1b1"
|
||||
sa-east-1: "ami-0af6e9042ea5a4e3e"
|
||||
|
||||
tasks:
|
||||
- name: Validate AWS credentials
|
||||
assert:
|
||||
that:
|
||||
- aws_access_key is defined and aws_access_key != ""
|
||||
- aws_secret_key is defined and aws_secret_key != ""
|
||||
fail_msg: "AWS credentials are required"
|
||||
|
||||
# Set region for redirector - fix for noop task error
|
||||
- name: Set region for redirector
|
||||
set_fact:
|
||||
aws_redirector_region: "{{ redirector_region | default(aws_region) }}"
|
||||
|
||||
# Load shared infrastructure state if available - fixed implementation
|
||||
- name: Check for shared infrastructure state
|
||||
block:
|
||||
- name: Check if state file exists
|
||||
stat:
|
||||
path: "infrastructure_state_{{ deployment_id }}.json"
|
||||
register: infra_state_file
|
||||
|
||||
- name: Include vars if file exists
|
||||
include_vars:
|
||||
file: "infrastructure_state_{{ deployment_id }}.json"
|
||||
name: shared_infra
|
||||
when: infra_state_file.stat.exists | default(false)
|
||||
when: use_shared_infra | bool
|
||||
|
||||
# After loading shared infrastructure state
|
||||
- name: Validate shared VPC exists
|
||||
amazon.aws.ec2_vpc_net_info:
|
||||
region: "{{ shared_infra.region }}"
|
||||
vpc_ids:
|
||||
- "{{ shared_infra.vpc_id }}"
|
||||
register: vpc_check
|
||||
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Delete stale infrastructure state file
|
||||
file:
|
||||
path: "infrastructure_state.json"
|
||||
state: absent
|
||||
when: use_shared_infra | bool and vpc_check.vpcs is defined and vpc_check.vpcs | length == 0
|
||||
|
||||
- name: Disable shared infrastructure when VPC doesn't exist
|
||||
set_fact:
|
||||
use_shared_infra: false
|
||||
when: use_shared_infra | bool and vpc_check.vpcs is defined and vpc_check.vpcs | length == 0
|
||||
|
||||
- name: Set region variables from shared infra
|
||||
set_fact:
|
||||
aws_redirector_region: "{{ shared_infra.region | default(aws_region) }}"
|
||||
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
|
||||
|
||||
- name: Set AMI ID for selected region
|
||||
set_fact:
|
||||
ami_id: "{{ ubuntu_ami_map[aws_redirector_region] | default(ubuntu_ami_map['us-east-1']) }}"
|
||||
|
||||
# Create new infrastructure only if not using shared
|
||||
- name: Create VPC
|
||||
amazon.aws.ec2_vpc_net:
|
||||
name: "{{ redirector_name }}-vpc"
|
||||
cidr_block: "10.0.0.0/16"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
tags:
|
||||
Name: "{{ redirector_name }}-vpc"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
state: present
|
||||
register: vpc_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Create internet gateway for VPC
|
||||
amazon.aws.ec2_vpc_igw:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
state: present
|
||||
tags:
|
||||
Name: "{{ redirector_name }}-igw"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: igw_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Create subnet in VPC
|
||||
amazon.aws.ec2_vpc_subnet:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
cidr: "10.0.1.0/24"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
az: "{{ aws_redirector_region }}a"
|
||||
map_public: yes
|
||||
tags:
|
||||
Name: "{{ redirector_name }}-subnet"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: subnet_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Create routing table for internet access
|
||||
amazon.aws.ec2_vpc_route_table:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
tags:
|
||||
Name: "{{ redirector_name }}-rtb"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
routes:
|
||||
- dest: "0.0.0.0/0"
|
||||
gateway_id: "{{ igw_result.gateway_id }}"
|
||||
subnets:
|
||||
- "{{ subnet_result.subnet.id }}"
|
||||
register: route_table_result
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
# Set VPC ID based on shared or created
|
||||
- name: Set VPC ID from shared infrastructure
|
||||
set_fact:
|
||||
vpc_id: "{{ shared_infra.vpc_id }}"
|
||||
subnet_id: "{{ shared_infra.subnet_id }}"
|
||||
redirector_vpc_id: "{{ shared_infra.vpc_id }}" # Store for cleanup reference
|
||||
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
|
||||
|
||||
- name: Set VPC ID from created infrastructure
|
||||
set_fact:
|
||||
vpc_id: "{{ vpc_result.vpc.id }}"
|
||||
subnet_id: "{{ subnet_result.subnet.id }}"
|
||||
redirector_vpc_id: "{{ vpc_result.vpc.id }}" # Store for cleanup reference
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
- name: Set default c2_ip for redirector-only deployments
|
||||
set_fact:
|
||||
c2_ip: "{{ operator_ip }}"
|
||||
when: redirector_only | default(false) | bool and c2_ip is not defined
|
||||
|
||||
- name: Load c2_ip from infrastructure state
|
||||
block:
|
||||
- name: Check if infrastructure state file exists
|
||||
stat:
|
||||
path: "infrastructure_state_{{ deployment_id }}.json"
|
||||
register: infra_state_check
|
||||
|
||||
- name: Load infrastructure state
|
||||
include_vars:
|
||||
file: "infrastructure_state_{{ deployment_id }}.json"
|
||||
name: infra_state
|
||||
when: infra_state_check.stat.exists
|
||||
|
||||
- name: Set c2 IP from infrastructure state
|
||||
set_fact:
|
||||
c2_ip: "{{ infra_state.c2_ip | default(operator_ip) }}"
|
||||
when: infra_state_check.stat.exists and infra_state.c2_ip is defined
|
||||
|
||||
- name: Debug c2 IP
|
||||
debug:
|
||||
msg: "Using c2 IP: {{ c2_ip | default('undefined') }}"
|
||||
when: c2_ip is undefined
|
||||
|
||||
- name: Default to operator IP if c2 IP is still undefined
|
||||
set_fact:
|
||||
c2_ip: "{{ operator_ip }}"
|
||||
when: c2_ip is undefined
|
||||
|
||||
- name: Create security group for redirector
|
||||
amazon.aws.ec2_security_group:
|
||||
name: "{{ redirector_name }}-sg"
|
||||
description: "Security group for redirector {{ redirector_name }}"
|
||||
vpc_id: "{{ vpc_id }}"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
rules:
|
||||
# Management access only from operator IP
|
||||
- proto: tcp
|
||||
ports: 22
|
||||
cidr_ip: "{{ operator_ip }}/32"
|
||||
# Public-facing services are open to internet
|
||||
- proto: tcp
|
||||
ports:
|
||||
- 80
|
||||
- 443
|
||||
- "{{ effective_listen_port }}"
|
||||
cidr_ip: 0.0.0.0/0
|
||||
rules_egress:
|
||||
- proto: -1
|
||||
cidr_ip: 0.0.0.0/0
|
||||
state: present
|
||||
register: security_group
|
||||
|
||||
# Save infrastructure state for reuse
|
||||
- name: Save infrastructure state for reuse
|
||||
copy:
|
||||
content: |
|
||||
{
|
||||
"vpc_id": "{{ vpc_id }}",
|
||||
"subnet_id": "{{ subnet_id }}",
|
||||
"security_group_id": "{{ security_group.group_id }}",
|
||||
"region": "{{ aws_redirector_region }}",
|
||||
"deployment_id": "{{ deployment_id }}"
|
||||
}
|
||||
dest: "infrastructure_state_{{ deployment_id }}.json"
|
||||
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
|
||||
|
||||
# Generate or import SSH key for the deployment - FIXED KEY HANDLING
|
||||
- name: Check if deployment SSH key already exists locally
|
||||
stat:
|
||||
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
register: ssh_key_file
|
||||
|
||||
- name: Generate SSH key pair if it doesn't exist
|
||||
block:
|
||||
- name: Create SSH key pair
|
||||
command: ssh-keygen -t rsa -b 2048 -f ~/.ssh/c2deploy_{{ deployment_id }} -N ""
|
||||
args:
|
||||
creates: "~/.ssh/c2deploy_{{ deployment_id }}"
|
||||
|
||||
- name: Rename private key to .pem format
|
||||
command: mv ~/.ssh/c2deploy_{{ deployment_id }} ~/.ssh/c2deploy_{{ deployment_id }}.pem
|
||||
args:
|
||||
creates: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
removes: "~/.ssh/c2deploy_{{ deployment_id }}"
|
||||
when: not ssh_key_file.stat.exists
|
||||
|
||||
- name: Ensure proper permissions on SSH key
|
||||
file:
|
||||
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
mode: '0600'
|
||||
state: file
|
||||
|
||||
# Check if key pair exists in AWS
|
||||
- name: Check if key pair exists in AWS
|
||||
amazon.aws.ec2_key_info:
|
||||
region: "{{ aws_redirector_region }}"
|
||||
filters:
|
||||
key-name: "c2deploy_{{ deployment_id }}"
|
||||
register: existing_key_pair
|
||||
|
||||
# Import SSH key to AWS - FIXED CONDITION
|
||||
- name: Import SSH key to AWS
|
||||
amazon.aws.ec2_key:
|
||||
name: "c2deploy_{{ deployment_id }}"
|
||||
key_material: "{{ lookup('file', '~/.ssh/c2deploy_{{ deployment_id }}.pub') }}"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
state: present
|
||||
when: existing_key_pair.keypairs | length == 0
|
||||
|
||||
# Launch the redirector instance
|
||||
- name: Launch redirector instance
|
||||
amazon.aws.ec2_instance:
|
||||
name: "{{ redirector_name }}"
|
||||
key_name: "c2deploy_{{ deployment_id }}"
|
||||
instance_type: "{{ instance_type | default('t2.micro') }}"
|
||||
vpc_subnet_id: "{{ subnet_id }}"
|
||||
security_groups:
|
||||
- "{{ security_group.group_id }}"
|
||||
image_id: "{{ ami_id }}"
|
||||
region: "{{ aws_redirector_region }}"
|
||||
state: present
|
||||
wait: yes
|
||||
volumes:
|
||||
- device_name: "/dev/xvda"
|
||||
ebs:
|
||||
volume_size: 20
|
||||
delete_on_termination: true
|
||||
tags:
|
||||
Name: "{{ redirector_name }}"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
register: redirector_instance
|
||||
|
||||
- name: Set redirector_ip for later use
|
||||
set_fact:
|
||||
redirector_ip: "{{ redirector_instance.instances[0].public_ip_address }}"
|
||||
redirector_instance_id: "{{ redirector_instance.instances[0].instance_id }}"
|
||||
|
||||
- name: Display redirector instance details for debugging
|
||||
debug:
|
||||
msg:
|
||||
- "Redirector IP: {{ redirector_ip }}"
|
||||
- "Redirector Instance ID: {{ redirector_instance_id }}"
|
||||
- "SSH User to use: {{ ssh_user }}"
|
||||
- "SSH Key path: ~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
|
||||
- name: Wait for instance initialization
|
||||
pause:
|
||||
seconds: 120
|
||||
when: redirector_instance.changed
|
||||
|
||||
- name: Set correct permissions on SSH key
|
||||
file:
|
||||
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
mode: "0600"
|
||||
|
||||
- name: Wait for redirector SSH to be available
|
||||
wait_for:
|
||||
host: "{{ redirector_ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
state: started
|
||||
|
||||
- name: Add redirector to inventory
|
||||
add_host:
|
||||
name: "redirector"
|
||||
groups: "redirectors"
|
||||
ansible_host: "{{ redirector_ip }}"
|
||||
ansible_user: "ubuntu"
|
||||
ansible_ssh_private_key_file: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes"
|
||||
ansible_python_interpreter: "/usr/bin/python3"
|
||||
shell_handler_port: "{{ effective_listen_port }}"
|
||||
# Add necessary variables for redirector configuration
|
||||
c2_ip: "{{ c2_ip }}"
|
||||
domain: "{{ domain }}"
|
||||
redirector_subdomain: "{{ redirector_subdomain }}"
|
||||
havoc_teamserver_port: "{{ havoc_teamserver_port | default(40056) }}"
|
||||
havoc_http_port: "{{ havoc_http_port | default(8080) }}"
|
||||
havoc_https_port: "{{ havoc_https_port | default(443) }}"
|
||||
zero_logs: "{{ zero_logs | default(true) }}"
|
||||
|
||||
# Rest of the playbook for configuring the redirector
|
||||
- name: Configure redirector
|
||||
hosts: redirectors
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}"
|
||||
shell_handler_port: "{{ hostvars['localhost']['effective_listen_port'] }}"
|
||||
|
||||
# Include the rest of your redirector configuration tasks here
|
||||
tasks:
|
||||
- name: Include common redirector configuration tasks
|
||||
include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml"
|
||||
|
||||
- name: Configure shell handler script with listening port
|
||||
template:
|
||||
src: "../../modules/c2/files/havoc_shell_handler.sh"
|
||||
dest: "/root/Tools/shell_handler.sh"
|
||||
mode: 0755
|
||||
vars:
|
||||
listen_port: "{{ effective_listen_port }}"
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "Redirector Deployment Complete!"
|
||||
- "-----------------------------"
|
||||
- "Redirector IP: {{ ansible_host }}"
|
||||
- "Redirector Domain: {{ redirector_subdomain | default('cdn') }}.{{ domain }} (Update DNS A record)"
|
||||
- "SSH Key: ~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
|
||||
when: not disable_summary | default(false)
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"deployment_id": "{{ deployment_id }}",
|
||||
"deployment_type": "{{ deployment_type }}",
|
||||
"provider": "{{ provider }}",
|
||||
"domain": "{{ phishing_domain | default(domain) }}",
|
||||
"timestamp": "{{ ansible_date_time.iso8601 }}",
|
||||
"components": {{ deployment_components | to_json }},
|
||||
"results": {{ phishing_deployment_results | default({}) | to_json }},
|
||||
"status": "completed"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
# AWS provision tasks for one WEBRUNNER node
|
||||
# Called in a loop — loop_var: node_chunk
|
||||
# Requires: aws_access_key, aws_secret_key, aws_region, aws_instance_type,
|
||||
# ssh_key_name, webrunner_name, deployment_id, operator_ip,
|
||||
# scanner_ip_log, results_dir
|
||||
|
||||
- name: Read public key for {{ node_chunk.node_name }}
|
||||
slurp:
|
||||
src: "~/.ssh/{{ ssh_key_name }}.pub"
|
||||
register: wr_pubkey_aws
|
||||
|
||||
- name: Import SSH key to AWS ({{ node_chunk.node_name }})
|
||||
amazon.aws.ec2_key:
|
||||
name: "{{ webrunner_name }}"
|
||||
key_material: "{{ wr_pubkey_aws.content | b64decode | trim }}"
|
||||
region: "{{ aws_region | default('us-east-1') }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
state: present
|
||||
ignore_errors: true
|
||||
|
||||
- name: Create security group for {{ node_chunk.node_name }}
|
||||
amazon.aws.ec2_security_group:
|
||||
name: "wr-{{ deployment_id }}-sg"
|
||||
description: "WEBRUNNER {{ deployment_id }} scanner nodes"
|
||||
region: "{{ aws_region | default('us-east-1') }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
rules:
|
||||
- proto: tcp
|
||||
ports: [22]
|
||||
cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32"
|
||||
rules_egress:
|
||||
- proto: all
|
||||
cidr_ip: "0.0.0.0/0"
|
||||
tags:
|
||||
Name: "wr-{{ deployment_id }}-sg"
|
||||
DeploymentID: "{{ deployment_id }}"
|
||||
state: present
|
||||
register: wr_sg
|
||||
ignore_errors: true
|
||||
|
||||
- name: Launch EC2 instance {{ node_chunk.node_name }}
|
||||
amazon.aws.ec2_instance:
|
||||
name: "{{ node_chunk.node_name }}"
|
||||
key_name: "{{ webrunner_name }}"
|
||||
instance_type: "{{ aws_instance_type | default('t3.small') }}"
|
||||
image_id: "{{ aws_ami | default('ami-0c55b159cbfafe1f0') }}"
|
||||
region: "{{ aws_region | default('us-east-1') }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_group: "wr-{{ deployment_id }}-sg"
|
||||
network:
|
||||
assign_public_ip: true
|
||||
tags:
|
||||
Name: "{{ node_chunk.node_name }}"
|
||||
DeploymentID: "{{ deployment_id }}"
|
||||
webrunner: "{{ webrunner_name }}"
|
||||
wait: true
|
||||
state: running
|
||||
register: wr_ec2
|
||||
|
||||
- name: Extract EC2 public IP
|
||||
set_fact:
|
||||
wr_node_ip: "{{ wr_ec2.instances[0].public_ip_address }}"
|
||||
|
||||
- name: Log scanner IP
|
||||
lineinfile:
|
||||
path: "{{ scanner_ip_log }}"
|
||||
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
|
||||
create: true
|
||||
|
||||
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
|
||||
wait_for:
|
||||
host: "{{ wr_node_ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
|
||||
- name: Add {{ node_chunk.node_name }} to inventory
|
||||
add_host:
|
||||
name: "{{ wr_node_ip }}"
|
||||
groups: webrunner_nodes
|
||||
ansible_host: "{{ wr_node_ip }}"
|
||||
ansible_user: admin
|
||||
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
|
||||
node_name: "{{ node_chunk.node_name }}"
|
||||
node_cidrs: "{{ node_chunk.cidrs }}"
|
||||
node_ip_count: "{{ node_chunk.ip_count }}"
|
||||
node_idx: "{{ node_chunk.idx }}"
|
||||
ec2_instance_id: "{{ wr_ec2.instances[0].instance_id }}"
|
||||
provider: aws
|
||||
|
||||
- name: Show {{ node_chunk.node_name }} ready
|
||||
debug:
|
||||
msg: "AWS node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
|
||||
@@ -0,0 +1,634 @@
|
||||
---
|
||||
# FlokiNET full deployment playbook (C2 + Redirector)
|
||||
|
||||
- name: Prepare FlokiNET infrastructure deployment
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Generate random shell handler port if not provided
|
||||
shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}"
|
||||
redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
|
||||
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate required FlokiNET configuration
|
||||
assert:
|
||||
that:
|
||||
- redirector_ip is defined and redirector_ip != ""
|
||||
- c2_ip is defined and c2_ip != ""
|
||||
fail_msg: "FlokiNET requires both redirector_ip and c2_ip. Set these values in vars.yaml or via command line arguments."
|
||||
|
||||
- name: Add redirector to inventory
|
||||
add_host:
|
||||
name: "redirector"
|
||||
groups: "redirectors"
|
||||
ansible_host: "{{ redirector_ip }}"
|
||||
ansible_user: "{{ ssh_user | default('root') }}"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_port: "{{ ssh_port | default(22) }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
- name: Add C2 server to inventory
|
||||
add_host:
|
||||
name: "c2"
|
||||
groups: "c2servers"
|
||||
ansible_host: "{{ c2_ip }}"
|
||||
ansible_user: "{{ ssh_user | default('root') }}"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_port: "{{ ssh_port | default(22) }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
- name: Verify SSH connection to redirector
|
||||
wait_for:
|
||||
host: "{{ redirector_ip }}"
|
||||
port: "{{ ssh_port | default(22) }}"
|
||||
delay: 10
|
||||
timeout: 60
|
||||
state: started
|
||||
ignore_errors: true
|
||||
|
||||
- name: Verify SSH connection to C2 server
|
||||
wait_for:
|
||||
host: "{{ c2_ip }}"
|
||||
port: "{{ ssh_port | default(22) }}"
|
||||
delay: 10
|
||||
timeout: 60
|
||||
state: started
|
||||
ignore_errors: true
|
||||
|
||||
- name: Configure FlokiNET redirector
|
||||
hosts: redirectors
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
tasks:
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 5
|
||||
delay: 10
|
||||
|
||||
- name: Set hostname
|
||||
hostname:
|
||||
name: "redirector"
|
||||
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Upgrade all packages
|
||||
apt:
|
||||
upgrade: dist
|
||||
|
||||
- name: Install base utilities and tools via apt
|
||||
apt:
|
||||
name:
|
||||
- git
|
||||
- wget
|
||||
- curl
|
||||
- unzip
|
||||
- python3-pip
|
||||
- python3-virtualenv
|
||||
- tmux
|
||||
- pipx
|
||||
- nmap
|
||||
- tcpdump
|
||||
- nginx
|
||||
- certbot
|
||||
- python3-certbot-nginx
|
||||
- socat
|
||||
- netcat-openbsd
|
||||
- secure-delete
|
||||
state: present
|
||||
|
||||
- name: Set a custom MOTD
|
||||
template:
|
||||
src: motd-flokinet.j2
|
||||
dest: /etc/motd
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
vars:
|
||||
letsencrypt_email: "{{ letsencrypt_email }}"
|
||||
domain: "{{ domain }}"
|
||||
redirector_subdomain: "{{ redirector_subdomain }}"
|
||||
|
||||
- name: Create directories for operational scripts
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
with_items:
|
||||
- /opt/c2
|
||||
- /opt/shell-handler
|
||||
|
||||
- name: Copy clean-logs.sh script
|
||||
copy:
|
||||
src: "../../common/files/clean-logs.sh"
|
||||
dest: /opt/c2/clean-logs.sh
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Copy secure-exit.sh script
|
||||
copy:
|
||||
src: "../../common/files/secure-exit.sh"
|
||||
dest: /opt/c2/secure-exit.sh
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Copy shell handler script
|
||||
copy:
|
||||
src: "../../common/files/persistent-listener.sh"
|
||||
dest: /opt/shell-handler/persistent-listener.sh
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Configure shell handler script with C2 IP
|
||||
replace:
|
||||
path: /opt/shell-handler/persistent-listener.sh
|
||||
regexp: 'C2_HOST="127.0.0.1"'
|
||||
replace: 'C2_HOST="{{ c2_ip }}"'
|
||||
|
||||
- name: Configure shell handler script with listening port
|
||||
replace:
|
||||
path: /opt/shell-handler/persistent-listener.sh
|
||||
regexp: 'LISTEN_PORT=4444'
|
||||
replace: 'LISTEN_PORT={{ shell_handler_port }}'
|
||||
|
||||
- name: Create shell handler service
|
||||
template:
|
||||
src: shell-handler.service.j2
|
||||
dest: /etc/systemd/system/shell-handler.service
|
||||
mode: '0644'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Configure NGINX for zero-logging if enabled
|
||||
template:
|
||||
src: nginx.conf.j2
|
||||
dest: /etc/nginx/nginx.conf
|
||||
mode: '0644'
|
||||
owner: root
|
||||
group: root
|
||||
when: zero_logs | bool
|
||||
|
||||
- name: Configure NGINX for C2 redirection
|
||||
template:
|
||||
src: redirector-site.conf.j2
|
||||
dest: /etc/nginx/sites-available/default
|
||||
mode: '0644'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Create legitimate-looking index.html
|
||||
template:
|
||||
src: redirector-index.html.j2
|
||||
dest: /var/www/html/index.html
|
||||
mode: '0644'
|
||||
owner: www-data
|
||||
group: www-data
|
||||
|
||||
- name: Start and enable shell handler service
|
||||
systemd:
|
||||
name: shell-handler
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Set up cron job for log cleaning if zero-logs enabled
|
||||
cron:
|
||||
name: "Clean logs"
|
||||
minute: "0"
|
||||
hour: "*/6"
|
||||
job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
|
||||
when: zero_logs | bool
|
||||
|
||||
- name: Install Let's Encrypt certificate if domain specified
|
||||
shell: |
|
||||
certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem
|
||||
when: domain != "example.com"
|
||||
|
||||
- name: Restart NGINX
|
||||
systemd:
|
||||
name: nginx
|
||||
state: restarted
|
||||
|
||||
- name: FlokiNET-specific security configurations
|
||||
include_tasks: flokinet-security.yml
|
||||
when: enable_hardened_security | default(true)
|
||||
|
||||
- name: Configure FlokiNET C2 server
|
||||
hosts: c2servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
tasks:
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 5
|
||||
delay: 10
|
||||
|
||||
- name: Set hostname
|
||||
hostname:
|
||||
name: "c2"
|
||||
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Upgrade all packages
|
||||
apt:
|
||||
upgrade: dist
|
||||
|
||||
- name: Set a custom MOTD
|
||||
template:
|
||||
src: motd-flokinet.j2
|
||||
dest: /etc/motd
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
vars:
|
||||
letsencrypt_email: "{{ letsencrypt_email }}"
|
||||
domain: "{{ domain }}"
|
||||
c2_subdomain: "{{ c2_subdomain }}"
|
||||
|
||||
- name: Install base utilities and tools via apt
|
||||
apt:
|
||||
name:
|
||||
- git
|
||||
- wget
|
||||
- curl
|
||||
- unzip
|
||||
- python3-pip
|
||||
- python3-virtualenv
|
||||
- tmux
|
||||
- pipx
|
||||
- nmap
|
||||
- tcpdump
|
||||
- hydra
|
||||
- john
|
||||
- hashcat
|
||||
- sqlmap
|
||||
- gobuster
|
||||
- dirb
|
||||
- enum4linux
|
||||
- dnsenum
|
||||
- seclists
|
||||
- responder
|
||||
- golang
|
||||
- proxychains
|
||||
- tor
|
||||
- crackmapexec
|
||||
- jq
|
||||
- unzip
|
||||
- postfix
|
||||
- certbot
|
||||
- opendkim
|
||||
- opendkim-tools
|
||||
- dovecot-core
|
||||
- dovecot-imapd
|
||||
- dovecot-pop3d
|
||||
- dovecot-sieve
|
||||
- dovecot-managesieved
|
||||
- yq
|
||||
state: present
|
||||
|
||||
- name: Create Tools directory
|
||||
file:
|
||||
path: /root/Tools
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Ensure pipx path is configured
|
||||
shell: |
|
||||
pipx ensurepath
|
||||
args:
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Install tools via pipx
|
||||
shell: |
|
||||
export PATH=$PATH:/root/.local/bin
|
||||
pipx ensurepath
|
||||
pipx install git+https://github.com/Pennyw0rth/NetExec
|
||||
pipx install git+https://github.com/blacklanternsecurity/TREVORspray
|
||||
pipx install impacket
|
||||
args:
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Download Kerbrute
|
||||
shell: |
|
||||
mkdir -p ~/Tools/Kerbrute
|
||||
wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O ~/Tools/Kerbrute/kerbrute
|
||||
chmod +x ~/Tools/Kerbrute/kerbrute
|
||||
args:
|
||||
executable: /bin/bash
|
||||
creates: /root/Tools/Kerbrute/kerbrute
|
||||
|
||||
- name: Clone SharpCollection nightly builds
|
||||
git:
|
||||
repo: https://github.com/Flangvik/SharpCollection.git
|
||||
dest: ~/Tools/SharpCollection
|
||||
version: master
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Clone PEASS-ng
|
||||
git:
|
||||
repo: https://github.com/carlospolop/PEASS-ng.git
|
||||
dest: ~/Tools/PEASS-ng
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Clone MailSniper
|
||||
git:
|
||||
repo: https://github.com/dafthack/MailSniper.git
|
||||
dest: ~/Tools/MailSniper
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Clone Inveigh
|
||||
git:
|
||||
repo: https://github.com/Kevin-Robertson/Inveigh.git
|
||||
dest: ~/Tools/Inveigh
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Install Sliver C2 server
|
||||
shell: |
|
||||
curl https://sliver.sh/install | bash
|
||||
systemctl enable sliver
|
||||
systemctl start sliver
|
||||
|
||||
- name: Install Metasploit Framework (Nightly Build)
|
||||
shell: |
|
||||
curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > ~/Tools/msfinstall
|
||||
chmod 755 ~/Tools/msfinstall
|
||||
~/Tools/msfinstall
|
||||
args:
|
||||
executable: /bin/bash
|
||||
creates: /usr/bin/msfconsole
|
||||
|
||||
- name: Grab GoPhish
|
||||
shell: |
|
||||
curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o ~/Tools/gophish.zip
|
||||
unzip ~/Tools/gophish.zip -d ~/Tools/gophish
|
||||
rm -rf ~/Tools/gophish.zip
|
||||
chmod +x ~/Tools/gophish/gophish
|
||||
args:
|
||||
creates: /root/Tools/gophish/gophish
|
||||
|
||||
- name: Deploy Gophish config.json with custom admin port
|
||||
template:
|
||||
src: gophish-config.j2
|
||||
dest: ~/Tools/gophish/config.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
vars:
|
||||
gophish_admin_port: "{{ gophish_admin_port }}"
|
||||
domain: "{{ domain }}"
|
||||
|
||||
- name: Configure Postfix main.cf
|
||||
lineinfile:
|
||||
path: /etc/postfix/main.cf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
with_items:
|
||||
- { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" }
|
||||
- { regexp: '^mydomain', line: "mydomain = {{ domain }}" }
|
||||
- { regexp: '^myorigin', line: "myorigin = $mydomain" }
|
||||
- { regexp: '^inet_interfaces', line: "inet_interfaces = all" }
|
||||
- { regexp: '^inet_protocols', line: "inet_protocols = ipv4" }
|
||||
- { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" }
|
||||
- { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" }
|
||||
- { regexp: '^relay_domains', line: "relay_domains = $mydestination" }
|
||||
- { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" }
|
||||
- { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" }
|
||||
- { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" }
|
||||
- { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" }
|
||||
- { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" }
|
||||
- { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" }
|
||||
- { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" }
|
||||
- { regexp: '^milter_default_action', line: "milter_default_action = accept" }
|
||||
- { regexp: '^milter_protocol', line: "milter_protocol = 6" }
|
||||
- { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
|
||||
- { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
|
||||
|
||||
- name: Configure OpenDKIM
|
||||
lineinfile:
|
||||
path: /etc/opendkim.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
with_items:
|
||||
- { regexp: '^Domain', line: "Domain {{ domain }}" }
|
||||
- { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" }
|
||||
- { regexp: '^Selector', line: "Selector mail" }
|
||||
- { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" }
|
||||
- { regexp: '^Syslog', line: "Syslog yes" }
|
||||
- { regexp: '^UMask', line: "UMask 002" }
|
||||
- { regexp: '^Mode', line: "Mode sv" }
|
||||
|
||||
- name: Create DKIM directory
|
||||
file:
|
||||
path: /etc/opendkim/keys/{{ domain }}
|
||||
state: directory
|
||||
owner: opendkim
|
||||
group: opendkim
|
||||
mode: 0700
|
||||
|
||||
- name: Generate DKIM keys
|
||||
command: >
|
||||
opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail
|
||||
args:
|
||||
creates: /etc/opendkim/keys/{{ domain }}/mail.private
|
||||
|
||||
- name: Set permissions for DKIM keys
|
||||
file:
|
||||
path: /etc/opendkim/keys/{{ domain }}/mail.private
|
||||
owner: opendkim
|
||||
group: opendkim
|
||||
mode: 0600
|
||||
|
||||
- name: Configure OpenDKIM TrustedHosts
|
||||
copy:
|
||||
content: |
|
||||
127.0.0.1
|
||||
::1
|
||||
localhost
|
||||
{{ domain }}
|
||||
dest: /etc/opendkim/TrustedHosts
|
||||
owner: opendkim
|
||||
group: opendkim
|
||||
mode: 0644
|
||||
|
||||
- name: Enable submission port (587) in master.cf
|
||||
blockinfile:
|
||||
path: /etc/postfix/master.cf
|
||||
insertafter: '^#submission'
|
||||
block: |
|
||||
submission inet n - y - - smtpd
|
||||
-o syslog_name=postfix/submission
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_sasl_auth_enable=yes
|
||||
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
|
||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||
|
||||
- name: Configure Dovecot for Postfix SASL
|
||||
blockinfile:
|
||||
path: /etc/dovecot/conf.d/10-master.conf
|
||||
insertafter: '^service auth {'
|
||||
block: |
|
||||
# Postfix smtp-auth
|
||||
unix_listener /var/spool/postfix/private/auth {
|
||||
mode = 0660
|
||||
user = postfix
|
||||
group = postfix
|
||||
}
|
||||
|
||||
- name: Set Dovecot auth_mechanisms
|
||||
lineinfile:
|
||||
path: /etc/dovecot/conf.d/10-auth.conf
|
||||
regexp: '^auth_mechanisms'
|
||||
line: 'auth_mechanisms = plain login'
|
||||
|
||||
- name: Create Dovecot password file for SASL authentication
|
||||
file:
|
||||
path: /etc/dovecot/passwd
|
||||
state: touch
|
||||
mode: '0600'
|
||||
owner: dovecot
|
||||
group: dovecot
|
||||
|
||||
- name: Add SMTP auth user to Dovecot
|
||||
lineinfile:
|
||||
path: /etc/dovecot/passwd
|
||||
line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}"
|
||||
|
||||
- name: Disable system auth and use passwd-file
|
||||
lineinfile:
|
||||
path: /etc/dovecot/conf.d/10-auth.conf
|
||||
regexp: '^!include auth-system.conf.ext'
|
||||
line: '#!include auth-system.conf.ext'
|
||||
|
||||
- name: Add auth-passwdfile configuration
|
||||
blockinfile:
|
||||
path: /etc/dovecot/conf.d/10-auth.conf
|
||||
insertafter: '^auth_mechanisms ='
|
||||
block: |
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = scheme=sha512_crypt /etc/dovecot/passwd
|
||||
}
|
||||
userdb {
|
||||
driver = static
|
||||
args = uid=vmail gid=vmail home=/var/vmail/%u
|
||||
}
|
||||
|
||||
- name: Create vmail user/group
|
||||
group:
|
||||
name: vmail
|
||||
gid: 5000
|
||||
state: present
|
||||
|
||||
- name: Create vmail user
|
||||
user:
|
||||
name: vmail
|
||||
uid: 5000
|
||||
group: vmail
|
||||
create_home: no
|
||||
|
||||
- name: Create directories for operational scripts
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
with_items:
|
||||
- /opt/c2
|
||||
- /opt/beacons
|
||||
|
||||
- name: Copy clean-logs.sh script
|
||||
copy:
|
||||
src: "../../common/files/clean-logs.sh"
|
||||
dest: /opt/c2/clean-logs.sh
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Copy secure-exit.sh script
|
||||
copy:
|
||||
src: "../../common/files/secure-exit.sh"
|
||||
dest: /opt/c2/secure-exit.sh
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Copy serve-beacons.sh script
|
||||
copy:
|
||||
src: "../../common/files/serve-beacons.sh"
|
||||
dest: /opt/c2/serve-beacons.sh
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Configure serve-beacons.sh with C2 IP
|
||||
replace:
|
||||
path: /opt/c2/serve-beacons.sh
|
||||
regexp: 'C2_HOST=.*'
|
||||
replace: 'C2_HOST="{{ c2_ip }}"'
|
||||
|
||||
- name: Set up cron job for log cleaning if zero-logs enabled
|
||||
cron:
|
||||
name: "Clean logs"
|
||||
minute: "0"
|
||||
hour: "*/6"
|
||||
job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
|
||||
when: zero_logs | bool
|
||||
|
||||
- name: Install Let's Encrypt certificate if domain specified
|
||||
shell: |
|
||||
certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem
|
||||
when: domain != "example.com"
|
||||
|
||||
- name: Restart Postfix
|
||||
service:
|
||||
name: postfix
|
||||
state: restarted
|
||||
|
||||
- name: Restart Dovecot
|
||||
service:
|
||||
name: dovecot
|
||||
state: restarted
|
||||
|
||||
- name: FlokiNET-specific security configurations
|
||||
include_tasks: flokinet-security.yml
|
||||
when: enable_hardened_security | default(true)
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "FlokiNET Deployment Complete!"
|
||||
- "-------------------------------"
|
||||
- "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
|
||||
- "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
|
||||
- "Shell Handler Port: {{ shell_handler_port }}"
|
||||
- "GoPhish Admin Port: {{ gophish_admin_port }}"
|
||||
when: not disable_summary | default(false)
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
# FlokiNET C2-only Configuration Playbook
|
||||
# Note: FlokiNET requires pre-provisioned servers
|
||||
|
||||
- name: Prepare FlokiNET C2 configuration
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate required FlokiNET configuration
|
||||
assert:
|
||||
that:
|
||||
- c2_ip is defined and c2_ip != ""
|
||||
fail_msg: "FlokiNET requires C2 IP address. Set c2_ip in vars.yaml or via --flokinet-c2-ip."
|
||||
|
||||
- name: Add C2 to inventory
|
||||
add_host:
|
||||
name: "c2"
|
||||
groups: "c2servers"
|
||||
ansible_host: "{{ c2_ip }}"
|
||||
ansible_user: "{{ ssh_user | default('root') }}"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_port: "{{ ssh_port | default(22) }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
- name: Verify SSH connection to C2
|
||||
wait_for:
|
||||
host: "{{ c2_ip }}"
|
||||
port: "{{ ssh_port | default(22) }}"
|
||||
delay: 10
|
||||
timeout: 60
|
||||
state: started
|
||||
ignore_errors: true
|
||||
|
||||
- name: Provision FlokiNET C2 server
|
||||
hosts: c2servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
tasks:
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 5
|
||||
delay: 10
|
||||
|
||||
- name: Set hostname
|
||||
hostname:
|
||||
name: "c2"
|
||||
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Upgrade all packages
|
||||
apt:
|
||||
upgrade: dist
|
||||
|
||||
- name: Install base security packages
|
||||
apt:
|
||||
name:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
- unattended-upgrades
|
||||
- ufw
|
||||
- fail2ban
|
||||
- secure-delete
|
||||
state: present
|
||||
|
||||
- name: Include common security hardening tasks
|
||||
include_tasks: "../../common/tasks/security_hardening.yml"
|
||||
|
||||
- name: Include common tool installation tasks
|
||||
include_tasks: "../../common/tasks/install_tools.yml"
|
||||
|
||||
- name: Include common C2 configuration tasks
|
||||
include_tasks: "../../modules/c2/tasks/configure_c2.yml"
|
||||
|
||||
- name: Include common mail server configuration tasks
|
||||
include_tasks: "../../common/tasks/configure_mail.yml"
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "C2 Server Configuration Complete!"
|
||||
- "-------------------------------"
|
||||
- "C2 Server IP: {{ ansible_host }}"
|
||||
- "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
|
||||
- "GoPhish Admin Port: {{ gophish_admin_port }}"
|
||||
when: not disable_summary | default(false)
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
# FlokiNET Cleanup Playbook
|
||||
# Used for documentation since FlokiNET requires manual cleanup through their interface
|
||||
|
||||
- name: Document FlokiNET cleanup procedure
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
cleanup_redirector: "{{ (redirector_ip is defined and redirector_ip != '') | ternary(true, false) }}"
|
||||
cleanup_c2: "{{ (c2_ip is defined and c2_ip != '') | ternary(true, false) }}"
|
||||
confirm_cleanup: false # Skip confirmation in automated teardown
|
||||
|
||||
tasks:
|
||||
- name: Confirm cleanup if required
|
||||
pause:
|
||||
prompt: "WARNING: This script provides guidance for manual FlokiNET cleanup. Type 'yes' to continue"
|
||||
register: confirmation
|
||||
when: confirm_cleanup
|
||||
|
||||
- name: Exit if not confirmed
|
||||
meta: end_play
|
||||
when: confirm_cleanup and confirmation.user_input != 'yes'
|
||||
|
||||
- name: Display cleanup instructions
|
||||
debug:
|
||||
msg:
|
||||
- "FlokiNET Cleanup Instructions"
|
||||
- "========================================"
|
||||
- "Since FlokiNET resources must be manually terminated through their control panel,"
|
||||
- "this playbook provides guidance on the steps needed for cleanup."
|
||||
- ""
|
||||
- "Resources to clean up:"
|
||||
- "{{ cleanup_redirector | ternary('- Redirector server: ' + redirector_ip, '') }}"
|
||||
- "{{ cleanup_c2 | ternary('- C2 server: ' + c2_ip, '') }}"
|
||||
- ""
|
||||
- "Steps to terminate FlokiNET servers:"
|
||||
- "1. Log in to your FlokiNET control panel"
|
||||
- "2. Navigate to the Virtual Servers section"
|
||||
- "3. Select each server from the list"
|
||||
- "4. Click 'Terminate' and confirm termination"
|
||||
- ""
|
||||
- "For security, consider also:"
|
||||
- "- Manually executing the secure-exit.sh script on each server before termination"
|
||||
- "- Removing DNS records associated with these servers"
|
||||
- "- Ensuring any SSH keys used for these servers are removed or rotated"
|
||||
|
||||
- name: Remove SSH key file if it's not a shared key
|
||||
file:
|
||||
path: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
state: absent
|
||||
when:
|
||||
- ssh_key_path is defined
|
||||
- ssh_key_path is search('c2deploy_')
|
||||
ignore_errors: true
|
||||
|
||||
- name: Remove SSH public key file if it's not a shared key
|
||||
file:
|
||||
path: "{{ ssh_key_path }}"
|
||||
state: absent
|
||||
when:
|
||||
- ssh_key_path is defined
|
||||
- ssh_key_path is search('c2deploy_')
|
||||
ignore_errors: true
|
||||
|
||||
- name: Pre-termination security recommendations
|
||||
debug:
|
||||
msg:
|
||||
- "Security Recommendations before manual termination:"
|
||||
- "------------------------------------------------"
|
||||
- " SSH Command: ssh -i {{ ssh_key_path | replace('.pub', '') }} {{ ssh_user | default('root') }}@SERVER_IP -p {{ ssh_port | default('22') }}"
|
||||
- " Then run: /opt/c2/secure-exit.sh"
|
||||
when: (cleanup_redirector or cleanup_c2) and ssh_key_path is defined
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
# FlokiNET-specific security tasks
|
||||
# Enhanced security features for FlokiNET servers
|
||||
|
||||
- name: Configure FlokiNET networking for maximum anonymity
|
||||
lineinfile:
|
||||
path: /etc/sysctl.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
state: present
|
||||
with_items:
|
||||
- { regexp: '^net.ipv4.tcp_timestamps', line: 'net.ipv4.tcp_timestamps = 0' }
|
||||
- { regexp: '^net.ipv4.tcp_syncookies', line: 'net.ipv4.tcp_syncookies = 1' }
|
||||
- { regexp: '^net.ipv4.conf.all.accept_redirects', line: 'net.ipv4.conf.all.accept_redirects = 0' }
|
||||
- { regexp: '^net.ipv6.conf.all.accept_redirects', line: 'net.ipv6.conf.all.accept_redirects = 0' }
|
||||
- { regexp: '^net.ipv4.conf.all.send_redirects', line: 'net.ipv4.conf.all.send_redirects = 0' }
|
||||
- { regexp: '^net.ipv4.conf.all.accept_source_route', line: 'net.ipv4.conf.all.accept_source_route = 0' }
|
||||
- { regexp: '^net.ipv6.conf.all.accept_source_route', line: 'net.ipv6.conf.all.accept_source_route = 0' }
|
||||
- { regexp: '^net.ipv4.conf.all.log_martians', line: 'net.ipv4.conf.all.log_martians = 1' }
|
||||
notify: Apply sysctl settings
|
||||
|
||||
- name: Install Tor for anonymous outbound connections
|
||||
apt:
|
||||
name:
|
||||
- tor
|
||||
- torsocks
|
||||
- obfs4proxy
|
||||
state: present
|
||||
update_cache: yes
|
||||
register: tor_installed
|
||||
when: use_tor_proxy | default(true)
|
||||
|
||||
- name: Create Tor configuration directory
|
||||
file:
|
||||
path: /etc/tor
|
||||
state: directory
|
||||
mode: '0755'
|
||||
when: use_tor_proxy | default(true) and tor_installed is succeeded
|
||||
|
||||
- name: Configure Tor for hardened privacy settings
|
||||
template:
|
||||
src: torrc.j2
|
||||
dest: /etc/tor/torrc
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Restart Tor service
|
||||
when: use_tor_proxy | default(true) and tor_installed is succeeded
|
||||
|
||||
- name: Configure ProxyChains for routing through Tor
|
||||
template:
|
||||
src: proxychains.conf.j2
|
||||
dest: /etc/proxychains.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
when: use_tor_proxy | default(true) and tor_installed is succeeded
|
||||
|
||||
- name: Install iptables-persistent for firewall persistence
|
||||
apt:
|
||||
name: iptables-persistent
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Configure hardened iptables rules for FlokiNET
|
||||
template:
|
||||
src: iptables-rules.j2
|
||||
dest: /etc/iptables/rules.v4
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Apply iptables rules
|
||||
|
||||
- name: Create SSH security hardening script
|
||||
template:
|
||||
src: secure-ssh.sh.j2
|
||||
dest: /opt/c2/secure-ssh.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0700'
|
||||
when: use_hardened_ssh | default(true)
|
||||
|
||||
- name: Run SSH security hardening script
|
||||
command: /opt/c2/secure-ssh.sh
|
||||
args:
|
||||
creates: /opt/c2/.ssh_hardened
|
||||
when: use_hardened_ssh | default(true)
|
||||
|
||||
- name: Install timezone data
|
||||
apt:
|
||||
name: tzdata
|
||||
state: present
|
||||
|
||||
- name: Set timezone to UTC
|
||||
timezone:
|
||||
name: UTC
|
||||
|
||||
- name: Configure DNS to use secure DNS servers
|
||||
template:
|
||||
src: resolv.conf.j2
|
||||
dest: /etc/resolv.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
when: enable_dns_encryption | default(true)
|
||||
|
||||
- name: Create directory for DNS cache configuration
|
||||
file:
|
||||
path: /etc/systemd/resolved.conf.d
|
||||
state: directory
|
||||
mode: '0755'
|
||||
when: enable_dns_encryption | default(true)
|
||||
|
||||
- name: Configure DNS caching with DNSCrypt
|
||||
template:
|
||||
src: dnscrypt.conf.j2
|
||||
dest: /etc/systemd/resolved.conf.d/dnscrypt.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Restart systemd-resolved
|
||||
when: enable_dns_encryption | default(true)
|
||||
|
||||
# Configure memory security if enabled
|
||||
- name: Set memory security measures
|
||||
lineinfile:
|
||||
path: /etc/sysctl.conf
|
||||
line: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- "vm.swappiness=0"
|
||||
- "kernel.randomize_va_space=2"
|
||||
when: secure_memory | default(true)
|
||||
notify: Apply sysctl settings
|
||||
|
||||
# Configure history settings if disabled
|
||||
- name: Disable system history
|
||||
lineinfile:
|
||||
path: "{{ item.file }}"
|
||||
line: "{{ item.line }}"
|
||||
state: present
|
||||
create: yes
|
||||
with_nested:
|
||||
- [{ file: '/etc/profile' }, { file: '/root/.bashrc' }]
|
||||
- [{ line: 'export HISTFILESIZE=0' }, { line: 'export HISTSIZE=0' }, { line: 'unset HISTFILE' }]
|
||||
when: disable_history | default(true)
|
||||
|
||||
handlers:
|
||||
- name: Apply sysctl settings
|
||||
command: sysctl -p
|
||||
|
||||
- name: Restart Tor service
|
||||
service:
|
||||
name: tor
|
||||
state: restarted
|
||||
enabled: yes
|
||||
when: use_tor_proxy | default(true)
|
||||
|
||||
- name: Apply iptables rules
|
||||
command: iptables-restore < /etc/iptables/rules.v4
|
||||
|
||||
- name: Restart systemd-resolved
|
||||
service:
|
||||
name: systemd-resolved
|
||||
state: restarted
|
||||
enabled: yes
|
||||
when: enable_dns_encryption | default(true)
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
# FlokiNET-specific phishing infrastructure tasks
|
||||
|
||||
- name: Create FlokiNET instances for phishing
|
||||
uri:
|
||||
url: "{{ flokinet_api_endpoint }}/instances"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ flokinet_api_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
plan: "{{ flokinet_plan | default('basic') }}"
|
||||
region: "{{ flokinet_region | default('romania') }}"
|
||||
os: "{{ flokinet_os | default('ubuntu-22.04') }}"
|
||||
hostname: "{{ deployment_id }}-{{ item }}"
|
||||
ssh_keys:
|
||||
- "{{ ssh_public_key }}"
|
||||
tags:
|
||||
- "c2itall"
|
||||
- "phishing"
|
||||
- "{{ deployment_id }}"
|
||||
status_code: [200, 201]
|
||||
register: flokinet_instances
|
||||
loop: "{{ deployment_components }}"
|
||||
when: item in deployment_components
|
||||
|
||||
- name: Wait for instances to be active
|
||||
uri:
|
||||
url: "{{ flokinet_api_endpoint }}/instances/{{ item.json.id }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ flokinet_api_token }}"
|
||||
register: instance_status
|
||||
until: instance_status.json.status == "active"
|
||||
retries: 30
|
||||
delay: 10
|
||||
loop: "{{ flokinet_instances.results }}"
|
||||
when: flokinet_instances.results is defined
|
||||
|
||||
- name: Get instance details
|
||||
uri:
|
||||
url: "{{ flokinet_api_endpoint }}/instances/{{ item.json.id }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ flokinet_api_token }}"
|
||||
register: instance_details
|
||||
loop: "{{ flokinet_instances.results }}"
|
||||
when: flokinet_instances.results is defined
|
||||
|
||||
- name: Set instance facts
|
||||
set_fact:
|
||||
phishing_instances: >-
|
||||
{% set instances = [] %}
|
||||
{% for result in instance_details.results %}
|
||||
{% set instance = {
|
||||
'id': result.json.id,
|
||||
'hostname': result.json.hostname,
|
||||
'ip': result.json.main_ip,
|
||||
'region': result.json.region,
|
||||
'plan': result.json.plan,
|
||||
'status': result.json.status
|
||||
} %}
|
||||
{% set _ = instances.append(instance) %}
|
||||
{% endfor %}
|
||||
{{ instances }}
|
||||
when: instance_details.results is defined
|
||||
|
||||
- name: Wait for SSH connectivity
|
||||
wait_for:
|
||||
host: "{{ item.ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
loop: "{{ phishing_instances }}"
|
||||
when: phishing_instances is defined
|
||||
|
||||
- name: Update instance inventory
|
||||
add_host:
|
||||
name: "{{ item.ip }}"
|
||||
groups: "phishing_{{ item.hostname.split('-')[-1] }}"
|
||||
ansible_host: "{{ item.ip }}"
|
||||
ansible_user: root
|
||||
ansible_ssh_private_key_file: "{{ ssh_private_key_path }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
instance_id: "{{ item.id }}"
|
||||
instance_hostname: "{{ item.hostname }}"
|
||||
provider: "flokinet"
|
||||
loop: "{{ phishing_instances }}"
|
||||
when: phishing_instances is defined
|
||||
|
||||
- name: Configure Gophish servers
|
||||
include_tasks: ../common/configure_gophish.yml
|
||||
when: "'gophish' in deployment_components"
|
||||
|
||||
- name: Configure MTA fronts
|
||||
include_tasks: ../common/configure_mta.yml
|
||||
when: "'mta_front' in deployment_components"
|
||||
|
||||
- name: Configure redirectors
|
||||
include_tasks: ../common/configure_redirector.yml
|
||||
when: "'redirector' in deployment_components"
|
||||
|
||||
- name: Configure web servers
|
||||
include_tasks: ../common/configure_webserver.yml
|
||||
when: "'webserver' in deployment_components"
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg: |
|
||||
🎯 FlokiNET Phishing Infrastructure Deployed
|
||||
{{ phishing_instances | length }} instances created
|
||||
Provider: FlokiNET
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Instances:
|
||||
{% for instance in phishing_instances %}
|
||||
- {{ instance.hostname }}: {{ instance.ip }} ({{ instance.plan }})
|
||||
{% endfor %}
|
||||
when: phishing_instances is defined
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
# Phishing infrastructure deployment playbook
|
||||
# This is a comprehensive playbook that handles all phishing components
|
||||
|
||||
- name: Deploy Phishing Infrastructure
|
||||
hosts: localhost
|
||||
gather_facts: true
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
|
||||
tasks:
|
||||
- name: Display deployment information
|
||||
debug:
|
||||
msg: |
|
||||
Deploying phishing infrastructure
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Provider: {{ provider }}
|
||||
Domain: {{ domain | default(phishing_domain) }}
|
||||
Components: {{ deployment_type }}
|
||||
|
||||
- name: Set deployment facts
|
||||
set_fact:
|
||||
deployment_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
phishing_deployment_results: {}
|
||||
deployment_components: >-
|
||||
{% set components = [] %}
|
||||
{% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %}
|
||||
{% set _ = components.append('gophish') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %}
|
||||
{% set _ = components.append('mta_front') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %}
|
||||
{% set _ = components.append('redirector') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %}
|
||||
{% set _ = components.append('webserver') %}
|
||||
{% endif %}
|
||||
{{ components }}
|
||||
|
||||
- name: Include provider-specific tasks
|
||||
include_tasks: "{{ provider }}_phishing.yml"
|
||||
when: deployment_components | length > 0
|
||||
|
||||
- name: Ensure logs directory exists
|
||||
file:
|
||||
path: "../../logs"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Save deployment information
|
||||
template:
|
||||
src: phishing_deployment_info.j2
|
||||
dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Display success message
|
||||
debug:
|
||||
msg: |
|
||||
✅ Phishing infrastructure deployment completed
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Components deployed: {{ deployment_components | join(', ') }}
|
||||
Check logs/phishing_deployment_{{ deployment_id }}.json for details
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
- name: Common provisioning for FlokiNET servers
|
||||
hosts: all
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
tasks:
|
||||
- name: Set hostname
|
||||
ansible.builtin.hostname:
|
||||
name: "{{ inventory_hostname }}"
|
||||
become: true
|
||||
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: yes
|
||||
become: true
|
||||
|
||||
- name: Upgrade all packages
|
||||
ansible.builtin.apt:
|
||||
upgrade: dist
|
||||
become: true
|
||||
|
||||
- name: Install base security packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
- unattended-upgrades
|
||||
- ufw
|
||||
- fail2ban
|
||||
- secure-delete
|
||||
state: present
|
||||
become: true
|
||||
|
||||
- name: Configure UFW
|
||||
ansible.builtin.ufw:
|
||||
state: enabled
|
||||
policy: deny
|
||||
logging: 'on'
|
||||
become: true
|
||||
|
||||
- name: Add SSH rule to UFW
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: "{{ ssh_port }}"
|
||||
proto: tcp
|
||||
become: true
|
||||
|
||||
- name: Configure SSH for better security
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
state: present
|
||||
with_items:
|
||||
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin prohibit-password' }
|
||||
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
|
||||
- { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
|
||||
- { regexp: '^#?AllowTcpForwarding', line: 'AllowTcpForwarding no' }
|
||||
- { regexp: '^#?Port', line: 'Port {{ ssh_port }}' }
|
||||
- { regexp: '^#?LogLevel', line: 'LogLevel ERROR' }
|
||||
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
|
||||
- { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
|
||||
become: true
|
||||
|
||||
- name: Restart SSH service
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
become: true
|
||||
|
||||
- name: Setup directory for operational scripts
|
||||
ansible.builtin.file:
|
||||
path: /opt/c2
|
||||
state: directory
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
become: true
|
||||
|
||||
- name: Copy operational scripts
|
||||
ansible.builtin.copy:
|
||||
src: "../../common/files/{{ item }}"
|
||||
dest: "/opt/c2/{{ item }}"
|
||||
mode: '0700'
|
||||
owner: root
|
||||
group: root
|
||||
with_items:
|
||||
- clean-logs.sh
|
||||
- secure-exit.sh
|
||||
become: true
|
||||
|
||||
- name: Setup cron to clean logs
|
||||
ansible.builtin.cron:
|
||||
name: "Log cleanup"
|
||||
minute: "*/{{ log_rotation_hours * 60 }}"
|
||||
job: "/opt/c2/clean-logs.sh >/dev/null 2>&1"
|
||||
become: true
|
||||
when: disable_history | bool
|
||||
|
||||
- name: Disable system history
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item }}"
|
||||
line: "{{ line_item }}"
|
||||
state: present
|
||||
create: yes
|
||||
with_items:
|
||||
- /etc/profile
|
||||
- /root/.bashrc
|
||||
with_nested:
|
||||
- [ 'export HISTFILESIZE=0', 'export HISTSIZE=0', 'unset HISTFILE' ]
|
||||
loop_control:
|
||||
loop_var: line_item
|
||||
become: true
|
||||
when: disable_history | bool
|
||||
|
||||
- name: Set memory security measures
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sysctl.conf
|
||||
line: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- "vm.swappiness=0"
|
||||
- "kernel.randomize_va_space=2"
|
||||
become: true
|
||||
when: secure_memory | bool
|
||||
|
||||
- name: Apply sysctl settings
|
||||
ansible.builtin.command: sysctl -p
|
||||
become: true
|
||||
when: secure_memory | bool
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
# FlokiNET Redirector-only Configuration Playbook
|
||||
# Note: FlokiNET requires pre-provisioned servers
|
||||
|
||||
- name: Prepare FlokiNET redirector configuration
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Generate random shell handler port if not provided
|
||||
shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}"
|
||||
redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate required FlokiNET configuration
|
||||
assert:
|
||||
that:
|
||||
- redirector_ip is defined and redirector_ip != ""
|
||||
fail_msg: "FlokiNET requires redirector IP address. Set redirector_ip in vars.yaml or via --flokinet-redirector-ip."
|
||||
|
||||
- name: Add redirector to inventory
|
||||
add_host:
|
||||
name: "redirector"
|
||||
groups: "redirectors"
|
||||
ansible_host: "{{ redirector_ip }}"
|
||||
ansible_user: "{{ ssh_user | default('root') }}"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_port: "{{ ssh_port | default(22) }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
- name: Verify SSH connection to redirector
|
||||
wait_for:
|
||||
host: "{{ redirector_ip }}"
|
||||
port: "{{ ssh_port | default(22) }}"
|
||||
delay: 10
|
||||
timeout: 60
|
||||
state: started
|
||||
ignore_errors: true
|
||||
|
||||
- name: Provision FlokiNET redirector
|
||||
hosts: redirectors
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
tasks:
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 5
|
||||
delay: 10
|
||||
|
||||
- name: Set hostname
|
||||
hostname:
|
||||
name: "redirector"
|
||||
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
|
||||
- name: Upgrade all packages
|
||||
apt:
|
||||
upgrade: dist
|
||||
|
||||
- name: Install base security packages
|
||||
apt:
|
||||
name:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
- unattended-upgrades
|
||||
- ufw
|
||||
- fail2ban
|
||||
- secure-delete
|
||||
state: present
|
||||
|
||||
- name: Include common security hardening tasks
|
||||
include_tasks: "../../common/tasks/security_hardening.yml"
|
||||
|
||||
- name: Include common redirector configuration tasks
|
||||
include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml"
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "Redirector Configuration Complete!"
|
||||
- "---------------------------------"
|
||||
- "Redirector IP: {{ ansible_host }}"
|
||||
- "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
|
||||
- "Shell Handler Port: {{ shell_handler_port }}"
|
||||
when: not disable_summary | default(false)
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"deployment_id": "{{ deployment_id }}",
|
||||
"deployment_type": "{{ deployment_type }}",
|
||||
"provider": "{{ provider }}",
|
||||
"domain": "{{ phishing_domain | default(domain) }}",
|
||||
"timestamp": "{{ ansible_date_time.iso8601 }}",
|
||||
"components": {{ deployment_components | to_json }},
|
||||
"results": {{ phishing_deployment_results | default({}) | to_json }},
|
||||
"status": "completed"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
# FlokiNET provision tasks for WEBRUNNER nodes
|
||||
# FlokiNET does not expose a public API — manual provisioning required.
|
||||
|
||||
- name: FlokiNET not supported for automated WEBRUNNER deployment
|
||||
fail:
|
||||
msg: >
|
||||
FlokiNET does not provide a public provisioning API.
|
||||
To use FlokiNET nodes with WEBRUNNER, provision VPS instances manually,
|
||||
add their IPs to the [webrunner_nodes] inventory group, and re-run
|
||||
the scan plays directly. Node chunk {{ node_chunk.node_name }} assigned
|
||||
{{ node_chunk.ip_count }} IPs.
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
# Linode Attack Box Deployment Playbook
|
||||
# Based on attk-box-setup but optimized for headless server deployment
|
||||
|
||||
- name: Deploy Attack Box on Linode
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
deployment_type: "attack_box"
|
||||
attack_box_name: "{{ attack_box_name | default('a-' + deployment_id) }}"
|
||||
attack_box_type: "{{ attack_box_type | default('kali') }}"
|
||||
linode_instance_type: "{{ linode_instance_type | default('g6-standard-4') }}"
|
||||
linode_region: "{{ linode_region | default('us-east') }}"
|
||||
ssh_key_name: "{{ ssh_key_path | default('c2deploy_' + attack_box_name) | basename | regex_replace('\\.pub$', '') }}"
|
||||
|
||||
# Attack box image mapping (headless versions)
|
||||
attack_box_images:
|
||||
kali: "linode/kali"
|
||||
custom: "linode/kali" # Use Kali as base for custom builds
|
||||
quick_recon: "linode/kali" # Kali Linux for easy tool expansion
|
||||
|
||||
tasks:
|
||||
- name: Debug attack box deployment
|
||||
debug:
|
||||
msg: "Deploying {{ attack_box_type }} attack box on Linode in {{ linode_region }}"
|
||||
|
||||
- name: Set attack box image
|
||||
set_fact:
|
||||
attack_box_image: "{{ attack_box_images[attack_box_type] | default('linode/kali') }}"
|
||||
|
||||
- name: Check if SSH key exists (should be pre-generated)
|
||||
stat:
|
||||
path: "~/.ssh/{{ ssh_key_name }}"
|
||||
register: ssh_key_check
|
||||
|
||||
- name: Generate SSH key pair for attack box (if not exists)
|
||||
openssh_keypair:
|
||||
path: "~/.ssh/{{ ssh_key_name }}"
|
||||
type: rsa
|
||||
size: 2048
|
||||
force: no
|
||||
register: ssh_key_result
|
||||
when: not ssh_key_check.stat.exists
|
||||
|
||||
- name: Read public key content
|
||||
slurp:
|
||||
src: "~/.ssh/{{ ssh_key_name }}.pub"
|
||||
register: public_key_content
|
||||
|
||||
- name: Add SSH key to Linode
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/profile/sshkeys"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
label: "{{ ssh_key_name }}"
|
||||
ssh_key: "{{ public_key_content.content | b64decode | trim }}"
|
||||
status_code: [200, 201]
|
||||
register: linode_ssh_key
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Generate strong root password
|
||||
set_fact:
|
||||
root_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,!@#$%^&*-_=+ length=32') }}"
|
||||
when: ansible_password is not defined
|
||||
|
||||
- name: Set ansible_password if not defined
|
||||
set_fact:
|
||||
ansible_password: "{{ root_password }}"
|
||||
when: ansible_password is not defined and root_password is defined
|
||||
|
||||
- name: Create Linode instance for attack box
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/linode/instances"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
label: "{{ attack_box_name }}"
|
||||
type: "{{ linode_instance_type }}"
|
||||
region: "{{ linode_region }}"
|
||||
image: "{{ attack_box_image }}"
|
||||
root_pass: "{{ ansible_password | default(root_password) }}"
|
||||
authorized_keys:
|
||||
- "{{ public_key_content.content | b64decode | trim }}"
|
||||
booted: true
|
||||
backups_enabled: false
|
||||
private_ip: false
|
||||
tags:
|
||||
- "attack-box"
|
||||
- "c2itall"
|
||||
- "{{ deployment_id }}"
|
||||
status_code: [200, 201]
|
||||
register: linode_instance
|
||||
|
||||
- name: Get current timestamp
|
||||
setup:
|
||||
gather_subset: min
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Ensure logs directory exists
|
||||
file:
|
||||
path: "{{ playbook_dir }}/logs"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Save attack box credentials to local file
|
||||
copy:
|
||||
content: |
|
||||
Attack Box: {{ attack_box_name }}
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Instance IP: {{ linode_instance.json.ipv4[0] if linode_instance.json.ipv4 else 'Pending' }}
|
||||
SSH Key: ~/.ssh/{{ ssh_key_name }}
|
||||
Root Password: {{ ansible_password | default(root_password) }}
|
||||
SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ linode_instance.json.ipv4[0] if linode_instance.json.ipv4 else 'IP_ADDRESS' }}
|
||||
|
||||
Generated at: {{ ansible_date_time.iso8601 }}
|
||||
dest: "{{ playbook_dir }}/logs/deployment_info_{{ deployment_id }}.txt"
|
||||
mode: '0600'
|
||||
when: linode_instance is succeeded
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Save instance information
|
||||
set_fact:
|
||||
attack_box_ip: "{{ linode_instance.json.ipv4[0] }}"
|
||||
attack_box_id: "{{ linode_instance.json.id }}"
|
||||
|
||||
- name: Display attack box information
|
||||
debug:
|
||||
msg:
|
||||
- "Attack Box Created Successfully!"
|
||||
- "Name: {{ attack_box_name }}"
|
||||
- "IP Address: {{ attack_box_ip }}"
|
||||
- "Instance ID: {{ attack_box_id }}"
|
||||
- "Type: {{ attack_box_type }}"
|
||||
- "SSH Key: ~/.ssh/{{ ssh_key_name }}"
|
||||
|
||||
- name: Wait for instance to be fully booted
|
||||
wait_for:
|
||||
host: "{{ attack_box_ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Test SSH connectivity
|
||||
command: ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/{{ ssh_key_name }} root@{{ attack_box_ip }} echo "SSH connection successful"
|
||||
register: ssh_test
|
||||
retries: 5
|
||||
delay: 30
|
||||
until: ssh_test.rc == 0
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Create dynamic inventory for attack box configuration
|
||||
add_host:
|
||||
name: "{{ attack_box_ip }}"
|
||||
groups: attack_boxes
|
||||
ansible_host: "{{ attack_box_ip }}"
|
||||
ansible_user: root
|
||||
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
|
||||
attack_box_type: "{{ attack_box_type }}"
|
||||
deployment_id: "{{ deployment_id }}"
|
||||
|
||||
- name: Save deployment information
|
||||
copy:
|
||||
content: |
|
||||
# Attack Box Deployment Information
|
||||
DEPLOYMENT_ID={{ deployment_id }}
|
||||
ATTACK_BOX_NAME={{ attack_box_name }}
|
||||
ATTACK_BOX_IP={{ attack_box_ip }}
|
||||
ATTACK_BOX_ID={{ attack_box_id }}
|
||||
ATTACK_BOX_TYPE={{ attack_box_type }}
|
||||
SSH_KEY_PATH=~/.ssh/{{ ssh_key_name }}
|
||||
PROVIDER=linode
|
||||
REGION={{ linode_region }}
|
||||
INSTANCE_TYPE={{ linode_instance_type }}
|
||||
DEPLOYED_DATE={{ ansible_date_time.iso8601 }}
|
||||
dest: "{{ playbook_dir }}/logs/attack_box_{{ deployment_id }}.env"
|
||||
|
||||
- name: Configure Attack Box
|
||||
hosts: attack_boxes
|
||||
gather_facts: yes
|
||||
become: yes
|
||||
vars:
|
||||
setup_workspace: "{{ setup_workspace | default(true) }}"
|
||||
setup_vpn: "{{ setup_vpn | default(false) }}"
|
||||
setup_tor: "{{ setup_tor | default(false) }}"
|
||||
|
||||
tasks:
|
||||
- name: Wait for system to be ready
|
||||
wait_for_connection:
|
||||
delay: 30
|
||||
timeout: 300
|
||||
|
||||
- name: Update package cache only (avoid grub-pc issues)
|
||||
apt:
|
||||
update_cache: yes
|
||||
cache_valid_time: 3600
|
||||
when: ansible_os_family == "Debian"
|
||||
retries: 3
|
||||
delay: 10
|
||||
|
||||
- name: Include attack box configuration tasks
|
||||
include_tasks: "{{ playbook_dir }}/../../modules/attack-box/tasks/configure_attack_box.yml"
|
||||
when: deployment_type != "quick_recon_box"
|
||||
|
||||
- name: Include quick recon configuration tasks
|
||||
include_tasks: "{{ playbook_dir }}/../../modules/attack-box/tasks/configure_quick_recon.yml"
|
||||
when: deployment_type == "quick_recon_box"
|
||||
|
||||
- name: Display final setup information (Full Attack Box)
|
||||
debug:
|
||||
msg:
|
||||
- "🎯 Attack Box Configuration Complete!"
|
||||
- "📦 Instance: {{ attack_box_name }}"
|
||||
- "🔑 SSH Command: ssh -i ~/.ssh/a-{{ deployment_id }} root@{{ ansible_host }}"
|
||||
- "🔒 Root Password: {{ ansible_password | default('Check deployment_info_' + deployment_id + '.txt') }}"
|
||||
- "📁 Credentials saved to: logs/deployment_info_{{ deployment_id }}.txt"
|
||||
- ""
|
||||
- "🛠️ Available Commands:"
|
||||
- " recon <target> - Run reconnaissance automation"
|
||||
- " portscan <target> - Run port scan automation"
|
||||
- " webenum <target> - Run web enumeration automation"
|
||||
when: deployment_type != "quick_recon_box"
|
||||
|
||||
- name: Display final setup information (Quick Recon Box)
|
||||
debug:
|
||||
msg:
|
||||
- "🎯 Quick Recon Box Configuration Complete!"
|
||||
- "📦 Instance: {{ attack_box_name }}"
|
||||
- "🔑 SSH Command: ssh -i ~/.ssh/qr-{{ deployment_id }} root@{{ ansible_host }}"
|
||||
- "🔒 Root Password: {{ ansible_password | default('Check deployment_info_' + deployment_id + '.txt') }}"
|
||||
- "📁 Credentials saved to: logs/deployment_info_{{ deployment_id }}.txt"
|
||||
- ""
|
||||
- "🎯 Quick Recon Commands:"
|
||||
- " qr - Go to working directory"
|
||||
- " toolkit - Show available tools"
|
||||
- " portscan <target> - Port scan"
|
||||
- " subfind <domain> - Subdomain enumeration"
|
||||
- " webscan <url> - Web application scan"
|
||||
- " tor-recon <target> - Anonymous reconnaissance"
|
||||
when: deployment_type == "quick_recon_box"
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
---
|
||||
# Linode C2 Deployment Playbook
|
||||
|
||||
- name: Deploy Linode C2 server
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Default values for required variables
|
||||
linode_region: "{{ linode_region | default(region_choices | random) }}"
|
||||
plan: "{{ plan | default('g6-standard-2') }}"
|
||||
# Use static value to avoid recursive templating
|
||||
c2_image_static: "linode/kali"
|
||||
|
||||
# Generate random instance name if not provided
|
||||
c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate required Linode token
|
||||
assert:
|
||||
that:
|
||||
- linode_token is defined and linode_token != ""
|
||||
fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
|
||||
|
||||
# Replace these tasks at the beginning of the playbook
|
||||
- name: Set region for C2 server
|
||||
set_fact:
|
||||
c2_region_value: "{{ selected_region | default(linode_region, true) | default('us-east', true) }}"
|
||||
|
||||
- name: Display region selection information for debugging
|
||||
debug:
|
||||
msg:
|
||||
- "Selected Region: {{ selected_region | default('Not set') }}"
|
||||
- "Linode Region: {{ linode_region | default('Not set') }}"
|
||||
- "Using Region: {{ c2_region_value }}"
|
||||
when: debug | default(false) | bool
|
||||
|
||||
- name: Set random region only if absolutely no region specified
|
||||
set_fact:
|
||||
c2_region_value: "{{ region_choices | random }}"
|
||||
when: region_choices is defined and region_choices|length > 0 and not selected_region is defined and not linode_region is defined and not c2_region_value is defined
|
||||
|
||||
- name: Create C2 Linode instance
|
||||
block:
|
||||
- name: Try creating instance in specified region
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ c2_name }}"
|
||||
type: "{{ plan }}"
|
||||
region: "{{ c2_region_value }}"
|
||||
image: "linode/kali"
|
||||
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
|
||||
authorized_keys:
|
||||
- "{{ lookup('file', ssh_key_path) }}"
|
||||
state: present
|
||||
register: c2_instance
|
||||
rescue:
|
||||
- name: Log region restriction error
|
||||
debug:
|
||||
msg: "Region {{ c2_region_value }} is restricted. Trying fallback regions..."
|
||||
|
||||
- name: Set fallback regions (most reliable ones first)
|
||||
set_fact:
|
||||
fallback_regions:
|
||||
- "us-east"
|
||||
- "us-central"
|
||||
- "eu-west"
|
||||
- "us-west"
|
||||
|
||||
- name: Try fallback regions
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ c2_name }}"
|
||||
type: "{{ plan }}"
|
||||
region: "{{ item }}"
|
||||
image: "linode/kali"
|
||||
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
|
||||
authorized_keys:
|
||||
- "{{ lookup('file', ssh_key_path) }}"
|
||||
state: present
|
||||
register: c2_instance
|
||||
loop: "{{ fallback_regions }}"
|
||||
when: c2_instance is not defined or c2_instance.failed
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Update deployment region with successful fallback
|
||||
set_fact:
|
||||
c2_region_value: "{{ item }}"
|
||||
loop: "{{ fallback_regions }}"
|
||||
when: c2_instance.results is defined and c2_instance.results[ansible_loop.index0] is defined and not c2_instance.results[ansible_loop.index0].failed
|
||||
|
||||
- name: Fail if all regions are restricted
|
||||
fail:
|
||||
msg: "All attempted regions are restricted. Please try again later or contact Linode support."
|
||||
when: c2_instance.failed | default(true)
|
||||
|
||||
- name: Set c2_ip for later use
|
||||
block:
|
||||
- name: Extract instance info from direct creation
|
||||
set_fact:
|
||||
c2_ip: "{{ c2_instance.instance.ipv4[0] }}"
|
||||
c2_instance_id: "{{ c2_instance.instance.id }}"
|
||||
when: c2_instance.instance is defined
|
||||
|
||||
- name: Extract instance info from fallback creation
|
||||
set_fact:
|
||||
c2_ip: "{{ item.instance.ipv4[0] }}"
|
||||
c2_instance_id: "{{ item.instance.id }}"
|
||||
loop: "{{ c2_instance.results | default([]) }}"
|
||||
when: c2_instance.results is defined and item.instance is defined and not item.failed
|
||||
|
||||
- name: Display final C2 deployment region and IP
|
||||
debug:
|
||||
msg: "C2 server deployed successfully in region {{ c2_region_value }} with IP {{ c2_ip }}"
|
||||
|
||||
# Enhanced SSH wait task for Linode/c2.yml
|
||||
- name: Wait for C2 SSH to be available
|
||||
block:
|
||||
- name: Initial wait for port to be open
|
||||
wait_for:
|
||||
host: "{{ c2_instance.instance.ipv4[0] }}"
|
||||
port: 22
|
||||
delay: 60
|
||||
timeout: 180
|
||||
state: started
|
||||
|
||||
- name: Additional pause for SSH initialization
|
||||
pause:
|
||||
seconds: 60
|
||||
|
||||
- name: Test SSH connection
|
||||
command: >
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10
|
||||
-i {{ ssh_key_path | replace('.pub', '') }} root@{{ c2_instance.instance.ipv4[0] }} echo "SSH Ready"
|
||||
register: ssh_test
|
||||
retries: 5
|
||||
delay: 20
|
||||
until: ssh_test.rc == 0
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Fail if SSH test unsuccessful
|
||||
fail:
|
||||
msg: "Could not connect to C2 server via SSH after multiple attempts"
|
||||
when: ssh_test.rc != 0
|
||||
|
||||
- name: Add C2 to inventory
|
||||
add_host:
|
||||
name: "c2"
|
||||
groups: "c2servers"
|
||||
ansible_host: "{{ c2_ip }}"
|
||||
ansible_user: "root"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
ansible_python_interpreter: "/usr/bin/python3" # Use remote system's Python
|
||||
|
||||
- name: Configure C2 server
|
||||
hosts: c2servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}"
|
||||
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
|
||||
tasks:
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 5
|
||||
delay: 10
|
||||
|
||||
- name: Disable root password authentication for SSH immediately
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^#?PasswordAuthentication'
|
||||
line: 'PasswordAuthentication no'
|
||||
state: present
|
||||
|
||||
- name: Restart SSH service to apply changes
|
||||
service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Include common tool installation tasks
|
||||
include_tasks: "../../common/tasks/install_tools.yml"
|
||||
|
||||
- name: Include common C2 configuration tasks
|
||||
include_tasks: "../../modules/c2/tasks/configure_c2.yml"
|
||||
|
||||
- name: Include common mail server configuration tasks
|
||||
include_tasks: "../../common/tasks/configure_mail.yml"
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "C2 Server Deployment Complete!"
|
||||
- "-----------------------------"
|
||||
- "C2 Server IP: {{ ansible_host }}"
|
||||
- "C2 Server Domain: {{ domain }}"
|
||||
- "GoPhish Admin Port: {{ gophish_admin_port }}"
|
||||
when: not disable_summary | default(false)
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
---
|
||||
# Linode/cleanup.yml
|
||||
- name: Clean up Linode infrastructure
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
cleanup_redirector: "{{ (redirector_name is defined and redirector_name != '') | ternary(true, false) }}"
|
||||
cleanup_c2: "{{ (c2_name is defined and c2_name != '') | ternary(true, false) }}"
|
||||
cleanup_tracker: "{{ (tracker_name is defined and tracker_name != '') | ternary(true, false) }}"
|
||||
cleanup_attack_box: "{{ (attack_box_name is defined and attack_box_name != '') | ternary(true, false) }}"
|
||||
confirm_cleanup: false # Skip confirmation in automated teardown
|
||||
|
||||
tasks:
|
||||
- name: Validate required Linode token
|
||||
assert:
|
||||
that:
|
||||
- linode_token is defined and linode_token != ""
|
||||
fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
|
||||
|
||||
- name: Cleanup resources notice
|
||||
debug:
|
||||
msg: |
|
||||
===============================================
|
||||
CLEANUP OPERATION
|
||||
===============================================
|
||||
The following resources will be DELETED PERMANENTLY:
|
||||
{% if cleanup_redirector and redirector_name is defined %}
|
||||
- Redirector instance: {{ redirector_name }}
|
||||
{% endif %}
|
||||
{% if cleanup_c2 and c2_name is defined %}
|
||||
- C2 instance: {{ c2_name }}
|
||||
{% endif %}
|
||||
{% if cleanup_tracker and tracker_name is defined %}
|
||||
- Tracker instance: {{ tracker_name }}
|
||||
{% endif %}
|
||||
{% if cleanup_attack_box and attack_box_name is defined %}
|
||||
- Attack Box instance: {{ attack_box_name }}
|
||||
{% endif %}
|
||||
when: confirm_cleanup | bool
|
||||
|
||||
- name: Confirm cleanup operation
|
||||
pause:
|
||||
prompt: "\n>>> Type 'yes' to confirm deletion or press Ctrl+C to abort <<<"
|
||||
register: confirmation
|
||||
when: confirm_cleanup | bool
|
||||
|
||||
- name: Skip cleanup if not confirmed
|
||||
meta: end_play
|
||||
when: confirm_cleanup | bool and (confirmation.user_input | default('')) != 'yes'
|
||||
|
||||
- name: Delete redirector instance
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ redirector_name }}"
|
||||
state: absent
|
||||
when: cleanup_redirector and redirector_name is defined and redirector_name != ""
|
||||
register: redirector_deletion
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Delete C2 instance
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ c2_name }}"
|
||||
state: absent
|
||||
when: cleanup_c2 and c2_name is defined and c2_name != ""
|
||||
register: c2_deletion
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Delete tracker instance
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ tracker_name }}"
|
||||
state: absent
|
||||
when: cleanup_tracker and tracker_name is defined and tracker_name != ""
|
||||
register: tracker_deletion
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Delete attack box instance
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ attack_box_name }}"
|
||||
state: absent
|
||||
when: cleanup_attack_box and attack_box_name is defined and attack_box_name != ""
|
||||
register: attack_box_deletion
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Clean up deployment state file
|
||||
file:
|
||||
path: "{{ playbook_dir }}/../deployment_state_{{ deployment_id }}.json"
|
||||
state: absent
|
||||
ignore_errors: yes
|
||||
register: state_file_deletion
|
||||
|
||||
- name: Report state file cleanup
|
||||
debug:
|
||||
msg: "Deployment state file {{ 'deleted' if state_file_deletion.changed else 'not found' }}"
|
||||
when: state_file_deletion is defined
|
||||
|
||||
- name: Report cleanup status
|
||||
debug:
|
||||
msg: |
|
||||
Cleanup results:
|
||||
{% if redirector_deletion is defined and redirector_name is defined %}
|
||||
- Redirector {{ redirector_name }}: {{ redirector_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
|
||||
{% endif %}
|
||||
{% if c2_deletion is defined and c2_name is defined %}
|
||||
- C2 {{ c2_name }}: {{ c2_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
|
||||
{% endif %}
|
||||
{% if tracker_deletion is defined and tracker_name is defined %}
|
||||
- Tracker {{ tracker_name }}: {{ tracker_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
|
||||
{% endif %}
|
||||
{% if attack_box_deletion is defined and attack_box_name is defined %}
|
||||
- Attack Box {{ attack_box_name }}: {{ attack_box_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
# Linode-specific phishing infrastructure tasks
|
||||
|
||||
- name: Create Linode instances for phishing
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/linode/instances"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_api_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
type: "{{ linode_instance_type | default('g6-nanode-1') }}"
|
||||
region: "{{ linode_region | default('us-east') }}"
|
||||
image: "{{ linode_image | default('linode/ubuntu22.04') }}"
|
||||
label: "{{ deployment_id }}-{{ item }}"
|
||||
root_pass: "{{ instance_password }}"
|
||||
authorized_keys:
|
||||
- "{{ ssh_public_key }}"
|
||||
tags:
|
||||
- "c2itall"
|
||||
- "phishing"
|
||||
- "{{ deployment_id }}"
|
||||
status_code: 200
|
||||
register: linode_instances
|
||||
loop: "{{ deployment_components }}"
|
||||
when: item in deployment_components
|
||||
|
||||
- name: Wait for instances to be running
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/linode/instances/{{ item.json.id }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_api_token }}"
|
||||
register: instance_status
|
||||
until: instance_status.json.status == "running"
|
||||
retries: 30
|
||||
delay: 10
|
||||
loop: "{{ linode_instances.results }}"
|
||||
when: linode_instances.results is defined
|
||||
|
||||
- name: Get instance details
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/linode/instances/{{ item.json.id }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_api_token }}"
|
||||
register: instance_details
|
||||
loop: "{{ linode_instances.results }}"
|
||||
when: linode_instances.results is defined
|
||||
|
||||
- name: Set instance facts
|
||||
set_fact:
|
||||
phishing_instances: >-
|
||||
{% set instances = [] %}
|
||||
{% for result in instance_details.results %}
|
||||
{% set instance = {
|
||||
'id': result.json.id,
|
||||
'label': result.json.label,
|
||||
'ipv4': result.json.ipv4[0],
|
||||
'region': result.json.region,
|
||||
'type': result.json.type,
|
||||
'status': result.json.status
|
||||
} %}
|
||||
{% set _ = instances.append(instance) %}
|
||||
{% endfor %}
|
||||
{{ instances }}
|
||||
when: instance_details.results is defined
|
||||
|
||||
- name: Wait for SSH connectivity
|
||||
wait_for:
|
||||
host: "{{ item.ipv4 }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
loop: "{{ phishing_instances }}"
|
||||
when: phishing_instances is defined
|
||||
|
||||
- name: Update instance inventory
|
||||
add_host:
|
||||
name: "{{ item.ipv4 }}"
|
||||
groups: "phishing_{{ item.label.split('-')[-1] }}"
|
||||
ansible_host: "{{ item.ipv4 }}"
|
||||
ansible_user: root
|
||||
ansible_ssh_private_key_file: "{{ ssh_private_key_path }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
instance_id: "{{ item.id }}"
|
||||
instance_label: "{{ item.label }}"
|
||||
provider: "linode"
|
||||
loop: "{{ phishing_instances }}"
|
||||
when: phishing_instances is defined
|
||||
|
||||
- name: Configure Gophish servers
|
||||
include_tasks: ../common/configure_gophish.yml
|
||||
when: "'gophish' in deployment_components"
|
||||
|
||||
- name: Configure MTA fronts
|
||||
include_tasks: ../common/configure_mta.yml
|
||||
when: "'mta_front' in deployment_components"
|
||||
|
||||
- name: Configure redirectors
|
||||
include_tasks: ../common/configure_redirector.yml
|
||||
when: "'redirector' in deployment_components"
|
||||
|
||||
- name: Configure web servers
|
||||
include_tasks: ../common/configure_webserver.yml
|
||||
when: "'webserver' in deployment_components"
|
||||
|
||||
- name: Display deployment summary
|
||||
debug:
|
||||
msg: |
|
||||
🎯 Linode Phishing Infrastructure Deployed
|
||||
{{ phishing_instances | length }} instances created
|
||||
Provider: Linode
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Instances:
|
||||
{% for instance in phishing_instances %}
|
||||
- {{ instance.label }}: {{ instance.ipv4 }} ({{ instance.type }})
|
||||
{% endfor %}
|
||||
when: phishing_instances is defined
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
# Phishing infrastructure deployment playbook
|
||||
# This is a comprehensive playbook that handles all phishing components
|
||||
|
||||
- name: Deploy Phishing Infrastructure
|
||||
hosts: localhost
|
||||
gather_facts: true
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
|
||||
tasks:
|
||||
- name: Display deployment information
|
||||
debug:
|
||||
msg: |
|
||||
Deploying phishing infrastructure
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Provider: {{ provider }}
|
||||
Domain: {{ domain | default(phishing_domain) }}
|
||||
Components: {{ deployment_type }}
|
||||
|
||||
- name: Set deployment facts
|
||||
set_fact:
|
||||
deployment_timestamp: "{{ ansible_date_time.epoch }}"
|
||||
phishing_deployment_results: {}
|
||||
deployment_components: >-
|
||||
{% set components = [] %}
|
||||
{% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %}
|
||||
{% set _ = components.append('gophish') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %}
|
||||
{% set _ = components.append('mta_front') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %}
|
||||
{% set _ = components.append('redirector') %}
|
||||
{% endif %}
|
||||
{% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %}
|
||||
{% set _ = components.append('webserver') %}
|
||||
{% endif %}
|
||||
{{ components }}
|
||||
|
||||
- name: Include provider-specific tasks
|
||||
include_tasks: "{{ provider }}_phishing.yml"
|
||||
when: deployment_components | length > 0
|
||||
|
||||
- name: Ensure logs directory exists
|
||||
file:
|
||||
path: "../../logs"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Save deployment information
|
||||
template:
|
||||
src: phishing_deployment_info.j2
|
||||
dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Display success message
|
||||
debug:
|
||||
msg: |
|
||||
✅ Phishing infrastructure deployment completed
|
||||
Deployment ID: {{ deployment_id }}
|
||||
Components deployed: {{ deployment_components | join(', ') }}
|
||||
Check logs/phishing_deployment_{{ deployment_id }}.json for details
|
||||
Executable
+190
@@ -0,0 +1,190 @@
|
||||
---
|
||||
# Linode Redirector-only Deployment Playbook
|
||||
|
||||
- name: Deploy Linode redirector
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Use constant values directly to avoid recursive template resolution
|
||||
redirector_image_static: "linode/debian12"
|
||||
|
||||
tasks:
|
||||
- name: Validate required Linode token
|
||||
assert:
|
||||
that:
|
||||
- linode_token is defined and linode_token != ""
|
||||
fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
|
||||
|
||||
# Update the region selection logic in Linode/redirector.yml
|
||||
- name: Set region for redirector
|
||||
set_fact:
|
||||
deployment_region: "{{ selected_region | default(linode_region, true) | default('us-east', true) }}"
|
||||
|
||||
- name: Display region selection for redirector
|
||||
debug:
|
||||
msg: "Using region for redirector: {{ deployment_region }}"
|
||||
when: debug | default(false) | bool
|
||||
|
||||
# Only use random region as a last resort
|
||||
- name: Set random region only if no region specified
|
||||
set_fact:
|
||||
deployment_region: "{{ region_choices | random }}"
|
||||
when: region_choices is defined and region_choices|length > 0 and not selected_region is defined and not linode_region is defined and not deployment_region is defined
|
||||
|
||||
- name: Create redirector Linode instance
|
||||
block:
|
||||
- name: Try creating instance in specified region
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ redirector_name }}"
|
||||
type: "{{ redirector_plan | default('g6-nanode-1') }}"
|
||||
region: "{{ deployment_region }}"
|
||||
image: "linode/debian12"
|
||||
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
|
||||
authorized_keys:
|
||||
- "{{ lookup('file', ssh_key_path) }}"
|
||||
state: present
|
||||
register: redirector_instance
|
||||
rescue:
|
||||
- name: Log region restriction error
|
||||
debug:
|
||||
msg: "Region {{ deployment_region }} is restricted. Trying fallback regions..."
|
||||
|
||||
- name: Set fallback regions (most reliable ones first)
|
||||
set_fact:
|
||||
fallback_regions:
|
||||
- "us-east"
|
||||
- "us-central"
|
||||
- "eu-west"
|
||||
- "us-west"
|
||||
|
||||
- name: Try fallback regions
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ redirector_name }}"
|
||||
type: "{{ redirector_plan | default('g6-nanode-1') }}"
|
||||
region: "{{ item }}"
|
||||
image: "linode/debian12"
|
||||
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
|
||||
authorized_keys:
|
||||
- "{{ lookup('file', ssh_key_path) }}"
|
||||
state: present
|
||||
register: redirector_instance
|
||||
loop: "{{ fallback_regions }}"
|
||||
when: redirector_instance is not defined or redirector_instance.failed
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Update deployment region with successful fallback
|
||||
set_fact:
|
||||
deployment_region: "{{ item }}"
|
||||
loop: "{{ fallback_regions }}"
|
||||
when: redirector_instance.results is defined and redirector_instance.results[ansible_loop.index0] is defined and not redirector_instance.results[ansible_loop.index0].failed
|
||||
|
||||
- name: Fail if all regions are restricted
|
||||
fail:
|
||||
msg: "All attempted regions are restricted. Please try again later or contact Linode support."
|
||||
when: redirector_instance.failed | default(true)
|
||||
|
||||
- name: Set redirector_ip for later use
|
||||
block:
|
||||
- name: Extract instance info from direct creation
|
||||
set_fact:
|
||||
redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}"
|
||||
redirector_instance_id: "{{ redirector_instance.instance.id }}"
|
||||
when: redirector_instance.instance is defined
|
||||
|
||||
- name: Extract instance info from fallback creation
|
||||
set_fact:
|
||||
redirector_ip: "{{ item.instance.ipv4[0] }}"
|
||||
redirector_instance_id: "{{ item.instance.id }}"
|
||||
loop: "{{ redirector_instance.results | default([]) }}"
|
||||
when: redirector_instance.results is defined and item.instance is defined and not item.failed
|
||||
|
||||
- name: Display final deployment region and IP
|
||||
debug:
|
||||
msg: "Redirector deployed successfully in region {{ deployment_region }} with IP {{ redirector_ip }}"
|
||||
|
||||
# Enhanced SSH wait task with better retry mechanism
|
||||
- name: Wait for redirector SSH to be available
|
||||
block:
|
||||
- name: Initial wait for port to be open
|
||||
wait_for:
|
||||
host: "{{ redirector_instance.instance.ipv4[0] }}"
|
||||
port: 22
|
||||
delay: 60
|
||||
timeout: 180
|
||||
state: started
|
||||
|
||||
- name: Additional pause for SSH initialization
|
||||
pause:
|
||||
seconds: 60
|
||||
|
||||
- name: Test SSH connection
|
||||
command: >
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10
|
||||
-i {{ ssh_key_path | replace('.pub', '') }} root@{{ redirector_instance.instance.ipv4[0] }} echo "SSH Ready"
|
||||
register: ssh_test
|
||||
retries: 5
|
||||
delay: 20
|
||||
until: ssh_test.rc == 0
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Fail if SSH test unsuccessful
|
||||
fail:
|
||||
msg: "Could not connect to redirector via SSH after multiple attempts"
|
||||
when: ssh_test.rc != 0
|
||||
|
||||
- name: Add redirector to inventory
|
||||
add_host:
|
||||
name: "redirector"
|
||||
groups: "redirectors"
|
||||
ansible_host: "{{ redirector_ip }}"
|
||||
ansible_user: "root"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
- name: Configure redirector server
|
||||
hosts: redirectors
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}"
|
||||
redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
|
||||
ansible_python_interpreter: auto # Explicitly add this line
|
||||
tasks:
|
||||
- name: Wait for apt to be available
|
||||
apt:
|
||||
update_cache: yes
|
||||
register: apt_result
|
||||
until: apt_result is success
|
||||
retries: 5
|
||||
delay: 10
|
||||
|
||||
- name: Disable root password authentication for SSH immediately
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^#?PasswordAuthentication'
|
||||
line: 'PasswordAuthentication no'
|
||||
state: present
|
||||
|
||||
- name: Restart SSH service to apply changes
|
||||
service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Include common redirector configuration tasks
|
||||
include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml"
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "Redirector Deployment Complete!"
|
||||
- "-------------------------------"
|
||||
- "Redirector IP: {{ ansible_host }}"
|
||||
- "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
|
||||
when: not disable_summary | default(false)
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"deployment_id": "{{ deployment_id }}",
|
||||
"deployment_type": "{{ deployment_type }}",
|
||||
"provider": "{{ provider }}",
|
||||
"domain": "{{ phishing_domain | default(domain) }}",
|
||||
"timestamp": "{{ ansible_date_time.iso8601 }}",
|
||||
"components": {{ deployment_components | to_json }},
|
||||
"results": {{ phishing_deployment_results | default({}) | to_json }},
|
||||
"status": "completed"
|
||||
}
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
---
|
||||
# Linode Tracker Deployment Playbook
|
||||
|
||||
- name: Deploy Linode email tracking server
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
# Default values - using minimalist settings
|
||||
linode_region: "{{ linode_region | default(region_choices | random) }}"
|
||||
plan: "{{ plan | default('g6-nanode-1') }}" # Use smallest plan for tracker
|
||||
tracker_image: "{{ image | default('linode/debian12') }}" # Use Debian instead of Kali
|
||||
|
||||
# Generate random instance name if not provided
|
||||
tracker_name: "{{ tracker_name | default('t-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
|
||||
|
||||
tasks:
|
||||
- name: Validate required Linode token
|
||||
assert:
|
||||
that:
|
||||
- linode_token is defined and linode_token != ""
|
||||
fail_msg: "Linode API token is required. Set linode_token in vars.yaml."
|
||||
|
||||
- name: Set region for tracker
|
||||
set_fact:
|
||||
tracker_region_value: "{{ selected_region | default(region, true) | default(linode_region, true) | default('us-east', true) }}"
|
||||
when: region_choices is not defined or region_choices|length == 0
|
||||
|
||||
- name: Set random region for tracker
|
||||
set_fact:
|
||||
tracker_region_value: "{{ region_choices | random }}"
|
||||
when: region_choices is defined and region_choices|length > 0 and tracker_region_value is not defined
|
||||
|
||||
- name: Create tracker Linode instance
|
||||
community.general.linode_v4:
|
||||
access_token: "{{ linode_token }}"
|
||||
label: "{{ tracker_name }}"
|
||||
type: "{{ plan }}"
|
||||
region: "{{ tracker_region_value }}"
|
||||
image: "{{ image | default('linode/debian12') }}"
|
||||
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
|
||||
authorized_keys:
|
||||
- "{{ lookup('file', ssh_key_path) }}"
|
||||
state: present
|
||||
register: tracker_instance
|
||||
|
||||
- name: Set tracker_ip for later use
|
||||
set_fact:
|
||||
tracker_ip: "{{ tracker_instance.instance.ipv4[0] }}"
|
||||
tracker_instance_id: "{{ tracker_instance.instance.id }}"
|
||||
|
||||
- name: Wait for tracker SSH to be available
|
||||
wait_for:
|
||||
host: "{{ tracker_ip }}"
|
||||
port: 22
|
||||
delay: 60
|
||||
timeout: 300
|
||||
state: started
|
||||
|
||||
- name: Add tracker to inventory
|
||||
add_host:
|
||||
name: "tracker"
|
||||
groups: "trackers"
|
||||
ansible_host: "{{ tracker_ip }}"
|
||||
ansible_user: "root"
|
||||
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
|
||||
- name: Configure tracker server
|
||||
hosts: trackers
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars_files:
|
||||
- vars.yaml
|
||||
vars:
|
||||
tracker_domain: "{{ tracker_domain | default('track.' + domain) }}"
|
||||
tasks:
|
||||
- name: Update and install minimal required packages
|
||||
apt:
|
||||
name:
|
||||
- nginx
|
||||
- certbot
|
||||
- python3-certbot-nginx
|
||||
- git
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
update_cache: yes
|
||||
state: present
|
||||
|
||||
- name: Clone email tracker repository
|
||||
git:
|
||||
repo: https://github.com/Datalux/Osint-Tracker.git
|
||||
dest: /root/Tools/tracker
|
||||
version: master
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Install tracker dependencies
|
||||
pip:
|
||||
requirements: /root/Tools/tracker/requirements.txt
|
||||
virtualenv: /root/Tools/tracker/venv
|
||||
virtualenv_command: python3 -m venv
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Configure tracker settings
|
||||
template:
|
||||
src: "../../modules/tracker/templates/tracker-config.j2"
|
||||
dest: /root/Tools/tracker/config.py
|
||||
mode: '0644'
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Set up SSL if requested
|
||||
block:
|
||||
- name: Install Let's Encrypt certificate
|
||||
shell: |
|
||||
certbot --nginx -d {{ tracker_domain }} --non-interactive --agree-tos -m {{ tracker_email }}
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem
|
||||
when: tracker_setup_ssl | default(true) | bool
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Create systemd service for tracker
|
||||
template:
|
||||
src: "../../modules/tracker/templates/tracker.service.j2"
|
||||
dest: /etc/systemd/system/tracker.service
|
||||
mode: '0644'
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Update tracker service to use virtualenv
|
||||
blockinfile:
|
||||
path: /etc/systemd/system/tracker.service
|
||||
insertafter: "^\\[Service\\]"
|
||||
block: |
|
||||
Environment="PATH=/root/Tools/tracker/venv/bin:$PATH"
|
||||
ExecStart=/root/Tools/tracker/venv/bin/python /root/Tools/tracker/app.py
|
||||
|
||||
- name: Start and enable tracker service
|
||||
systemd:
|
||||
name: tracker
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Print deployment summary
|
||||
debug:
|
||||
msg:
|
||||
- "Email Tracker Deployment Complete!"
|
||||
- "------------------------------"
|
||||
- "Tracker IP: {{ ansible_host }}"
|
||||
- "Tracker Domain: {{ tracker_domain }} (Update DNS A record)"
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
# Linode provision tasks for one WEBRUNNER node
|
||||
# Called in a loop — loop_var: node_chunk
|
||||
# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id,
|
||||
# linode_instance_type, linode_region, scanner_ip_log, results_dir
|
||||
|
||||
- name: Generate root password for {{ node_chunk.node_name }}
|
||||
set_fact:
|
||||
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
|
||||
|
||||
- name: Read public key
|
||||
slurp:
|
||||
src: "~/.ssh/{{ ssh_key_name }}.pub"
|
||||
register: wr_pubkey
|
||||
|
||||
- name: Register SSH key in Linode ({{ node_chunk.node_name }})
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/profile/sshkeys"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
label: "{{ webrunner_name }}"
|
||||
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
|
||||
status_code: [200, 201]
|
||||
ignore_errors: true
|
||||
|
||||
- name: Create Linode instance {{ node_chunk.node_name }}
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/linode/instances"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
label: "{{ node_chunk.node_name }}"
|
||||
type: "{{ linode_instance_type | default('g6-standard-2') }}"
|
||||
region: "{{ linode_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]
|
||||
register: wr_linode
|
||||
|
||||
- name: Extract node IP
|
||||
set_fact:
|
||||
wr_node_ip: "{{ wr_linode.json.ipv4[0] }}"
|
||||
|
||||
- name: Log scanner IP
|
||||
lineinfile:
|
||||
path: "{{ scanner_ip_log }}"
|
||||
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
|
||||
create: true
|
||||
|
||||
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
|
||||
wait_for:
|
||||
host: "{{ wr_node_ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
|
||||
- name: Add {{ node_chunk.node_name }} to inventory
|
||||
add_host:
|
||||
name: "{{ wr_node_ip }}"
|
||||
groups: webrunner_nodes
|
||||
ansible_host: "{{ wr_node_ip }}"
|
||||
ansible_user: root
|
||||
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
|
||||
node_name: "{{ node_chunk.node_name }}"
|
||||
node_cidrs: "{{ node_chunk.cidrs }}"
|
||||
node_ip_count: "{{ node_chunk.ip_count }}"
|
||||
node_idx: "{{ node_chunk.idx }}"
|
||||
linode_instance_id: "{{ wr_linode.json.id }}"
|
||||
provider: linode
|
||||
|
||||
- name: Show {{ node_chunk.node_name }} ready
|
||||
debug:
|
||||
msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
# Linode provision tasks for one WEBRUNNER node
|
||||
# Called in a loop — loop_var: node_chunk
|
||||
# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id,
|
||||
# linode_instance_type, linode_region, scanner_ip_log, results_dir
|
||||
|
||||
- name: Generate root password for {{ node_chunk.node_name }}
|
||||
set_fact:
|
||||
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
|
||||
|
||||
- name: Read public key
|
||||
slurp:
|
||||
src: "~/.ssh/{{ ssh_key_name }}.pub"
|
||||
register: wr_pubkey
|
||||
|
||||
- name: Register SSH key in Linode ({{ node_chunk.node_name }})
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/profile/sshkeys"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
label: "{{ webrunner_name }}"
|
||||
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
|
||||
status_code: [200, 201]
|
||||
ignore_errors: true
|
||||
|
||||
- name: Create Linode instance {{ node_chunk.node_name }}
|
||||
uri:
|
||||
url: "https://api.linode.com/v4/linode/instances"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ linode_token }}"
|
||||
Content-Type: "application/json"
|
||||
body_format: json
|
||||
body:
|
||||
label: "{{ node_chunk.node_name }}"
|
||||
type: "{{ linode_instance_type | default('g6-standard-2') }}"
|
||||
region: "{{ linode_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]
|
||||
register: wr_linode
|
||||
|
||||
- name: Extract node IP
|
||||
set_fact:
|
||||
wr_node_ip: "{{ wr_linode.json.ipv4[0] }}"
|
||||
|
||||
- name: Log scanner IP
|
||||
lineinfile:
|
||||
path: "{{ scanner_ip_log }}"
|
||||
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
|
||||
create: true
|
||||
|
||||
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
|
||||
wait_for:
|
||||
host: "{{ wr_node_ip }}"
|
||||
port: 22
|
||||
delay: 30
|
||||
timeout: 300
|
||||
|
||||
- name: Add {{ node_chunk.node_name }} to inventory
|
||||
add_host:
|
||||
name: "{{ wr_node_ip }}"
|
||||
groups: webrunner_nodes
|
||||
ansible_host: "{{ wr_node_ip }}"
|
||||
ansible_user: root
|
||||
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
|
||||
node_name: "{{ node_chunk.node_name }}"
|
||||
node_cidrs: "{{ node_chunk.cidrs }}"
|
||||
node_ip_count: "{{ node_chunk.ip_count }}"
|
||||
node_idx: "{{ node_chunk.idx }}"
|
||||
linode_instance_id: "{{ wr_linode.json.id }}"
|
||||
provider: linode
|
||||
|
||||
- name: Show {{ node_chunk.node_name }} ready
|
||||
debug:
|
||||
msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
|
||||
@@ -0,0 +1 @@
|
||||
# Provider utils package
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AWS provider utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
from ..utils.common import COLORS, load_vars_file, confirm_action
|
||||
|
||||
def get_aws_credentials(provider_vars=None):
|
||||
"""Get AWS credentials from user or vars file"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('aws')
|
||||
|
||||
default_aws_key = provider_vars.get('aws_access_key', '')
|
||||
default_aws_secret = provider_vars.get('aws_secret_key', '')
|
||||
|
||||
print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}")
|
||||
aws_key = input(f"AWS Access Key [{'*****' if default_aws_key else 'leave blank to use AWS CLI profile'}]: ") or default_aws_key
|
||||
aws_secret = input(f"AWS Secret Key [{'*****' if default_aws_secret else 'leave blank to use AWS CLI profile'}]: ") or default_aws_secret
|
||||
|
||||
return {
|
||||
'aws_access_key': aws_key,
|
||||
'aws_secret_key': aws_secret
|
||||
}
|
||||
|
||||
def select_aws_region(provider_vars=None, component=None):
|
||||
"""Let the user select an AWS region"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('aws')
|
||||
|
||||
regions = provider_vars.get('aws_region_choices', [])
|
||||
component_str = f" for {component}" if component else ""
|
||||
|
||||
if not regions:
|
||||
print(f"{COLORS['YELLOW']}No regions found for AWS, using us-east-1{COLORS['RESET']}")
|
||||
return "us-east-1"
|
||||
|
||||
print(f"\nAvailable AWS regions{component_str}:")
|
||||
for i, region in enumerate(regions, 1):
|
||||
print(f" {i}. {region}")
|
||||
|
||||
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
|
||||
|
||||
if not region_input:
|
||||
return random.choice(regions)
|
||||
|
||||
try:
|
||||
region_choice = int(region_input)
|
||||
if 1 <= region_choice <= len(regions):
|
||||
return regions[region_choice - 1]
|
||||
else:
|
||||
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
|
||||
def gather_aws_config():
|
||||
"""Gather all AWS-specific configuration"""
|
||||
provider_vars = load_vars_file('aws')
|
||||
config = {}
|
||||
|
||||
# Get credentials
|
||||
aws_creds = get_aws_credentials(provider_vars)
|
||||
config.update(aws_creds)
|
||||
|
||||
# Get region
|
||||
config['aws_region'] = select_aws_region(provider_vars)
|
||||
|
||||
# Additional AWS-specific settings
|
||||
config['aws_instance_type'] = provider_vars.get('aws_instance_type', 't3.micro')
|
||||
config['aws_volume_size'] = provider_vars.get('aws_volume_size', 20)
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# Common Gophish server configuration tasks
|
||||
|
||||
- name: Configure Gophish servers
|
||||
debug:
|
||||
msg: |
|
||||
Configuring Gophish servers for deployment {{ deployment_id }}
|
||||
Admin port: {{ gophish_admin_port | default('8090') }}
|
||||
Domain: {{ phishing_domain | default(domain) }}
|
||||
|
||||
- name: Install Gophish
|
||||
debug:
|
||||
msg: "Would install and configure Gophish on target hosts"
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['phishing_gophish'] | default([]) }}"
|
||||
when: groups['phishing_gophish'] is defined
|
||||
|
||||
- name: Configure Gophish templates
|
||||
debug:
|
||||
msg: |
|
||||
Would configure Gophish with:
|
||||
- Email template: {{ email_template | default('office365_login') }}
|
||||
- Campaign: {{ campaign_name | default('test-campaign') }}
|
||||
- Sender: {{ sender_name | default('IT Support') }}
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
# Common MTA front server configuration tasks
|
||||
|
||||
- name: Configure MTA front servers
|
||||
debug:
|
||||
msg: |
|
||||
Configuring MTA front servers for deployment {{ deployment_id }}
|
||||
MTA hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }}
|
||||
SMTP auth user: {{ smtp_auth_user | default('admin') }}
|
||||
|
||||
- name: Install MTA software
|
||||
debug:
|
||||
msg: "Would install and configure MTA (Postfix/Exim) on target hosts"
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['phishing_mta_front'] | default([]) }}"
|
||||
when: groups['phishing_mta_front'] is defined
|
||||
|
||||
- name: Configure DKIM/SPF
|
||||
debug:
|
||||
msg: |
|
||||
Would configure DKIM/SPF records for:
|
||||
- Domain: {{ phishing_domain | default(domain) }}
|
||||
- MTA hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }}
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
# Common redirector configuration tasks
|
||||
|
||||
- name: Configure redirector servers
|
||||
debug:
|
||||
msg: |
|
||||
Configuring redirector servers for deployment {{ deployment_id }}
|
||||
Backend host: {{ backend_host | default('N/A') }}
|
||||
Redirector type: {{ redirector_type | default('http') }}
|
||||
|
||||
- name: Install redirector software
|
||||
debug:
|
||||
msg: "Would install and configure Apache/Nginx redirectors on target hosts"
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['phishing_redirector'] | default([]) }}"
|
||||
when: groups['phishing_redirector'] is defined
|
||||
|
||||
- name: Configure SSL certificates
|
||||
debug:
|
||||
msg: |
|
||||
Would configure SSL certificates for:
|
||||
- Domain: {{ phishing_domain | default(domain) }}
|
||||
- Let's Encrypt email: {{ letsencrypt_email | default('admin@' + (phishing_domain | default(domain))) }}
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# Common web server configuration tasks
|
||||
|
||||
- name: Configure web servers
|
||||
debug:
|
||||
msg: |
|
||||
Configuring web servers for deployment {{ deployment_id }}
|
||||
Phishing hostname: {{ phishing_hostname | default('portal.' + (phishing_domain | default(domain))) }}
|
||||
|
||||
- name: Install web server software
|
||||
debug:
|
||||
msg: "Would install and configure Apache/Nginx web servers on target hosts"
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['phishing_webserver'] | default([]) }}"
|
||||
when: groups['phishing_webserver'] is defined
|
||||
|
||||
- name: Deploy phishing pages
|
||||
debug:
|
||||
msg: |
|
||||
Would deploy phishing pages:
|
||||
- Template: {{ email_template | default('office365_login') }}
|
||||
- Domain: {{ phishing_domain | default(domain) }}
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FlokiNET provider utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import logging
|
||||
from ..utils.common import COLORS, load_vars_file, validate_ip_address
|
||||
|
||||
def get_flokinet_credentials(provider_vars=None):
|
||||
"""Get FlokiNET server IPs from user or vars file"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('flokinet')
|
||||
|
||||
default_redirector_ip = provider_vars.get('redirector_ip', '')
|
||||
default_c2_ip = provider_vars.get('c2_ip', '')
|
||||
|
||||
print(f"\n{COLORS['BLUE']}FlokiNET Configuration{COLORS['RESET']}")
|
||||
print(f"{COLORS['YELLOW']}Note: FlokiNET requires pre-provisioned servers{COLORS['RESET']}")
|
||||
|
||||
redirector_ip = input(f"FlokiNET Redirector IP Address [default: {default_redirector_ip}]: ") or default_redirector_ip
|
||||
c2_ip = input(f"FlokiNET C2 Server IP Address [default: {default_c2_ip}]: ") or default_c2_ip
|
||||
|
||||
# Validate IP addresses
|
||||
if redirector_ip and not validate_ip_address(redirector_ip):
|
||||
print(f"{COLORS['RED']}Invalid redirector IP address{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
if c2_ip and not validate_ip_address(c2_ip):
|
||||
print(f"{COLORS['RED']}Invalid C2 server IP address{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
return {
|
||||
'flokinet_redirector_ip': redirector_ip,
|
||||
'flokinet_c2_ip': c2_ip
|
||||
}
|
||||
|
||||
def gather_flokinet_config():
|
||||
"""Gather all FlokiNET-specific configuration"""
|
||||
provider_vars = load_vars_file('flokinet')
|
||||
config = {}
|
||||
|
||||
# Get server IPs
|
||||
flokinet_ips = get_flokinet_credentials(provider_vars)
|
||||
if not flokinet_ips:
|
||||
return None
|
||||
config.update(flokinet_ips)
|
||||
|
||||
# FlokiNET-specific settings
|
||||
config['ssh_user'] = 'root' # FlokiNET typically uses root
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Linode provider utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
from ..utils.common import COLORS, load_vars_file
|
||||
|
||||
def get_linode_credentials(provider_vars=None):
|
||||
"""Get Linode API token from user or vars file"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('linode')
|
||||
|
||||
default_token = provider_vars.get('linode_token', '')
|
||||
|
||||
print(f"\n{COLORS['BLUE']}Linode Configuration{COLORS['RESET']}")
|
||||
token = input(f"Linode API Token [{'*****' if default_token else 'required'}]: ") or default_token
|
||||
|
||||
if not token:
|
||||
print(f"{COLORS['RED']}Linode API token is required{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
return {'linode_token': token}
|
||||
|
||||
def select_linode_region(provider_vars=None, component=None):
|
||||
"""Let the user select a Linode region"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('linode')
|
||||
|
||||
regions = provider_vars.get('region_choices', [])
|
||||
component_str = f" for {component}" if component else ""
|
||||
|
||||
if not regions:
|
||||
print(f"{COLORS['YELLOW']}No regions found for Linode, using us-east{COLORS['RESET']}")
|
||||
return "us-east"
|
||||
|
||||
print(f"\nAvailable Linode regions{component_str}:")
|
||||
for i, region in enumerate(regions, 1):
|
||||
print(f" {i}. {region}")
|
||||
|
||||
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
|
||||
|
||||
if not region_input:
|
||||
return random.choice(regions)
|
||||
|
||||
try:
|
||||
region_choice = int(region_input)
|
||||
if 1 <= region_choice <= len(regions):
|
||||
return regions[region_choice - 1]
|
||||
else:
|
||||
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
|
||||
def gather_linode_config():
|
||||
"""Gather all Linode-specific configuration"""
|
||||
provider_vars = load_vars_file('linode')
|
||||
config = {}
|
||||
|
||||
# Get credentials
|
||||
linode_creds = get_linode_credentials(provider_vars)
|
||||
if not linode_creds:
|
||||
return None
|
||||
config.update(linode_creds)
|
||||
|
||||
# Get region
|
||||
config['linode_region'] = select_linode_region(provider_vars)
|
||||
|
||||
# Additional Linode-specific settings
|
||||
config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1')
|
||||
config['linode_image'] = provider_vars.get('linode_image', 'linode/kali')
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Provider selection and configuration utilities
|
||||
"""
|
||||
|
||||
from ..utils.common import COLORS, PROVIDERS
|
||||
from .aws_utils import gather_aws_config
|
||||
from .linode_utils import gather_linode_config
|
||||
from .flokinet_utils import gather_flokinet_config
|
||||
|
||||
def select_provider():
|
||||
"""Let the user select a cloud provider"""
|
||||
print(f"\n{COLORS['BLUE']}Available cloud providers:{COLORS['RESET']}")
|
||||
for i, provider in enumerate(PROVIDERS, 1):
|
||||
print(f" {i}. {provider.capitalize()}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
provider_choice = input(f"\nSelect a provider (1-{len(PROVIDERS)} or 99 to cancel): ")
|
||||
if provider_choice == "99":
|
||||
return None
|
||||
|
||||
provider_choice = int(provider_choice)
|
||||
if 1 <= provider_choice <= len(PROVIDERS):
|
||||
return PROVIDERS[provider_choice - 1]
|
||||
else:
|
||||
print(f"{COLORS['RED']}Please enter a number between 1 and {len(PROVIDERS)}{COLORS['RESET']}")
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Please enter a valid number{COLORS['RESET']}")
|
||||
|
||||
def gather_provider_config(provider):
|
||||
"""Gather configuration for the specified provider"""
|
||||
if provider == "aws":
|
||||
return gather_aws_config()
|
||||
elif provider == "linode":
|
||||
return gather_linode_config()
|
||||
elif provider == "flokinet":
|
||||
return gather_flokinet_config()
|
||||
else:
|
||||
print(f"{COLORS['RED']}Unknown provider: {provider}{COLORS['RESET']}")
|
||||
return None
|
||||
@@ -0,0 +1,344 @@
|
||||
---
|
||||
# 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
|
||||
Reference in New Issue
Block a user