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:
Operator
2026-06-23 16:12:14 -04:00
commit 98103466d8
239 changed files with 40012 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# Utils package for C2ingRed
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
AWS provider utilities for C2ingRed deployment system
"""
import os
import subprocess
import logging
from .common import COLORS, load_vars_file
def get_aws_credentials(provider_vars=None):
"""Get AWS credentials — Infisical first, vars.yaml fallback, then prompt."""
key = ''
secret = ''
try:
key = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'AWS_ACCESS_KEY_ID', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
secret = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'AWS_SECRET_ACCESS_KEY', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
except Exception:
pass
if not key:
if not provider_vars:
provider_vars = load_vars_file('aws')
key = provider_vars.get('aws_access_key', '')
secret = provider_vars.get('aws_secret_key', '')
if key and 'YOUR_AWS' in key:
key = ''
secret = ''
print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}")
if key:
print(f" AWS credentials loaded")
else:
key = input("AWS Access Key: ").strip()
secret = input("AWS Secret Key: ").strip()
return {'aws_access_key': key, 'aws_secret_key': secret}
def select_aws_regions(provider_vars=None) -> list[str]:
"""Return region list — single entry if user specified one, all regions if blank (random per-node)."""
if not provider_vars:
provider_vars = load_vars_file('aws')
regions = provider_vars.get('aws_region_choices', ['us-east-1'])
print(f"\nAvailable AWS regions:")
for i, region in enumerate(regions, 1):
print(f" {i:2}. {region}")
region_input = input("\nSelect region (number or leave blank for random per-node): ").strip()
if not region_input:
return regions
try:
idx = int(region_input)
if 1 <= idx <= len(regions):
return [regions[idx - 1]]
except ValueError:
pass
print(f"{COLORS['RED']}Invalid input, using random per-node regions{COLORS['RESET']}")
return regions
def gather_aws_config():
"""Gather all AWS-specific configuration"""
provider_vars = load_vars_file('aws')
config = {}
aws_creds = get_aws_credentials(provider_vars)
config.update(aws_creds)
aws_regions = select_aws_regions(provider_vars)
config['aws_regions'] = aws_regions
config['aws_region'] = aws_regions[0]
config['ami_map'] = provider_vars.get('ami_map', {})
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
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import ipaddress
from utils.cidr_resolver import get_country_cidrs
def ip_count(cidr: str) -> int:
try:
return ipaddress.ip_network(cidr, strict=False).num_addresses
except ValueError:
return 0
def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]:
chunks = []
current_cidrs: list[str] = []
current_count = 0
for cidr in all_cidrs:
n = ip_count(cidr)
if n == 0:
continue
if current_count > 0 and current_count + n > chunk_size * 1.5:
chunks.append({'cidrs': current_cidrs, 'ip_count': current_count})
current_cidrs = []
current_count = 0
current_cidrs.append(cidr)
current_count += n
if current_count >= chunk_size:
chunks.append({'cidrs': current_cidrs, 'ip_count': current_count})
current_cidrs = []
current_count = 0
if current_cidrs:
chunks.append({'cidrs': current_cidrs, 'ip_count': current_count})
return chunks
__all__ = ['get_country_cidrs', 'chunk_cidrs', 'ip_count']
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
import ipaddress
import math
import time
from pathlib import Path
from urllib.request import urlretrieve
from urllib.error import URLError
CACHE_DIR = Path.home() / '.cache' / 'c2itall' / 'cidr'
CACHE_TTL = 86400 # 24 hours
RIR_URLS = {
'arin': 'https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest',
'ripe': 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest',
'apnic': 'https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest',
'lacnic': 'https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest',
'afrinic': 'https://ftp.afrinic.net/stats/delegated-afrinic-extended-latest',
}
def _cache_path(rir: str) -> Path:
return CACHE_DIR / f'{rir}.txt'
def _is_stale(path: Path) -> bool:
if not path.exists():
return True
return (time.time() - path.stat().st_mtime) > CACHE_TTL
def _fetch_rir(rir: str, url: str) -> Path:
path = _cache_path(rir)
CACHE_DIR.mkdir(parents=True, exist_ok=True)
if _is_stale(path):
try:
urlretrieve(url, path)
except (URLError, OSError):
pass # use stale cache if download fails
return path
def _count_to_cidrs(start: str, count: int) -> list[str]:
try:
start_addr = ipaddress.IPv4Address(start)
end_addr = ipaddress.IPv4Address(int(start_addr) + count - 1)
return [str(n) for n in ipaddress.summarize_address_range(start_addr, end_addr)]
except (ipaddress.AddressValueError, ValueError):
return []
def _parse_rir_file(path: Path, target_ccs: set[str]) -> dict[str, list[str]]:
result: dict[str, list[str]] = {cc: [] for cc in target_ccs}
try:
with open(path, encoding='latin-1') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split('|')
if len(parts) < 7:
continue
_, cc, type_, start, count_str, _, status = parts[:7]
if type_ != 'ipv4':
continue
if status not in ('allocated', 'assigned'):
continue
cc = cc.upper()
if cc not in target_ccs:
continue
try:
count = int(count_str)
except ValueError:
continue
result[cc].extend(_count_to_cidrs(start, count))
except OSError:
pass
return result
def get_country_cidrs(country_codes: list[str], exclude_map: dict[str, list] | None = None) -> dict[str, list[str]]:
if exclude_map is None:
exclude_map = {}
target_ccs = {cc.upper() for cc in country_codes}
combined: dict[str, list[str]] = {cc: [] for cc in target_ccs}
for rir, url in RIR_URLS.items():
path = _fetch_rir(rir, url)
partial = _parse_rir_file(path, target_ccs)
for cc, cidrs in partial.items():
combined[cc].extend(cidrs)
for cc, excludes in exclude_map.items():
cc = cc.upper()
if cc not in combined or not excludes:
continue
exclude_nets = []
for ex in excludes:
try:
exclude_nets.append(ipaddress.ip_network(ex, strict=False))
except ValueError:
pass
if not exclude_nets:
continue
filtered = []
for cidr in combined[cc]:
try:
net = ipaddress.ip_network(cidr, strict=False)
if not any(net.overlaps(ex) for ex in exclude_nets):
filtered.append(cidr)
except ValueError:
filtered.append(cidr)
combined[cc] = filtered
return combined
+318
View File
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
"""
Cleanup and teardown engine for C2ingRed deployments
"""
import os
import sys
import subprocess
import logging
import json
import glob
# Add project root to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from utils.common import COLORS, PROVIDER_DIRS, confirm_action
def teardown_by_deployment_id(deployment_id):
"""Teardown infrastructure by deployment ID"""
print(f"\n{COLORS['BLUE']}Teardown Infrastructure by ID: {deployment_id}{COLORS['RESET']}")
# Look for deployment logs to determine provider and configuration
log_files = glob.glob(f"logs/deployment_{deployment_id}*.log")
if not log_files:
print(f"{COLORS['RED']}No deployment logs found for ID: {deployment_id}{COLORS['RESET']}")
return False
# Try to find deployment info files
info_files = glob.glob(f"logs/deployment_info_{deployment_id}*.txt")
if info_files:
config = parse_deployment_info(info_files[0])
if config:
return execute_teardown(config)
print(f"{COLORS['YELLOW']}Could not determine deployment configuration from logs{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Manual cleanup may be required{COLORS['RESET']}")
return False
def teardown_all_infrastructure():
"""Teardown all deployed infrastructure"""
print(f"\n{COLORS['RED']}⚠️ WARNING: This will attempt to destroy ALL infrastructure!{COLORS['RESET']}")
if not confirm_action("Are you absolutely sure?", default=False):
return False
# Find all deployment info files
info_files = glob.glob("logs/deployment_info_*.txt")
if not info_files:
print(f"{COLORS['GREEN']}No deployment info files found - nothing to teardown{COLORS['RESET']}")
return True
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"\nTearing down deployment: {deployment_id}")
if execute_teardown(config):
success_count += 1
print(f"\n{COLORS['GREEN']}Successfully tore down {success_count}/{len(info_files)} deployments{COLORS['RESET']}")
return success_count == len(info_files)
def parse_deployment_info(info_file):
"""Parse deployment info from file to extract configuration"""
try:
config = {}
with open(info_file, 'r') as f:
lines = f.readlines()
in_config_section = False
for line in lines:
line = line.strip()
if line == "Configuration:":
in_config_section = True
continue
elif line.startswith("-" * 30):
continue
elif line.startswith("Access Information:"):
break
if in_config_section and ": " in line:
key, value = line.split(": ", 1)
# Convert string values back to appropriate types
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:
logging.error(f"Failed to parse deployment info from {info_file}: {e}")
return None
def execute_teardown(config):
"""Execute teardown based on configuration"""
provider = config.get('provider')
deployment_id = config.get('deployment_id')
if not provider or not deployment_id:
print(f"{COLORS['RED']}Missing provider or deployment ID{COLORS['RESET']}")
return False
print(f"{COLORS['BLUE']}Executing teardown for {provider} deployment {deployment_id}...{COLORS['RESET']}")
# Set up logging for teardown
from utils.common import setup_logging
setup_logging(deployment_id, "teardown")
try:
# Set provider-specific environment variables
set_provider_environment_for_teardown(config)
# Get correct provider directory
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
# Execute teardown playbook
playbook = f"providers/{provider_dir}/cleanup.yml"
if not os.path.exists(playbook):
print(f"{COLORS['YELLOW']}Teardown playbook not found: {playbook}{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Manual cleanup required{COLORS['RESET']}")
return manual_cleanup_guidance(config)
success = run_teardown_playbook(playbook, config)
if success:
# Clean up local SSH keys
cleanup_ssh_keys_for_deployment(deployment_id)
# Move deployment logs to cleanup folder
archive_deployment_logs(deployment_id)
print(f"{COLORS['GREEN']}Teardown completed successfully{COLORS['RESET']}")
else:
print(f"{COLORS['RED']}Teardown failed - manual cleanup may be required{COLORS['RESET']}")
return success
except Exception as e:
logging.error(f"Teardown execution failed: {e}")
print(f"{COLORS['RED']}Teardown execution failed: {e}{COLORS['RESET']}")
return False
def set_provider_environment_for_teardown(config):
"""Set provider-specific environment variables for teardown"""
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":
# Try to get token from config first, then from vars file
if config.get('linode_token'):
os.environ['LINODE_TOKEN'] = config['linode_token']
else:
# Load token from provider vars file
try:
from utils.common import load_vars_file, PROVIDER_DIRS
vars_data = load_vars_file(provider)
if vars_data and vars_data.get('linode_token'):
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
config['linode_token'] = vars_data['linode_token'] # Add to config for later use
else:
logging.warning("Linode token not found in vars file - cleanup may fail")
except Exception as e:
logging.warning(f"Failed to load Linode token from vars file: {e}")
def run_teardown_playbook(playbook, config):
"""Run the teardown Ansible playbook"""
try:
cmd = [
'ansible-playbook',
playbook,
'--extra-vars', create_teardown_vars(config)
]
if config.get('debug'):
cmd.append('-vvv')
logging.info(f"Executing teardown: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
if result.returncode == 0:
logging.info("Teardown playbook executed successfully")
return True
else:
logging.error(f"Teardown playbook failed with return code {result.returncode}")
logging.error(f"STDOUT: {result.stdout}")
logging.error(f"STDERR: {result.stderr}")
return False
except Exception as e:
logging.error(f"Error executing teardown playbook: {e}")
return False
def create_teardown_vars(config):
"""Create Ansible extra vars for teardown"""
teardown_vars = {
'deployment_id': config.get('deployment_id'),
'provider': config.get('provider'),
'operation': 'teardown'
}
# Add instance names for proper cleanup
if config.get('redirector_name'):
teardown_vars['redirector_name'] = config['redirector_name']
if config.get('c2_name'):
teardown_vars['c2_name'] = config['c2_name']
if config.get('tracker_name'):
teardown_vars['tracker_name'] = config['tracker_name']
if config.get('attack_box_name'):
teardown_vars['attack_box_name'] = config['attack_box_name']
# Add provider-specific vars if present
if config.get('aws_access_key'):
teardown_vars['aws_access_key'] = config['aws_access_key']
if config.get('aws_secret_key'):
teardown_vars['aws_secret_key'] = config['aws_secret_key']
if config.get('aws_region'):
teardown_vars['aws_region'] = config['aws_region']
if config.get('linode_token'):
teardown_vars['linode_token'] = config['linode_token']
if config.get('linode_region'):
teardown_vars['linode_region'] = config['linode_region']
return json.dumps(teardown_vars)
def cleanup_ssh_keys_for_deployment(deployment_id):
"""Clean up SSH keys for a specific deployment"""
ssh_dir = os.path.expanduser("~/.ssh")
key_pattern = f"c2deploy_{deployment_id}*"
for key_file in glob.glob(os.path.join(ssh_dir, key_pattern)):
try:
os.remove(key_file)
logging.info(f"Removed SSH key: {key_file}")
except Exception as e:
logging.warning(f"Failed to remove SSH key {key_file}: {e}")
def archive_deployment_logs(deployment_id):
"""Archive deployment logs after successful teardown"""
try:
# Create archive directory
archive_dir = "logs/archive"
os.makedirs(archive_dir, exist_ok=True)
# Move all files related to this deployment
log_patterns = [
f"logs/deployment_{deployment_id}*",
f"logs/deployment_info_{deployment_id}*",
f"logs/teardown_{deployment_id}*"
]
for pattern in log_patterns:
for file_path in glob.glob(pattern):
archive_path = os.path.join(archive_dir, os.path.basename(file_path))
os.rename(file_path, archive_path)
logging.info(f"Archived: {file_path} -> {archive_path}")
except Exception as e:
logging.warning(f"Failed to archive logs for {deployment_id}: {e}")
def manual_cleanup_guidance(config):
"""Provide manual cleanup guidance when automated teardown isn't available"""
provider = config.get('provider')
deployment_id = config.get('deployment_id')
print(f"\n{COLORS['YELLOW']}Manual Cleanup Required{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}========================{COLORS['RESET']}")
print(f"Deployment ID: {deployment_id}")
print(f"Provider: {provider}")
if provider == "aws":
print(f"\nAWS Resources to check:")
print(f"- EC2 instances with tags containing: {deployment_id}")
print(f"- Security groups with names containing: {deployment_id}")
print(f"- Key pairs with names containing: {deployment_id}")
print(f"- EIPs associated with the deployment")
elif provider == "linode":
print(f"\nLinode Resources to check:")
print(f"- Linodes with labels containing: {deployment_id}")
print(f"- NodeBalancers with labels containing: {deployment_id}")
print(f"- Firewalls with labels containing: {deployment_id}")
elif provider == "flokinet":
print(f"\nFlokiNET Resources to check:")
print(f"- Check your FlokiNET control panel for resources created")
print(f"- Look for servers with the deployment ID: {deployment_id}")
print(f"\nLocal cleanup:")
print(f"- SSH keys: ~/.ssh/c2deploy_{deployment_id}*")
print(f"- Log files: logs/*{deployment_id}*")
return False # Manual cleanup required
if __name__ == "__main__":
print("Cleanup engine loaded")
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Common utilities and constants for C2ingRed deployment system
"""
import os
import sys
import random
import string
import logging
import subprocess
import re
from datetime import datetime
# Constants for providers
PROVIDERS = ["aws", "linode", "flokinet"]
DEFAULT_SSH_USER = {
"aws": "kali",
"linode": "root",
"flokinet": "root"
}
# Directory names - maintain correct case for each provider
PROVIDER_DIRS = {
"aws": "AWS",
"linode": "Linode",
"flokinet": "FlokiNET"
}
# Color codes for terminal output
COLORS = {
"RESET": "\033[0m",
"RED": "\033[91m",
"GREEN": "\033[92m",
"YELLOW": "\033[93m",
"BLUE": "\033[94m",
"PURPLE": "\033[95m",
"CYAN": "\033[96m",
"WHITE": "\033[97m",
"GRAY": "\033[90m"
}
def clear_screen():
"""Clear the terminal screen"""
os.system('cls' if os.name == 'nt' else 'clear')
def print_banner():
"""Print the C2ingRed banner"""
banner = f"""
{COLORS['BLUE']}========================================================{COLORS['RESET']}
{COLORS['BLUE']} ██████╗██████╗ ██╗███╗ ██╗ ██████╗ ██████╗ ███████╗██████╗{COLORS['RESET']}
{COLORS['BLUE']} ██╔════╝╚════██╗██║████╗ ██║██╔════╝ ██╔══██╗██╔════╝██╔══██╗{COLORS['RESET']}
{COLORS['BLUE']} ██║ █████╔╝██║██╔██╗ ██║██║ ███╗██████╔╝█████╗ ██║ ██║{COLORS['RESET']}
{COLORS['BLUE']} ██║ ██╔═══╝ ██║██║╚██╗██║██║ ██║██╔══██╗██╔══╝ ██║ ██║{COLORS['RESET']}
{COLORS['BLUE']} ╚██████╗███████╗██║██║ ╚████║╚██████╔╝██║ ██║███████╗██████╔╝{COLORS['RESET']}
{COLORS['BLUE']} ╚═════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═════╝{COLORS['RESET']}
{COLORS['BLUE']} {COLORS['RESET']}
{COLORS['BLUE']} Red Team Infrastructure Deployment Tool {COLORS['RESET']}
{COLORS['BLUE']}========================================================{COLORS['RESET']}
"""
print(banner)
def generate_random_string(length=8):
"""Generate a random string of letters and digits."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
def generate_deployment_id():
"""Generate a consistent deployment ID for all resources in this deployment"""
from utils.name_generator import generate_deployment_id as generate_verb_animal_id
return generate_verb_animal_id()
def archive_old_logs(max_logs_to_keep=2):
"""Archive old log files to keep logs directory clean - more aggressive archiving"""
import glob
import shutil
from pathlib import Path
log_dir = "logs"
archive_dir = os.path.join(log_dir, "archive")
if not os.path.exists(log_dir):
return
# Create archive directory if it doesn't exist
os.makedirs(archive_dir, exist_ok=True)
# Get all log files (excluding archive directory) - ANY existing logs should be archived
log_files = []
for pattern in ["deployment_*.log", "teardown_*.log", "20*.log"]:
log_files.extend(glob.glob(os.path.join(log_dir, pattern)))
# Remove duplicates and filter out archive directory
log_files = [f for f in set(log_files) if "archive" not in f]
# Sort by modification time (newest first)
log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
# Archive ALL deployment logs to keep directory clean for new deployments
if len(log_files) > 0:
print(f"Found {len(log_files)} log files to archive")
archived_count = 0
for log_file in log_files:
try:
filename = os.path.basename(log_file)
# Add timestamp to archived filename to prevent conflicts
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archived_filename = f"{timestamp}_{filename}"
archive_path = os.path.join(archive_dir, archived_filename)
shutil.move(log_file, archive_path)
archived_count += 1
print(f"Archived: {filename} -> archive/{archived_filename}")
except Exception as e:
print(f"Failed to archive {log_file}: {e}")
if archived_count > 0:
print(f"Archived {archived_count} log files to {archive_dir}")
# Also archive old deployment info files
info_files = glob.glob(os.path.join(log_dir, "deployment_info_*.txt"))
if len(info_files) > 0:
print(f"Found {len(info_files)} info files to archive")
for info_file in info_files:
try:
filename = os.path.basename(info_file)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archived_filename = f"{timestamp}_{filename}"
archive_path = os.path.join(archive_dir, archived_filename)
shutil.move(info_file, archive_path)
print(f"Archived: {filename} -> archive/{archived_filename}")
except Exception as e:
print(f"Failed to archive {info_file}: {e}")
def setup_logging(deployment_id=None, operation_type="deployment"):
"""Set up logging for the deployment or teardown"""
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
# Archive old logs before starting new deployment
if operation_type == "deployment":
archive_old_logs()
# Create distinct log files for deployment vs teardown operations
if operation_type == "teardown":
log_file = os.path.join(log_dir, f"teardown_{deployment_id}.log")
else:
log_file = os.path.join(log_dir, f"deployment_{deployment_id}.log")
# Clear any existing handlers
root_logger = logging.getLogger()
root_logger.handlers.clear()
# Configure file handler to log DEBUG and above for full verbosity
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
# Add console handler for INFO level and above
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('[%(levelname)s] %(message)s')
console_handler.setFormatter(console_formatter)
# Configure root logger
root_logger.setLevel(logging.DEBUG) # Capture everything at root level
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
logging.info(f"{operation_type.capitalize()} operation started")
logging.info(f"Deployment ID: {deployment_id}")
logging.info(f"Full verbose output will be captured in: {log_file}")
return log_file
def load_vars_file(provider):
"""Load vars.yaml for the specified provider"""
if provider not in PROVIDER_DIRS:
return {}
# Use correct case for directory
provider_dir = PROVIDER_DIRS[provider]
vars_file = f"providers/{provider_dir}/vars.yaml"
if os.path.exists(vars_file):
try:
import yaml
with open(vars_file, 'r') as f:
return yaml.safe_load(f) or {}
except Exception as e:
logging.error(f"Failed to load {vars_file}: {e}")
return {}
else:
logging.warning(f"Vars file not found: {vars_file}")
return {}
def validate_ip_address(ip):
"""Validate IP address format"""
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
return re.match(pattern, ip) is not None
def get_public_ip():
"""Get the user's public IP address"""
try:
import requests
return requests.get('https://api.ipify.org', timeout=5).text.strip()
except:
return None
def confirm_action(message, default=False):
"""Ask for user confirmation with a yes/no prompt"""
prompt = f"{message} ({'Y/n' if default else 'y/N'}): "
response = input(prompt).lower().strip()
if not response:
return default
return response in ['y', 'yes']
def wait_for_input(message="Press Enter to continue..."):
"""Wait for user input before continuing"""
input(f"\n{message}")
+369
View File
@@ -0,0 +1,369 @@
#!/usr/bin/env python3
"""
Deployment engine for C2ingRed infrastructure deployments
"""
import os
import sys
import subprocess
import logging
import json
# Add project root to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from utils.common import COLORS, PROVIDER_DIRS
_SECRET_KEYS = frozenset({
'linode_token', 'aws_access_key', 'aws_secret_key', 'flokinet_api_key',
'smtp_auth_pass', 'smtp_password', 'ftp_password', 'api_key',
'password', 'secret', 'token',
})
def deploy_infrastructure(config):
"""
Deploy infrastructure based on the provided configuration.
Args:
config: Dictionary containing deployment configuration
Returns:
bool: True if deployment succeeded, False otherwise
"""
provider = config.get('provider')
deployment_id = config.get('deployment_id')
if not provider or not deployment_id:
logging.error("Missing provider or deployment ID in configuration")
return False
# Determine which playbook to use
playbook = determine_playbook(config)
if not playbook:
logging.error("Could not determine appropriate playbook for deployment")
return False
if not os.path.exists(playbook):
logging.error(f"Playbook not found: {playbook}")
print(f"{COLORS['RED']}Playbook not found: {playbook}{COLORS['RESET']}")
return False
logging.info(f"Using playbook: {playbook}")
# Set provider-specific environment variables
set_provider_environment(config)
# Execute the deployment
return execute_playbook(playbook, config)
def determine_playbook(config):
"""
Determine the correct playbook based on deployment configuration.
Args:
config: Deployment configuration dictionary
Returns:
str: Path to the playbook file
"""
provider = config.get('provider')
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
base_path = f"providers/{provider_dir}"
# Attack box deployment
if config.get('attack_box_deployment'):
return f"{base_path}/attack_box.yml"
# Redirector-only deployment
if config.get('redirector_only'):
return f"{base_path}/redirector.yml"
# Tracker deployment
if config.get('tracker_deployment'):
return f"{base_path}/tracker.yml"
# Phishing infrastructure deployment
if config.get('phishing_deployment'):
# Check for provider-specific phishing playbook first
provider_specific = f"{base_path}/{provider}_phishing.yml"
if os.path.exists(provider_specific):
return provider_specific
return f"{base_path}/phishing.yml"
# Chat server deployment
if config.get('chat_deployment'):
return f"{base_path}/chat_server.yml"
# C2 deployment (default for C2 infrastructure)
if config.get('c2_only') or config.get('deploy_c2') or config.get('c2_framework'):
return f"{base_path}/c2.yml"
# Default to C2 playbook
return f"{base_path}/c2.yml"
def set_provider_environment(config):
"""
Set provider-specific environment variables for deployment.
Args:
config: Deployment configuration dictionary
"""
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']
if config.get('aws_region'):
os.environ['AWS_DEFAULT_REGION'] = config['aws_region']
elif provider == "linode":
if config.get('linode_token'):
os.environ['LINODE_TOKEN'] = config['linode_token']
else:
# Pull from Infisical at runtime
try:
token = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'LINODE_TOKEN', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
if token:
os.environ['LINODE_TOKEN'] = token
config['linode_token'] = token
except Exception as e:
logging.warning(f"Failed to load Linode token from Infisical: {e}")
elif provider == "flokinet":
if config.get('flokinet_api_key'):
os.environ['FLOKINET_API_KEY'] = config['flokinet_api_key']
def execute_playbook(playbook, config):
"""
Execute an Ansible playbook with the provided configuration.
Args:
playbook: Path to the Ansible playbook
config: Deployment configuration dictionary
Returns:
bool: True if playbook executed successfully, False otherwise
"""
import tempfile
secret_vars_file = None
try:
# Build the ansible-playbook command
cmd = [
'ansible-playbook',
playbook,
'--extra-vars', create_extra_vars(config)
]
# Webrunner runs many nodes in parallel — increase fork count
if 'webrunner' in playbook:
cmd += ['-f', '50']
# Write sensitive vars to a temp file (0o600) so they reach Ansible as
# variables without appearing in process listings or the main extra-vars JSON.
# Read from environment (set by set_provider_environment) so this works even
# when config was passed as a copy ({**config}) that didn't capture the token.
secret_vars = {}
if os.environ.get('LINODE_TOKEN'):
secret_vars['linode_token'] = os.environ['LINODE_TOKEN']
if os.environ.get('AWS_ACCESS_KEY_ID'):
secret_vars['aws_access_key'] = os.environ['AWS_ACCESS_KEY_ID']
if os.environ.get('AWS_SECRET_ACCESS_KEY'):
secret_vars['aws_secret_key'] = os.environ['AWS_SECRET_ACCESS_KEY']
if os.environ.get('FLOKINET_API_KEY'):
secret_vars['flokinet_api_key'] = os.environ['FLOKINET_API_KEY']
# Also pick up any remaining secret keys from config dict (e.g. smtp_auth_pass)
for k, v in config.items():
if v is not None and k not in secret_vars and any(s in k.lower() for s in _SECRET_KEYS):
secret_vars[k] = v
if secret_vars:
fd, secret_vars_file = tempfile.mkstemp(suffix='.json', prefix='ansible_secret_')
os.chmod(secret_vars_file, 0o600)
with os.fdopen(fd, 'w') as f:
json.dump(secret_vars, f)
cmd += ['--extra-vars', f'@{secret_vars_file}']
# Add verbosity if debug mode is enabled
if config.get('debug'):
cmd.append('-vvv')
logging.info(f"Executing: {' '.join(cmd[:3])} ...")
# Get log file path for output
log_file = f"logs/deployment_{config['deployment_id']}.log"
# Execute the playbook — log file restricted to owner-only (contains Ansible output)
log_fd = os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
with open(log_fd, 'a') as log_output:
# Write command to log
log_output.write(f"\n{'='*60}\n")
log_output.write(f"Executing playbook: {playbook}\n")
log_output.write(f"{'='*60}\n\n")
log_output.flush()
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
# Stream output to both console and log file
for line in process.stdout:
# Write to log file
log_output.write(line)
log_output.flush()
# Print select lines to console for user feedback
if should_print_line(line):
print(line.rstrip())
process.wait()
if process.returncode == 0:
logging.info("Playbook executed successfully")
save_deployment_info(config)
return True
else:
logging.error(f"Playbook failed with return code {process.returncode}")
print(f"\n{COLORS['RED']}Deployment failed. Check logs for details: {log_file}{COLORS['RESET']}")
return False
except FileNotFoundError:
logging.error("ansible-playbook command not found. Is Ansible installed?")
print(f"{COLORS['RED']}Error: ansible-playbook not found. Please install Ansible.{COLORS['RESET']}")
return False
except Exception as e:
logging.error(f"Error executing playbook: {e}")
print(f"{COLORS['RED']}Error executing playbook: {e}{COLORS['RESET']}")
return False
finally:
if secret_vars_file and os.path.exists(secret_vars_file):
os.unlink(secret_vars_file)
def should_print_line(line):
"""
Determine if a line should be printed to console.
Filters out verbose Ansible output to show only key information.
Args:
line: Output line from Ansible
Returns:
bool: True if line should be printed
"""
# Always show task names and important messages
important_patterns = [
'TASK [',
'PLAY [',
'ok:',
'changed:',
'failed:',
'fatal:',
'skipping:',
'PLAY RECAP',
'=> {',
'msg:',
'IP Address:',
'Instance ID:',
]
for pattern in important_patterns:
if pattern in line:
return True
return False
def create_extra_vars(config):
"""
Create JSON string of extra variables for Ansible.
Args:
config: Deployment configuration dictionary
Returns:
str: JSON string of variables
"""
# Credentials are passed via environment variables (set_provider_environment).
# Strip them here so they never appear in --extra-vars or process listings.
extra_vars = {}
for key, value in config.items():
if value is None:
continue
if any(secret in key.lower() for secret in _SECRET_KEYS):
continue
if isinstance(value, bool):
extra_vars[key] = value
elif isinstance(value, (str, int, float, list, dict)):
extra_vars[key] = value
return json.dumps(extra_vars)
def save_deployment_info(config):
"""
Save deployment information to a file for later reference/cleanup.
Args:
config: Deployment configuration dictionary
"""
try:
deployment_id = config.get('deployment_id')
info_file = f"logs/deployment_info_{deployment_id}.txt"
with open(info_file, 'w') as f:
f.write(f"Deployment Information\n")
f.write(f"{'='*50}\n")
f.write(f"Deployment ID: {deployment_id}\n")
f.write(f"Provider: {config.get('provider')}\n")
f.write(f"Deployment Type: {config.get('deployment_type', 'unknown')}\n")
f.write(f"\n")
f.write(f"Configuration:\n")
f.write(f"{'-'*30}\n")
# Write key configuration items
keys_to_save = [
'provider', 'deployment_id', 'deployment_type',
'attack_box_name', 'c2_name', 'redirector_name', 'tracker_name',
'chat_server_name',
'domain', 'c2_subdomain', 'redirector_subdomain',
'c2_framework', 'redirector_type',
'linode_region', 'aws_region',
'setup_vpn', 'setup_tor',
'ssh_key_path',
'matrix_admin_user', 'enable_bot_support'
]
for key in keys_to_save:
if key in config and config[key] is not None:
# Don't save sensitive data
if 'password' in key.lower() or 'token' in key.lower() or 'secret' in key.lower():
continue
f.write(f"{key}: {config[key]}\n")
f.write(f"\n")
f.write(f"Access Information:\n")
f.write(f"{'-'*30}\n")
f.write(f"(Instance IPs will be displayed in Ansible output above)\n")
logging.info(f"Deployment info saved to: {info_file}")
except Exception as e:
logging.warning(f"Failed to save deployment info: {e}")
if __name__ == "__main__":
print("Deployment engine loaded")
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
FlokiNET provider utilities for C2ingRed deployment system
"""
import logging
from .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
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Linode provider utilities for C2ingRed deployment system
"""
import os
import logging
import subprocess
from .common import COLORS, load_vars_file
def get_linode_credentials(provider_vars=None):
"""Get Linode API token — Infisical first, vars.yaml fallback, then prompt."""
default_token = ''
try:
default_token = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'LINODE_TOKEN', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
except Exception:
pass
if not default_token:
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_regions(provider_vars=None) -> list[str]:
"""Return region list — single entry if user specified one, all regions if blank (random per-node)."""
if not provider_vars:
provider_vars = load_vars_file('linode')
regions = provider_vars.get('region_choices', ['us-east'])
print(f"\nAvailable Linode regions:")
for i, region in enumerate(regions, 1):
print(f" {i:2}. {region}")
region_input = input("\nSelect region (number or leave blank for random per-node): ").strip()
if not region_input:
return regions
try:
idx = int(region_input)
if 1 <= idx <= len(regions):
return [regions[idx - 1]]
except ValueError:
pass
print(f"{COLORS['RED']}Invalid input, using random per-node regions{COLORS['RESET']}")
return regions
def gather_linode_config():
"""Gather all Linode-specific configuration"""
provider_vars = load_vars_file('linode')
config = {}
linode_creds = get_linode_credentials(provider_vars)
if not linode_creds:
return None
config.update(linode_creds)
linode_regions = select_linode_regions(provider_vars)
config['linode_regions'] = linode_regions
config['linode_region'] = linode_regions[0]
config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1')
config['linode_image'] = provider_vars.get('linode_image', 'linode/debian12')
return config
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Name generation utility for c2itall deployments
Generates verb-animal names similar to FourEyes with shared deployment IDs
"""
import random
import os
def load_word_list(filename):
"""Load words from a text file, one word per line"""
try:
# Check if we have FourEyes word lists
foureyes_path = "/opt/redteam/FourEyes"
if os.path.exists(foureyes_path):
file_path = os.path.join(foureyes_path, filename)
if os.path.exists(file_path):
with open(file_path, 'r') as f:
words = [line.strip().lower() for line in f if line.strip()]
return [word for word in words if word] # Remove empty strings
except Exception:
pass
# Fallback word lists if FourEyes not available
if 'verbs' in filename:
return [
"blazing", "soaring", "charging", "prowling", "hunting", "stalking",
"striking", "rushing", "dashing", "racing", "flying", "diving",
"leaping", "climbing", "sliding", "spinning", "rolling", "sneaking",
"roaming", "wandering", "running", "jumping", "swimming", "crawling",
"fighting", "defending", "attacking", "scanning", "searching", "finding"
]
else: # animals
return [
"wolf", "eagle", "tiger", "falcon", "bear", "lion", "shark", "hawk",
"panther", "cobra", "viper", "rhino", "bull", "fox", "raven", "crow",
"spider", "scorpion", "mantis", "dragon", "phoenix", "griffin",
"badger", "wolverine", "lynx", "jaguar", "cheetah", "leopard"
]
def _generate_verb_animal():
"""Generate a verb-animal combination"""
verbs = load_word_list('verbs.txt')
animals = load_word_list('animals.txt')
verb = random.choice(verbs)
animal = random.choice(animals)
return f"{verb}{animal}"
def generate_deployment_id():
"""Generate a deployment ID using verb-animal combination"""
return _generate_verb_animal()
def generate_attack_box_name(deployment_id):
"""Generate attack box name with a- prefix using shared deployment ID"""
return f"a-{deployment_id}"
def generate_redirector_name(deployment_id):
"""Generate redirector name with r- prefix using shared deployment ID"""
return f"r-{deployment_id}"
def generate_c2_name(deployment_id):
"""Generate C2 server name with s- prefix using shared deployment ID"""
return f"s-{deployment_id}"
def generate_phishing_name(deployment_id):
"""Generate phishing server name with p- prefix using shared deployment ID"""
return f"p-{deployment_id}"
def generate_tracker_name(deployment_id):
"""Generate tracker name with t- prefix using shared deployment ID"""
return f"t-{deployment_id}"
if __name__ == "__main__":
# Test the name generation
deployment_id = generate_deployment_id()
print(f"Testing shared deployment ID: {deployment_id}")
print(f"Attack Box: {generate_attack_box_name(deployment_id)}")
print(f"Redirector: {generate_redirector_name(deployment_id)}")
print(f"C2 Server: {generate_c2_name(deployment_id)}")
print(f"Phishing: {generate_phishing_name(deployment_id)}")
print(f"Tracker: {generate_tracker_name(deployment_id)}")
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""
Common naming utilities for C2itall deployments
Provides consistent naming options across all deployment types
"""
import os
import glob
from utils.common import COLORS
def get_existing_deployments():
"""Get list of existing deployments from deployment info files"""
try:
info_files = glob.glob("logs/deployment_info_*.txt")
deployments = []
for info_file in info_files:
try:
deployment_info = {}
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
deployment_info[key] = value
if deployment_info.get('deployment_id'):
deployments.append(deployment_info)
except Exception as e:
continue # Skip problematic files
return deployments
except Exception as e:
return []
def select_deployment_for_naming(exclude_types=None, current_deployment_id=None):
"""
Allow user to select an existing deployment to base naming on
Args:
exclude_types: List of deployment types to exclude (e.g., ['attack_box'])
current_deployment_id: Current deployment ID to exclude from list
"""
deployments = get_existing_deployments()
if not deployments:
print(f"{COLORS['YELLOW']}No existing deployments found{COLORS['RESET']}")
return None
print(f"\n{COLORS['CYAN']}Existing Deployments:{COLORS['RESET']}")
print(f"{COLORS['CYAN']}==================={COLORS['RESET']}")
# Filter deployments based on criteria
filtered_deployments = []
for deployment in deployments:
deployment_id = deployment.get('deployment_id', 'unknown')
# Skip current deployment
if current_deployment_id and deployment_id == current_deployment_id:
continue
# Skip excluded types
deployment_type = deployment.get('deployment_type', 'unknown')
if exclude_types and deployment_type in exclude_types:
continue
# Check if this is an attack box deployment (legacy check)
is_attack_box = (
deployment.get('deployment_type') == 'attack_box' or
deployment.get('attack_box_deployment') == 'True' or
deployment.get('attack_box_name')
)
if exclude_types and 'attack_box' in exclude_types and is_attack_box:
continue
filtered_deployments.append(deployment)
if not filtered_deployments:
print(f"{COLORS['YELLOW']}No suitable deployments found for naming{COLORS['RESET']}")
return None
# Display available deployments
for i, deployment in enumerate(filtered_deployments, 1):
deployment_id = deployment.get('deployment_id', 'unknown')
provider = deployment.get('provider', 'unknown')
domain = deployment.get('domain', 'N/A')
deployment_type = deployment.get('deployment_type', 'unknown')
print(f"{i}. {deployment_id}")
print(f" Type: {deployment_type}")
print(f" Provider: {provider}")
print(f" Domain: {domain}")
# Show instance names if available
instances = []
if deployment.get('redirector_name'):
instances.append(f"Redirector: {deployment.get('redirector_name')}")
if deployment.get('c2_name'):
instances.append(f"C2: {deployment.get('c2_name')}")
if deployment.get('tracker_name'):
instances.append(f"Tracker: {deployment.get('tracker_name')}")
if deployment.get('attack_box_name'):
instances.append(f"Attack Box: {deployment.get('attack_box_name')}")
if instances:
print(f" Instances: {', '.join(instances)}")
print()
# Get user selection
while True:
try:
choice = input(f"Select deployment (1-{len(filtered_deployments)}) or 'c' to cancel: ").strip()
if choice.lower() == 'c':
return None
choice_num = int(choice)
if 1 <= choice_num <= len(filtered_deployments):
selected = filtered_deployments[choice_num - 1]
return selected.get('deployment_id')
else:
print(f"{COLORS['RED']}Invalid choice. Please try again.{COLORS['RESET']}")
except ValueError:
print(f"{COLORS['RED']}Invalid input. Please enter a number or 'c'.{COLORS['RESET']}")
def get_deployment_name_with_options(deployment_type, deployment_id, prefix="", existing_name=None):
"""
Get deployment name with multiple naming options
Args:
deployment_type: Type of deployment (c2, redirector, tracker, attack_box, etc.)
deployment_id: Current deployment ID
prefix: Prefix for the name (e.g., 'r-', 'c-', 'a-', etc.)
existing_name: Existing name if updating
Returns:
Chosen name for the deployment
"""
print(f"\n{COLORS['BLUE']}{deployment_type.title()} Naming Options:{COLORS['RESET']}")
print(f"1) Auto-generate name ({prefix}{deployment_id})")
print(f"2) Name after existing deployment")
print(f"3) Custom name")
if existing_name:
print(f"4) Keep current name ({existing_name})")
default_choice = "4"
else:
default_choice = "1"
naming_choice = input(f"Select naming option [{default_choice}]: ").strip() or default_choice
if naming_choice == "1":
# Auto-generate using deployment ID
chosen_name = f"{prefix}{deployment_id}"
print(f"Using auto-generated name: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
elif naming_choice == "2":
# Name after existing deployment
# Exclude attack boxes when naming other types, but allow other types when naming attack boxes
exclude_types = ['attack_box'] if deployment_type != 'attack_box' else []
selected_deployment_id = select_deployment_for_naming(
exclude_types=exclude_types,
current_deployment_id=deployment_id
)
if selected_deployment_id:
chosen_name = f"{prefix}{selected_deployment_id}"
print(f"{deployment_type.title()} will be named: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
print(f"This associates it with deployment: {COLORS['YELLOW']}{selected_deployment_id}{COLORS['RESET']}")
else:
print(f"{COLORS['YELLOW']}No deployment selected, using auto-generated name{COLORS['RESET']}")
chosen_name = f"{prefix}{deployment_id}"
elif naming_choice == "3":
# Custom name
while True:
custom_name = input(f"Enter custom {deployment_type} name: ").strip()
if custom_name:
# Ensure it starts with the correct prefix for consistency
if prefix and not custom_name.startswith(prefix):
chosen_name = f"{prefix}{custom_name}"
print(f"Prefixed with '{prefix}': {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
else:
chosen_name = custom_name
break
else:
print(f"{COLORS['RED']}Name cannot be empty. Please try again.{COLORS['RESET']}")
elif naming_choice == "4" and existing_name:
# Keep existing name
chosen_name = existing_name
print(f"Keeping current name: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
else:
# Default fallback
print(f"{COLORS['YELLOW']}Invalid choice, using auto-generated name{COLORS['RESET']}")
chosen_name = f"{prefix}{deployment_id}"
return chosen_name
def show_naming_relationship(name, deployment_id, deployment_type):
"""Show the relationship between the chosen name and deployment"""
if not name:
return
# Determine prefix based on deployment type
prefix_map = {
'redirector': 'r-',
'c2': 's-', # s for server
'tracker': 't-',
'attack_box': 'a-',
'payload': 'p-',
'webrunner': 'wr-',
}
expected_prefix = prefix_map.get(deployment_type, '')
if expected_prefix and name.startswith(expected_prefix):
target_deployment = name[len(expected_prefix):] # Remove prefix
if target_deployment != deployment_id:
return {
'target_deployment': target_deployment,
'relationship_text': f"Named after deployment: {target_deployment}",
'purpose_text': f"This {deployment_type} supports the {target_deployment} engagement"
}
return None
def get_deployment_type_prefix(deployment_type):
"""Get the standard prefix for a deployment type"""
prefix_map = {
'redirector': 'r-',
'c2': 's-', # s for server
'tracker': 't-',
'attack_box': 'a-',
'payload': 'p-',
'phishing': 'p-',
'webrunner': 'wr-',
}
return prefix_map.get(deployment_type, '')
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
import math
INSTANCE_RATES = {
'linode': {
'g6-nanode-1': 0.0075,
'g6-standard-2': 0.018,
'g6-standard-4': 0.036,
'g6-standard-8': 0.072,
},
'aws': {
't3.micro': 0.0104,
't3.small': 0.0208,
't3.medium': 0.0416,
't3.large': 0.0832,
},
'flokinet': {
'vps-1': 0.0083,
'vps-2': 0.0139,
'vps-4': 0.0278,
},
}
SCAN_MODES = {
'geo-scout': {'rate': 3000, 'desc': 'masscan + nmap + probe fingerprinting'},
'masscan-only': {'rate': 10000, 'desc': 'masscan port discovery only'},
'nmap-only': {'rate': 500, 'desc': 'nmap full fingerprint only'},
'masscan+nmap': {'rate': 5000, 'desc': 'masscan + nmap (no probes)'},
'masscan+nuclei': {'rate': 5000, 'desc': 'masscan discovery + nuclei CVE template'},
}
PRESETS = {
'sprint': {'chunk_size': 500_000, 'label': 'Sprint', 'desc': '~500K IPs/node'},
'balanced': {'chunk_size': 2_000_000, 'label': 'Balanced', 'desc': '~2M IPs/node (recommended)'},
'economy': {'chunk_size': 5_000_000, 'label': 'Economy', 'desc': '~5M IPs/node'},
}
DEFAULT_INSTANCE = {
'linode': 'g6-nanode-1',
'aws': 't3.small',
'flokinet': 'vps-2',
}
BILLING_MINIMUM = {
'linode': 1.0,
'aws': 0.017, # billed per second, ~1 min minimum in practice
'flokinet': 1.0,
}
# ── Empirical timing constants ────────────────────────────────────────────────
# Per-host nmap time by timing template (seconds, -sV --version-intensity 5)
NMAP_TIME_BY_TIMING = {
1: 60.0, # T1 sneaky
2: 30.0, # T2 polite
3: 15.0, # T3 normal
4: 10.0, # T4 aggressive (baseline)
}
# Average per-target time for typical nuclei CVE template (13 HTTP requests).
# Heavy templates with many requests/matchers will run 25× longer.
NUCLEI_TIME_PER_TARGET_SEC = 5.0
# TCP banner grab time (geo-scout probe phase)
PROBE_TIME_PER_HOST_SEC = 3.0
# Default fraction of scanned IPs with open ports on common ports.
# Real-world range: 0.5%5% depending on ports & geography.
MASSCAN_HIT_RATE = 0.01
# Defaults — must match WEBRUNNER tuning defaults
_NMAP_WORKERS = 10
_NUCLEI_RATE = 150
_NUCLEI_CONCURRENCY = 25
# Tor latency multiplier — applies ONLY to TCP probe phases (nmap/nuclei/probe).
# Masscan uses raw sockets and bypasses proxychains entirely — Tor cannot
# protect masscan SYN packets. The cloud node's IP is exposed to every
# masscan target regardless of this setting.
TOR_PHASE_MULTIPLIER = 5.0
# Per-node provisioning overhead (apt install, optional nuclei download).
# Nodes provision in parallel so this is roughly constant.
PROVISION_OVERHEAD_HOURS = 5 / 60
def estimate_scan_hours(
ip_count: int,
n_ports: int,
rate: int,
scan_mode: str = 'masscan-only',
*,
nmap_timing: int = 4,
nmap_workers: int = _NMAP_WORKERS,
nuclei_rate: int = _NUCLEI_RATE,
nuclei_concurrency: int = _NUCLEI_CONCURRENCY,
hit_rate: float = MASSCAN_HIT_RATE,
use_tor: bool = False,
) -> float:
"""Phase-decomposed scan time estimate. Returns hours per node."""
if rate <= 0:
return PROVISION_OVERHEAD_HOURS
nmap_workers = max(nmap_workers, 1)
seconds = 0.0
# masscan phase: raw sockets, no Tor penalty (Tor cannot proxy raw sockets)
if scan_mode in ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei'):
seconds += ip_count * n_ports / rate
# nmap-only phase: every IP gets full -sV fingerprint
if scan_mode == 'nmap-only':
per_host = NMAP_TIME_BY_TIMING.get(nmap_timing, 10.0)
if use_tor:
per_host *= TOR_PHASE_MULTIPLIER
seconds += (ip_count * per_host) / nmap_workers
# nmap fingerprint phase: only on masscan hits
if scan_mode in ('masscan+nmap', 'geo-scout'):
nmap_hosts = ip_count * hit_rate
per_host = NMAP_TIME_BY_TIMING.get(nmap_timing, 10.0)
if use_tor:
per_host *= TOR_PHASE_MULTIPLIER
seconds += (nmap_hosts * per_host) / nmap_workers
# probe banner phase: geo-scout only, on masscan hits
if scan_mode == 'geo-scout':
probe_hosts = ip_count * hit_rate
per_probe = PROBE_TIME_PER_HOST_SEC
if use_tor:
per_probe *= TOR_PHASE_MULTIPLIER
seconds += (probe_hosts * per_probe) / nmap_workers
# nuclei phase: only on masscan-discovered ip:port pairs
if scan_mode == 'masscan+nuclei':
nuclei_targets = ip_count * hit_rate
per_target = NUCLEI_TIME_PER_TARGET_SEC
if use_tor:
per_target *= TOR_PHASE_MULTIPLIER
rate_throughput = float(nuclei_rate)
parallel_throughput = nuclei_concurrency / per_target
effective_rps = max(min(rate_throughput, parallel_throughput), 1.0)
seconds += nuclei_targets / effective_rps
return seconds / 3600 + PROVISION_OVERHEAD_HOURS
def node_cost(provider: str, instance_type: str, scan_hours: float) -> float:
rate = INSTANCE_RATES.get(provider, {}).get(instance_type, 0.018)
billed_hours = max(scan_hours, BILLING_MINIMUM.get(provider, 1.0))
return rate * billed_hours
def fmt_hours(h: float) -> str:
total_mins = int(h * 60)
hrs, mins = divmod(total_mins, 60)
return f"{hrs}h {mins:02d}m" if hrs else f"{mins}m"
def fmt_ip_count(n: int) -> str:
if n >= 1_000_000:
return f"{n / 1_000_000:.1f}M"
if n >= 1_000:
return f"{n / 1_000:.0f}K"
return str(n)
def build_estimate_table(
total_ips: int,
n_ports: int,
providers: list[str],
scan_mode: str,
use_tor: bool = False,
tuning: dict | None = None,
) -> list[dict]:
tuning = tuning or {}
masscan_rate = int(tuning.get('masscan_rate') or SCAN_MODES.get(scan_mode, {'rate': 3000})['rate'])
kwargs = dict(
nmap_timing=int(tuning.get('nmap_timing', 4)),
nmap_workers=int(tuning.get('nmap_workers', _NMAP_WORKERS)),
nuclei_rate=int(tuning.get('nuclei_rate', _NUCLEI_RATE)),
nuclei_concurrency=int(tuning.get('nuclei_concurrency', _NUCLEI_CONCURRENCY)),
hit_rate=float(tuning.get('hit_rate', MASSCAN_HIT_RATE)),
use_tor=use_tor,
)
rows = []
for preset_key, preset in PRESETS.items():
n_chunks = max(1, math.ceil(total_ips / preset['chunk_size']))
typical_ips = min(preset['chunk_size'], total_ips)
hours_per_node = estimate_scan_hours(typical_ips, n_ports, masscan_rate, scan_mode, **kwargs)
total_cost = 0.0
for i in range(n_chunks):
chunk_ips = min(preset['chunk_size'], total_ips - i * preset['chunk_size'])
chunk_hours = estimate_scan_hours(chunk_ips, n_ports, masscan_rate, scan_mode, **kwargs)
provider = providers[i % len(providers)]
instance = DEFAULT_INSTANCE.get(provider, 'g6-standard-2')
total_cost += node_cost(provider, instance, chunk_hours)
rows.append({
'preset': preset_key,
'label': preset['label'],
'desc': preset['desc'],
'n_nodes': n_chunks,
'hours_per_node': hours_per_node,
'total_cost_usd': total_cost,
})
return rows
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Provider selection and configuration utilities
"""
from .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
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
SSH utilities for C2ingRed deployment system
"""
import os
import subprocess
import logging
import re
import glob
from .common import COLORS, generate_random_string
def generate_ssh_key(deployment_id=None):
"""Generate an SSH key for deployment with proper tracking for cleanup"""
# Use deployment_id if provided, otherwise generate random suffix
if not deployment_id:
deployment_id = generate_random_string(6)
ssh_key_path = os.path.expanduser(f"~/.ssh/c2deploy_{deployment_id}")
ssh_key_pub_path = f"{ssh_key_path}.pub"
# Check if key already exists
if os.path.exists(ssh_key_path):
logging.info(f"SSH key already exists at {ssh_key_path}")
return ssh_key_path
try:
# Generate the SSH key
subprocess.run([
"ssh-keygen", "-t", "rsa", "-b", "4096",
"-f", ssh_key_path, "-q", "-N", ""
], check=True)
# Set proper permissions
os.chmod(ssh_key_path, 0o600)
# Track generated keys for cleanup
if not hasattr(generate_ssh_key, 'generated_keys'):
generate_ssh_key.generated_keys = []
generate_ssh_key.generated_keys.append(ssh_key_path)
logging.info(f"Generated SSH key: {ssh_key_path}")
return ssh_key_path
except subprocess.CalledProcessError as e:
logging.error(f"Failed to generate SSH key: {e}")
return None
def get_ssh_public_key(private_key_path):
"""Get the public key content from a private key file"""
public_key_path = f"{private_key_path}.pub"
if not os.path.exists(public_key_path):
logging.error(f"Public key file not found: {public_key_path}")
return None
try:
with open(public_key_path, 'r') as f:
return f.read().strip()
except Exception as e:
logging.error(f"Failed to read public key: {e}")
return None
def extract_attack_box_ip_from_logs(config):
"""Extract attack box IP from deployment logs or Ansible output"""
deployment_id = config.get('deployment_id', 'unknown')
# Check deployment log file first
log_files_to_check = [
f"logs/deployment_{deployment_id}.log",
# Also check archived logs
f"logs/archive/deployment_{deployment_id}.log"
]
# Check for timestamped archived logs
archive_pattern = f"logs/archive/*_deployment_{deployment_id}.log"
archived_logs = glob.glob(archive_pattern)
if archived_logs:
# Get the most recent archived log
log_files_to_check.append(max(archived_logs, key=os.path.getmtime))
for log_file in log_files_to_check:
if os.path.exists(log_file):
try:
with open(log_file, 'r') as f:
content = f.read()
# Look for various IP patterns in the log
ip_patterns = [
r'"attack_box_ip":\s*"([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})"',
r'attack_box_ip.*?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'"ipv4":\s*\["([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})"\]',
r'ansible_host["\s]*=[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'instance_ip["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'Target: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'IP["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
# Look for IP in Ansible task output patterns
r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\s+:\s+ok=',
r'PLAY RECAP.*?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'changed: \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]',
r'ok: \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]'
]
for pattern in ip_patterns:
match = re.search(pattern, content)
if match:
ip = match.group(1)
logging.info(f"Extracted attack box IP from logs: {ip}")
return ip
except Exception as e:
logging.warning(f"Could not read deployment log {log_file}: {e}")
# Check deployment info file
info_file = f"logs/deployment_info_{deployment_id}.txt"
if os.path.exists(info_file):
try:
with open(info_file, 'r') as f:
content = f.read()
# Look for IP in SSH command or other contexts
ip_patterns = [
r'root@([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'Instance IP:\s*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
r'IP["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'
]
for pattern in ip_patterns:
match = re.search(pattern, content)
if match:
ip = match.group(1)
logging.info(f"Extracted attack box IP from deployment info: {ip}")
return ip
except Exception as e:
logging.warning(f"Could not read deployment info: {e}")
logging.warning("Could not extract attack box IP from logs")
return None
def ssh_to_instance(config):
"""SSH into an instance after deployment"""
ssh_key_path = config.get('ssh_key_path', '').replace('.pub', '')
# Handle attack box deployments
if config.get('attack_box_deployment'):
instance_ip = config.get('attack_box_ip')
instance_name = config.get('attack_box_name', 'attack box')
# If no IP stored in config, try to extract from deployment logs
if not instance_ip:
instance_ip = extract_attack_box_ip_from_logs(config)
# Determine which instance to connect to for C2 deployments
elif config.get('c2_only') or (not config.get('redirector_only') and not config.get('deploy_tracker')):
# Connect to C2 server
instance_ip = config.get('c2_ip')
instance_name = config.get('c2_name', 'C2 server')
elif config.get('redirector_only'):
# Connect to redirector
instance_ip = config.get('redirector_ip')
instance_name = config.get('redirector_name', 'redirector')
elif config.get('deploy_tracker') and not config.get('integrated_tracker'):
# Connect to tracker
instance_ip = config.get('tracker_ip')
instance_name = config.get('tracker_name', 'tracker')
else:
# Default to C2 server
instance_ip = config.get('c2_ip')
instance_name = config.get('c2_name', 'C2 server')
if not instance_ip:
print(f"{COLORS['RED']}No instance IP found for SSH connection{COLORS['RESET']}")
return
ssh_user = config.get('ssh_user', 'root')
print(f"{COLORS['GREEN']}Connecting to {instance_name} ({instance_ip})...{COLORS['RESET']}")
ssh_command = [
"ssh",
"-i", ssh_key_path,
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "IdentitiesOnly=yes",
f"{ssh_user}@{instance_ip}"
]
try:
subprocess.run(ssh_command)
except KeyboardInterrupt:
print(f"\n{COLORS['YELLOW']}SSH session ended{COLORS['RESET']}")
except Exception as e:
print(f"{COLORS['RED']}SSH connection failed: {e}{COLORS['RESET']}")
def cleanup_ssh_keys(deployment_id=None, keep_keys=False):
"""Clean up generated SSH keys"""
if keep_keys:
return
if hasattr(generate_ssh_key, 'generated_keys'):
for key_path in generate_ssh_key.generated_keys:
# Only remove keys we generated for this deployment
if deployment_id and f"_{deployment_id}" in key_path:
try:
if os.path.exists(key_path):
os.remove(key_path)
if os.path.exists(f"{key_path}.pub"):
os.remove(f"{key_path}.pub")
logging.info(f"Removed SSH key: {key_path}")
except Exception as e:
logging.error(f"Failed to remove SSH key {key_path}: {e}")