Add Android forensics, IOC threat-intel DB, Compose companion; scrub secrets from configs
This commit is contained in:
@@ -87,7 +87,7 @@ class AgentInterface:
|
||||
self.agent = Agent(
|
||||
llm=self.llm,
|
||||
tools=self.tools,
|
||||
max_steps=20,
|
||||
max_steps=200,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
|
||||
372
modules/android_forensics.py
Normal file
372
modules/android_forensics.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
Autarch Android Forensics — forensic acquisition and IOC scanning for Android devices.
|
||||
"""
|
||||
|
||||
DESCRIPTION = "Android forensic acquisition and spyware IOC scanning"
|
||||
AUTHOR = "AUTARCH"
|
||||
VERSION = "1.0"
|
||||
CATEGORY = "counter"
|
||||
|
||||
|
||||
class AutarchAndroidModule:
|
||||
"""Interactive Android forensics CLI menu."""
|
||||
|
||||
def __init__(self):
|
||||
from core.android_forensics import get_android_forensics_manager
|
||||
from core.hardware import get_hardware_manager
|
||||
self.mgr = get_android_forensics_manager()
|
||||
self.hw = get_hardware_manager()
|
||||
self.serial = None
|
||||
|
||||
def _pick_device(self):
|
||||
devices = self.hw.adb_devices()
|
||||
if not devices:
|
||||
print(" No ADB devices connected.")
|
||||
return None
|
||||
if len(devices) == 1:
|
||||
self.serial = devices[0]['serial']
|
||||
print(f" Selected: {self.serial}")
|
||||
return self.serial
|
||||
print("\n Select device:")
|
||||
for i, d in enumerate(devices, 1):
|
||||
model = d.get('model', '')
|
||||
print(f" {i}) {d['serial']} {model}")
|
||||
try:
|
||||
choice = int(input(" > ").strip())
|
||||
if 1 <= choice <= len(devices):
|
||||
self.serial = devices[choice - 1]['serial']
|
||||
return self.serial
|
||||
except (ValueError, EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
return None
|
||||
|
||||
def _ensure_device(self):
|
||||
if self.serial:
|
||||
return self.serial
|
||||
return self._pick_device()
|
||||
|
||||
def _sev_marker(self, sev):
|
||||
return {'critical': '[!!!]', 'high': '[!! ]', 'medium': '[! ]', 'low': '[ ]'}.get(sev, '[? ]')
|
||||
|
||||
def show_menu(self):
|
||||
status = self.hw.get_status()
|
||||
serial_str = self.serial or 'None selected'
|
||||
stats = self.mgr.get_ioc_stats()
|
||||
counts = stats.get('counts', {})
|
||||
ioc_str = (f"{counts.get('packages', 0)} pkgs, "
|
||||
f"{counts.get('domains', 0)} domains, "
|
||||
f"{counts.get('certs', 0)} certs, "
|
||||
f"{counts.get('hashes', 0)} hashes")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" Autarch Android Forensics")
|
||||
print(f"{'='*60}")
|
||||
print(f" ADB: {'Available' if status.get('adb') else 'Not found'}")
|
||||
print(f" Device: {serial_str}")
|
||||
print(f" IOC DB: {ioc_str}")
|
||||
print()
|
||||
print(" -- Acquisition --")
|
||||
print(" 1) Full Acquisition (all modules)")
|
||||
print(" 2) Package List")
|
||||
print(" 3) Running Processes")
|
||||
print(" 4) System Properties (getprop)")
|
||||
print(" 5) Settings")
|
||||
print(" 6) Services")
|
||||
print(" 7) Dumpsys")
|
||||
print(" 8) Logcat")
|
||||
print(" 9) Temp Files")
|
||||
print()
|
||||
print(" -- IOC Scanning --")
|
||||
print(" 20) Scan Apps vs IOC Database")
|
||||
print(" 21) Scan APK Certificates")
|
||||
print(" 22) Scan Network Indicators")
|
||||
print(" 23) Scan File Artifacts")
|
||||
print(" 24) Scan Running Processes")
|
||||
print(" 25) Full IOC Scan (all)")
|
||||
print()
|
||||
print(" -- Reports --")
|
||||
print(" 30) List Past Acquisitions")
|
||||
print(" 31) View Acquisition Report")
|
||||
print()
|
||||
print(" -- Database --")
|
||||
print(" 40) IOC Database Stats")
|
||||
print(" 41) Reload IOC Database")
|
||||
print()
|
||||
print(" [s] Select Device | [0] Back")
|
||||
print()
|
||||
|
||||
# ── Acquisition ─────────────────────────────────────────────────
|
||||
|
||||
def do_full_acquisition(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Running full acquisition on {self.serial}...")
|
||||
print(" This may take several minutes.")
|
||||
result = self.mgr.full_acquisition(self.serial)
|
||||
summary = result.get('scan_summary', {})
|
||||
modules = result.get('modules', {})
|
||||
print(f"\n Acquisition complete: {result.get('acq_id', '')}")
|
||||
print(f" Modules collected: {len(modules)}")
|
||||
print(f" IOC findings: {summary.get('total_findings', 0)}")
|
||||
print(f" Critical: {summary.get('critical', 0)}")
|
||||
print(f" High: {summary.get('high', 0)}")
|
||||
if not summary.get('clean', True):
|
||||
print("\n Findings:")
|
||||
for f in result.get('ioc_findings', [])[:20]:
|
||||
marker = self._sev_marker(f.get('severity', 'high'))
|
||||
print(f" {marker} [{f.get('type', '?')}] {f.get('threat_name', '?')}")
|
||||
detail = f.get('package') or f.get('process') or f.get('path') or f.get('remote_ip', '')
|
||||
if detail:
|
||||
print(f" {detail}")
|
||||
else:
|
||||
print(" Device appears clean.")
|
||||
|
||||
def do_acquire_module(self, module_name):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
func_map = {
|
||||
'packages': self.mgr.acquire_packages,
|
||||
'processes': self.mgr.acquire_processes,
|
||||
'getprop': self.mgr.acquire_getprop,
|
||||
'settings': self.mgr.acquire_settings,
|
||||
'services': self.mgr.acquire_services,
|
||||
'dumpsys': self.mgr.acquire_dumpsys,
|
||||
'logcat': self.mgr.acquire_logcat,
|
||||
'tmp_files': self.mgr.acquire_tmp_files,
|
||||
}
|
||||
func = func_map.get(module_name)
|
||||
if not func:
|
||||
print(f" Unknown module: {module_name}")
|
||||
return
|
||||
print(f"\n Acquiring {module_name} from {self.serial}...")
|
||||
result = func(self.serial)
|
||||
if not result.get('ok'):
|
||||
print(f" Error: {result.get('error', 'Unknown')}")
|
||||
return
|
||||
if module_name == 'packages':
|
||||
pkgs = result.get('packages', [])
|
||||
print(f" Found {len(pkgs)} packages ({sum(1 for p in pkgs if p.get('system'))} system, "
|
||||
f"{sum(1 for p in pkgs if not p.get('system'))} user)")
|
||||
for p in pkgs[:20]:
|
||||
marker = "[sys]" if p.get('system') else "[usr]"
|
||||
print(f" {marker} {p['package']}")
|
||||
if len(pkgs) > 20:
|
||||
print(f" ... and {len(pkgs) - 20} more")
|
||||
elif module_name == 'getprop':
|
||||
props = result.get('props', {})
|
||||
print(f" {len(props)} properties")
|
||||
for k in ('ro.product.model', 'ro.build.version.release', 'ro.build.fingerprint',
|
||||
'ro.secure', 'ro.debuggable', 'net.dns1', 'net.dns2'):
|
||||
if k in props:
|
||||
print(f" {k} = {props[k]}")
|
||||
else:
|
||||
out = result.get('output', '')
|
||||
lines = out.splitlines()
|
||||
print(f" {len(lines)} lines")
|
||||
for line in lines[:30]:
|
||||
print(f" {line}")
|
||||
if len(lines) > 30:
|
||||
print(f" ... {len(lines) - 30} more lines")
|
||||
|
||||
# ── IOC Scanning ─────────────────────────────────────────────────
|
||||
|
||||
def _print_findings(self, result, scan_name):
|
||||
if not result.get('ok'):
|
||||
print(f" Error: {result.get('error', 'Unknown')}")
|
||||
return
|
||||
findings = result.get('findings', [])
|
||||
note = result.get('note', '')
|
||||
if note:
|
||||
print(f" Note: {note}")
|
||||
if findings:
|
||||
print(f"\n {scan_name}: {len(findings)} finding(s)")
|
||||
for f in findings:
|
||||
marker = self._sev_marker(f.get('severity', 'high'))
|
||||
name = f.get('threat_name', '?')
|
||||
detail = f.get('package') or f.get('process') or f.get('path') or f.get('remote_ip', '')
|
||||
print(f" {marker} {name}")
|
||||
if detail:
|
||||
print(f" {detail}")
|
||||
print(f" Source: {f.get('source', '?')}")
|
||||
else:
|
||||
scanned = result.get('total_scanned', '')
|
||||
scanned_str = f" ({scanned} scanned)" if scanned else ""
|
||||
print(f" {scan_name}: Clean{scanned_str}")
|
||||
|
||||
def do_scan_packages(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Scanning packages on {self.serial}...")
|
||||
self._print_findings(self.mgr.scan_packages(self.serial), "Package Scan")
|
||||
|
||||
def do_scan_certs(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Scanning APK certificates (may take a while)...")
|
||||
self._print_findings(self.mgr.scan_certificates(self.serial), "Certificate Scan")
|
||||
|
||||
def do_scan_network(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Scanning network indicators on {self.serial}...")
|
||||
self._print_findings(self.mgr.scan_network_indicators(self.serial), "Network Scan")
|
||||
|
||||
def do_scan_files(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Scanning file artifacts on {self.serial}...")
|
||||
self._print_findings(self.mgr.scan_file_indicators(self.serial), "File Scan")
|
||||
|
||||
def do_scan_processes(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Scanning running processes on {self.serial}...")
|
||||
self._print_findings(self.mgr.scan_process_indicators(self.serial), "Process Scan")
|
||||
|
||||
def do_full_scan(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
print(f"\n Running full IOC scan on {self.serial}...")
|
||||
all_findings = []
|
||||
for scan_name, func in [
|
||||
("Packages", self.mgr.scan_packages),
|
||||
("Certs", self.mgr.scan_certificates),
|
||||
("Network", self.mgr.scan_network_indicators),
|
||||
("Files", self.mgr.scan_file_indicators),
|
||||
("Processes", self.mgr.scan_process_indicators),
|
||||
]:
|
||||
print(f" Scanning {scan_name}...", end='', flush=True)
|
||||
result = func(self.serial)
|
||||
found = len(result.get('findings', []))
|
||||
print(f" {found} findings")
|
||||
all_findings.extend(result.get('findings', []))
|
||||
|
||||
print(f"\n Total findings: {len(all_findings)}")
|
||||
if all_findings:
|
||||
for f in all_findings:
|
||||
marker = self._sev_marker(f.get('severity', 'high'))
|
||||
detail = f.get('package') or f.get('process') or f.get('path') or f.get('remote_ip', '')
|
||||
print(f" {marker} {f.get('threat_name', '?')} — {detail}")
|
||||
else:
|
||||
print(" Device appears clean.")
|
||||
|
||||
# ── Reports ──────────────────────────────────────────────────────
|
||||
|
||||
def do_list_acquisitions(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
acqs = self.mgr.get_acquisition_list(self.serial)
|
||||
if not acqs:
|
||||
print(f" No acquisitions found for {self.serial}")
|
||||
return
|
||||
print(f"\n Acquisitions for {self.serial}:")
|
||||
for i, a in enumerate(acqs, 1):
|
||||
ts = a.get('timestamp', a['acq_id'])
|
||||
findings = a.get('findings', 0)
|
||||
modules = ', '.join(a.get('modules', []))
|
||||
print(f" {i:2}) {ts} findings={findings} [{modules}]")
|
||||
|
||||
def do_view_report(self):
|
||||
if not self._ensure_device():
|
||||
return
|
||||
acqs = self.mgr.get_acquisition_list(self.serial)
|
||||
if not acqs:
|
||||
print(f" No acquisitions found for {self.serial}")
|
||||
return
|
||||
print(f"\n Select acquisition:")
|
||||
for i, a in enumerate(acqs, 1):
|
||||
print(f" {i}) {a.get('timestamp', a['acq_id'])}")
|
||||
try:
|
||||
choice = int(input(" > ").strip())
|
||||
if 1 <= choice <= len(acqs):
|
||||
result = self.mgr.export_report(self.serial, acqs[choice - 1]['acq_id'])
|
||||
if result.get('ok'):
|
||||
report = result['report']
|
||||
summary = report.get('scan_summary', {})
|
||||
print(f"\n Report: {report.get('acq_id', '')}")
|
||||
print(f" Device: {report.get('serial', '')}")
|
||||
print(f" Time: {report.get('timestamp', '')}")
|
||||
print(f" Total findings: {summary.get('total_findings', 0)}")
|
||||
print(f" Critical: {summary.get('critical', 0)}, High: {summary.get('high', 0)}")
|
||||
for f in report.get('ioc_findings', []):
|
||||
marker = self._sev_marker(f.get('severity', 'high'))
|
||||
detail = f.get('package') or f.get('process') or f.get('path') or ''
|
||||
print(f" {marker} {f.get('threat_name', '?')}: {detail}")
|
||||
else:
|
||||
print(f" Error: {result.get('error')}")
|
||||
except (ValueError, EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
# ── Database ─────────────────────────────────────────────────────
|
||||
|
||||
def do_ioc_stats(self):
|
||||
stats = self.mgr.get_ioc_stats()
|
||||
counts = stats.get('counts', {})
|
||||
sources = stats.get('sources', {})
|
||||
print(f"\n IOC Database Statistics:")
|
||||
print(f" Generated: {stats.get('generated', 'N/A')}")
|
||||
print(f" Packages: {counts.get('packages', 0)}")
|
||||
print(f" Domains: {counts.get('domains', 0)}")
|
||||
print(f" Certificates: {counts.get('certs', 0)}")
|
||||
print(f" File paths: {counts.get('file_paths', 0)}")
|
||||
print(f" Process names: {counts.get('processes', 0)}")
|
||||
print(f" Hashes: {counts.get('hashes', 0)}")
|
||||
print(f" IPs: {counts.get('ips', 0)}")
|
||||
if sources:
|
||||
print(f"\n Sources:")
|
||||
print(f" Stalkerware YAML: {'Yes' if sources.get('stalkerware_ioc_yaml') else 'No'}")
|
||||
print(f" Amnesty campaigns: {sources.get('amnesty_investigations', 0)}")
|
||||
print(f" MVT campaigns: {sources.get('mvt_campaigns', 0)}")
|
||||
print(f" Citizen Lab: {sources.get('citizen_lab_campaigns', 0)}")
|
||||
print(f" YARA rules: {sources.get('yara_rules', 0)}")
|
||||
|
||||
def do_reload_db(self):
|
||||
print(" Reloading IOC database from disk...")
|
||||
db = self.mgr.reload_ioc_database()
|
||||
print(f" Done — {len(db.get('packages', {}))} packages, "
|
||||
f"{len(db.get('domains', {}))} domains loaded")
|
||||
|
||||
# ── Main Loop ─────────────────────────────────────────────────────
|
||||
|
||||
def run_interactive(self):
|
||||
while True:
|
||||
self.show_menu()
|
||||
try:
|
||||
choice = input(" Select > ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if choice == '0':
|
||||
break
|
||||
|
||||
actions = {
|
||||
'1': self.do_full_acquisition,
|
||||
'2': lambda: self.do_acquire_module('packages'),
|
||||
'3': lambda: self.do_acquire_module('processes'),
|
||||
'4': lambda: self.do_acquire_module('getprop'),
|
||||
'5': lambda: self.do_acquire_module('settings'),
|
||||
'6': lambda: self.do_acquire_module('services'),
|
||||
'7': lambda: self.do_acquire_module('dumpsys'),
|
||||
'8': lambda: self.do_acquire_module('logcat'),
|
||||
'9': lambda: self.do_acquire_module('tmp_files'),
|
||||
'20': self.do_scan_packages,
|
||||
'21': self.do_scan_certs,
|
||||
'22': self.do_scan_network,
|
||||
'23': self.do_scan_files,
|
||||
'24': self.do_scan_processes,
|
||||
'25': self.do_full_scan,
|
||||
'30': self.do_list_acquisitions,
|
||||
'31': self.do_view_report,
|
||||
'40': self.do_ioc_stats,
|
||||
'41': self.do_reload_db,
|
||||
's': self._pick_device,
|
||||
}
|
||||
action = actions.get(choice)
|
||||
if action:
|
||||
action()
|
||||
else:
|
||||
print(" Invalid choice.")
|
||||
|
||||
|
||||
def run():
|
||||
m = AutarchAndroidModule()
|
||||
m.run_interactive()
|
||||
@@ -26,12 +26,63 @@ 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.
|
||||
|
||||
@@ -103,11 +154,16 @@ class SnoopDecoder:
|
||||
|
||||
return str(output_path)
|
||||
|
||||
def import_to_database(self, data: dict) -> dict:
|
||||
"""Import decoded Snoop data into AUTARCH sites database.
|
||||
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.
|
||||
@@ -127,50 +183,61 @@ class SnoopDecoder:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Get error type - handle encoding issues in key name
|
||||
# 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
|
||||
|
||||
# Map Snoop error types to detection methods
|
||||
detection_method = 'status'
|
||||
if error_type:
|
||||
if 'message' in str(error_type).lower():
|
||||
detection_method = 'content'
|
||||
elif 'redirect' in str(error_type).lower():
|
||||
detection_method = 'redirect'
|
||||
|
||||
# Get error message pattern
|
||||
error_pattern = None
|
||||
for key in ['errorMsg', 'errorMsg2']:
|
||||
if key in entry and entry[key]:
|
||||
error_pattern = str(entry[key])
|
||||
# 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,
|
||||
'url_main': entry.get('urlMain'),
|
||||
'detection_method': detection_method,
|
||||
'error_pattern': error_pattern,
|
||||
'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: {skipped:,}{Colors.RESET}")
|
||||
print(f"{Colors.DIM} Skipped (no template): {skipped:,}{Colors.RESET}")
|
||||
|
||||
# Add to database
|
||||
# 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: {stats['added']:,}{Colors.RESET}")
|
||||
print(f"{Colors.DIM} Errors: {stats['errors']:,}{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.
|
||||
|
||||
@@ -243,6 +310,9 @@ def display_menu():
|
||||
{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
|
||||
""")
|
||||
|
||||
@@ -323,8 +393,9 @@ def run():
|
||||
if confirm == 'y':
|
||||
# Save first
|
||||
decoder.save_decoded(data, "snoop_imported.json")
|
||||
# Then import
|
||||
# Then import + deduplicate
|
||||
decoder.import_to_database(data)
|
||||
decoder.deduplicate_database()
|
||||
|
||||
# Show final stats
|
||||
db_stats = decoder.sites_db.get_stats()
|
||||
@@ -364,6 +435,7 @@ def run():
|
||||
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}")
|
||||
@@ -388,10 +460,37 @@ def run():
|
||||
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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user