534 lines
20 KiB
Python
Executable File
534 lines
20 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Standalone teardown script for C2itall deployments
|
|
This script can be used to teardown infrastructure when the main menu fails
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import logging
|
|
import json
|
|
import glob
|
|
import argparse
|
|
from datetime import datetime
|
|
|
|
# Add current directory to path for imports
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
try:
|
|
from utils.common import COLORS, PROVIDER_DIRS, setup_logging
|
|
except ImportError as e:
|
|
print(f"Error importing utils: {e}")
|
|
print("Make sure you're running this from the c2itall directory")
|
|
sys.exit(1)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='C2itall Standalone Teardown Tool')
|
|
parser.add_argument('--deployment-id', help='Specific deployment ID to teardown')
|
|
parser.add_argument('--list', action='store_true', help='List all deployments')
|
|
parser.add_argument('--all', action='store_true', help='Teardown all deployments')
|
|
parser.add_argument('--select', action='store_true', help='Interactively select deployment to teardown')
|
|
parser.add_argument('--debug', action='store_true', help='Enable debug output')
|
|
parser.add_argument('--force', action='store_true', help='Skip confirmation prompts')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.debug:
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
print(f"{COLORS['BLUE']}C2itall Standalone Teardown Tool{COLORS['RESET']}")
|
|
print(f"{COLORS['BLUE']}================================{COLORS['RESET']}")
|
|
|
|
if args.list:
|
|
list_deployments()
|
|
elif args.all:
|
|
teardown_all_deployments(force=args.force)
|
|
elif args.deployment_id:
|
|
teardown_deployment(args.deployment_id, force=args.force)
|
|
elif args.select:
|
|
select_and_teardown_deployment()
|
|
else:
|
|
interactive_mode()
|
|
|
|
def list_deployments():
|
|
"""List all available deployments"""
|
|
print(f"\n{COLORS['CYAN']}Available Deployments:{COLORS['RESET']}")
|
|
|
|
# Find deployment info files
|
|
info_files = glob.glob("logs/deployment_info_*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}")
|
|
return
|
|
|
|
deployments = []
|
|
for info_file in info_files:
|
|
config = parse_deployment_info(info_file)
|
|
if config:
|
|
deployments.append(config)
|
|
|
|
if not deployments:
|
|
print(f"{COLORS['YELLOW']}Found info files but could not parse deployment data{COLORS['RESET']}")
|
|
return
|
|
|
|
print(f"Found {len(deployments)} deployments:\n")
|
|
|
|
for i, config in enumerate(deployments, 1):
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
provider = config.get('provider', 'unknown')
|
|
domain = config.get('domain', 'N/A')
|
|
status = config.get('status', 'unknown')
|
|
|
|
print(f"{i}. Deployment ID: {deployment_id}")
|
|
print(f" Provider: {provider}")
|
|
print(f" Domain: {domain}")
|
|
print(f" Status: {status}")
|
|
|
|
# Show instance names if available
|
|
instances = []
|
|
if config.get('redirector_name'):
|
|
instances.append(f"Redirector: {config['redirector_name']}")
|
|
if config.get('c2_name'):
|
|
instances.append(f"C2: {config['c2_name']}")
|
|
if config.get('tracker_name'):
|
|
instances.append(f"Tracker: {config['tracker_name']}")
|
|
if config.get('attack_box_name'):
|
|
instances.append(f"Attack Box: {config['attack_box_name']}")
|
|
|
|
if instances:
|
|
print(f" Instances: {', '.join(instances)}")
|
|
|
|
print()
|
|
|
|
def interactive_mode():
|
|
"""Interactive teardown mode"""
|
|
while True:
|
|
print(f"\n{COLORS['WHITE']}Teardown Options:{COLORS['RESET']}")
|
|
print("1) List all deployments")
|
|
print("2) Select deployment to teardown")
|
|
print("3) Teardown all deployments")
|
|
print("4) Manual cleanup guidance")
|
|
print("5) Exit")
|
|
|
|
choice = input(f"\nSelect option (1-5): ").strip()
|
|
|
|
if choice == "1":
|
|
list_deployments()
|
|
elif choice == "2":
|
|
select_and_teardown_deployment()
|
|
elif choice == "3":
|
|
teardown_all_deployments()
|
|
elif choice == "4":
|
|
show_manual_cleanup_guidance()
|
|
elif choice == "5":
|
|
print(f"{COLORS['GREEN']}Goodbye!{COLORS['RESET']}")
|
|
break
|
|
else:
|
|
print(f"{COLORS['RED']}Invalid option{COLORS['RESET']}")
|
|
|
|
def teardown_deployment(deployment_id, force=False):
|
|
"""Teardown a specific deployment"""
|
|
print(f"\n{COLORS['BLUE']}Tearing down deployment: {deployment_id}{COLORS['RESET']}")
|
|
|
|
# Find deployment info
|
|
info_files = glob.glob(f"logs/deployment_info_{deployment_id}*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['RED']}No deployment info found for ID: {deployment_id}{COLORS['RESET']}")
|
|
print(f"{COLORS['YELLOW']}Available deployments:{COLORS['RESET']}")
|
|
list_deployments()
|
|
return False
|
|
|
|
config = parse_deployment_info(info_files[0])
|
|
if not config:
|
|
print(f"{COLORS['RED']}Could not parse deployment configuration{COLORS['RESET']}")
|
|
return False
|
|
|
|
# Show what will be torn down
|
|
provider = config.get('provider', 'unknown')
|
|
print(f"Provider: {provider}")
|
|
|
|
instances_to_delete = []
|
|
if config.get('redirector_name'):
|
|
instances_to_delete.append(f"Redirector: {config['redirector_name']}")
|
|
if config.get('c2_name'):
|
|
instances_to_delete.append(f"C2: {config['c2_name']}")
|
|
if config.get('tracker_name'):
|
|
instances_to_delete.append(f"Tracker: {config['tracker_name']}")
|
|
if config.get('attack_box_name'):
|
|
instances_to_delete.append(f"Attack Box: {config['attack_box_name']}")
|
|
|
|
if instances_to_delete:
|
|
print(f"Instances to delete:")
|
|
for instance in instances_to_delete:
|
|
print(f" - {instance}")
|
|
else:
|
|
print(f"{COLORS['YELLOW']}No instances found in configuration{COLORS['RESET']}")
|
|
|
|
# Confirm deletion
|
|
if not force:
|
|
confirm = input(f"\n{COLORS['YELLOW']}Proceed with teardown? (yes/no): {COLORS['RESET']}").strip().lower()
|
|
if confirm != 'yes':
|
|
print(f"{COLORS['GREEN']}Teardown cancelled{COLORS['RESET']}")
|
|
return False
|
|
|
|
# Set up logging
|
|
log_file = setup_logging(deployment_id, "teardown")
|
|
print(f"Teardown logs will be written to: {log_file}")
|
|
|
|
# Execute teardown
|
|
success = execute_teardown(config)
|
|
|
|
if success:
|
|
print(f"\n{COLORS['GREEN']}Teardown completed successfully!{COLORS['RESET']}")
|
|
# Clean up SSH keys
|
|
cleanup_ssh_keys(deployment_id)
|
|
# Archive logs
|
|
archive_logs(deployment_id)
|
|
else:
|
|
print(f"\n{COLORS['RED']}Teardown failed or incomplete{COLORS['RESET']}")
|
|
print(f"Check logs for details: {log_file}")
|
|
|
|
return success
|
|
|
|
def teardown_all_deployments(force=False):
|
|
"""Teardown all deployments"""
|
|
print(f"\n{COLORS['RED']}⚠️ WARNING: This will teardown ALL deployments!{COLORS['RESET']}")
|
|
|
|
if not force:
|
|
confirm = input(f"{COLORS['YELLOW']}Type 'DESTROY' to confirm: {COLORS['RESET']}")
|
|
if confirm != "DESTROY":
|
|
print(f"{COLORS['GREEN']}Operation cancelled{COLORS['RESET']}")
|
|
return
|
|
|
|
info_files = glob.glob("logs/deployment_info_*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}")
|
|
return
|
|
|
|
print(f"Found {len(info_files)} deployments to teardown")
|
|
|
|
success_count = 0
|
|
for info_file in info_files:
|
|
config = parse_deployment_info(info_file)
|
|
if config:
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
print(f"\n{COLORS['CYAN']}Tearing down: {deployment_id}{COLORS['RESET']}")
|
|
|
|
if execute_teardown(config):
|
|
success_count += 1
|
|
cleanup_ssh_keys(deployment_id)
|
|
archive_logs(deployment_id)
|
|
print(f"{COLORS['GREEN']}✓ {deployment_id} torn down{COLORS['RESET']}")
|
|
else:
|
|
print(f"{COLORS['RED']}✗ {deployment_id} teardown failed{COLORS['RESET']}")
|
|
|
|
print(f"\n{COLORS['BLUE']}Summary: {success_count}/{len(info_files)} deployments torn down{COLORS['RESET']}")
|
|
|
|
def parse_deployment_info(info_file):
|
|
"""Parse deployment info file"""
|
|
try:
|
|
config = {}
|
|
with open(info_file, 'r') as f:
|
|
lines = f.readlines()
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if ": " in line and not line.startswith("-"):
|
|
parts = line.split(": ", 1)
|
|
if len(parts) == 2:
|
|
key, value = parts
|
|
|
|
# Convert string values
|
|
if value.lower() == 'true':
|
|
value = True
|
|
elif value.lower() == 'false':
|
|
value = False
|
|
elif value.lower() == 'none':
|
|
value = None
|
|
|
|
config[key] = value
|
|
|
|
return config
|
|
except Exception as e:
|
|
print(f"{COLORS['RED']}Error parsing {info_file}: {e}{COLORS['RESET']}")
|
|
return None
|
|
|
|
def execute_teardown(config):
|
|
"""Execute the actual teardown"""
|
|
provider = config.get('provider')
|
|
deployment_id = config.get('deployment_id')
|
|
|
|
if not provider:
|
|
print(f"{COLORS['RED']}No provider specified in configuration{COLORS['RESET']}")
|
|
return False
|
|
|
|
# Set provider environment
|
|
set_provider_environment(config)
|
|
|
|
# Get provider directory
|
|
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
|
cleanup_playbook = f"providers/{provider_dir}/cleanup.yml"
|
|
|
|
if not os.path.exists(cleanup_playbook):
|
|
print(f"{COLORS['RED']}Cleanup playbook not found: {cleanup_playbook}{COLORS['RESET']}")
|
|
show_manual_cleanup_guidance(config)
|
|
return False
|
|
|
|
print(f"Using cleanup playbook: {cleanup_playbook}")
|
|
|
|
# Create extra vars
|
|
extra_vars = create_extra_vars(config)
|
|
|
|
# Build ansible command - use venv ansible if available
|
|
venv_ansible = os.path.join(os.path.dirname(__file__), 'venv', 'bin', 'ansible-playbook')
|
|
if os.path.exists(venv_ansible):
|
|
ansible_cmd = venv_ansible
|
|
# Set Python interpreter for the venv
|
|
venv_python = os.path.join(os.path.dirname(__file__), 'venv', 'bin', 'python')
|
|
os.environ['ANSIBLE_PYTHON_INTERPRETER'] = venv_python
|
|
else:
|
|
ansible_cmd = 'ansible-playbook'
|
|
|
|
cmd = [
|
|
ansible_cmd,
|
|
cleanup_playbook,
|
|
'--extra-vars', extra_vars,
|
|
'-v'
|
|
]
|
|
|
|
print(f"Executing: {' '.join(cmd)}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
print(f"Return code: {result.returncode}")
|
|
if result.stdout:
|
|
print(f"STDOUT:\n{result.stdout}")
|
|
if result.stderr:
|
|
print(f"STDERR:\n{result.stderr}")
|
|
|
|
return result.returncode == 0
|
|
|
|
except subprocess.TimeoutExpired:
|
|
print(f"{COLORS['RED']}Teardown timed out after 5 minutes{COLORS['RESET']}")
|
|
return False
|
|
except Exception as e:
|
|
print(f"{COLORS['RED']}Error executing teardown: {e}{COLORS['RESET']}")
|
|
return False
|
|
|
|
def set_provider_environment(config):
|
|
"""Set provider-specific environment variables"""
|
|
provider = config.get('provider')
|
|
|
|
if provider == "aws":
|
|
if config.get('aws_access_key'):
|
|
os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key']
|
|
if config.get('aws_secret_key'):
|
|
os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key']
|
|
elif provider == "linode":
|
|
if config.get('linode_token'):
|
|
os.environ['LINODE_TOKEN'] = config['linode_token']
|
|
else:
|
|
# Try to load from vars file
|
|
try:
|
|
import yaml
|
|
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
|
vars_file = f"providers/{provider_dir}/vars.yaml"
|
|
if os.path.exists(vars_file):
|
|
with open(vars_file, 'r') as f:
|
|
vars_data = yaml.safe_load(f)
|
|
if vars_data and vars_data.get('linode_token'):
|
|
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
|
|
print(f"Loaded Linode token from {vars_file}")
|
|
except Exception as e:
|
|
print(f"{COLORS['YELLOW']}Warning: Could not load Linode token: {e}{COLORS['RESET']}")
|
|
|
|
def create_extra_vars(config):
|
|
"""Create Ansible extra variables"""
|
|
extra_vars = {
|
|
'deployment_id': config.get('deployment_id'),
|
|
'provider': config.get('provider'),
|
|
'skip_confirmation': True
|
|
}
|
|
|
|
# Add instance names
|
|
if config.get('redirector_name'):
|
|
extra_vars['redirector_name'] = config['redirector_name']
|
|
if config.get('c2_name'):
|
|
extra_vars['c2_name'] = config['c2_name']
|
|
if config.get('tracker_name'):
|
|
extra_vars['tracker_name'] = config['tracker_name']
|
|
if config.get('attack_box_name'):
|
|
extra_vars['attack_box_name'] = config['attack_box_name']
|
|
|
|
return json.dumps(extra_vars)
|
|
|
|
def cleanup_ssh_keys(deployment_id):
|
|
"""Clean up SSH keys for deployment"""
|
|
ssh_dir = os.path.expanduser("~/.ssh")
|
|
patterns = [
|
|
f"c2deploy_{deployment_id}*",
|
|
f"attack-box-{deployment_id}*",
|
|
f"a-{deployment_id}*"
|
|
]
|
|
|
|
removed_keys = []
|
|
for pattern in patterns:
|
|
for key_file in glob.glob(os.path.join(ssh_dir, pattern)):
|
|
try:
|
|
os.remove(key_file)
|
|
removed_keys.append(key_file)
|
|
except Exception as e:
|
|
print(f"{COLORS['YELLOW']}Warning: Could not remove {key_file}: {e}{COLORS['RESET']}")
|
|
|
|
if removed_keys:
|
|
print(f"Removed SSH keys: {', '.join([os.path.basename(k) for k in removed_keys])}")
|
|
else:
|
|
print("No SSH keys found to remove")
|
|
|
|
def archive_logs(deployment_id):
|
|
"""Archive logs for the deployment"""
|
|
try:
|
|
archive_dir = "logs/archive"
|
|
os.makedirs(archive_dir, exist_ok=True)
|
|
|
|
patterns = [
|
|
f"logs/deployment_{deployment_id}*",
|
|
f"logs/deployment_info_{deployment_id}*",
|
|
f"logs/teardown_{deployment_id}*"
|
|
]
|
|
|
|
archived_files = []
|
|
for pattern in patterns:
|
|
for file_path in glob.glob(pattern):
|
|
filename = os.path.basename(file_path)
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
archive_filename = f"{timestamp}_{filename}"
|
|
archive_path = os.path.join(archive_dir, archive_filename)
|
|
|
|
os.rename(file_path, archive_path)
|
|
archived_files.append(archive_filename)
|
|
|
|
if archived_files:
|
|
print(f"Archived logs: {', '.join(archived_files)}")
|
|
else:
|
|
print("No logs found to archive")
|
|
|
|
except Exception as e:
|
|
print(f"{COLORS['YELLOW']}Warning: Could not archive logs: {e}{COLORS['RESET']}")
|
|
|
|
def show_manual_cleanup_guidance(config=None):
|
|
"""Show manual cleanup guidance"""
|
|
print(f"\n{COLORS['CYAN']}Manual Cleanup Guidance{COLORS['RESET']}")
|
|
print(f"{COLORS['CYAN']}========================{COLORS['RESET']}")
|
|
|
|
if config:
|
|
provider = config.get('provider', 'unknown')
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
|
|
print(f"Deployment ID: {deployment_id}")
|
|
print(f"Provider: {provider}")
|
|
|
|
if provider == "linode":
|
|
print(f"\nLinode cleanup steps:")
|
|
print(f"1. Go to https://cloud.linode.com/")
|
|
print(f"2. Check 'Linodes' section for instances with labels containing: {deployment_id}")
|
|
print(f"3. Delete any instances found")
|
|
print(f"4. Check 'Firewalls' section for firewalls with labels containing: {deployment_id}")
|
|
print(f"5. Delete any firewalls found")
|
|
|
|
elif provider == "aws":
|
|
print(f"\nAWS cleanup steps:")
|
|
print(f"1. Go to AWS EC2 Console")
|
|
print(f"2. Check instances with tags containing: {deployment_id}")
|
|
print(f"3. Delete instances and associated resources")
|
|
print(f"4. Check security groups, key pairs, and elastic IPs")
|
|
|
|
print(f"\nLocal cleanup:")
|
|
print(f"1. SSH keys in ~/.ssh/ starting with c2deploy_, attack-box-, or a-")
|
|
print(f"2. Log files in logs/ directory")
|
|
print(f"3. Any temporary files or state files")
|
|
|
|
def select_and_teardown_deployment():
|
|
"""Interactive deployment selection for teardown"""
|
|
print(f"\n{COLORS['CYAN']}Select Deployment to Teardown:{COLORS['RESET']}")
|
|
|
|
# Find deployment info files
|
|
info_files = glob.glob("logs/deployment_info_*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}")
|
|
return
|
|
|
|
deployments = []
|
|
for info_file in info_files:
|
|
config = parse_deployment_info(info_file)
|
|
if config:
|
|
deployments.append(config)
|
|
|
|
if not deployments:
|
|
print(f"{COLORS['YELLOW']}Found info files but could not parse deployment data{COLORS['RESET']}")
|
|
return
|
|
|
|
print(f"\nFound {len(deployments)} deployments:")
|
|
print("=" * 40)
|
|
|
|
# Display deployments with selection numbers
|
|
for i, config in enumerate(deployments, 1):
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
provider = config.get('provider', 'unknown')
|
|
domain = config.get('domain', 'N/A')
|
|
|
|
print(f"{i}. {COLORS['WHITE']}{deployment_id}{COLORS['RESET']}")
|
|
print(f" Provider: {provider}")
|
|
print(f" Domain: {domain}")
|
|
|
|
# Show instance names if available
|
|
instances = []
|
|
if config.get('redirector_name'):
|
|
instances.append(f"Redirector: {config['redirector_name']}")
|
|
if config.get('c2_name'):
|
|
instances.append(f"C2: {config['c2_name']}")
|
|
if config.get('tracker_name'):
|
|
instances.append(f"Tracker: {config['tracker_name']}")
|
|
if config.get('attack_box_name'):
|
|
instances.append(f"Attack Box: {config['attack_box_name']}")
|
|
|
|
if instances:
|
|
print(f" Instances: {', '.join(instances)}")
|
|
print()
|
|
|
|
# Get user selection
|
|
while True:
|
|
try:
|
|
selection = input(f"\nSelect deployment to teardown (1-{len(deployments)}), or 'q' to quit: ").strip()
|
|
|
|
if selection.lower() == 'q':
|
|
print(f"{COLORS['GREEN']}Selection cancelled{COLORS['RESET']}")
|
|
return
|
|
|
|
selection_num = int(selection)
|
|
if 1 <= selection_num <= len(deployments):
|
|
selected_deployment = deployments[selection_num - 1]
|
|
deployment_id = selected_deployment.get('deployment_id', 'unknown')
|
|
|
|
print(f"\n{COLORS['YELLOW']}Selected deployment: {deployment_id}{COLORS['RESET']}")
|
|
|
|
# Confirm selection and proceed with teardown
|
|
confirm = input(f"Proceed with teardown of {deployment_id}? (yes/no): ").strip().lower()
|
|
if confirm == 'yes':
|
|
teardown_deployment(deployment_id)
|
|
else:
|
|
print(f"{COLORS['GREEN']}Teardown cancelled{COLORS['RESET']}")
|
|
return
|
|
else:
|
|
print(f"{COLORS['RED']}Please enter a number between 1 and {len(deployments)}{COLORS['RESET']}")
|
|
except ValueError:
|
|
print(f"{COLORS['RED']}Please enter a valid number or 'q' to quit{COLORS['RESET']}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|