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 ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ phonsint - Advanced Silent Phone Reconnaissance & Identity Footprinting Suite
3
+ """
4
+
5
+ __version__ = "0.1.0"
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()
@@ -0,0 +1,3 @@
1
+ """
2
+ Core modules for phonsint
3
+ """
@@ -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))
@@ -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
+ )