Files
autarch/modules/snoop_decoder.py

500 lines
19 KiB
Python
Raw Normal View History

2026-03-13 15:17:15 -07:00
"""
AUTARCH Snoop Database Decoder Module
Decrypts and imports Snoop Project databases into AUTARCH
"""
import base64
import json
import os
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from core.banner import Colors
from core.sites_db import SitesDatabase
# Module metadata
NAME = "Snoop Decoder"
DESCRIPTION = "Decrypt and import Snoop Project databases"
AUTHOR = "darkHal Security Group"
VERSION = "1.0"
CATEGORY = "osint"
class SnoopDecoder:
"""Decoder for Snoop Project encoded databases."""
# Raw GitHub locations of the Snoop Project site databases.
SNOOP_DB_URLS = {
'BDfull': "https://raw.githubusercontent.com/snooppr/snoop/master/BDfull",
'BDdemo': "https://raw.githubusercontent.com/snooppr/snoop/master/BDdemo",
}
2026-03-13 15:17:15 -07:00
def __init__(self):
self.sites_db = SitesDatabase()
from core.paths import get_data_dir
self.data_dir = get_data_dir() / "sites"
self.data_dir.mkdir(parents=True, exist_ok=True)
def download_database(self, which: str = "BDfull", dest: str = None) -> str:
"""Download a Snoop database from the official GitHub repository.
Args:
which: Which database to fetch ("BDfull" or "BDdemo").
dest: Optional destination path. Defaults to
data/sites/snoop/<which>.
Returns:
Path to the downloaded file, or None on failure.
"""
import urllib.request
import urllib.error
which = 'BDfull' if which.lower() == 'bdfull' else ('BDdemo' if which.lower() == 'bddemo' else which)
url = self.SNOOP_DB_URLS.get(which)
if not url:
print(f"{Colors.RED}[X] Unknown Snoop database: {which}{Colors.RESET}")
return None
if dest is None:
snoop_dir = self.data_dir / "snoop"
snoop_dir.mkdir(parents=True, exist_ok=True)
dest_path = snoop_dir / which
else:
dest_path = Path(dest)
dest_path.parent.mkdir(parents=True, exist_ok=True)
print(f"{Colors.CYAN}[*] Downloading {which} from GitHub...{Colors.RESET}")
print(f"{Colors.DIM} {url}{Colors.RESET}")
try:
req = urllib.request.Request(url, headers={'User-Agent': 'AUTARCH-SnoopDecoder'})
with urllib.request.urlopen(req, timeout=60) as resp:
content = resp.read()
except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e:
print(f"{Colors.RED}[X] Download failed: {e}{Colors.RESET}")
return None
dest_path.write_bytes(content)
size_mb = len(content) / 1024 / 1024
print(f"{Colors.GREEN}[+] Downloaded {size_mb:.2f} MB -> {dest_path}{Colors.RESET}")
return str(dest_path)
2026-03-13 15:17:15 -07:00
def decode_database(self, filepath: str) -> dict:
"""Decode a Snoop database file.
Args:
filepath: Path to the encoded database file (BDdemo, BDfull, etc.)
Returns:
Decoded dictionary of sites.
"""
print(f"{Colors.CYAN}[*] Reading encoded database...{Colors.RESET}")
with open(filepath, 'r', encoding='utf8') as f:
db = f.read().strip()
original_size = len(db)
print(f"{Colors.DIM} Original size: {original_size:,} chars{Colors.RESET}")
# Step 1: Decode base32
print(f"{Colors.CYAN}[*] Decoding base32...{Colors.RESET}")
try:
db_bytes = base64.b32decode(db)
except Exception as e:
print(f"{Colors.RED}[X] Base32 decode failed: {e}{Colors.RESET}")
return None
print(f"{Colors.DIM} After base32: {len(db_bytes):,} bytes{Colors.RESET}")
# Step 2: Reverse bytes
print(f"{Colors.CYAN}[*] Reversing byte order...{Colors.RESET}")
db_bytes = db_bytes[::-1]
# Step 3: Decode UTF-8 with error handling
print(f"{Colors.CYAN}[*] Decoding UTF-8...{Colors.RESET}")
content = db_bytes.decode('utf-8', errors='replace')
# Step 4: Reverse string
print(f"{Colors.CYAN}[*] Reversing string...{Colors.RESET}")
content = content[::-1]
# Step 5: Parse JSON
print(f"{Colors.CYAN}[*] Parsing JSON...{Colors.RESET}")
try:
data = json.loads(content)
except json.JSONDecodeError as e:
print(f"{Colors.RED}[X] JSON parse failed: {e}{Colors.RESET}")
return None
print(f"{Colors.GREEN}[+] Successfully decoded {len(data):,} sites!{Colors.RESET}")
return data
def save_decoded(self, data: dict, output_name: str = "snoop_decoded.json") -> str:
"""Save decoded database to JSON file.
Args:
data: Decoded site dictionary.
output_name: Output filename.
Returns:
Path to saved file.
"""
output_path = self.data_dir / output_name
with open(output_path, 'w', encoding='utf8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
size_mb = output_path.stat().st_size / 1024 / 1024
print(f"{Colors.GREEN}[+] Saved to: {output_path}{Colors.RESET}")
print(f"{Colors.DIM} File size: {size_mb:.2f} MB{Colors.RESET}")
return str(output_path)
def import_to_database(self, data: dict, source: str = 'snoop') -> dict:
"""Import decoded Snoop data into the AUTARCH sites database.
Only sites whose name isn't already in the database are added; the
caller should run deduplicate() afterwards to collapse any URL-level
duplicates against existing entries.
2026-03-13 15:17:15 -07:00
Args:
data: Decoded site dictionary.
source: Source label stored with each imported site.
2026-03-13 15:17:15 -07:00
Returns:
Import statistics.
"""
print(f"\n{Colors.CYAN}[*] Importing to AUTARCH database...{Colors.RESET}")
sites_to_add = []
skipped = 0
for name, entry in data.items():
if not isinstance(entry, dict):
skipped += 1
continue
url = entry.get('url', '')
if not url or '{}' not in url:
skipped += 1
continue
# Snoop's errorType key name is byte-corrupted in the dumps
# ("errorTyp" + garbage), so match on the prefix. Values line up
# with the sites DB error_type vocabulary directly
# (status_code / message / redirection / response_url).
2026-03-13 15:17:15 -07:00
error_type = None
for key in entry.keys():
if 'errorTyp' in key or 'errortype' in key.lower():
error_type = entry[key]
break
# First non-empty error message = the "not found" signature string.
error_string = None
for key in ('errorMsg', 'errorMsg2', 'errorMsg3'):
if entry.get(key):
error_string = str(entry[key])
2026-03-13 15:17:15 -07:00
break
# bad_site is a non-empty string when Snoop flags a site as
# broken/unreliable — import it but leave it disabled.
bad = bool(str(entry.get('bad_site', '')).strip())
2026-03-13 15:17:15 -07:00
sites_to_add.append({
'name': name,
'url_template': url,
'category': 'other',
'source': source,
2026-03-13 15:17:15 -07:00
'nsfw': 0,
'enabled': 0 if bad else 1,
'error_type': error_type,
'error_string': error_string,
2026-03-13 15:17:15 -07:00
})
print(f"{Colors.DIM} Valid sites: {len(sites_to_add):,}{Colors.RESET}")
print(f"{Colors.DIM} Skipped (no template): {skipped:,}{Colors.RESET}")
2026-03-13 15:17:15 -07:00
# Add to database (new names only; existing names are left untouched)
2026-03-13 15:17:15 -07:00
stats = self.sites_db.add_sites_bulk(sites_to_add)
print(f"{Colors.GREEN}[+] Import complete!{Colors.RESET}")
print(f"{Colors.DIM} Added (new): {stats['added']:,}{Colors.RESET}")
print(f"{Colors.DIM} Already present: {stats['skipped']:,}{Colors.RESET}")
print(f"{Colors.DIM} Errors: {stats['errors']:,}{Colors.RESET}")
2026-03-13 15:17:15 -07:00
return stats
def deduplicate_database(self) -> dict:
"""Collapse duplicate URL templates in the sites database."""
print(f"\n{Colors.CYAN}[*] Deduplicating sites database...{Colors.RESET}")
stats = self.sites_db.deduplicate()
print(f"{Colors.GREEN}[+] Deduplication complete!{Colors.RESET}")
print(f"{Colors.DIM} Duplicate groups: {stats['duplicate_groups']:,}{Colors.RESET}")
print(f"{Colors.DIM} Rows removed: {stats['removed']:,}{Colors.RESET}")
print(f"{Colors.DIM} Sites remaining: {stats['remaining']:,}{Colors.RESET}")
return stats
2026-03-13 15:17:15 -07:00
def show_sample(self, data: dict, count: int = 10):
"""Display sample sites from decoded database.
Args:
data: Decoded site dictionary.
count: Number of samples to show.
"""
print(f"\n{Colors.CYAN}Sample Sites ({count}):{Colors.RESET}")
print("-" * 60)
for i, (name, info) in enumerate(list(data.items())[:count]):
url = info.get('url', 'N/A')
country = info.get('country', '')
print(f" {country} {Colors.GREEN}{name}{Colors.RESET}")
print(f" {Colors.DIM}{url[:55]}...{Colors.RESET}" if len(url) > 55 else f" {Colors.DIM}{url}{Colors.RESET}")
def get_stats(self, data: dict) -> dict:
"""Get statistics about decoded database.
Args:
data: Decoded site dictionary.
Returns:
Statistics dictionary.
"""
stats = {
'total_sites': len(data),
'by_country': {},
'detection_methods': {'status_code': 0, 'message': 0, 'redirection': 0, 'other': 0},
}
for name, info in data.items():
# Country stats
country = info.get('country_klas', 'Unknown')
stats['by_country'][country] = stats['by_country'].get(country, 0) + 1
# Detection method stats
error_type = None
for key in info.keys():
if 'errorTyp' in key:
error_type = str(info[key]).lower()
break
if error_type:
if 'status' in error_type:
stats['detection_methods']['status_code'] += 1
elif 'message' in error_type:
stats['detection_methods']['message'] += 1
elif 'redirect' in error_type:
stats['detection_methods']['redirection'] += 1
else:
stats['detection_methods']['other'] += 1
else:
stats['detection_methods']['other'] += 1
return stats
def display_menu():
"""Display the Snoop Decoder menu."""
print(f"""
{Colors.CYAN} Snoop Database Decoder{Colors.RESET}
{Colors.DIM} Decrypt and import Snoop Project databases{Colors.RESET}
{Colors.DIM}{'' * 50}{Colors.RESET}
{Colors.GREEN}[1]{Colors.RESET} Decode Snoop Database File
{Colors.GREEN}[2]{Colors.RESET} Decode & Import to AUTARCH
{Colors.GREEN}[3]{Colors.RESET} View Current Sites Database Stats
{Colors.GREEN}[4]{Colors.RESET} Quick Import (BDfull from snoop-master)
{Colors.GREEN}[5]{Colors.RESET} Quick Import (BDdemo from snoop-master)
{Colors.GREEN}[6]{Colors.RESET} Download Latest BDfull from GitHub + Import + Dedup
{Colors.GREEN}[7]{Colors.RESET} Deduplicate Sites Database
2026-03-13 15:17:15 -07:00
{Colors.RED}[0]{Colors.RESET} Back to OSINT Menu
""")
def get_file_path() -> str:
"""Prompt user for file path."""
print(f"\n{Colors.CYAN}Enter path to Snoop database file:{Colors.RESET}")
print(f"{Colors.DIM}(e.g., /path/to/BDfull or /path/to/BDdemo){Colors.RESET}")
filepath = input(f"\n{Colors.GREEN}Path: {Colors.RESET}").strip()
if not filepath:
return None
if not os.path.exists(filepath):
print(f"{Colors.RED}[X] File not found: {filepath}{Colors.RESET}")
return None
return filepath
def run():
"""Main entry point for the module."""
decoder = SnoopDecoder()
# Common paths for Snoop databases
from core.paths import get_app_dir, get_data_dir
_app = get_app_dir()
_data = get_data_dir()
snoop_paths = {
'bdfull': _app / "snoop" / "snoop-master" / "BDfull",
'bddemo': _app / "snoop" / "snoop-master" / "BDdemo",
'bdfull_alt': _data / "snoop" / "BDfull",
'bddemo_alt': _data / "snoop" / "BDdemo",
}
while True:
display_menu()
choice = input(f"{Colors.GREEN}Select option: {Colors.RESET}").strip()
if choice == '0':
break
elif choice == '1':
# Decode only
filepath = get_file_path()
if not filepath:
continue
data = decoder.decode_database(filepath)
if data:
decoder.show_sample(data)
stats = decoder.get_stats(data)
print(f"\n{Colors.CYAN}Database Statistics:{Colors.RESET}")
print(f" Total sites: {stats['total_sites']:,}")
print(f" Detection methods: {stats['detection_methods']}")
print(f" Top countries: {dict(sorted(stats['by_country'].items(), key=lambda x: -x[1])[:10])}")
# Ask to save
save = input(f"\n{Colors.YELLOW}Save decoded JSON? (y/n): {Colors.RESET}").strip().lower()
if save == 'y':
name = input(f"{Colors.GREEN}Output filename [snoop_decoded.json]: {Colors.RESET}").strip()
decoder.save_decoded(data, name if name else "snoop_decoded.json")
elif choice == '2':
# Decode and import
filepath = get_file_path()
if not filepath:
continue
data = decoder.decode_database(filepath)
if data:
decoder.show_sample(data, 5)
confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites to AUTARCH? (y/n): {Colors.RESET}").strip().lower()
if confirm == 'y':
# Save first
decoder.save_decoded(data, "snoop_imported.json")
# Then import + deduplicate
2026-03-13 15:17:15 -07:00
decoder.import_to_database(data)
decoder.deduplicate_database()
2026-03-13 15:17:15 -07:00
# Show final stats
db_stats = decoder.sites_db.get_stats()
print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
elif choice == '3':
# View current stats
stats = decoder.sites_db.get_stats()
print(f"\n{Colors.CYAN}AUTARCH Sites Database:{Colors.RESET}")
print(f" Total sites: {stats['total_sites']:,}")
print(f" NSFW sites: {stats['nsfw_sites']:,}")
print(f" Database size: {stats['db_size_mb']:.2f} MB")
print(f"\n {Colors.CYAN}By Source:{Colors.RESET}")
for source, count in sorted(stats['by_source'].items(), key=lambda x: -x[1]):
print(f" {source}: {count:,}")
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == '4':
# Quick import BDfull
bdpath = None
for key in ['bdfull', 'bdfull_alt']:
if snoop_paths[key].exists():
bdpath = str(snoop_paths[key])
break
if not bdpath:
print(f"{Colors.RED}[X] BDfull not found in known locations{Colors.RESET}")
print(f"{Colors.DIM} Checked: {snoop_paths['bdfull']}{Colors.RESET}")
print(f"{Colors.DIM} Checked: {snoop_paths['bdfull_alt']}{Colors.RESET}")
continue
print(f"{Colors.GREEN}[+] Found BDfull: {bdpath}{Colors.RESET}")
data = decoder.decode_database(bdpath)
if data:
confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites? (y/n): {Colors.RESET}").strip().lower()
if confirm == 'y':
decoder.save_decoded(data, "snoop_full.json")
decoder.import_to_database(data)
decoder.deduplicate_database()
2026-03-13 15:17:15 -07:00
db_stats = decoder.sites_db.get_stats()
print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
elif choice == '5':
# Quick import BDdemo
bdpath = None
for key in ['bddemo', 'bddemo_alt']:
if snoop_paths[key].exists():
bdpath = str(snoop_paths[key])
break
if not bdpath:
print(f"{Colors.RED}[X] BDdemo not found in known locations{Colors.RESET}")
continue
print(f"{Colors.GREEN}[+] Found BDdemo: {bdpath}{Colors.RESET}")
data = decoder.decode_database(bdpath)
if data:
confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites? (y/n): {Colors.RESET}").strip().lower()
if confirm == 'y':
decoder.save_decoded(data, "snoop_demo.json")
decoder.import_to_database(data)
decoder.deduplicate_database()
db_stats = decoder.sites_db.get_stats()
print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
elif choice == '6':
# Download latest BDfull from GitHub, decode, import, deduplicate
bdpath = decoder.download_database("BDfull")
if not bdpath:
continue
data = decoder.decode_database(bdpath)
if data:
decoder.show_sample(data, 5)
confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites to AUTARCH? (y/n): {Colors.RESET}").strip().lower()
if confirm == 'y':
decoder.save_decoded(data, "snoop_full.json")
decoder.import_to_database(data)
decoder.deduplicate_database()
2026-03-13 15:17:15 -07:00
db_stats = decoder.sites_db.get_stats()
print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
elif choice == '7':
# Deduplicate the sites database
decoder.deduplicate_database()
db_stats = decoder.sites_db.get_stats()
print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
2026-03-13 15:17:15 -07:00
else:
print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")
if __name__ == "__main__":
run()