Files
n0mad1k 1d95a868a0 Fix 11 bugs found in full codebase review
- deploy_phishing.py: undefined extra_vars on orchestration playbook
- freebird/main_menu.py: set_variable() called as function, is a method; fix to config.prompt_set_variable()
- freebird/tool_manager.py: shell=True with f-string paths; replace with direct pip binary call
- certipy-enum.py: password exposed in process list and logs; pass via env var
- cleanup_engine.py: load_vars_file() called with path string, expects provider name
- ioc-u.py: scapy import not guarded; tool crashed if scapy missing
- um_ops.py + um-vault.py: SQL table names f-stringed directly; quote identifiers
- ops-logger.sh: 63-line duplicate function block; removed second copy; quote config_script var
- recon_tools.py: ir-sandbox + link-sandbox not in TOOLS dict; forensics menu option 5 hidden from user
- um-crack.py: missing --scope flag inconsistent with all other Umbra tools
- heartbeat_ingest.py: openssl subprocess not catching FileNotFoundError; wrap with clear error
2026-04-01 22:11:01 -04:00

921 lines
34 KiB
Python
Executable File

#!/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()