phonsint 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phonsint/__init__.py +5 -0
- phonsint/__main__.py +150 -0
- phonsint/core/__init__.py +3 -0
- phonsint/core/helpers.py +74 -0
- phonsint/core/impersonate.py +97 -0
- phonsint/core/orchestrator.py +176 -0
- phonsint/core/parser.py +60 -0
- phonsint/core/result.py +276 -0
- phonsint/modules/__init__.py +3 -0
- phonsint/modules/auth/__init__.py +1 -0
- phonsint/modules/auth/microsoft.py +133 -0
- phonsint/modules/crm/__init__.py +0 -0
- phonsint/modules/crm/zoho.py +129 -0
- phonsint/modules/shopping/__init__.py +1 -0
- phonsint/modules/shopping/amazon.py +95 -0
- phonsint/modules/social/__init__.py +1 -0
- phonsint/modules/social/facebook.py +243 -0
- phonsint/modules/social/instagram.py +227 -0
- phonsint-0.1.0.dist-info/METADATA +189 -0
- phonsint-0.1.0.dist-info/RECORD +23 -0
- phonsint-0.1.0.dist-info/WHEEL +4 -0
- phonsint-0.1.0.dist-info/entry_points.txt +3 -0
- phonsint-0.1.0.dist-info/licenses/LICENSE +21 -0
phonsint/__init__.py
ADDED
phonsint/__main__.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import csv
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from colorama import Fore, Style, init
|
|
7
|
+
|
|
8
|
+
from phonsint import __version__
|
|
9
|
+
from phonsint.core.helpers import ScanConfig, load_categories, load_modules
|
|
10
|
+
from phonsint.core.orchestrator import run_scan
|
|
11
|
+
from phonsint.core.parser import parse_phone_number
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
init(autoreset=True)
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
BANNER = rf"""{Fore.CYAN}
|
|
19
|
+
____ __ _ __
|
|
20
|
+
/ __ \/ /_ ____ ____ _____ (_)___ / /_
|
|
21
|
+
/ /_/ / __ \/ __ \/ __ \/ ___// / __ \/ __/
|
|
22
|
+
/ ____/ / / / /_/ / / / (__ )/ / / / / /_
|
|
23
|
+
/_/ /_/ /_/\____/_/ /_/____//_/_/ /_/\__/
|
|
24
|
+
{Fore.LIGHTBLACK_EX} phonsint v{__version__} | Silent Phone Reconnaissance & Intelligence Suite
|
|
25
|
+
{Style.RESET_ALL}"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_banner() -> None:
|
|
29
|
+
print(BANNER)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def list_modules() -> None:
|
|
35
|
+
print_banner()
|
|
36
|
+
categories = load_categories()
|
|
37
|
+
if not categories:
|
|
38
|
+
print(f"{Fore.YELLOW}[!] No modules found.{Style.RESET_ALL}")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
table = Table(title="[bold cyan]Available Scan Modules[/bold cyan]", border_style="cyan")
|
|
42
|
+
table.add_column("Category", style="magenta", width=18)
|
|
43
|
+
table.add_column("Module", style="green", width=22)
|
|
44
|
+
table.add_column("Path", style="dim white")
|
|
45
|
+
|
|
46
|
+
for cat_name, cat_path in sorted(categories.items()):
|
|
47
|
+
mods = load_modules(cat_path)
|
|
48
|
+
for m in mods:
|
|
49
|
+
table.add_row(cat_name.capitalize(), m.__name__.split(".")[-1], str(m.__file__))
|
|
50
|
+
|
|
51
|
+
console.print(table)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def export_results(results, file_path: str, fmt: str = "json") -> None:
|
|
55
|
+
out = Path(file_path)
|
|
56
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
if fmt == "csv" or out.suffix.lower() == ".csv":
|
|
59
|
+
with open(out, "w", newline="", encoding="utf-8") as f:
|
|
60
|
+
writer = csv.writer(f)
|
|
61
|
+
writer.writerow(["site_name", "category", "phone", "status", "url", "extra", "media", "reason"])
|
|
62
|
+
for r in results:
|
|
63
|
+
writer.writerow([
|
|
64
|
+
r.site_name,
|
|
65
|
+
r.category,
|
|
66
|
+
r.phone,
|
|
67
|
+
r.status.to_label(),
|
|
68
|
+
r.url,
|
|
69
|
+
json.dumps(r.extra) if r.extra else "",
|
|
70
|
+
json.dumps(r.media) if r.media else "",
|
|
71
|
+
r.reason or "",
|
|
72
|
+
])
|
|
73
|
+
else:
|
|
74
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
75
|
+
json.dump([r.to_dict() for r in results], f, indent=2)
|
|
76
|
+
|
|
77
|
+
print(f"\n{Fore.GREEN}[+] Results successfully exported to: {out}{Style.RESET_ALL}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main() -> None:
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="phonsint",
|
|
83
|
+
description="phonsint: High-Throughput Silent Phone Number Reconnaissance & Identity Footprinting",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument("-p", "--phone", help="Target phone number (e.g. +14155552671)")
|
|
86
|
+
parser.add_argument("-r", "--region", help="Default ISO country code if number lacks '+' prefix (e.g. US, GB, IN, FR)")
|
|
87
|
+
parser.add_argument("-c", "--category", help="Limit scan to specific comma-separated categories (e.g. auth,social)")
|
|
88
|
+
parser.add_argument("-m", "--module", help="Limit scan to specific comma-separated modules (e.g. microsoft,facebook)")
|
|
89
|
+
parser.add_argument("-l", "--list", action="store_true", help="List all available categories and modules")
|
|
90
|
+
parser.add_argument("--all", action="store_true", help="Show all results including not registered and errors")
|
|
91
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Display URLs and diagnostic reasons")
|
|
92
|
+
parser.add_argument("-t", "--timeout", type=float, default=15.0, help="Per-module request timeout in seconds")
|
|
93
|
+
parser.add_argument("-C", "--concurrency", type=int, default=15, help="Maximum concurrent requests")
|
|
94
|
+
parser.add_argument("--allow-loud", action="store_true", help="Allow modules that may trigger SMS/notifications")
|
|
95
|
+
parser.add_argument("-f", "--format", choices=["json", "csv"], default="json", help="Export format (json/csv)")
|
|
96
|
+
parser.add_argument("-o", "--output", help="Save scan results to file")
|
|
97
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
98
|
+
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
if args.list:
|
|
102
|
+
list_modules()
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if not args.phone:
|
|
106
|
+
print_banner()
|
|
107
|
+
parser.print_help()
|
|
108
|
+
print(f"\n{Fore.RED}[!] Error: Please provide a target phone number using -p / --phone.{Style.RESET_ALL}")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
print_banner()
|
|
112
|
+
|
|
113
|
+
# Step 1: Parse and validate phone number
|
|
114
|
+
phone_info = parse_phone_number(args.phone, args.region)
|
|
115
|
+
print(f"\n{Fore.CYAN} Checking phone: {phone_info.e164}{Style.RESET_ALL}")
|
|
116
|
+
|
|
117
|
+
if not phone_info.is_valid:
|
|
118
|
+
print(f"{Fore.YELLOW}[!] Warning: '{args.phone}' is not a standard allocated number according to E.164.{Style.RESET_ALL}")
|
|
119
|
+
print(f"{Fore.YELLOW}[*] Proceeding with best-effort scan anyway...{Style.RESET_ALL}\n")
|
|
120
|
+
|
|
121
|
+
# Step 2: Build scan configuration
|
|
122
|
+
configs = ScanConfig(
|
|
123
|
+
allow_loud=args.allow_loud,
|
|
124
|
+
show_all=args.all,
|
|
125
|
+
verbose=args.verbose,
|
|
126
|
+
timeout=args.timeout,
|
|
127
|
+
concurrency=args.concurrency,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Step 3: Run platform scan
|
|
131
|
+
results = run_scan(
|
|
132
|
+
phone_info,
|
|
133
|
+
configs,
|
|
134
|
+
category_filter=args.category,
|
|
135
|
+
module_filter=args.module,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Summary count
|
|
139
|
+
taken_count = sum(1 for r in results if r.is_found())
|
|
140
|
+
print(f"\n{Fore.CYAN}== SUMMARY =={Style.RESET_ALL}")
|
|
141
|
+
print(f"Total Sites Scanned: {len(results)}")
|
|
142
|
+
print(f"Registered / Found: {Fore.GREEN}{taken_count}{Style.RESET_ALL}")
|
|
143
|
+
|
|
144
|
+
# Step 4: Export if requested
|
|
145
|
+
if args.output:
|
|
146
|
+
export_results(results, args.output, args.format)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
phonsint/core/helpers.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import inspect
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, cast
|
|
7
|
+
|
|
8
|
+
# List of modules that could potentially alert the target via SMS or phone call
|
|
9
|
+
LOUD_MODULES: List[str] = []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ScanConfig:
|
|
14
|
+
allow_loud: bool = False
|
|
15
|
+
show_all: bool = False
|
|
16
|
+
verbose: bool = False
|
|
17
|
+
timeout: Optional[float] = None
|
|
18
|
+
concurrency: int = 15
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_loud(site_name: str) -> bool:
|
|
22
|
+
return site_name.lower() in LOUD_MODULES
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_site_name(module: ModuleType) -> str:
|
|
26
|
+
name = module.__name__.split(".")[-1].replace("_", " ").title()
|
|
27
|
+
return name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_category(module: ModuleType) -> str:
|
|
31
|
+
parts = module.__name__.split(".")
|
|
32
|
+
if len(parts) >= 3 and parts[-3] == "modules":
|
|
33
|
+
return parts[-2].capitalize()
|
|
34
|
+
return "General"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_categories() -> Dict[str, Path]:
|
|
38
|
+
root = Path(__file__).resolve().parent.parent / "modules"
|
|
39
|
+
categories: Dict[str, Path] = {}
|
|
40
|
+
if not root.exists():
|
|
41
|
+
return categories
|
|
42
|
+
|
|
43
|
+
for subfolder in root.iterdir():
|
|
44
|
+
if subfolder.is_dir() and not subfolder.name.startswith("__"):
|
|
45
|
+
categories[subfolder.name] = subfolder.resolve()
|
|
46
|
+
|
|
47
|
+
return categories
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_modules(category_path: Path) -> List[ModuleType]:
|
|
51
|
+
modules = []
|
|
52
|
+
for file in category_path.glob("*.py"):
|
|
53
|
+
if file.name.startswith("__"):
|
|
54
|
+
continue
|
|
55
|
+
spec = importlib.util.spec_from_file_location(file.stem, str(file))
|
|
56
|
+
if spec is None or spec.loader is None:
|
|
57
|
+
continue
|
|
58
|
+
module = importlib.util.module_from_spec(spec)
|
|
59
|
+
try:
|
|
60
|
+
spec.loader.exec_module(module)
|
|
61
|
+
modules.append(module)
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
return modules
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_scan_func(module: ModuleType) -> Optional[Callable[..., Any]]:
|
|
68
|
+
for attr_name in dir(module):
|
|
69
|
+
if not attr_name.startswith("validate_"):
|
|
70
|
+
continue
|
|
71
|
+
f = getattr(module, attr_name)
|
|
72
|
+
if inspect.isfunction(f) or inspect.iscoroutinefunction(f):
|
|
73
|
+
return cast(Callable[..., Any], f)
|
|
74
|
+
return None
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Literal, Optional, cast
|
|
4
|
+
from curl_cffi import requests as cffi
|
|
5
|
+
|
|
6
|
+
DEFAULT_IMPERSONATE = "chrome"
|
|
7
|
+
DEFAULT_TIMEOUT = 15.0
|
|
8
|
+
|
|
9
|
+
_sessions: dict[tuple, cffi.Session] = {}
|
|
10
|
+
_key_locks: dict[tuple, threading.Lock] = {}
|
|
11
|
+
_warmed: set[tuple] = set()
|
|
12
|
+
_lock = threading.Lock()
|
|
13
|
+
_global_timeout: Optional[float] = None
|
|
14
|
+
_global_proxy: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_global_timeout(val: Optional[float]) -> None:
|
|
18
|
+
global _global_timeout
|
|
19
|
+
_global_timeout = val
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_global_timeout() -> float:
|
|
23
|
+
return _global_timeout if _global_timeout is not None else DEFAULT_TIMEOUT
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_global_proxy(val: Optional[str]) -> None:
|
|
27
|
+
global _global_proxy
|
|
28
|
+
_global_proxy = val
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_global_proxy() -> Optional[str]:
|
|
32
|
+
return _global_proxy
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_warm_session(
|
|
36
|
+
impersonate: str,
|
|
37
|
+
proxy: Optional[str],
|
|
38
|
+
warmup_url: Optional[str] = None,
|
|
39
|
+
) -> cffi.Session:
|
|
40
|
+
key = (impersonate, proxy)
|
|
41
|
+
with _lock:
|
|
42
|
+
session = _sessions.get(key)
|
|
43
|
+
if session is None:
|
|
44
|
+
session = cffi.Session(
|
|
45
|
+
impersonate=impersonate, # type: ignore[arg-type]
|
|
46
|
+
proxies={"http": proxy, "https": proxy} if proxy else None,
|
|
47
|
+
)
|
|
48
|
+
_sessions[key] = session
|
|
49
|
+
_key_locks[key] = threading.Lock()
|
|
50
|
+
|
|
51
|
+
key_lock = _key_locks.get(key)
|
|
52
|
+
if key_lock is None:
|
|
53
|
+
key_lock = threading.Lock()
|
|
54
|
+
_key_locks[key] = key_lock
|
|
55
|
+
|
|
56
|
+
if warmup_url and key not in _warmed:
|
|
57
|
+
with key_lock:
|
|
58
|
+
if key not in _warmed:
|
|
59
|
+
try:
|
|
60
|
+
session.get(warmup_url, timeout=get_global_timeout())
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
_warmed.add(key)
|
|
64
|
+
|
|
65
|
+
return session
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def impersonate_request(
|
|
69
|
+
url: str,
|
|
70
|
+
method: Literal["GET", "POST"] = "GET",
|
|
71
|
+
warmup_url: Optional[str] = None,
|
|
72
|
+
impersonate: str = DEFAULT_IMPERSONATE,
|
|
73
|
+
**kwargs,
|
|
74
|
+
) -> cffi.Response:
|
|
75
|
+
"""Issue a request through a cookie-persistent browser-impersonating curl_cffi session."""
|
|
76
|
+
session = _get_warm_session(impersonate, get_global_proxy(), warmup_url)
|
|
77
|
+
kwargs.setdefault("timeout", get_global_timeout())
|
|
78
|
+
kwargs.setdefault("allow_redirects", False)
|
|
79
|
+
return cast(cffi.Response, session.request(method, url, **kwargs))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def impersonate_request_async(
|
|
83
|
+
url: str,
|
|
84
|
+
method: Literal["GET", "POST"] = "GET",
|
|
85
|
+
warmup_url: Optional[str] = None,
|
|
86
|
+
impersonate: str = DEFAULT_IMPERSONATE,
|
|
87
|
+
**kwargs,
|
|
88
|
+
) -> cffi.Response:
|
|
89
|
+
"""Async wrapper around impersonate_request."""
|
|
90
|
+
return await asyncio.to_thread(
|
|
91
|
+
impersonate_request,
|
|
92
|
+
url,
|
|
93
|
+
method=method,
|
|
94
|
+
warmup_url=warmup_url,
|
|
95
|
+
impersonate=impersonate,
|
|
96
|
+
**kwargs,
|
|
97
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
from typing import Callable, List, Optional, Set
|
|
5
|
+
from colorama import Fore, Style
|
|
6
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, SpinnerColumn, TextColumn
|
|
7
|
+
|
|
8
|
+
from phonsint.core.helpers import (
|
|
9
|
+
ScanConfig,
|
|
10
|
+
find_category,
|
|
11
|
+
get_scan_func,
|
|
12
|
+
get_site_name,
|
|
13
|
+
is_loud,
|
|
14
|
+
load_categories,
|
|
15
|
+
load_modules,
|
|
16
|
+
)
|
|
17
|
+
from phonsint.core.parser import PhoneInfo
|
|
18
|
+
from phonsint.core.result import Result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _async_worker(
|
|
22
|
+
module: ModuleType,
|
|
23
|
+
phone_info: PhoneInfo,
|
|
24
|
+
sem: asyncio.Semaphore,
|
|
25
|
+
configs: ScanConfig,
|
|
26
|
+
cat_override: Optional[str] = None,
|
|
27
|
+
on_start: Optional[Callable[[str], None]] = None,
|
|
28
|
+
) -> Result:
|
|
29
|
+
async with sem:
|
|
30
|
+
site_name = get_site_name(module)
|
|
31
|
+
if on_start:
|
|
32
|
+
on_start(site_name)
|
|
33
|
+
|
|
34
|
+
func = get_scan_func(module)
|
|
35
|
+
category = cat_override or find_category(module)
|
|
36
|
+
|
|
37
|
+
params = {
|
|
38
|
+
"site_name": site_name,
|
|
39
|
+
"phone": phone_info.e164,
|
|
40
|
+
"category": category,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if not func:
|
|
44
|
+
return Result.error(f"{site_name} has no validate_ function", **params)
|
|
45
|
+
|
|
46
|
+
if not configs.allow_loud and is_loud(site_name):
|
|
47
|
+
return Result.skipped(reason="Loud module skipped (use --allow-loud)", **params)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
timeout_val = (configs.timeout or 15.0) + 5.0
|
|
51
|
+
# Check if function accepts phone_info object or string
|
|
52
|
+
sig = inspect.signature(func)
|
|
53
|
+
arg = phone_info if len(sig.parameters) == 1 and next(iter(sig.parameters.values())).name in ("phone_info", "info") else phone_info.e164
|
|
54
|
+
|
|
55
|
+
if inspect.iscoroutinefunction(func):
|
|
56
|
+
raw_res = await asyncio.wait_for(func(arg), timeout=timeout_val)
|
|
57
|
+
else:
|
|
58
|
+
raw_res = await asyncio.wait_for(asyncio.to_thread(func, arg), timeout=timeout_val)
|
|
59
|
+
|
|
60
|
+
if isinstance(raw_res, Result):
|
|
61
|
+
result = raw_res
|
|
62
|
+
else:
|
|
63
|
+
result = Result.error("Module returned invalid result type")
|
|
64
|
+
except asyncio.TimeoutError:
|
|
65
|
+
result = Result.error(f"Timed out after {timeout_val}s")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
result = Result.error(str(e))
|
|
68
|
+
|
|
69
|
+
return result.update(**params)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def run_scan_async(
|
|
73
|
+
phone_info: PhoneInfo,
|
|
74
|
+
configs: ScanConfig,
|
|
75
|
+
category_filter: Optional[str] = None,
|
|
76
|
+
module_filter: Optional[str] = None,
|
|
77
|
+
) -> List[Result]:
|
|
78
|
+
all_categories = load_categories()
|
|
79
|
+
if not all_categories:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
# Apply category filter if requested
|
|
83
|
+
if category_filter:
|
|
84
|
+
cat_targets = [c.strip().lower() for c in category_filter.split(",") if c.strip()]
|
|
85
|
+
categories = {k: v for k, v in all_categories.items() if k.lower() in cat_targets}
|
|
86
|
+
else:
|
|
87
|
+
categories = all_categories
|
|
88
|
+
|
|
89
|
+
# Collect modules to run
|
|
90
|
+
category_modules: List[tuple[str, List[ModuleType]]] = []
|
|
91
|
+
total_modules = 0
|
|
92
|
+
|
|
93
|
+
mod_targets = (
|
|
94
|
+
[m.strip().lower() for m in module_filter.split(",") if m.strip()]
|
|
95
|
+
if module_filter
|
|
96
|
+
else None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
for cat_name, cat_path in categories.items():
|
|
100
|
+
mods = load_modules(cat_path)
|
|
101
|
+
if mod_targets:
|
|
102
|
+
mods = [m for m in mods if m.__name__.split(".")[-1].lower() in mod_targets]
|
|
103
|
+
if mods:
|
|
104
|
+
display_name = cat_name.capitalize()
|
|
105
|
+
category_modules.append((display_name, mods))
|
|
106
|
+
total_modules += len(mods)
|
|
107
|
+
|
|
108
|
+
if total_modules == 0:
|
|
109
|
+
print(f"{Fore.YELLOW}[!] No matching scan modules found.{Style.RESET_ALL}")
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
sem = asyncio.Semaphore(configs.concurrency)
|
|
113
|
+
all_results: List[Result] = []
|
|
114
|
+
printed_cats: Set[str] = set()
|
|
115
|
+
|
|
116
|
+
with Progress(
|
|
117
|
+
SpinnerColumn(),
|
|
118
|
+
TextColumn("[progress.description]{task.description}"),
|
|
119
|
+
BarColumn(),
|
|
120
|
+
MofNCompleteColumn(),
|
|
121
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
122
|
+
transient=True,
|
|
123
|
+
) as progress:
|
|
124
|
+
task_id = progress.add_task(f"[cyan]Scanning {phone_info.e164}...", total=total_modules)
|
|
125
|
+
|
|
126
|
+
def on_start_cb(site: str):
|
|
127
|
+
progress.update(task_id, description=f"[cyan]Scanning {phone_info.e164}... ({site})")
|
|
128
|
+
|
|
129
|
+
spawned_category_tasks = []
|
|
130
|
+
for display_name, modules in category_modules:
|
|
131
|
+
tasks = []
|
|
132
|
+
for module in modules:
|
|
133
|
+
t = asyncio.create_task(
|
|
134
|
+
_async_worker(
|
|
135
|
+
module,
|
|
136
|
+
phone_info,
|
|
137
|
+
sem,
|
|
138
|
+
configs,
|
|
139
|
+
cat_override=display_name,
|
|
140
|
+
on_start=on_start_cb,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
t.add_done_callback(lambda _: progress.advance(task_id))
|
|
144
|
+
tasks.append(t)
|
|
145
|
+
spawned_category_tasks.append((display_name, tasks))
|
|
146
|
+
|
|
147
|
+
for display_name, tasks in spawned_category_tasks:
|
|
148
|
+
if not tasks:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if configs.show_all:
|
|
152
|
+
if display_name not in printed_cats:
|
|
153
|
+
print(f"\n{Fore.MAGENTA}== {display_name.upper()} SITES =={Style.RESET_ALL}")
|
|
154
|
+
printed_cats.add(display_name)
|
|
155
|
+
|
|
156
|
+
for coro in asyncio.as_completed(tasks):
|
|
157
|
+
result = await coro
|
|
158
|
+
if configs.show_all or result.is_visible(configs):
|
|
159
|
+
cat = result.category or display_name
|
|
160
|
+
if cat not in printed_cats:
|
|
161
|
+
print(f"\n{Fore.MAGENTA}== {cat.upper()} SITES =={Style.RESET_ALL}")
|
|
162
|
+
printed_cats.add(cat)
|
|
163
|
+
|
|
164
|
+
result.show(configs)
|
|
165
|
+
all_results.append(result)
|
|
166
|
+
|
|
167
|
+
return all_results
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run_scan(
|
|
171
|
+
phone_info: PhoneInfo,
|
|
172
|
+
configs: ScanConfig,
|
|
173
|
+
category_filter: Optional[str] = None,
|
|
174
|
+
module_filter: Optional[str] = None,
|
|
175
|
+
) -> List[Result]:
|
|
176
|
+
return asyncio.run(run_scan_async(phone_info, configs, category_filter, module_filter))
|
phonsint/core/parser.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import phonenumbers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class PhoneInfo:
|
|
8
|
+
"""Represents a validated and normalized phone number for platform checks."""
|
|
9
|
+
raw_input: str
|
|
10
|
+
is_valid: bool
|
|
11
|
+
e164: str
|
|
12
|
+
international_format: str
|
|
13
|
+
national_format: str
|
|
14
|
+
country_code: int
|
|
15
|
+
national_number: int
|
|
16
|
+
region_code: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_phone_number(raw: str, default_region: Optional[str] = None) -> PhoneInfo:
|
|
20
|
+
"""
|
|
21
|
+
Parse and normalize a raw phone number into standard formats (E.164, national, country code).
|
|
22
|
+
"""
|
|
23
|
+
cleaned = raw.strip()
|
|
24
|
+
test_parse = cleaned if (cleaned.startswith("+") or default_region) else ("+" + cleaned)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
parsed = phonenumbers.parse(cleaned if cleaned.startswith("+") else test_parse, default_region)
|
|
28
|
+
except phonenumbers.NumberParseException:
|
|
29
|
+
try:
|
|
30
|
+
parsed = phonenumbers.parse(cleaned, default_region or "US")
|
|
31
|
+
except phonenumbers.NumberParseException:
|
|
32
|
+
return PhoneInfo(
|
|
33
|
+
raw_input=raw,
|
|
34
|
+
is_valid=False,
|
|
35
|
+
e164=cleaned,
|
|
36
|
+
international_format=cleaned,
|
|
37
|
+
national_format=cleaned,
|
|
38
|
+
country_code=0,
|
|
39
|
+
national_number=0,
|
|
40
|
+
region_code="",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
is_valid = phonenumbers.is_valid_number(parsed)
|
|
44
|
+
e164 = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
|
45
|
+
intl = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
|
|
46
|
+
natl = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.NATIONAL)
|
|
47
|
+
country_code = parsed.country_code
|
|
48
|
+
national_number = parsed.national_number
|
|
49
|
+
region_code = phonenumbers.region_code_for_number(parsed) or ""
|
|
50
|
+
|
|
51
|
+
return PhoneInfo(
|
|
52
|
+
raw_input=raw,
|
|
53
|
+
is_valid=is_valid,
|
|
54
|
+
e164=e164,
|
|
55
|
+
international_format=intl,
|
|
56
|
+
national_format=natl,
|
|
57
|
+
country_code=country_code or 0,
|
|
58
|
+
national_number=int(national_number) if national_number is not None else 0,
|
|
59
|
+
region_code=region_code,
|
|
60
|
+
)
|