phantomprobe 0.9.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.
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PhantomProbe - Reconnaissance Scanner for Penetration Testing
4
+
5
+ Public API. Optional integrations (dashboard, screenshots, Burp) are imported
6
+ lazily so that the dependency-free core keeps working without extras installed.
7
+ """
8
+
9
+ from .active import ActiveReconEngine
10
+ from .aggressive import AggressiveScanner
11
+ from .cli import main, print_banner
12
+ from .cookies import CookieScanner
13
+ from .constants import BROWSER_USER_AGENT, USER_AGENT, __version__
14
+ from .cve import CVE, CVEMatcher
15
+ from .dns_security import DnsSecurityScanner
16
+ from .http_checks import HstsPreloadScanner, RedirectScanner, SecurityTxtScanner
17
+ from .js import JSEngine, ScriptTagParser
18
+ from .models import Finding, Severity
19
+ from .passive import ReconEngine
20
+ from .report import ReportGenerator
21
+ from .screenshot import ScreenshotCapture
22
+ from .takeover import TakeoverScanner
23
+ from .waf import WafScanner, detect_waf
24
+
25
+ __all__ = [
26
+ "__version__",
27
+ "USER_AGENT",
28
+ "BROWSER_USER_AGENT",
29
+ "Finding",
30
+ "Severity",
31
+ "ReconEngine",
32
+ "ActiveReconEngine",
33
+ "AggressiveScanner",
34
+ "CVE",
35
+ "CVEMatcher",
36
+ "ScreenshotCapture",
37
+ "TakeoverScanner",
38
+ "WafScanner",
39
+ "CookieScanner",
40
+ "DnsSecurityScanner",
41
+ "HstsPreloadScanner",
42
+ "RedirectScanner",
43
+ "SecurityTxtScanner",
44
+ "detect_waf",
45
+ "JSEngine",
46
+ "ScriptTagParser",
47
+ "ReportGenerator",
48
+ "BurpSuiteEngine",
49
+ "DashboardServer",
50
+ "main",
51
+ "print_banner",
52
+ ]
53
+
54
+
55
+ def __getattr__(name):
56
+ """
57
+ Lazily expose optional components.
58
+
59
+ Importing them eagerly would make `import phantomprobe` fail (or pay the
60
+ import cost) when FastAPI / requests are not installed.
61
+ """
62
+ if name in ("DashboardServer", "FASTAPI_AVAILABLE"):
63
+ from . import dashboard
64
+ return getattr(dashboard, name)
65
+ if name in ("BurpSuiteEngine", "BURP_AVAILABLE"):
66
+ from . import burp
67
+ return getattr(burp, name)
68
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PhantomProbe - Entry point for `python -m phantomprobe`
4
+ """
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
phantomprobe/active.py ADDED
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 2: active reconnaissance (port scan, subdomain enumeration, fingerprinting).
4
+
5
+ These checks send traffic directly to the target. Only run them against systems
6
+ you are authorized to test.
7
+ """
8
+
9
+ import concurrent.futures
10
+ import socket
11
+ import ssl
12
+ from datetime import datetime
13
+ from typing import List
14
+ from urllib.request import Request
15
+
16
+ from .http_client import safe_urlopen
17
+
18
+ from .constants import USER_AGENT
19
+ from .models import Finding, Severity
20
+
21
+
22
+ class ActiveReconEngine:
23
+ """Phase 2: Active Reconnaissance"""
24
+
25
+ COMMON_PORTS = [
26
+ 21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 993, 995,
27
+ 1433, 1521, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 27017
28
+ ]
29
+
30
+ COMMON_SUBDOMAINS = [
31
+ # web front doors
32
+ 'www', 'www2', 'web', 'app', 'apps', 'portal', 'secure', 'my',
33
+ 'account', 'accounts', 'login', 'sso', 'auth', 'id', 'oauth',
34
+ # api and services
35
+ 'api', 'apis', 'api-dev', 'api-staging', 'rest', 'graphql', 'grpc',
36
+ 'gateway', 'service', 'services', 'ws', 'socket', 'rpc',
37
+ # environments
38
+ 'dev', 'develop', 'development', 'staging', 'stage', 'stg', 'test',
39
+ 'testing', 'qa', 'uat', 'sandbox', 'demo', 'beta', 'alpha', 'preview',
40
+ 'preprod', 'prod', 'production', 'live', 'internal', 'int',
41
+ # mail
42
+ 'mail', 'smtp', 'imap', 'pop', 'pop3', 'webmail', 'mx', 'mx1', 'mx2',
43
+ 'email', 'exchange', 'autodiscover', 'mailgun', 'newsletter',
44
+ # infra and ops
45
+ 'vpn', 'remote', 'gitlab', 'git', 'jenkins', 'ci', 'cd', 'build',
46
+ 'deploy', 'monitor', 'monitoring', 'grafana', 'kibana', 'prometheus',
47
+ 'status', 'health', 'metrics', 'logs', 'log', 'jira', 'confluence',
48
+ 'wiki', 'nexus', 'registry', 'docker', 'k8s', 'kubernetes',
49
+ # data
50
+ 'db', 'database', 'mysql', 'postgres', 'mongo', 'redis', 'sql',
51
+ 'phpmyadmin', 'pma', 'adminer', 'backup', 'backups', 'ftp', 'sftp',
52
+ 'files', 'file', 'share', 'drive', 'cloud', 's3', 'storage',
53
+ # content and cdn
54
+ 'cdn', 'static', 'assets', 'media', 'img', 'images', 'image', 'video',
55
+ 'download', 'downloads', 'uploads', 'content', 'cache', 'edge',
56
+ # sites and apps
57
+ 'blog', 'news', 'shop', 'store', 'cart', 'checkout', 'pay', 'payment',
58
+ 'billing', 'support', 'help', 'helpdesk', 'docs', 'documentation',
59
+ 'kb', 'forum', 'community', 'events', 'careers', 'jobs', 'about',
60
+ # admin surfaces
61
+ 'admin', 'administrator', 'adm', 'panel', 'cpanel', 'webadmin',
62
+ 'dashboard', 'manage', 'management', 'console', 'control',
63
+ # network
64
+ 'ns', 'ns1', 'ns2', 'ns3', 'dns', 'proxy', 'lb', 'router', 'firewall',
65
+ 'gw', 'dmz', 'ipv4', 'ipv6', 'origin',
66
+ # regions and misc
67
+ 'us', 'eu', 'uk', 'de', 'asia', 'east', 'west', 'mobile', 'm', 'wap',
68
+ 'go', 'link', 'app1', 'app2', 'web1', 'web2', 'old', 'new', 'legacy',
69
+ 'v1', 'v2', 'stats', 'analytics', 'track', 'ads', 'partner', 'partners',
70
+ ]
71
+
72
+ TECH_SIGNATURES = {
73
+ 'nginx': ['nginx'],
74
+ 'apache': ['apache', 'httpd'],
75
+ 'cloudflare': ['cloudflare', 'cf-ray'],
76
+ 'aws': ['amazon', 'aws', 'ec2', 's3'],
77
+ 'google': ['gstatic', 'google', 'gws'],
78
+ 'php': ['php', 'x-powered-by: php'],
79
+ 'asp.net': ['asp.net', 'iis', '.net'],
80
+ 'node.js': ['express', 'node'],
81
+ 'python': ['python', 'django', 'flask', 'gunicorn'],
82
+ 'ruby': ['ruby', 'rails', 'passenger'],
83
+ 'java': ['java', 'tomcat', 'jsp'],
84
+ 'wordpress': ['wordpress', 'wp-'],
85
+ 'drupal': ['drupal'],
86
+ 'joomla': ['joomla'],
87
+ 'laravel': ['laravel'],
88
+ }
89
+
90
+ def __init__(self, target: str):
91
+ self.target = target
92
+ self.findings: List[Finding] = []
93
+ self.subdomain_candidates: List[str] = []
94
+ self.found_subdomains: List[str] = []
95
+
96
+ def scan_ports(self, ports: List[int] = None) -> List[Finding]:
97
+ """Scan common ports"""
98
+ findings = []
99
+ ports = ports or self.COMMON_PORTS
100
+ print(f"[*] Scanning {len(ports)} common ports...")
101
+
102
+ open_ports = []
103
+
104
+ def check_port(port):
105
+ try:
106
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
107
+ sock.settimeout(2)
108
+ if sock.connect_ex((self.target, port)) == 0:
109
+ return port
110
+ except OSError:
111
+ # Unreachable host, refused connection or exhausted sockets:
112
+ # treat as closed rather than aborting the whole sweep.
113
+ pass
114
+ return None
115
+
116
+ with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
117
+ results = executor.map(check_port, ports)
118
+ open_ports = [p for p in results if p is not None]
119
+
120
+ for port in open_ports:
121
+ service = self._identify_service(port)
122
+ findings.append(Finding(
123
+ id=f"PORT-{port}",
124
+ title=f"Open Port: {port}",
125
+ description=f"Port {port} is open ({service})",
126
+ severity=Severity.INFORMATIONAL,
127
+ category="Port Scan",
128
+ evidence=f"Port {port}/{service} is accepting connections",
129
+ remediation="N/A - Information gathering",
130
+ references=["https://en.wikipedia.org/wiki/Port_scanner"],
131
+ discovered_at=datetime.now().isoformat(),
132
+ target=self.target
133
+ ))
134
+
135
+ print(f"[+] Port scan: {len(open_ports)} open ports")
136
+ return findings
137
+
138
+ def _identify_service(self, port: int) -> str:
139
+ """Identify common service by port"""
140
+ services = {
141
+ 21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
142
+ 53: 'DNS', 80: 'HTTP', 110: 'POP3', 143: 'IMAP',
143
+ 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS', 995: 'POP3S',
144
+ 1433: 'MSSQL', 1521: 'Oracle', 3306: 'MySQL',
145
+ 3389: 'RDP', 5432: 'PostgreSQL', 5900: 'VNC',
146
+ 6379: 'Redis', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt',
147
+ 27017: 'MongoDB'
148
+ }
149
+ return services.get(port, 'Unknown')
150
+
151
+ def _resolve_cnames(self, hosts: List[str]) -> dict:
152
+ """CNAME target per host, resolved concurrently over DoH."""
153
+ from . import doh
154
+
155
+ def lookup(host):
156
+ targets = doh.records(host, "CNAME")
157
+ return host, (targets[0].rstrip(".").lower() if targets else None)
158
+
159
+ results = {}
160
+ if not hosts:
161
+ return results
162
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
163
+ for host, cname in executor.map(lookup, hosts):
164
+ if cname:
165
+ results[host] = cname
166
+ return results
167
+
168
+ def enumerate_subdomains(self, wordlist: List[str] = None) -> List[Finding]:
169
+ """Enumerate common subdomains"""
170
+ findings = []
171
+ wordlist = wordlist or self.COMMON_SUBDOMAINS
172
+ print(f"[*] Enumerating {len(wordlist)} common subdomains...")
173
+
174
+ found_subdomains = []
175
+
176
+ def check_subdomain(subdomain):
177
+ try:
178
+ full_domain = f"{subdomain}.{self.target}"
179
+ socket.getaddrinfo(full_domain, None)
180
+ return full_domain
181
+ except socket.gaierror:
182
+ return None
183
+
184
+ with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
185
+ results = executor.map(check_subdomain, wordlist)
186
+ found_subdomains = [s for s in results if s is not None]
187
+
188
+ # Record every candidate tried, not only those that resolved: a
189
+ # dangling CNAME to a dead service has no A record, so the takeover
190
+ # check needs the full list to catch the NXDOMAIN takeovers.
191
+ self.subdomain_candidates = [f"{s}.{self.target}" for s in wordlist]
192
+ self.found_subdomains = found_subdomains
193
+
194
+ # Resolve where each hit points. A CNAME onto a third party is the
195
+ # interesting part of a subdomain finding: it names the cloud service
196
+ # or SaaS the host depends on, which is both recon context and the
197
+ # same signal the takeover check acts on. Only the hits are resolved,
198
+ # so the cost stays proportional to what was actually found.
199
+ cnames = self._resolve_cnames(found_subdomains)
200
+
201
+ for subdomain in found_subdomains:
202
+ cname = cnames.get(subdomain)
203
+ offsite = bool(cname) and not cname.endswith(self.target)
204
+ evidence = f"{subdomain} exists"
205
+ if cname:
206
+ evidence += f"\nCNAME: {cname}"
207
+ if offsite:
208
+ evidence += " (third-party host)"
209
+ findings.append(Finding(
210
+ id=f"SUBDOMAIN-{subdomain.split('.')[0]}",
211
+ title=f"Subdomain Found: {subdomain}",
212
+ description=(
213
+ f"Subdomain {subdomain} resolves"
214
+ + (f" via CNAME to {cname}" if cname else "")
215
+ ),
216
+ severity=Severity.INFORMATIONAL,
217
+ category="Subdomain Enumeration",
218
+ evidence=evidence,
219
+ remediation="N/A - Information gathering",
220
+ references=["https://en.wikipedia.org/wiki/Subdomain"],
221
+ discovered_at=datetime.now().isoformat(),
222
+ target=self.target
223
+ ))
224
+
225
+ print(f"[+] Subdomain enumeration: {len(found_subdomains)} found")
226
+ return findings
227
+
228
+ def fingerprint_tech(self) -> List[Finding]:
229
+ """Technology fingerprinting"""
230
+ findings = []
231
+ print(f"[*] Fingerprinting technologies...")
232
+
233
+ detected_tech = set()
234
+
235
+ try:
236
+ ctx = ssl.create_default_context()
237
+ ctx.check_hostname = False
238
+ ctx.verify_mode = ssl.CERT_NONE
239
+
240
+ req = Request(f"https://{self.target}", method='GET')
241
+ req.add_header('User-Agent', USER_AGENT)
242
+
243
+ with safe_urlopen(req, context=ctx, timeout=10) as response:
244
+ headers_str = str(dict(response.headers)).lower()
245
+ content = response.read(5000).decode('utf-8', errors='ignore').lower()
246
+
247
+ combined = headers_str + content
248
+
249
+ for tech, signatures in self.TECH_SIGNATURES.items():
250
+ for sig in signatures:
251
+ if sig.lower() in combined:
252
+ detected_tech.add(tech)
253
+ break
254
+
255
+ except Exception as e:
256
+ findings.append(Finding(
257
+ id="TECH-Error",
258
+ title="Technology Fingerprinting Error",
259
+ description=f"Could not fingerprint: {str(e)}",
260
+ severity=Severity.INFORMATIONAL,
261
+ category="Technology",
262
+ evidence=str(e),
263
+ remediation="Check target accessibility",
264
+ references=[],
265
+ discovered_at=datetime.now().isoformat(),
266
+ target=self.target
267
+ ))
268
+
269
+ for tech in detected_tech:
270
+ findings.append(Finding(
271
+ id=f"TECH-{tech.replace('.', '').replace(' ', '')}",
272
+ title=f"Technology Detected: {tech}",
273
+ description=f"Target appears to use {tech}",
274
+ severity=Severity.INFORMATIONAL,
275
+ category="Technology",
276
+ evidence=f"{tech} signature detected",
277
+ remediation="N/A - Information gathering",
278
+ references=[],
279
+ discovered_at=datetime.now().isoformat(),
280
+ target=self.target
281
+ ))
282
+
283
+ print(f"[+] Technology fingerprinting: {len(detected_tech)} detected")
284
+ return findings
285
+
286
+ def check_takeovers(self) -> List[Finding]:
287
+ """
288
+ Check enumerated hosts for dangling-CNAME subdomain takeover.
289
+
290
+ Kept separate from the resolve-only enumeration because it reaches out
291
+ over DoH and HTTP to third-party services, which the rest of Phase 2
292
+ does not. Runs against every candidate, resolving or not, since a
293
+ NXDOMAIN takeover has no A record to have been found by.
294
+ """
295
+ from .takeover import TakeoverScanner
296
+
297
+ hosts = self.subdomain_candidates or self.found_subdomains
298
+ findings = TakeoverScanner(self.target).run(hosts)
299
+ self.findings.extend(findings)
300
+ return findings
301
+
302
+ def run(self, check_takeover: bool = True) -> List[Finding]:
303
+ """Run all Phase 2 active reconnaissance"""
304
+ print()
305
+ print("=" * 60)
306
+ print("PHASE 2: Active Reconnaissance")
307
+ print("=" * 60)
308
+ print()
309
+
310
+ # Port scanning
311
+ port_findings = self.scan_ports()
312
+ self.findings.extend(port_findings)
313
+
314
+ # Subdomain enumeration
315
+ subdomain_findings = self.enumerate_subdomains()
316
+ self.findings.extend(subdomain_findings)
317
+
318
+ # Technology fingerprinting
319
+ tech_findings = self.fingerprint_tech()
320
+ self.findings.extend(tech_findings)
321
+
322
+ # Subdomain takeover (uses the hosts enumerated just above)
323
+ if check_takeover:
324
+ self.check_takeovers()
325
+
326
+ print()
327
+ print("=" * 60)
328
+ print("PHASE 2 COMPLETE")
329
+ print("=" * 60)
330
+ print(f"Total findings: {len(self.findings)}")
331
+ print(f" - Ports: {len([f for f in self.findings if f.category == 'Port Scan'])}")
332
+ print(f" - Subdomains: {len([f for f in self.findings if f.category == 'Subdomain Enumeration'])}")
333
+ print(f" - Technologies: {len([f for f in self.findings if f.category == 'Technology'])}")
334
+ print(f" - Takeover: {len([f for f in self.findings if f.category == 'Subdomain Takeover'])}")
335
+ print()
336
+
337
+ return self.findings
338
+
339
+