From 59ff9a2a0e54a31b22c523ca0c8548ed944028b0 Mon Sep 17 00:00:00 2001 From: Dhruv Amoldeep Shinde <172916227+DkHrR@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:09:08 +0530 Subject: [PATCH 1/4] Enhance deep username research and real-time scan timers --- .gitignore | 53 +++ README.md | 14 +- phomber.py | 942 ++++++++++++++++++++++++++++++++++++++++++----- pyproject.toml | 7 +- requirements.txt | 8 + 5 files changed, 921 insertions(+), 103 deletions(-) create mode 100644 .gitignore create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5698378a --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# History files +.ph0mber_history +.phomber_history + +# Config files with API keys (sensitive) +config.ini +*.apikey +.apikeys + +# Environment +venv/ +env/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Test/temp +*.tmp +*.temp +test_output/ diff --git a/README.md b/README.md index f4994394..fef0fb07 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ An open source infomation grathering & reconnaissance framework! ``` git clone https://github.com/s41r4j/phomber cd phomber -pip3 install -r pyproject.toml +pip3 install -r requirements.txt ``` - __pip__ @@ -152,9 +152,19 @@ phomber ``` - `PH0MBER` is back with all new features and user interface -- `v3` has osint tools with no _api key_ requirement +- `number` scan supports API-first mode if `PHOMBER_NUMLOOKUP_API_KEY` (or `NUMLOOKUP_API_KEY`) is set +- if API is unavailable, `number` automatically falls back to the local basic scan +- all scanners now display estimated and real-time research duration +- `username` scan now performs deep profile research across direct platform profiles and best-effort public identity extraction +- tune depth via `PHOMBER_USERNAME_IDENTITY_SOURCES` (default: `20`) +- tune per-profile fetch timeout via `PHOMBER_PROFILE_FETCH_TIMEOUT` (default: `8` seconds) - Release windows (exe) & linux (lsb) direct executables, no python req. - `v4` will have web-interface + automated scans features + (custom-scanner feature; create, distribute and deploy your own scanner) ``` +### Credits + +- Core author: `@s41r4j` +- Contributor: `@DkHrR` +

