#!/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()