1d95a868a0
- 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
921 lines
34 KiB
Python
Executable File
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()
|