diff --git a/phomber.py b/phomber.py index 7126baa3..56a5af46 100644 --- a/phomber.py +++ b/phomber.py @@ -34,11 +34,15 @@ import sys # Default import random # Default import time # Default +import threading # Default +import json # Default import re # Default import uuid # Default import socket # Default import platform # Default import getpass # Default +import ipaddress # Default (for IP validation) +from urllib.parse import quote_plus import psutil # pip install psutil import bs4 # pip install beautifulsoup4 import phonenumbers # pip install phonenumbers @@ -54,12 +58,178 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +# ------------------------ Utility functions ------------------------ # +def pad_string(value, width): + """Safely pad a string to a given width without crashing on overflow.""" + value_str = str(value) + padding = max(0, width - len(value_str)) + return value_str + ' ' * padding + + +def is_valid_ipv4(ip_address): + """Validate IPv4 addresses properly (rejects 999.999.999.999, etc.).""" + try: + ipaddress.IPv4Address(ip_address) + return True + except (ipaddress.AddressValueError, ValueError): + return False + + +def normalize_scan_target(raw_value): + """Normalize scanner target values and allow optional `check` keyword.""" + value = raw_value.strip() + if value.lower().startswith('check '): + value = value[6:].strip() + elif value.lower() == 'check': + value = '' + return value + + +def get_research_time_estimate(scanner_name): + """Return estimated duration range for each scanner.""" + estimate_map = { + 'number': '5-15 seconds', + 'ip': '3-12 seconds', + 'mac': '1-5 seconds', + 'whois': '4-20 seconds', + 'dns': '2-10 seconds', + 'username': '20-300 seconds', + } + return estimate_map.get(scanner_name, '3-30 seconds') + + +def get_research_time_estimate_seconds(scanner_name): + """Return average estimated duration (seconds) for live remaining-time updates.""" + estimate_seconds_map = { + 'number': 10, + 'ip': 8, + 'mac': 3, + 'whois': 12, + 'dns': 6, + 'username': 160, + } + return float(estimate_seconds_map.get(scanner_name, 16)) + + +def _can_render_live_progress(): + """Return True when stdout supports TTY-style live progress rendering.""" + try: + return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() + except Exception: + return False + + +def _clear_live_progress_line(): + """Clear the current live progress line if one is rendered.""" + if not _can_render_live_progress(): + return + sys.stdout.write('\r' + (' ' * 140) + '\r') + sys.stdout.flush() + + +def _start_live_scan_progress(scanner_name, started_at, estimate_seconds): + """Start a background ticker that updates elapsed and estimated remaining time.""" + if not _can_render_live_progress(): + return None + + stop_event = threading.Event() + + def _worker(): + while not stop_event.wait(1.0): + elapsed_seconds = max(0.0, time.perf_counter() - started_at) + remaining_seconds = max(0.0, estimate_seconds - elapsed_seconds) + status_line = ( + f' [+] {scanner_name} scan running | ' + f'elapsed: {elapsed_seconds:6.1f}s | ' + f'est remaining: {remaining_seconds:6.1f}s' + ) + sys.stdout.write('\r\33[1;49;93m' + status_line.ljust(140) + '\033[0m') + sys.stdout.flush() + + _clear_live_progress_line() + + timer_thread = threading.Thread(target=_worker, daemon=True) + timer_thread.start() + + return { + 'stop_event': stop_event, + 'thread': timer_thread, + 'estimate_seconds': estimate_seconds, + } + + +def announce_scan_timing(scanner_name, started_at=None): + """Print estimate and start real-time remaining-time updates for this scanner.""" + estimate_text = get_research_time_estimate(scanner_name) + estimate_seconds = get_research_time_estimate_seconds(scanner_name) + printit(f' [+] Estimated research time: {estimate_text} (real-time)', coledt=[1, 49, 93], space_down=True) + + previous_state = _active_scan_timers.pop(scanner_name, None) + if previous_state: + previous_state['stop_event'].set() + previous_state['thread'].join(timeout=0.3) + _clear_live_progress_line() + + if started_at is None: + started_at = time.perf_counter() + + live_timer_state = _start_live_scan_progress(scanner_name, started_at, estimate_seconds) + if live_timer_state: + _active_scan_timers[scanner_name] = live_timer_state + + return estimate_text + + +def complete_scan_timing(scanner_name, started_at, result): + """Stop live timer updates, print final timing summary, and return result.""" + timer_state = _active_scan_timers.pop(scanner_name, None) + estimate_seconds = get_research_time_estimate_seconds(scanner_name) + if timer_state: + estimate_seconds = float(timer_state.get('estimate_seconds', estimate_seconds)) + timer_state['stop_event'].set() + timer_state['thread'].join(timeout=0.3) + _clear_live_progress_line() + + elapsed_seconds = max(0.0, time.perf_counter() - started_at) + variance_seconds = elapsed_seconds - estimate_seconds + printit( + f' [+] Real-time research time ({scanner_name}): {elapsed_seconds:.2f}s | estimated remaining: 0.00s | variance: {variance_seconds:+.2f}s', + coledt=[1, 49, 96], + space_down=True, + ) + return result + + # ------------------------ Global variables ------------------------ # try: session = PromptSession(history=FileHistory('./.ph0mber_history')) except: session = PromptSession(history=InMemoryHistory()) available_commands = ['change', 'check', 'clear', 'dns', 'dork', 'email', 'exit', 'exp', 'hash','help', 'info', 'ip', 'mac', 'number', 'quit', 'save', 'shell', 'username', 'whois'] prv_op = '' full_cmd = '' +PHOMBER_NUMLOOKUP_API_KEY = ( + os.environ.get('PHOMBER_NUMLOOKUP_API_KEY', '').strip() + or os.environ.get('NUMLOOKUP_API_KEY', '').strip() +) +PHOMBER_SHERLOCK_DATA_URL = ( + os.environ.get('PHOMBER_SHERLOCK_DATA_URL', '').strip() + or 'https://raw.githubusercontent.com/sherlock-project/sherlock/master/sherlock_project/resources/data.json' +) +try: + PHOMBER_USERNAME_SITE_LIMIT = int(os.environ.get('PHOMBER_USERNAME_SITE_LIMIT', '80')) +except (TypeError, ValueError): + PHOMBER_USERNAME_SITE_LIMIT = 80 +try: + PHOMBER_USERNAME_IDENTITY_SOURCES = int(os.environ.get('PHOMBER_USERNAME_IDENTITY_SOURCES', '20')) +except (TypeError, ValueError): + PHOMBER_USERNAME_IDENTITY_SOURCES = 20 +if PHOMBER_USERNAME_IDENTITY_SOURCES < 1: + PHOMBER_USERNAME_IDENTITY_SOURCES = 1 +try: + PHOMBER_PROFILE_FETCH_TIMEOUT = float(os.environ.get('PHOMBER_PROFILE_FETCH_TIMEOUT', '8')) +except (TypeError, ValueError): + PHOMBER_PROFILE_FETCH_TIMEOUT = 8.0 +_sherlock_site_cache = None +_active_scan_timers = {} silent_mode = False @@ -86,6 +256,12 @@ def number_lookup(phone_number): # Global variables global prv_op prv_op = '' # Resetting previous output + phone_number = normalize_scan_target(phone_number) + started_at = time.perf_counter() + announce_scan_timing('number', started_at) + + def finalize_scan(result): + return complete_scan_timing('number', started_at, result) # Information gathering about the number printit(f' [+] Grathering information about \33[1;49;96m{phone_number}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) @@ -94,7 +270,62 @@ def number_lookup(phone_number): # Check if the number is valid or not (regex) if not re.match(r'^\+[1-9]\d{1,14}$', phone_number): printit(' > Invalid phone number format! (Example: +44xxxxxxxxxx)', coledt=[1, 49, 91]) - return False + return finalize_scan(False) + + # API-first lookup (Numlookup), fallback to local basic scanner. + if PHOMBER_NUMLOOKUP_API_KEY: + if check_connection(): + headers = { + 'apikey': PHOMBER_NUMLOOKUP_API_KEY, + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0' + } + try: + response = requests.get( + f'https://api.numlookupapi.com/v1/validate/{phone_number}', + headers=headers, + timeout=15, + ) + if response.status_code == 200: + response_json = response.json() + + valid = bool(response_json.get('valid', False)) + country_code = pad_string(str(response_json.get('country_code') or response_json.get('country_prefix') or 'N/A'), 30) + country = pad_string(str(response_json.get('country_name') or response_json.get('country') or 'N/A'), 30) + location = pad_string(str(response_json.get('location') or response_json.get('region') or 'N/A'), 30) + provider = pad_string(str(response_json.get('carrier') or response_json.get('operator') or 'N/A'), 30) + line_type = pad_string(str(response_json.get('line_type') or response_json.get('type') or 'N/A'), 30) + scanned_phone = pad_string(str(phone_number), 30) + international = pad_string(str(response_json.get('international_format') or phone_number), 30) + national = pad_string(str(response_json.get('local_format') or response_json.get('national_format') or 'N/A'), 30) + valid_text = pad_string(str(valid), 30) + + final_output = f''' + ┌──────────────────────────────────────────────────────┐ + | Scanned Phone Number: \33[1;49;96m{scanned_phone}\033[0m | + |------------------------------------------------------| + | INFORMATION | DESCRIPTION | + |------------------------------------------------------| + | \33[1;49;97mValid\033[0m | {valid_text} | + |------------------------------------------------------| + | \33[1;49;97mCountry Code\033[0m | {country_code} | + | \33[1;49;97mCountry\033[0m | {country} | + | \33[1;49;97mLocation\033[0m | {location} | + | \33[1;49;97mCarrier\033[0m | {provider} | + | \33[1;49;97mLine Type\033[0m | {line_type} | + |------------------------------------------------------| + | \33[1;49;97mInternational Format\033[0m| {international} | + | \33[1;49;97mNational Format\033[0m | {national} | + |------------------------------------------------------| + | \33[1;49;97mSource\033[0m | Numlookup API | + └──────────────────────────────────────────────────────┘ + ''' + print(final_output) + prv_op += final_output+'\n' + return finalize_scan(valid) + except (requests.RequestException, ValueError, TypeError): + pass + + printit(' > API lookup unavailable, using basic local lookup...', coledt=[1, 49, 93], space_down=True) # Country code check try: @@ -102,7 +333,7 @@ def number_lookup(phone_number): phone_number_details = phonenumbers.parse(phone_number) except phonenumbers.phonenumberutil.NumberParseException: printit(' > Missing Country Code! (Example: +91, +44, +1)', coledt=[1, 49, 91]) - return False + return finalize_scan(False) # extract country_code & only_number from "Country Code: xx National Number: xxxxxxxxxx" country_code = str(phone_number_details).split('Country Code:', 1)[1].split('National Number:', 1)[0].strip() @@ -218,7 +449,7 @@ def number_lookup(phone_number): \033[0m''' print(social_media_platforms) prv_op += social_media_platforms+'\n' - return True + return finalize_scan(True) else: # Reconfiguring variables @@ -238,37 +469,101 @@ def number_lookup(phone_number): ''' print(final_output) prv_op += final_output+'\n' - return False + return finalize_scan(False) # Reverse ip address lookup def ip_lookup(ip_address): # Global variables global prv_op prv_op = '' # Resetting previous output + ip_address = normalize_scan_target(ip_address) + started_at = time.perf_counter() + announce_scan_timing('ip', started_at) + + def finalize_scan(result): + return complete_scan_timing('ip', started_at, result) # Checking internet connection printit(f' [+] Verifying internet connection...', coledt=[1, 49, 93], space_down=True) if not check_connection(): printit(' > Internet connection not available!', coledt=[1, 49, 91], space_down=True) - return False + return finalize_scan(False) # Information gathering about the ip address printit(f' [+] Grathering information about \33[1;49;96m{ip_address}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) prv_op += f' \33[1;49;93m[#] Reverse ip address lookup for \33[1;49;96m{ip_address}\33[1;49;93m:\033[0m'+'\n\n' # Check if the ip address is valid or not (regex) [add IPv6 support] - if not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip_address) : + if not is_valid_ipv4(ip_address): printit(' > Invalid ip address format! (Example: 8.8.8.8)', coledt=[1, 49, 91]) - return False + return finalize_scan(False) # IP address lookup APIs - ip_addresses = [f'http://ip-api.com/json/{ip_address}', f'https://ipapi.co/{ip_address}/json/', f'http://ipwho.is/{ip_address}'] + ip_addresses = [f'https://ip-api.com/json/{ip_address}', f'https://ipapi.co/{ip_address}/json/', f'https://ipwho.is/{ip_address}'] ip_address = str(ip_address)+' '*int(30-len(str(ip_address))) # Headers headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0' } + + # Optional token-based source (if PHOMBER_IPINFO_TOKEN/IPINFO_TOKEN is configured). + ipinfo_token = os.environ.get('PHOMBER_IPINFO_TOKEN', '').strip() or os.environ.get('IPINFO_TOKEN', '').strip() + if ipinfo_token: + try: + response = requests.get( + f'https://ipinfo.io/{normalize_scan_target(ip_address)}/json', + params={'token': ipinfo_token}, + timeout=10, + headers=headers, + ) + response_json = response.json() + + if response.status_code == 200 and 'error' not in response_json and 'bogon' not in response_json: + ip = str(response_json.get('ip', 'N/A')) + city = str(response_json.get('city', 'N/A')) + region = str(response_json.get('region', 'N/A')) + country = str(response_json.get('country', 'N/A')) + timezone = str(response_json.get('timezone', 'N/A')) + postal = str(response_json.get('postal', 'N/A')) + org = str(response_json.get('org', 'N/A')) + hostname = str(response_json.get('hostname', 'N/A')) + loc = str(response_json.get('loc', 'N/A')) + + ip = str(ip)+' '*int(30-len(str(ip))) + city = str(city)+' '*int(30-len(str(city))) + region = str(region)+' '*int(30-len(str(region))) + country = str(country)+' '*int(30-len(str(country))) + timezone = str(timezone)+' '*int(30-len(str(timezone))) + postal = str(postal)+' '*int(30-len(str(postal))) + org = str(org)+' '*int(30-len(str(org))) + hostname = str(hostname)+' '*int(30-len(str(hostname))) + loc = str(loc)+' '*int(30-len(str(loc))) + + final_output = f''' + ┌──────────────────────────────────────────────────────┐ + | Scanned IP Address: \33[1;49;96m{ip_address}\033[0m | + |------------------------------------------------------| + | INFORMATION | DESCRIPTION | + |------------------------------------------------------| + | \33[1;49;97mIP\033[0m | {ip} | + | \33[1;49;97mCountry\033[0m | {country} | + | \33[1;49;97mRegion\033[0m | {region} | + | \33[1;49;97mCity\033[0m | {city} | + |------------------------------------------------------| + | \33[1;49;97mPostal Code\033[0m | {postal} | + | \33[1;49;97mTimezone\033[0m | {timezone} | + | \33[1;49;97mCoordinates\033[0m | {loc} | + |------------------------------------------------------| + | \33[1;49;97mOrg\033[0m | {org} | + | \33[1;49;97mHostname\033[0m | {hostname} | + └──────────────────────────────────────────────────────┘ + ''' + print(final_output) + prv_op += final_output+'\n' + return finalize_scan(True) + except (requests.RequestException, ValueError, KeyError, AttributeError, IndexError, TypeError): + pass # Randomizing ip address lookup APIs random_api = random.choice([0, 1, 2]) @@ -458,7 +753,7 @@ def ip_lookup(ip_address): print(final_output) prv_op += final_output+'\n' - except: + except (requests.RequestException, ValueError, KeyError, AttributeError, IndexError, TypeError) as e: final_output = f''' ┌──────────────────────────────────────────────────────┐ | Scanned IP Address: \33[1;49;96m{ip_address}\033[0m | @@ -583,7 +878,7 @@ def ip_lookup(ip_address): print(final_output) prv_op += final_output+'\n' - except: + except (requests.RequestException, ValueError, KeyError, AttributeError, IndexError, TypeError) as e: final_output = f''' ┌──────────────────────────────────────────────────────┐ | Scanned IP Address: \33[1;49;96m{ip_address}\033[0m | @@ -594,7 +889,7 @@ def ip_lookup(ip_address): print(final_output) prv_op += final_output+'\n' - return True + return finalize_scan(True) # Reverse mac address lookup def mac_lookup(mac_address): @@ -603,6 +898,11 @@ def mac_lookup(mac_address): # Global variables global prv_op prv_op = '' # Resetting previous output + started_at = time.perf_counter() + announce_scan_timing('mac', started_at) + + def finalize_scan(result): + return complete_scan_timing('mac', started_at, result) # Information gathering about the mac address printit(f' [+] Grathering information about \33[1;49;96m{mac_address}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) @@ -611,7 +911,7 @@ def mac_lookup(mac_address): # Check if the mac address is valid or not (regex) if not re.match(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', mac_address) : printit(' > Invalid mac address format! (Example: 00:00:00:00:00:00)', coledt=[1, 49, 91]) - return False + return finalize_scan(False) # Getting mac address vendor try: @@ -638,19 +938,24 @@ def mac_lookup(mac_address): print(final_output) prv_op += final_output+'\n' - return bol_val + return finalize_scan(bol_val) # Reverse whois lookup def whois_lookup(domain): # Global variables global prv_op prv_op = '' # Resetting previous output + started_at = time.perf_counter() + announce_scan_timing('whois', started_at) + + def finalize_scan(result): + return complete_scan_timing('whois', started_at, result) # Checking internet connection printit(f' [+] Verifying internet connection...', coledt=[1, 49, 93], space_down=True) if not check_connection(): printit(' > Internet connection not available!', coledt=[1, 49, 91], space_down=True) - return False + return finalize_scan(False) # Information gathering about the domain printit(f' [+] Grathering information about \33[1;49;96m{domain}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) @@ -659,7 +964,7 @@ def whois_lookup(domain): # Domain validation (regex) if not re.match(r'^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$', domain) : printit(' > Invalid domain format! (Example: example.com)', coledt=[1, 49, 91]) - return False + return finalize_scan(False) # Getting whois information try: @@ -713,19 +1018,24 @@ def whois_lookup(domain): print(final_output) prv_op += final_output+'\n' - return bol_val + return finalize_scan(bol_val) # Reverse domain lookup def dns_lookup(ip_or_domain): # Global variables global prv_op prv_op = '' # Resetting previous output + started_at = time.perf_counter() + announce_scan_timing('dns', started_at) + + def finalize_scan(result): + return complete_scan_timing('dns', started_at, result) # Checking internet connection printit(f' [+] Verifying internet connection...', coledt=[1, 49, 93], space_down=True) if not check_connection(): printit(' > Internet connection not available!', coledt=[1, 49, 91], space_down=True) - return False + return finalize_scan(False) # Information gathering about the domain printit(f' [+] Grathering information about \33[1;49;96m{ip_or_domain}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) @@ -751,7 +1061,7 @@ def dns_lookup(ip_or_domain): domain = '\33[1;49;91mNot Found\033[0m' + ' '*int(36-len('Not Found')) else: printit(' > Invalid ip address or domain name!', coledt=[1, 49, 91], space_down=True) - return False + return finalize_scan(False) # Excepts: socket.gaierror, socket.herror @@ -768,116 +1078,556 @@ def dns_lookup(ip_or_domain): print('\n ┌──────────────────────────────────────────────────────┐\n'+final_output_scanned+final_output_remaining) prv_op += '\n ┌──────────────────────────────────────────────────────┐'+'\n'+final_output_scanned+final_output_remaining - return True + return finalize_scan(True) + + +def truncate_for_table(value, width): + """Trim text for fixed-width table cells while preserving readability.""" + value_text = str(value) + if len(value_text) <= width: + return value_text + if width <= 3: + return value_text[:width] + return value_text[:width - 3] + '...' + + +def load_sherlock_site_data(): + """Load Sherlock site manifest from a remote JSON source.""" + global _sherlock_site_cache + if _sherlock_site_cache is not None: + return _sherlock_site_cache + + headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)'} + try: + response = requests.get(PHOMBER_SHERLOCK_DATA_URL, timeout=20, headers=headers) + response.raise_for_status() + response_json = response.json() + _sherlock_site_cache = response_json if isinstance(response_json, dict) else {} + except (requests.RequestException, ValueError, TypeError): + _sherlock_site_cache = {} + + return _sherlock_site_cache + + +def sherlock_profile_check(site_data, username, headers): + """Check a single Sherlock site entry and return existence + profile URL.""" + if not isinstance(site_data, dict): + return False, None + if site_data.get('disabled', False): + return False, None + + url_template = str(site_data.get('url', '')).strip() + probe_template = str(site_data.get('urlProbe', '')).strip() + if not url_template: + return False, None + + try: + profile_url = url_template.format(username) + except (IndexError, KeyError, ValueError): + return False, None + + probe_url = profile_url + if probe_template: + try: + probe_url = probe_template.format(username) + except (IndexError, KeyError, ValueError): + probe_url = profile_url + + method = str(site_data.get('request_method', 'GET')).upper() + error_type = str(site_data.get('errorType', 'status_code')).lower() + + try: + response = requests.request(method, probe_url, timeout=10, headers=headers, allow_redirects=True) + except requests.RequestException: + return False, profile_url + + response_text = response.text or '' + exists = False + + if error_type in ['status_code', 'response_url']: + exists = response.status_code not in [404, 410] + if error_type == 'response_url': + exists = exists and username.lower() in str(response.url).lower() + elif error_type == 'message': + error_msg = site_data.get('errorMsg') + if isinstance(error_msg, list): + exists = not any(str(message) in response_text for message in error_msg if message) + elif error_msg: + exists = str(error_msg) not in response_text + else: + exists = response.status_code < 400 + elif error_type == 'regex': + regex_check = site_data.get('regexCheck') + if regex_check: + exists = re.search(str(regex_check), response_text, flags=re.IGNORECASE) is not None + else: + exists = response.status_code < 400 + else: + exists = response.status_code < 400 + + if response.status_code in [429, 500, 502, 503, 504]: + exists = False + + return exists, profile_url + + +def compact_text(value, max_len=120): + """Normalize text blocks and keep output compact.""" + if value is None: + return None + text = re.sub(r'\s+', ' ', str(value)).strip() + if not text: + return None + if len(text) > max_len: + text = text[:max_len - 3] + '...' + return text + + +def first_non_empty(*values): + """Return first non-empty cleaned value.""" + for value in values: + cleaned = compact_text(value) + if cleaned: + return cleaned + return None + + +def parse_age_from_text(text): + """Extract age if explicitly written in a profile text.""" + if not text: + return None + + patterns = [ + r'\b(?:age|aged)\s*[:\-]?\s*(\d{1,2})\b', + r'\b(\d{1,2})\s*(?:years old|yrs old|yo)\b', + ] + for pattern in patterns: + match = re.search(pattern, text, flags=re.IGNORECASE) + if match: + try: + age = int(match.group(1)) + except (TypeError, ValueError): + continue + if 10 <= age <= 100: + return str(age) + return None + + +def infer_gender_from_text(text): + """Infer gender only when explicit pronouns/keywords are present.""" + if not text: + return None + lowered = text.lower() + + if any(token in lowered for token in ['she/her', 'she / her', 'female', 'woman', 'girl']): + return 'Female (public hint)' + if any(token in lowered for token in ['he/him', 'he / him', 'male', 'man', 'boy']): + return 'Male (public hint)' + if any(token in lowered for token in ['they/them', 'they / them', 'non-binary', 'nonbinary']): + return 'Non-binary (public hint)' + + return None + + +def walk_json_nodes(payload): + """Yield every JSON object recursively from JSON-LD payloads.""" + if isinstance(payload, dict): + yield payload + for value in payload.values(): + yield from walk_json_nodes(value) + elif isinstance(payload, list): + for item in payload: + yield from walk_json_nodes(item) + + +def extract_public_profile_identity(profile_url, headers): + """Extract public identity hints from a single profile page.""" + try: + response = requests.get(profile_url, timeout=PHOMBER_PROFILE_FETCH_TIMEOUT, headers=headers, allow_redirects=True) + if response.status_code >= 400: + return {} + soup = bs4.BeautifulSoup(response.text, 'html.parser') + except (requests.RequestException, ValueError, TypeError): + return {} + + identity = { + 'name': None, + 'age': None, + 'gender': None, + 'location': None, + 'bio': None, + 'email': None, + 'occupation': None, + 'website': None, + } + + og_title = soup.find('meta', attrs={'property': 'og:title'}) + twitter_title = soup.find('meta', attrs={'name': 'twitter:title'}) + html_title = soup.find('title') + og_description = soup.find('meta', attrs={'property': 'og:description'}) + plain_description = soup.find('meta', attrs={'name': 'description'}) + twitter_description = soup.find('meta', attrs={'name': 'twitter:description'}) + canonical_link = soup.find('link', attrs={'rel': 'canonical'}) + + title_text = first_non_empty( + og_title.get('content') if og_title else None, + twitter_title.get('content') if twitter_title else None, + html_title.text if html_title else None, + ) + description_text = first_non_empty( + og_description.get('content') if og_description else None, + plain_description.get('content') if plain_description else None, + twitter_description.get('content') if twitter_description else None, + ) + + if description_text: + identity['bio'] = description_text + if canonical_link and canonical_link.get('href'): + identity['website'] = compact_text(canonical_link.get('href')) + + json_ld_scripts = soup.find_all('script', attrs={'type': re.compile(r'ld\+json', flags=re.IGNORECASE)}) + for script_tag in json_ld_scripts: + raw_text = script_tag.string or script_tag.get_text(strip=True) + if not raw_text: + continue + try: + payload = json.loads(raw_text) + except (ValueError, TypeError, json.JSONDecodeError): + continue + + for node in walk_json_nodes(payload): + if not isinstance(node, dict): + continue + + node_type = node.get('@type', '') + if isinstance(node_type, list): + node_type = ' '.join(str(item) for item in node_type) + node_type = str(node_type) + + if 'Person' in node_type or 'ProfilePage' in node_type: + identity['name'] = identity['name'] or compact_text(node.get('name')) + identity['gender'] = identity['gender'] or compact_text(node.get('gender')) + identity['bio'] = identity['bio'] or compact_text(node.get('description')) + identity['email'] = identity['email'] or compact_text(node.get('email')) + identity['website'] = identity['website'] or compact_text(node.get('url')) + + occupation = compact_text(node.get('jobTitle')) + works_for = node.get('worksFor') + works_for_name = None + if isinstance(works_for, dict): + works_for_name = compact_text(works_for.get('name')) + elif isinstance(works_for, str): + works_for_name = compact_text(works_for) + identity['occupation'] = identity['occupation'] or first_non_empty(occupation, works_for_name) + + address = node.get('address') + if isinstance(address, dict): + locality = compact_text(address.get('addressLocality')) + region = compact_text(address.get('addressRegion')) + country = compact_text(address.get('addressCountry')) + identity['location'] = identity['location'] or first_non_empty( + ', '.join(filter(None, [locality, region, country])) + ) + elif isinstance(address, str): + identity['location'] = identity['location'] or compact_text(address) + + birth_date = compact_text(node.get('birthDate')) + if birth_date and not identity['age']: + year_match = re.search(r'(19|20)\d{2}', birth_date) + if year_match: + birth_year = int(year_match.group(0)) + age = time.localtime().tm_year - birth_year + if 10 <= age <= 100: + identity['age'] = str(age) + + body_text = compact_text(soup.get_text(' ', strip=True), max_len=6000) + profile_text = first_non_empty(description_text, title_text, body_text) or '' + + if not identity['name']: + # Try to recover display names from titles like "Full Name (@handle)". + if title_text: + identity['name'] = compact_text(re.split(r'\||-|•|\(', title_text)[0], max_len=80) + + if not identity['age']: + identity['age'] = parse_age_from_text(profile_text) + if not identity['gender']: + identity['gender'] = infer_gender_from_text(profile_text) + if not identity['occupation']: + occupation_hint = re.search( + r'\b(founder|developer|engineer|designer|student|artist|researcher|manager|consultant|writer|creator)\b', + profile_text, + flags=re.IGNORECASE, + ) + if occupation_hint: + identity['occupation'] = compact_text(occupation_hint.group(1).title(), max_len=80) + + location_tokens = re.search( + r'\b(?:based in|from|location|located in)\s*[:\-]?\s*([A-Za-z0-9 .,_-]{3,60})', + profile_text, + flags=re.IGNORECASE, + ) + if location_tokens and not identity['location']: + identity['location'] = compact_text(location_tokens.group(1), max_len=80) + + if not identity['email']: + mailto_link = soup.find('a', href=re.compile(r'^mailto:', flags=re.IGNORECASE)) + if mailto_link and mailto_link.get('href'): + identity['email'] = compact_text(mailto_link.get('href').replace('mailto:', '').strip(), max_len=120) + + return {key: compact_text(value, max_len=120) for key, value in identity.items() if compact_text(value, max_len=120)} + + +def enrich_username_identity(username, found_profiles, headers): + """Aggregate best-effort public identity details from matched + deep profile sources.""" + default_identity = { + 'handle': username, + 'name': None, + 'age': None, + 'gender': None, + 'location': None, + 'bio': None, + 'email': None, + 'occupation': None, + 'website': None, + } + + site_priority = { + 'LinkedIn': 1, + 'GitHub': 2, + 'X/Twitter': 3, + 'Instagram': 4, + 'Facebook': 5, + 'Reddit': 6, + } + + deep_profile_sources = { + 'LinkedIn': f'https://www.linkedin.com/in/{username}', + 'GitHub': f'https://github.com/{username}', + 'X/Twitter': f'https://x.com/{username}', + 'Instagram': f'https://www.instagram.com/{username}', + 'Facebook': f'https://www.facebook.com/{username}', + 'Reddit': f'https://www.reddit.com/user/{username}', + 'TikTok': f'https://www.tiktok.com/@{username}', + 'YouTube': f'https://www.youtube.com/@{username}', + 'Pinterest': f'https://www.pinterest.com/{username}', + 'Twitch': f'https://www.twitch.tv/{username}', + } + + ranked_profiles = sorted(found_profiles, key=lambda item: site_priority.get(str(item[0]), 99)) + candidate_profiles = list(ranked_profiles) + for source_name, source_url in deep_profile_sources.items(): + candidate_profiles.append((source_name, source_url)) + + unique_profiles = [] + seen_urls = set() + for site_name, profile_url in candidate_profiles: + profile_key = str(profile_url).strip().lower() + if not profile_key or profile_key in seen_urls: + continue + seen_urls.add(profile_key) + unique_profiles.append((str(site_name), str(profile_url))) + + unique_profiles = unique_profiles[:PHOMBER_USERNAME_IDENTITY_SOURCES] + + merged_identity = dict(default_identity) + enriched_sources = [] + + for site_name, profile_url in unique_profiles: + profile_identity = extract_public_profile_identity(profile_url, headers) + if not profile_identity: + continue + + enriched_sources.append(str(site_name)) + for key in ['name', 'age', 'gender', 'location', 'bio', 'email', 'occupation', 'website']: + if not merged_identity.get(key) and profile_identity.get(key): + merged_identity[key] = profile_identity.get(key) + + return merged_identity, enriched_sources, len(unique_profiles) + + +def format_username_identity(identity_data, enriched_sources, attempted_count): + """Format public identity section for username scan output.""" + handle = identity_data.get('handle') or 'N/A' + name = identity_data.get('name') or 'Not public' + age = identity_data.get('age') or 'Not public' + gender = identity_data.get('gender') or 'Not public' + location = identity_data.get('location') or 'Not public' + bio = identity_data.get('bio') or 'Not public' + email = identity_data.get('email') or 'Not public' + occupation = identity_data.get('occupation') or 'Not public' + website = identity_data.get('website') or 'Not public' + + final_output = '\n \33[1;49;93m[+] Public identity details (best effort):\033[0m\n\n' + final_output += f' \33[1;49;92m> Handle: {handle}\033[0m\n' + final_output += f' \33[1;49;92m> Name: {name}\033[0m\n' + final_output += f' \33[1;49;92m> Age: {age}\033[0m\n' + final_output += f' \33[1;49;92m> Gender: {gender}\033[0m\n' + final_output += f' \33[1;49;92m> Occupation: {occupation}\033[0m\n' + final_output += f' \33[1;49;92m> Location: {location}\033[0m\n' + final_output += f' \33[1;49;92m> Bio: {bio}\033[0m\n' + final_output += f' \33[1;49;92m> Email: {email}\033[0m\n' + final_output += f' \33[1;49;92m> Website: {website}\033[0m\n' + final_output += f' \33[1;49;93m[+] Deep profile checks attempted: \33[1;49;96m{attempted_count}\033[0m\n' + + if enriched_sources: + final_output += f'\n \33[1;49;93m[+] Enriched from: \33[1;49;96m{", ".join(enriched_sources)}\033[0m\n' + else: + final_output += '\n \33[1;49;93m[+] Enriched from: \33[1;49;96mNo public identity fields found\033[0m\n' + + final_output += ' \33[1;49;93m[!] Age/Gender are shown only if explicitly public and may be unavailable.\033[0m\n' + return final_output + + +def username_search_links(username): + """Build direct profile and cross-engine search links for a username.""" + direct_profiles = { + 'X/Twitter': f'https://x.com/{username}', + 'Instagram': f'https://www.instagram.com/{username}', + 'Facebook': f'https://www.facebook.com/{username}', + 'LinkedIn': f'https://www.linkedin.com/in/{username}', + 'GitHub': f'https://github.com/{username}', + 'Reddit': f'https://www.reddit.com/user/{username}', + 'TikTok': f'https://www.tiktok.com/@{username}', + 'YouTube': f'https://www.youtube.com/@{username}', + 'Pinterest': f'https://www.pinterest.com/{username}', + 'Twitch': f'https://www.twitch.tv/{username}', + } + + search_engines = { + 'Google': 'https://www.google.com/search?q={query}', + 'Bing': 'https://www.bing.com/search?q={query}', + 'DuckDuckGo': 'https://duckduckgo.com/?q={query}', + 'Yandex': 'https://yandex.com/search/?text={query}', + 'Brave': 'https://search.brave.com/search?q={query}', + } + + platform_domains = { + 'X/Twitter': 'x.com', + 'Instagram': 'instagram.com', + 'Facebook': 'facebook.com', + 'LinkedIn': 'linkedin.com', + 'GitHub': 'github.com', + 'Reddit': 'reddit.com', + 'TikTok': 'tiktok.com', + 'YouTube': 'youtube.com', + 'Pinterest': 'pinterest.com', + 'Twitch': 'twitch.tv', + } + + final_output = '\n \33[1;49;93m[+] Direct profile URLs:\033[0m\n\n' + for platform_name, profile_url in direct_profiles.items(): + final_output += f' \33[1;49;92m> {platform_name}: {profile_url}\033[0m\n' + + final_output += '\n \33[1;49;93m[+] Search engine dorks (all major engines):\033[0m\n\n' + for platform_name, domain in platform_domains.items(): + final_output += f' \33[1;49;97m[{platform_name}]\033[0m\n' + query = quote_plus(f'"{username}" site:{domain}') + for engine_name, engine_url in search_engines.items(): + final_output += f' \33[1;49;92m> {engine_name}: {engine_url.format(query=query)}\033[0m\n' + + return final_output # Reverse username lookup def username_lookup(username): # Global variables global prv_op prv_op = '' # Resetting previous output + username = normalize_scan_target(username) + started_at = time.perf_counter() + announce_scan_timing('username', started_at) + + def finalize_scan(result): + return complete_scan_timing('username', started_at, result) # Checking internet connection printit(f' [+] Verifying internet connection...', coledt=[1, 49, 93], space_down=True) if not check_connection(): printit(' > Internet connection not available!', coledt=[1, 49, 91], space_down=True) - return False + return finalize_scan(False) # Information gathering about the username printit(f' [+] Grathering information about \33[1;49;96m{username}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) prv_op += f' \33[1;49;93m[#] Reverse username lookup for \33[1;49;96m{username}\33[1;49;93m:\033[0m'+'\n\n' - # Validating username (no spaces) - if ' ' in username: + # Validating username + if not re.match(r'^[A-Za-z0-9._-]{1,40}$', username): printit(' > Invalid username!', coledt=[1, 49, 91], space_down=True) - return False + return finalize_scan(False) # Headers headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0' } - # https://www.idcrawl.com/u/ - try: - # requesting the url - response = requests.get('https://www.idcrawl.com/u/'+username, timeout=10, headers=headers) - soup = bs4.BeautifulSoup(response.text, 'html.parser') + site_data = load_sherlock_site_data() + if not site_data: + printit(' > Username source manifest unavailable, continuing deep profile research with direct platforms...', coledt=[1, 49, 93], space_down=True) + site_data = {} - # Extracting data - # save information in a dictionary - info_dict = {} + found_profiles = [] + checked = 0 - # Main infomation to extract - # gl-page-content col-md-8 col-sm-12 col-xs-12 (main class div) - # gl-accordion-item > h2 tag (key of info_dict) - # panel-collapse > gl-job-position-company > h3 (link -a and text) & p (value of info_dict) - # extract h2 and h3 tags in dictionary (h2 as key and h3 as value) - - # class = "gl-page-content col-md-8 col-sm-12 col-xs-12" - soup = soup.find('div', {'class': 'gl-page-content col-md-8 col-sm-12 col-xs-12'}) + for site_name, site_info in site_data.items(): + if checked >= PHOMBER_USERNAME_SITE_LIMIT: + break - # save h2 and h3 tags in dictionary (h2 as key and h3 as value) - for h2 in soup.find_all('h2'): - try: - # if there is `Not Taken` text inside the same div as h2 then skip it !!NEED_TO_FIX_IT!! - if 'Not Taken' in h2.findNext('p').text: - continue - # if in `panel-heading-info` there is `Email Addresses` or `Secret Profiles` text then skip it - if 'Email Addresses' in h2 or 'Secret Profiles' in h2: - continue - # saving h2 and h3 tags in dictionary - info_dict[str(h2).split('')[1].split('')[0]] = h2.findNext('h3').text - except: - pass + exists, profile_url = sherlock_profile_check(site_info, username, headers) + checked += 1 + if exists and profile_url: + found_profiles.append((str(site_name), str(profile_url))) + printit(f' [+] Performing deep profile research (up to {PHOMBER_USERNAME_IDENTITY_SOURCES} profiles)...', coledt=[1, 49, 93], space_down=True) + identity_data, enriched_sources, attempted_count = enrich_username_identity(username, found_profiles, headers) - if info_dict: - final_output = f''' + if found_profiles: + final_output = f''' ┌──────────────────────────────────────────────────────┐ - | Scanned Username: \33[1;49;96m{username+' '*int(34-len(str(username)))}\033[0m | + | Scanned Username: \33[1;49;96m{pad_string(username, 34)}\033[0m | |------------------------------------------------------| | INFORMATION | DESCRIPTION | |------------------------------------------------------|''' - for site, username in info_dict.items(): - site = str(site)+' '*int(19-len(str(site))) - username = str(username)+' '*int(30-len(str(username))) - final_output += f'''\n | \33[1;49;97m{site}\033[0m | {username} |''' + for site_name, profile_url in found_profiles: + site_label = pad_string(truncate_for_table(site_name, 19), 19) + profile_label = pad_string(truncate_for_table(profile_url, 30), 30) + final_output += f'''\n | \33[1;49;97m{site_label}\033[0m | {profile_label} |''' - final_output += f'''\n └──────────────────────────────────────────────────────┘''' + final_output += f'''\n └──────────────────────────────────────────────────────┘\n + \33[1;49;93m[+] Checked sources: \33[1;49;96m{checked}\033[0m + \33[1;49;93m[+] Matches found: \33[1;49;96m{len(found_profiles)}\033[0m''' + final_output += format_username_identity(identity_data, enriched_sources, attempted_count) + final_output += username_search_links(username) - print(final_output) - prv_op += final_output+'\n' - return True + print(final_output) + prv_op += final_output+'\n' + return finalize_scan(True) - else: - final_output = f''' + final_output = f''' ┌──────────────────────────────────────────────────────┐ - | Scanned Username: \33[1;49;96m{username+' '*int(34-len(str(username)))}\033[0m | + | Scanned Username: \33[1;49;96m{pad_string(username, 34)}\033[0m | |------------------------------------------------------| | INFORMATION | DESCRIPTION | |------------------------------------------------------| - | \33[1;49;91mFailed to Fetch information from server!\033[0m | + | \33[1;49;93mNo public profile found in checked sources\033[0m | └──────────────────────────────────────────────────────┘ - ''' - print(final_output) - prv_op += final_output+'\n' - return False - - - - - - except Exception as e: - print(e) - + \33[1;49;93m[+] Checked sources: \33[1;49;96m{checked}\033[0m + ''' + final_output += format_username_identity(identity_data, enriched_sources, attempted_count) + final_output += username_search_links(username) + print(final_output) + prv_op += final_output+'\n' + return finalize_scan(False) # --------------------------- Basic functions ------------------------ # def printit(text, center='', line_up=False, line_down=False, space_up=False, space_down=False, coledt=[0, 0, 0], normaltxt_start='', normaltxt_end='', hide=False, verbose_mode=False, input_mode=False): if not hide or verbose_mode: - # get terminal width - width = os.get_terminal_size().columns + # get terminal width with fallback for non-interactive shells + try: + width = os.get_terminal_size().columns + except (AttributeError, OSError, ValueError): + width = 80 # printing text if space_up: print() @@ -905,8 +1655,7 @@ def save_output(prv_cmd): try: filename = prv_cmd+'_ph0mber_'+str(time.strftime("%d-%m-%Y_%H-%M-%S"))+'.txt' pwd = os.getcwd() - os_name = os.uname() - path = pwd+'/'+filename + path = os.path.join(pwd, filename) file_save_info = f''' \33[1;49;93m[#] Output File Name: \33[1;49;97m{filename}\033[0m @@ -931,7 +1680,7 @@ def save_output(prv_cmd): printit(f' [+] Excute following commands to view output:', coledt=[1, 49, 93], space_down=True, space_up=True) - if os_name == 'nt': + if os.name == 'nt': exe_cmd = f''' \33[1;49;93m> \33[1;49;92mshell more {filename}\033[0m \33[1;49;93m> \33[1;49;92mshell type {filename}\033[0m''' else: @@ -940,15 +1689,15 @@ def save_output(prv_cmd): print(exe_cmd) return True - except: - printit(f' [!] Output could not be saved!', coledt=[1, 49, 91], space_down=True) + except Exception as e: + printit(f' [!] Output could not be saved! Error: {e}', coledt=[1, 49, 91], space_down=True) return False def dork_query(): # Dork query from exploit-db try: headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0'} - dork_query = requests.get('https://www.exploit-db.com/ghdb/'+str(random.randint(1000, 8000)), headers=headers) + dork_query = requests.get('https://www.exploit-db.com/ghdb/'+str(random.randint(1000, 8000)), headers=headers, timeout=12) # If exploit-db is available if dork_query.status_code == 200: @@ -959,7 +1708,7 @@ def dork_query(): query = parsed_html.find('h1', attrs={'class': 'card-title'}).text.strip() return query - except: + except (requests.RequestException, AttributeError, ValueError, KeyError): pass # If dork query is not available, use local dork query @@ -980,7 +1729,8 @@ def logo(): printit('─ ▒█▀▀█ ▒█░▒█ █▀▀█ ▒█▀▄▀█ ▒█▀▀█ ▒█▀▀▀ ▒█▀▀█ ─', center=' ', coledt=[1, 49, random.choice([91, 92, 93, 94, 95, 96, 97])], space_up=True) printit('─ ▒█▄▄█ ▒█▀▀█ █▄▀█ ▒█▒█▒█ ▒█▀▀▄ ▒█▀▀▀ ▒█▄▄▀ ─', center=' ', coledt=[1, 49, random.choice([91, 92, 93, 94, 95, 96, 97])]) printit('─ ▒█░░░ ▒█░▒█ █▄▄█ ▒█░░▒█ ▒█▄▄█ ▒█▄▄▄ ▒█░▒█ ─', center=' ', coledt=[1, 49, random.choice([91, 92, 93, 94, 95, 96, 97])], space_down=True) - printit('OSINT-FRAMEWORK @s41r4j', center=' ', coledt=[1, 49, random.choice([91, 92, 93, 94, 95, 96, 97])], space_down=True) + printit('OSINT-FRAMEWORK @s41r4j', center=' ', coledt=[1, 49, random.choice([91, 92, 93, 94, 95, 96, 97])]) + printit(' @DkHrR', center=' ', coledt=[1, 49, random.choice([91, 92, 93, 94, 95, 96, 97])], space_down=True) # ------------------------- Main functions -------------------------- # @@ -1055,7 +1805,7 @@ def control_center(): | \33[1;49;97mName\033[0m | \33[1;49;96mPH0MBER\033[0m | | \33[1;49;97mVersion\033[0m | {ver} | | \33[1;49;97mType\033[0m | \33[1;49;96mOSINT Framework\033[0m | - | \33[1;49;97mDeveloper\033[0m | \33[1;49;96ms41r4j\033[0m | + | \33[1;49;97mDeveloper\033[0m | \33[1;49;96ms41r4j, DkHrR\033[0m | | \33[1;49;97mGithub\033[0m | \33[1;49;96mhttps://github.com/s41r4j/phomber\033[0m | └────────────────────────────────────────────────────┘ ''' @@ -1282,16 +2032,16 @@ def control_center(): elif cmd == 'check': printit(' [+] Checking internet connection...', coledt=[1, 49, 93], space_down=True) try: - requests.get('https://www.google.com/') + requests.get('https://www.google.com/', timeout=10) printit(' > Internet connection is available!', coledt=[1, 49, 92]) try: - public_ip = requests.get('https://httpbin.org/ip').json()['origin'] + public_ip = requests.get('https://httpbin.org/ip', timeout=10).json()['origin'] except: try: - public_ip = requests.get('https://ident.me').text + public_ip = requests.get('https://ident.me', timeout=10).text except: try: - public_ip = requests.get('https://api.ipify.org').text + public_ip = requests.get('https://api.ipify.org', timeout=10).text except: public_ip = False if public_ip: @@ -1487,7 +2237,7 @@ def main(): # Checking for silent mode try: - if (sys.argv[1] == '-s' or sys.argv[1] == '--silent') or (sys.argv[2] == '-s' or sys.argv[2] == '--silent'): + if '-s' in sys.argv or '--silent' in sys.argv: silent_mode = True except IndexError: pass @@ -1510,7 +2260,7 @@ def main(): printit(f'[!] An error occured: {e}', coledt=[1, 49, 91], space_up=True) # Checking for verbose errors try: - if (sys.argv[1] == '-e' or sys.argv[1] == '--verbose-errors') or (sys.argv[2] == '-e' or sys.argv[2] == '--verbose-errors'): + if '-e' in sys.argv or '--verbose-errors' in sys.argv: printit(f'[+] Detailed error: {sys.exc_info()}', coledt=[1, 49, 91], space_down=True) else: raise IndexError diff --git a/pyproject.toml b/pyproject.toml index b9e11a57..aada5430 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "Environment :: Console", "Development Status :: 5 - Production/Stable" ] -dependencies = ['requests', 'prompt_toolkit', 'uuid', 'psutil', 'beautifulsoup4', 'phonenumbers', 'mac-vendor-lookup', 'python-whois', 'dnspython'] +dependencies = ['requests', 'prompt_toolkit', 'psutil', 'beautifulsoup4', 'phonenumbers', 'mac-vendor-lookup', 'python-whois', 'dnspython'] [project.urls] @@ -32,7 +32,4 @@ dependencies = ['requests', 'prompt_toolkit', 'uuid', 'psutil', 'beautifulsoup4' "Bug Tracker" = "https://github.com/s41r4j/phomber/issues" [project.scripts] -phomber = "phomber.phomber:main" - -[project.entry-points."phomber"] -phomber = "phomber.phomber:main" \ No newline at end of file +phomber = "phomber:main" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..0bf5f829 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +requests>=2.28.0 +prompt_toolkit>=3.0.0 +psutil>=5.9.0 +beautifulsoup4>=4.11.0 +phonenumbers>=8.12.0 +mac-vendor-lookup>=0.1.0 +python-whois>=0.7.0 +dnspython>=2.2.0 From a8f80f11c69c0e86b4b224f4b9d9771a5111cd3e Mon Sep 17 00:00:00 2001 From: Dhruv Amoldeep Shinde <172916227+DkHrR@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:13:14 +0530 Subject: [PATCH 2/4] Add remaining archive updates and smoke script --- .../unreleasedv3/phomber/modules/config.ini | 8 ++-- .smoke_run.py | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 .smoke_run.py diff --git a/.archive/unreleasedv3/phomber/modules/config.ini b/.archive/unreleasedv3/phomber/modules/config.ini index f92c50cd..02cc9a40 100644 --- a/.archive/unreleasedv3/phomber/modules/config.ini +++ b/.archive/unreleasedv3/phomber/modules/config.ini @@ -1,12 +1,12 @@ [abstractapi.com] -apikey = 66c2d92058734c1f8c636dd30426ad1d +apikey = YOUR_API_KEY_HERE [apilayer.com] -apikey = QSCjR95uAi25gJ6BNdf2JBGZfa6ByZsO +apikey = YOUR_API_KEY_HERE [numlookupapi.com] -apikey = tO7jWVoA5ybEp71vFOy3txRlaOi1aIgAn9naHZOA +apikey = YOUR_API_KEY_HERE [veriphone.io] -apikey = 9563B57ECCC04712920BC8CCA73F0A3A +apikey = YOUR_API_KEY_HERE diff --git a/.smoke_run.py b/.smoke_run.py new file mode 100644 index 00000000..fcd45da9 --- /dev/null +++ b/.smoke_run.py @@ -0,0 +1,38 @@ +import io +import contextlib +import phomber +def run_check(name, fn): + output_buffer = io.StringIO() + try: + with contextlib.redirect_stdout(output_buffer): + fn() + output = output_buffer.getvalue() + if name == 'number': + fail_markers = [ + 'Missing API key', + 'Invalid or unauthorized API key', + 'API rate limit reached', + 'Failed to fetch phone data from API', + 'Internet connection not available', + ] + status = 'FAIL' if any(marker in output for marker in fail_markers) else 'PASS' + elif name == 'ip': + fail_markers = [ + 'Internet connection not available', + 'Invalid ip address format', + ] + status = 'FAIL' if any(marker in output for marker in fail_markers) else 'PASS' + elif name == 'username': + fail_markers = [ + 'Internet connection not available', + 'Failed to load username source data', + ] + status = 'FAIL' if any(marker in output for marker in fail_markers) else 'PASS' + else: + status = 'FAIL' + except Exception: + status = 'FAIL' + print(f'{name}: {status}') +run_check('number', lambda: phomber.number_lookup('+14155552671')) +run_check('ip', lambda: phomber.ip_lookup('8.8.8.8')) +run_check('username', lambda: phomber.username_lookup('torvalds')) From 56cdef33cb1876cf278a3c48321c6ca780bf5789 Mon Sep 17 00:00:00 2001 From: Dhruv Amoldeep Shinde <172916227+DkHrR@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:17:26 +0530 Subject: [PATCH 3/4] Include remaining archived README changes --- .archive/v2.0/README.md | 406 ++++++++++++++++++++-------------------- 1 file changed, 204 insertions(+), 202 deletions(-) diff --git a/.archive/v2.0/README.md b/.archive/v2.0/README.md index 112588e6..4362584e 100644 --- a/.archive/v2.0/README.md +++ b/.archive/v2.0/README.md @@ -1,202 +1,204 @@ - - - -``` -A Infomation Grathering tool that reverse search phone numbers and get their details ! -``` - - - - - - - - -
- - -
-What is phomber? -
- - - Phomber is one of the best tools available for Infomation Grathering. - - It reverse searches given number online and retrieves all data available. - -
-
- -
-Search & Scans -
- - - Basic search - - Advance search (Experimental) - - Phoneinfoga scan (Optional) - - Truecaller scan (Comming soon) - -
-
- -
-Operating Systems Tested -
- - - [![Supported OS](https://img.shields.io/badge/OS%20X-brightgreen?style=flat&logo=macos)](https://www.google.com/search?q=OS%20X) - - [![Supported OS](https://img.shields.io/badge/Unix%20%2F%20Linux-blueviolet?style=flat&logo=linux)](https://www.google.com/search?q=Unix+Linux) - - [![Supported OS](https://img.shields.io/badge/Microsoft%20Windows-red?style=flat&logo=windows)](https://www.google.com/search?q=Windows) - - -
-
- -
-Direct Downloads -
- - - [zip](https://github.com/s41r4j/phomber/archive/refs/tags/phomber-v2.0.zip) - - [tar.gz](https://github.com/s41r4j/phomber/archive/refs/tags/phomber-v2.0.tar.gz) - -
-
- -
-
- - ## Manually Installation 👨‍🦯 - - Clone the repo - - ``` - git clone https://github.com/s41r4j/phomber - ``` - Change the directory - - ``` - cd phomber - ``` - Install Requirements - - ``` - pip install -r requirements.txt - ``` -
-Additional Configuration -
- - - There is a `config.py` file persent in `phomber` folder. - - Click on '[*Additional Settings*](https://github.com/s41r4j/phomber/blob/main/.more/additional_config.md)' to see how to configure it. - -
- -
- - ## Automated Installation 🤖 - - ### Linux / Unix / Mac 🗃️ - - - Download `install.sh` from [__HERE__](https://github.com/s41r4j/phomber/releases/download/phomber-v2.0/install.sh) (OR get it from latest [releases](https://github.com/s41r4j/phomber/releases/tag/phomber-v2.0)) - - - Run the following command to install `phomber` & it's `dependencies` - ``` - bash install.sh - ``` - - - ### Windows 📂 - - - Download `install.bat` from [__HERE__](https://github.com/s41r4j/phomber/releases/download/phomber-v2.0/install.bat) (OR get it from latest [releases](https://github.com/s41r4j/phomber/releases/tag/phomber-v2.0)) - - - Run the following command to install `phomber` & it's `dependencies` - ``` - .\install.bat - ``` - -> [*Additional Configuration*](https://github.com/s41r4j/phomber/blob/main/.more/additional_config.md) same as mentioned in `Manual Installation` 🪛 - -
-
- -## Usage 👨‍💻 - -> The `PH0MBER` takes [command line arguments](https://www.google.com/search?q=Command+Line+Arguments). - -``` -$ python3 phomber.py +(country code)xxxxxxxxxx -``` - -
- -Example (Linux based): -``` -s41r4j@github:~/Desktop/phomber$ python3 phomber.py +001234567890 -``` - -
-Explaination -
- -- `python3 phomber.py` - Running phomber script with python3 -- `+001234567890` - Command line argument, the phone number you want to search. -
-Phone number breakdown / explained -
- - - `+00` is [country code](https://en.wikipedia.org/wiki/List_of_country_calling_codes), eg: +1 (Canada, US), +47 (Norway), +91 (India), +86 (China) - - `1234567890` is the phone number without spaces, dashes & brackets - -
-
-
- - - -
-
- -## Prerequisites ⚛️ - -
-Required -
- -- python3 -- git - -
-
- -
-Optional -
- -- GoLang ([Download Here](https://golang.org/dl/)) , for Phoneinfoga scan. -- OpenCage Account ([create a account here](https://opencagedata.com/users/sign_up)) , for Basic search. -- Truecaller Account ([create a account here](https://www.truecaller.com/auth/sign-in)) , for Truecaller scan. - -
- -
-
-
-
- - -> The [Developer](https://github.com/s41r4j/) of [PH0MBER](https://github.com/s41r4j/phomber/) is not responsible for an loss or misuse of the tool, it is end user's responsiblity. - -
- -## Want to Support ❤️ - -🎁 Please check out the [__Google Form__](https://forms.gle/Aqit6QhQTmoYoypD7) 🎁 - - - - -
-
-
-
- -![s41r4j's github stats](https://github-readme-stats.vercel.app/api?username=s41r4j&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) - - + + + +``` +A Infomation Grathering tool that reverse search phone numbers and get their details ! +``` + + + + + + + +
+ + + + +
+What is phomber? +
+ + - Phomber is one of the best tools available for Infomation Grathering. + - It reverse searches given number online and retrieves all data available. + +
+
+ +
+Available Scans +
+ + - Basic Scan + - Abstractapi Scan + - Apilayer Scan + - Find and Trace Scan + - Numlookupapi Scan + - Veriphone Scan + +
+
+ +
+Operating Systems Tested +
+ + - [![Supported OS](https://img.shields.io/badge/OS%20X-brightgreen?style=flat&logo=macos)](https://www.google.com/search?q=OS%20X)                      (pip) + - [![Supported OS](https://img.shields.io/badge/Unix%20%2F%20Linux-blueviolet?style=flat&logo=linux)](https://www.google.com/search?q=Unix+Linux)             (pip) + - [![Supported OS](https://img.shields.io/badge/Microsoft%20Windows-red?style=flat&logo=windows)](https://www.google.com/search?q=Windows)    (exe) + + +
+
+ +
+Direct Downloads +
+ + - [Windows (EXE)](https://github.com/s41r4j/phomber/archive/refs/tags/phomber-v2.0.zip) + +
+
+ +
+
+ + + + + + + +# 📜 Contents +
+ +- [Installation](#screwdriver-installation) + + - [Linux](#card_file_box-linux-based-systems) + - [Windows](#open_file_folder-windows) + +- [Usage](#cloud-usage) +- [Prerequisites](#atom_symbol-prerequisites) + + +

+ + + + + + + +# :screwdriver: Installation + + +## :card_file_box: Linux based systems + +- Now, install `PH0MBER` with ease (in _Linux_ or _Unix_ based systems) + +``` +pip install phomber +``` + + +

+ +## :open_file_folder: Windows + +- Download Windows executable from here [[download now](https://google.com)] + +- Also check this guide [[redirect to guide]()] for proper `PH0MBER` setup in _Windows_ + +- For ___Windows___ you don't need python or any other kind of installation + + + + + + +

+# :cloud: Usage + +``` +usage: phomber [-h] [-c] [-l] [-a] [-abs] [-lyr] [-fnt] [-nlu] + [-vp] + [Phone Number] + +PH0MBER — reverse phone number lookup + +positional arguments: + Phone Number Phone number to which perform reverse + lookup + +optional arguments: + -h, --help show this help message and exit + -c, --config_editor Opens config editor for entering apis + keys + -l, --logo Display random `PH0MBER` logo + -a, --all_apis Run all API scans + -abs, --abstractapi Abstract Api [abstractapi.com] + -lyr, --apilayer Apilayer [apilayer.com] + -fnt, --findandtrace Find and Trace [findandtrace.com] + -nlu, --numlookupapi Numlookup Api [numlookupapi.com] + -vp, --veriphone Veriphone [veriphone.io] +``` + + + + + + + +

+## :atom_symbol: Prerequisites + +
+Required (for unix/linux sys) +
+ +- python3 +- pip + +
+
+ +
+Optional (accounts for apikey) +
+ +- [Abstractapi](abstractapi.com) +- [Apilayer](apilayer.com) +- [Numlookupapi](numlookupapi.com) +- [Veriphone](veriphone.io) + +
+
+ +
+ +To setup API gateway, follow this guide [[redirect to guide](/.docs/apikeys.md)] + + + +
+
+
+
+ + +> The [Developer](https://github.com/s41r4j/) of [PH0MBER](https://github.com/s41r4j/phomber/) is not responsible for an loss or misuse of the tool, it is end user's responsiblity. + +
+ + + + +
+
+
+
+ +![s41r4j's github stats](https://github-readme-stats.vercel.app/api?username=s41r4j&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) + + From 678c8d788da53421b6169dfef10dd9955f5cb39a Mon Sep 17 00:00:00 2001 From: Dhruv Amoldeep Shinde <172916227+DkHrR@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:37:21 +0530 Subject: [PATCH 4/4] Harden startup compatibility and expand scanner coverage --- .github/ISSUE_TEMPLATE/bug_report.yml | 96 ++++++ README.md | 20 ++ phomber.py | 405 +++++++++++++++++++++++++- 3 files changed, 511 insertions(+), 10 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..1df1e260 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,96 @@ +name: Bug Report +description: Report a reproducible bug in PH0MBER +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. + Please provide full reproduction details so we can fix it quickly. + + - type: textarea + id: summary + attributes: + label: Bug Summary + description: What happened? + placeholder: Short description of the bug + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps To Reproduce + description: Exact commands and steps + placeholder: | + 1. Run command: python3 phomber.py -e + 2. Enter: info + 3. Crash happens + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior / Error Output + description: Paste complete traceback or terminal output (use -e flag) + placeholder: | + [!] An error occured: ... + [+] Detailed error: ... + validations: + required: true + + - type: input + id: os + attributes: + label: Operating System + description: Include distro/version if Linux (or Android shell details) + placeholder: Windows 11 / Ubuntu 22.04 / Termux Android 14 / iSH + validations: + required: true + + - type: input + id: python + attributes: + label: Python Version + placeholder: 3.11.9 + validations: + required: true + + - type: input + id: install_method + attributes: + label: Install Method + description: git clone / pip / docker + placeholder: git clone + validations: + required: true + + - type: dropdown + id: runtime + attributes: + label: Runtime Environment + options: + - Standard Linux/Windows/macOS shell + - Termux (Android) + - iSH (iOS) + - WSL + - Docker + - Other + validations: + required: true + + - type: textarea + id: extra + attributes: + label: Additional Context + description: Screenshots, logs, or related issue links diff --git a/README.md b/README.md index fef0fb07..cb99a51b 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ phomber | <(Scanner Commands)> | |----------------------------------------------------| | number | Reverse phone number lookup | + | email | Reverse email lookup * | | ip | Reverse ip address lookup * | | mac | Reverse mac address lookup | | whois | Reverse whois lookup * | @@ -155,13 +156,32 @@ phomber - `number` scan supports API-first mode if `PHOMBER_NUMLOOKUP_API_KEY` (or `NUMLOOKUP_API_KEY`) is set - if API is unavailable, `number` automatically falls back to the local basic scan - all scanners now display estimated and real-time research duration +- `email` scanner is available for reverse email exposure lookup - `username` scan now performs deep profile research across direct platform profiles and best-effort public identity extraction +- optional Hudson Rock exposure intelligence is integrated for `username`, `email`, `number`, and `whois` scans - tune depth via `PHOMBER_USERNAME_IDENTITY_SOURCES` (default: `20`) - tune per-profile fetch timeout via `PHOMBER_PROFILE_FETCH_TIMEOUT` (default: `8` seconds) +- disable Hudson Rock checks with `PHOMBER_ENABLE_HUDSON_ROCK=0` +- tune Hudson Rock request timeout with `PHOMBER_HUDSON_ROCK_TIMEOUT` (default: `10` seconds) +- compatibility mode auto-enables on restricted runtimes (Termux/iSH/Android-like); override with `PHOMBER_COMPATIBILITY_MODE=1` - Release windows (exe) & linux (lsb) direct executables, no python req. - `v4` will have web-interface + automated scans features + (custom-scanner feature; create, distribute and deploy your own scanner) ``` +### Troubleshooting + +- Startup error: `Permission denied: '/proc/stat'` +- Use compatibility mode: `PHOMBER_COMPATIBILITY_MODE=1` +- Run with verbose diagnostics: `python3 phomber.py -e` + +- Startup error: `No such file or directory` +- Run from project root (`phomber` directory) +- Reinstall dependencies: `pip3 install -r requirements.txt` + +- Mobile shell runtimes (Termux/iSH) can be partially restricted +- Use Linux/Windows/macOS for full feature parity +- If you use mobile shells, include full `-e` output when reporting issues + ### Credits - Core author: `@s41r4j` diff --git a/phomber.py b/phomber.py index 56a5af46..f547643f 100644 --- a/phomber.py +++ b/phomber.py @@ -36,6 +36,7 @@ import time # Default import threading # Default import json # Default +import importlib.util # Default import re # Default import uuid # Default import socket # Default @@ -85,10 +86,184 @@ def normalize_scan_target(raw_value): return value +def env_flag(name, default=False): + """Parse common boolean env values safely.""" + raw_value = os.environ.get(name) + if raw_value is None: + return default + return str(raw_value).strip().lower() in ['1', 'true', 'yes', 'on', 'enable', 'enabled'] + + +def safe_float_env(name, default_value): + """Read a float env var with a sane fallback.""" + try: + return float(os.environ.get(name, str(default_value))) + except (TypeError, ValueError): + return float(default_value) + + +def safe_int_env(name, default_value): + """Read an int env var with a sane fallback.""" + try: + return int(os.environ.get(name, str(default_value))) + except (TypeError, ValueError): + return int(default_value) + + +def detect_runtime_profile(): + """Detect restricted/mobile environments where startup metrics often fail.""" + system_name = str(platform.system()).strip().lower() + node_name = str(platform.node()).strip().lower() + release_name = str(platform.release()).strip().lower() + version_name = str(platform.version()).strip().lower() + + termux_detected = bool( + os.environ.get('TERMUX_VERSION') + or str(os.environ.get('PREFIX', '')).strip().startswith('/data/data/com.termux') + ) + android_detected = bool(termux_detected or os.environ.get('ANDROID_ROOT') or os.environ.get('ANDROID_DATA') or 'android' in f'{system_name} {release_name} {version_name}') + ish_detected = bool('ish' in f'{node_name} {release_name} {version_name}') + + proc_stat_readable = None + if os.path.exists('/proc/stat'): + try: + with open('/proc/stat', 'r', encoding='utf-8', errors='ignore') as proc_stat_file: + proc_stat_file.readline() + proc_stat_readable = True + except (PermissionError, OSError): + proc_stat_readable = False + + restricted_runtime = bool(termux_detected or android_detected or ish_detected or proc_stat_readable is False) + profile_label = 'standard' + if termux_detected: + profile_label = 'termux' + elif ish_detected: + profile_label = 'ish' + elif android_detected: + profile_label = 'android' + + return { + 'profile': profile_label, + 'is_termux': termux_detected, + 'is_android': android_detected, + 'is_ish': ish_detected, + 'proc_stat_readable': proc_stat_readable, + 'is_restricted': restricted_runtime, + } + + +def style_info_field(value, width): + """Color and pad system info fields without negative-padding crashes.""" + value_text = str(value) + return '\33[1;49;96m' + value_text + '\033[0m' + ' ' * max(0, width - len(value_text)) + + +def safe_metric_percent(metric_name, collector, warnings): + """Collect a metric percent safely and record a warning when unavailable.""" + try: + metric_value = collector() + if metric_value is None: + raise ValueError('metric collector returned None') + return f'{float(metric_value):.1f}%' + except Exception as error: + warnings.append(f'{metric_name} unavailable: {error}') + return 'N/A' + + +def build_system_snapshot(): + """Build robust system metrics with compatibility fallbacks.""" + runtime_profile = detect_runtime_profile() + warnings = [] + + if runtime_profile.get('proc_stat_readable') is False: + warnings.append('restricted /proc/stat access detected; CPU metrics may be unavailable') + + disk_target = os.path.abspath(os.sep) + if not os.path.exists(disk_target): + disk_target = os.getcwd() + + ram_usage = safe_metric_percent('RAM usage', lambda: psutil.virtual_memory().percent, warnings) + cpu_usage = safe_metric_percent('CPU usage', lambda: psutil.cpu_percent(), warnings) + disk_usage = safe_metric_percent('Disk usage', lambda: psutil.disk_usage(disk_target).percent, warnings) + + return { + 'runtime_profile': runtime_profile, + 'ram_usage': ram_usage, + 'cpu_usage': cpu_usage, + 'disk_usage': disk_usage, + 'warnings': warnings, + } + + +def detect_missing_runtime_dependencies(): + """Identify missing top-level modules for startup diagnostics.""" + dependency_map = [ + ('requests', 'requests'), + ('prompt_toolkit', 'prompt_toolkit'), + ('psutil', 'psutil'), + ('bs4', 'beautifulsoup4'), + ('phonenumbers', 'phonenumbers'), + ('mac_vendor_lookup', 'mac-vendor-lookup'), + ('whois', 'python-whois'), + ('dns', 'dnspython'), + ] + missing_packages = [] + for module_name, package_name in dependency_map: + if importlib.util.find_spec(module_name) is None: + missing_packages.append(package_name) + return missing_packages + + +def build_startup_diagnostics(error): + """Create actionable diagnostics for startup failures.""" + runtime_profile = detect_runtime_profile() + missing_packages = detect_missing_runtime_dependencies() + + diagnostic_items = [ + ('OS', platform.platform()), + ('Python', platform.python_version()), + ('Executable', sys.executable), + ('Current Directory', os.getcwd()), + ('Runtime Profile', runtime_profile.get('profile', 'unknown')), + ('Compatibility Mode', str(PHOMBER_COMPATIBILITY_MODE)), + ] + + suggestions = [ + 'Install dependencies: pip3 install -r requirements.txt', + 'Use `python3 phomber.py -e` to show verbose trace details', + ] + + error_text = str(error).lower() + if '/proc/stat' in error_text or isinstance(error, PermissionError): + suggestions.append('Restricted `/proc/stat` detected: run on full Linux/Windows/macOS or enable compatibility mode: PHOMBER_COMPATIBILITY_MODE=1') + if isinstance(error, FileNotFoundError): + suggestions.append('Run command from repository root and verify required files exist') + if runtime_profile.get('is_restricted'): + suggestions.append('Termux/iSH/Android runtimes may need extra permissions and can have partial feature support') + + if missing_packages: + suggestions.append('Missing packages: ' + ', '.join(missing_packages)) + + return diagnostic_items, suggestions + + +def print_startup_diagnostics(error): + """Print startup diagnostics in a user-friendly way.""" + diagnostic_items, suggestions = build_startup_diagnostics(error) + printit('[+] Startup diagnostics:', coledt=[1, 49, 93], space_up=True) + for key_name, key_value in diagnostic_items: + printit(f' > {key_name}: {key_value}', coledt=[1, 49, 97]) + + printit('[+] Suggested fixes:', coledt=[1, 49, 93], space_up=True) + for suggestion in suggestions: + printit(f' > {suggestion}', coledt=[1, 49, 92]) + + def get_research_time_estimate(scanner_name): """Return estimated duration range for each scanner.""" estimate_map = { 'number': '5-15 seconds', + 'email': '4-20 seconds', 'ip': '3-12 seconds', 'mac': '1-5 seconds', 'whois': '4-20 seconds', @@ -102,6 +277,7 @@ def get_research_time_estimate_seconds(scanner_name): """Return average estimated duration (seconds) for live remaining-time updates.""" estimate_seconds_map = { 'number': 10, + 'email': 10, 'ip': 8, 'mac': 3, 'whois': 12, @@ -228,6 +404,9 @@ def complete_scan_timing(scanner_name, started_at, result): PHOMBER_PROFILE_FETCH_TIMEOUT = float(os.environ.get('PHOMBER_PROFILE_FETCH_TIMEOUT', '8')) except (TypeError, ValueError): PHOMBER_PROFILE_FETCH_TIMEOUT = 8.0 +PHOMBER_ENABLE_HUDSON_ROCK = env_flag('PHOMBER_ENABLE_HUDSON_ROCK', default=True) +PHOMBER_HUDSON_ROCK_TIMEOUT = safe_float_env('PHOMBER_HUDSON_ROCK_TIMEOUT', 10) +PHOMBER_COMPATIBILITY_MODE = env_flag('PHOMBER_COMPATIBILITY_MODE', default=detect_runtime_profile().get('is_restricted', False)) _sherlock_site_cache = None _active_scan_timers = {} silent_mode = False @@ -449,6 +628,12 @@ def finalize_scan(result): \033[0m''' print(social_media_platforms) prv_op += social_media_platforms+'\n' + + hudson_output, _ = run_hudson_rock_section('phone', phone_number.strip()) + if hudson_output: + print(hudson_output) + prv_op += hudson_output+'\n' + return finalize_scan(True) else: @@ -1018,6 +1203,11 @@ def finalize_scan(result): print(final_output) prv_op += final_output+'\n' + hudson_output, _ = run_hudson_rock_section('domain', domain.strip()) + if hudson_output: + print(hudson_output) + prv_op += hudson_output+'\n' + return finalize_scan(bol_val) # Reverse domain lookup @@ -1478,6 +1668,116 @@ def format_username_identity(identity_data, enriched_sources, attempted_count): return final_output +def hudson_rock_lookup(search_type, target_value): + """Query Hudson Rock Cavalier API for exposure intelligence.""" + if not PHOMBER_ENABLE_HUDSON_ROCK: + return False, None, 'disabled' + + endpoint_map = { + 'username': 'search-by-username', + 'phone': 'search-by-username', + 'email': 'search-by-email', + 'domain': 'search-by-domain', + } + endpoint_name = endpoint_map.get(search_type) + if not endpoint_name: + return False, None, f'unsupported search type: {search_type}' + + base_url = 'https://cavalier.hudsonrock.com/api/json/v2/osint-tools' + query_key = 'username' if search_type in ['username', 'phone'] else search_type + query_value = str(target_value).strip() + + headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0'} + + try: + response = requests.get( + f'{base_url}/{endpoint_name}', + params={query_key: query_value}, + timeout=PHOMBER_HUDSON_ROCK_TIMEOUT, + headers=headers, + ) + except requests.RequestException as request_error: + return False, None, str(request_error) + + try: + response_json = response.json() + except ValueError: + response_json = {'message': response.text[:300] if response.text else ''} + + if response.status_code == 404: + if isinstance(response_json, dict): + response_json.setdefault('message', 'No data found') + else: + response_json = {'message': 'No data found'} + return True, response_json, None + + if response.status_code >= 400: + message = '' + if isinstance(response_json, dict): + message = str(response_json.get('message') or response_json.get('error') or '').strip() + error_text = f'HTTP {response.status_code}' + (f' - {message}' if message else '') + return False, None, error_text + + if not isinstance(response_json, dict): + response_json = {'data': response_json} + + return True, response_json, None + + +def hudson_rock_record_count(response_json): + """Estimate exposure record count from common response shapes.""" + if not isinstance(response_json, dict): + return 0 + + for total_key in ['total', 'count', 'records_count', 'results_count']: + total_value = response_json.get(total_key) + if isinstance(total_value, int) and total_value >= 0: + return total_value + + collection_keys = ['stealers', 'employees', 'users', 'records', 'results', 'items', 'data'] + for key_name in collection_keys: + key_value = response_json.get(key_name) + if isinstance(key_value, list): + return len(key_value) + + return 0 + + +def format_hudson_rock_section(scan_name, target_value, response_json): + """Render Hudson Rock API response in a compact terminal block.""" + message = '' + if isinstance(response_json, dict): + message = compact_text(response_json.get('message') or response_json.get('error') or '', max_len=100) or '' + + record_count = hudson_rock_record_count(response_json) + status_text = 'Potential exposure data found' if record_count > 0 else 'No exposure records reported' + + section_output = f''' + \33[1;49;93m[+] Hudson Rock intel ({scan_name}):\033[0m + \33[1;49;97m> Target:\033[0m {target_value} + \33[1;49;97m> Source:\033[0m Hudson Rock Cavalier API + \33[1;49;97m> Status:\033[0m {status_text} + \33[1;49;97m> Estimated records:\033[0m {record_count} +''' + if message: + section_output += f' \33[1;49;97m> API message:\033[0m {message}\n' + + return section_output, record_count > 0 + + +def run_hudson_rock_section(scan_name, target_value): + """Execute Hudson Rock lookup and return formatted output + success state.""" + success, response_json, error_text = hudson_rock_lookup(scan_name, target_value) + if not success: + if error_text == 'disabled': + return '', None + failure_output = f'''\n \33[1;49;93m[+] Hudson Rock intel ({scan_name}):\033[0m\n \33[1;49;91m> Lookup skipped: {error_text}\033[0m\n''' + return failure_output, None + + section_output, found_data = format_hudson_rock_section(scan_name, target_value, response_json) + return section_output, found_data + + def username_search_links(username): """Build direct profile and cross-engine search links for a username.""" direct_profiles = { @@ -1578,6 +1878,7 @@ def finalize_scan(result): printit(f' [+] Performing deep profile research (up to {PHOMBER_USERNAME_IDENTITY_SOURCES} profiles)...', coledt=[1, 49, 93], space_down=True) identity_data, enriched_sources, attempted_count = enrich_username_identity(username, found_profiles, headers) + hudson_output, _ = run_hudson_rock_section('username', username) if found_profiles: final_output = f''' @@ -1596,6 +1897,7 @@ def finalize_scan(result): \33[1;49;93m[+] Checked sources: \33[1;49;96m{checked}\033[0m \33[1;49;93m[+] Matches found: \33[1;49;96m{len(found_profiles)}\033[0m''' final_output += format_username_identity(identity_data, enriched_sources, attempted_count) + final_output += hudson_output final_output += username_search_links(username) print(final_output) @@ -1614,11 +1916,55 @@ def finalize_scan(result): \33[1;49;93m[+] Checked sources: \33[1;49;96m{checked}\033[0m ''' final_output += format_username_identity(identity_data, enriched_sources, attempted_count) + final_output += hudson_output final_output += username_search_links(username) print(final_output) prv_op += final_output+'\n' return finalize_scan(False) + +def email_lookup(email_address): + """Reverse email lookup using Hudson Rock exposure intelligence.""" + global prv_op + prv_op = '' + email_address = normalize_scan_target(email_address) + started_at = time.perf_counter() + announce_scan_timing('email', started_at) + + def finalize_scan(result): + return complete_scan_timing('email', started_at, result) + + printit(f' [+] Verifying internet connection...', coledt=[1, 49, 93], space_down=True) + if not check_connection(): + printit(' > Internet connection not available!', coledt=[1, 49, 91], space_down=True) + return finalize_scan(False) + + printit(f' [+] Grathering information about \33[1;49;96m{email_address}\33[1;49;93m...', coledt=[1, 49, 93], space_down=True) + prv_op += f' \33[1;49;93m[#] Reverse email lookup for \33[1;49;96m{email_address}\33[1;49;93m:\033[0m' + '\n\n' + + if not re.match(r'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$', email_address): + printit(' > Invalid email format! (Example: user@example.com)', coledt=[1, 49, 91]) + return finalize_scan(False) + + hudson_output, found_data = run_hudson_rock_section('email', email_address) + if not hudson_output: + hudson_output = '\n \33[1;49;93m[+] Hudson Rock intel (email):\033[0m\n \33[1;49;93m> No response data available\033[0m\n' + + search_links = f''' + \33[1;49;93m[+] Search engine lookup:\033[0m + + \33[1;49;92m> https://www.google.com/search?q={quote_plus(email_address)} + > https://www.bing.com/search?q={quote_plus(email_address)} + > https://duckduckgo.com/?q={quote_plus(email_address)} + > https://yandex.com/search/?text={quote_plus(email_address)} + > https://search.brave.com/search?q={quote_plus(email_address)} + \033[0m''' + + final_output = hudson_output + search_links + print(final_output) + prv_op += final_output + '\n' + return finalize_scan(True) + # --------------------------- Basic functions ------------------------ # def printit(text, center='', line_up=False, line_down=False, space_up=False, space_down=False, coledt=[0, 0, 0], normaltxt_start='', normaltxt_end='', hide=False, verbose_mode=False, input_mode=False): @@ -1772,17 +2118,24 @@ def control_center(): └────────────────────────────────────────────────────┘''' # Variables - user = '\33[1;49;96m'+str(getpass.getuser())+'\033[0m'+ ' '*int(34-len(str(getpass.getuser()))) - hostname = '\33[1;49;96m'+str(platform.node())+'\033[0m'+' '*int(34-len(str(platform.node()))) - os_name = '\33[1;49;96m'+str(platform.system())+'\033[0m'+' '*int(34-len(str(platform.system()))) - ram = '\33[1;49;96m'+str(psutil.virtual_memory().percent)+"%"+'\033[0m'+' '*int(33-len(str(psutil.virtual_memory().percent))) - cpu = '\33[1;49;96m'+str(psutil.cpu_percent())+"%"+'\033[0m'+' '*int(32-len(str(psutil.cpu_percent()))) - disk = '\33[1;49;96m'+str(psutil.disk_usage('/').percent)+"%"+'\033[0m'+' '*int(33-len(str(psutil.disk_usage('/').percent))) - sys_mac = '\33[1;49;96m'+str(':'.join(re.findall('..', '%012x' % uuid.getnode())))+'\033[0m'+' '*int(34-len(str(':'.join(re.findall('..', '%012x' % uuid.getnode()))))) - - arch = '\33[1;49;96m'+str(platform.machine())+'\033[0m'+' '*int(34-len(str(platform.machine()))) + system_snapshot = build_system_snapshot() + runtime_profile = system_snapshot.get('runtime_profile', {}) + compatibility_warnings = list(system_snapshot.get('warnings', [])) + if PHOMBER_COMPATIBILITY_MODE: + compatibility_warnings.append('compatibility mode enabled (reduced strictness for restricted runtimes)') + + user = style_info_field(str(getpass.getuser()), 34) + hostname = style_info_field(str(platform.node()), 34) + os_name = style_info_field(str(platform.system()), 34) + runtime_name = style_info_field(str(runtime_profile.get('profile', 'standard')), 34) + ram = style_info_field(system_snapshot.get('ram_usage', 'N/A'), 33) + cpu = style_info_field(system_snapshot.get('cpu_usage', 'N/A'), 32) + disk = style_info_field(system_snapshot.get('disk_usage', 'N/A'), 33) + sys_mac = style_info_field(str(':'.join(re.findall('..', '%012x' % uuid.getnode()))), 34) + + arch = style_info_field(str(platform.machine()), 34) - ver = '\33[1;49;96m3.1.1\033[0m'+' '*int(34-len(str(3.0))) + ver = style_info_field('3.1.1', 34) sysinfo = f''' ┌────────────────────────────────────────────────────┐ @@ -1791,6 +2144,7 @@ def control_center(): | \33[1;49;97mUser\033[0m | {user} | | \33[1;49;97mHostname\033[0m | {hostname} | | \33[1;49;97mOS\033[0m | {os_name} | + | \33[1;49;97mRuntime\033[0m | {runtime_name} | | \33[1;49;97mArchitecture\033[0m | {arch} | | \33[1;49;97mSystem MAC\033[0m | {sys_mac} | |----------------------------------------------------| @@ -1810,6 +2164,11 @@ def control_center(): └────────────────────────────────────────────────────┘ ''' + if not silent_mode and compatibility_warnings: + printit(' [!] Compatibility notices:', coledt=[1, 49, 93], space_down=True) + for warning_text in sorted(set(compatibility_warnings)): + printit(f' > {warning_text}', coledt=[1, 49, 93]) + help = ''' ┌────────────────────────────────────────────────────┐ | COMMANDS | DESCRIPTION | @@ -1833,6 +2192,7 @@ def control_center(): | <(Scanner Commands)> | |----------------------------------------------------| | \33[1;49;97mnumber\033[0m | Reverse phone number lookup | + | \33[1;49;97memail\033[0m | Reverse email lookup \33[1;49;91m*\033[0m | | \33[1;49;97mip\033[0m | Reverse ip address lookup \33[1;49;91m*\033[0m | | \33[1;49;97mmac\033[0m | Reverse mac address lookup | | \33[1;49;97mwhois\033[0m | Reverse whois lookup \33[1;49;91m*\033[0m | @@ -1967,6 +2327,14 @@ def control_center(): \33[1;49;92m> EXAMPLE: \33[1;49;97mnumber +1234567890 \033[0m''' print(number_help) + elif cmd == 'email': + crt_exp = exp[1] + email_help = ''' \33[1;49;93m[+] Command Info: \33[1;49;93memail\n + \33[1;49;92m> DESCRIPTION: \33[1;49;97mWhen you type `email`, it performs email exposure lookup using Hudson Rock and search-engine pivots + \33[1;49;92m> SYNTAX: \33[1;49;97memail + \33[1;49;92m> EXAMPLE: \33[1;49;97memail user@example.com + \033[0m''' + print(email_help) elif cmd == 'whois': crt_exp = exp[1] whois_help = ''' \33[1;49;93m[+] Command Info: \33[1;49;93mwhois\n @@ -2141,6 +2509,22 @@ def control_center(): else: crt_exp = exp[3] printit(' [!] Invalid command! Type `help` to see all available commands.', coledt=[1, 49, 91]) + elif cmd == 'email': + crt_exp = exp[2] + printit(' > No email found! Type `help email` to see more info', coledt=[1, 49, 91]) + elif cmd[:5] == 'email': + full_cmd = cmd + if (cmd+'#')[5] == ' ': + cmd = cmd[6:] + printit(' [+] Performing reverse email lookup...', coledt=[1, 49, 93], space_down=True) + if email_lookup(cmd): + crt_exp = exp[4] + else: + crt_exp = exp[3] + prv_op = '' + else: + crt_exp = exp[3] + printit(' [!] Invalid command! Type `help` to see all available commands.', coledt=[1, 49, 91]) elif cmd == 'ip': crt_exp = exp[2] printit(' > No ip address found! Type `help ip` to see more info', coledt=[1, 49, 91]) @@ -2258,6 +2642,7 @@ def main(): print() if not silent_mode: printit(f'[!] An error occured: {e}', coledt=[1, 49, 91], space_up=True) + print_startup_diagnostics(e) # Checking for verbose errors try: if '-e' in sys.argv or '--verbose-errors' in sys.argv: