Files
autarch/modules/snoop_decoder.py

500 lines
19 KiB
Python

"""
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",
}
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)
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.
Args:
data: Decoded site dictionary.
source: Source label stored with each imported site.
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).
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])
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())
sites_to_add.append({
'name': name,
'url_template': url,
'category': 'other',
'source': source,
'nsfw': 0,
'enabled': 0 if bad else 1,
'error_type': error_type,
'error_string': error_string,
})
print(f"{Colors.DIM} Valid sites: {len(sites_to_add):,}{Colors.RESET}")
print(f"{Colors.DIM} Skipped (no template): {skipped:,}{Colors.RESET}")
# Add to database (new names only; existing names are left untouched)
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}")
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
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
{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
decoder.import_to_database(data)
decoder.deduplicate_database()
# 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()
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()
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}")
else:
print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")
if __name__ == "__main__":
run()