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.
- phantomprobe/__init__.py +68 -0
- phantomprobe/__main__.py +10 -0
- phantomprobe/active.py +339 -0
- phantomprobe/aggressive.py +294 -0
- phantomprobe/asgi.py +19 -0
- phantomprobe/burp.py +358 -0
- phantomprobe/cli.py +372 -0
- phantomprobe/constants.py +18 -0
- phantomprobe/cookies.py +168 -0
- phantomprobe/cve.py +622 -0
- phantomprobe/dashboard.py +858 -0
- phantomprobe/dns_security.py +312 -0
- phantomprobe/doh.py +124 -0
- phantomprobe/http_checks.py +305 -0
- phantomprobe/http_client.py +60 -0
- phantomprobe/js.py +263 -0
- phantomprobe/models.py +33 -0
- phantomprobe/passive.py +512 -0
- phantomprobe/report.py +94 -0
- phantomprobe/screenshot.py +127 -0
- phantomprobe/takeover.py +203 -0
- phantomprobe/waf.py +152 -0
- phantomprobe-0.9.0.dist-info/METADATA +369 -0
- phantomprobe-0.9.0.dist-info/RECORD +27 -0
- phantomprobe-0.9.0.dist-info/WHEEL +4 -0
- phantomprobe-0.9.0.dist-info/entry_points.txt +2 -0
- phantomprobe-0.9.0.dist-info/licenses/LICENSE +21 -0
phantomprobe/__init__.py
ADDED
|
@@ -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}")
|
phantomprobe/__main__.py
ADDED
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
|
+
|