enumendpoint 7.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,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: enumendpoint
3
+ Version: 7.0
4
+ License-File: LICENSE
5
+ Requires-Dist: curl_cffi
6
+ Requires-Dist: beautifulsoup4
7
+ Requires-Dist: playwright
8
+ Requires-Dist: playwright-stealth
9
+ Dynamic: license-file
@@ -0,0 +1,7 @@
1
+ enumerateendpoint.py,sha256=nC-ttsmtVskSPRV-kWSgyNc30hPoxLXuSxZgns22WCA,26564
2
+ enumendpoint-7.0.dist-info/licenses/LICENSE,sha256=7rttKh3T46zBKmFfudbz3gF0SL4JkvR0wz81Z3ixUz8,1077
3
+ enumendpoint-7.0.dist-info/METADATA,sha256=VTIFIUVN1LK5XyaeYU_Dm1d5IMK4t9gt8VtSc0UeD_8,213
4
+ enumendpoint-7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ enumendpoint-7.0.dist-info/entry_points.txt,sha256=90VwG7YhGtwI-x2FIB0e7wkQNFhelXDDgEBuvcMNC0Q,56
6
+ enumendpoint-7.0.dist-info/top_level.txt,sha256=W6rq8WOng3JdtjeyETLwwRWFFrcFYGeAvDCw6xywTMQ,18
7
+ enumendpoint-7.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ enumendpoint = enumerateendpoint:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SphericalFlower52811
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ enumerateendpoint
enumerateendpoint.py ADDED
@@ -0,0 +1,566 @@
1
+ import asyncio
2
+ from curl_cffi import requests
3
+ import re
4
+ from bs4 import BeautifulSoup
5
+ from urllib.parse import urljoin, urlparse
6
+ import argparse
7
+ import time
8
+ from playwright.sync_api import sync_playwright #headless browser to solve captcha
9
+ from playwright_stealth import Stealth #Ensure strict firewalls do not block the playwright browser
10
+
11
+ #Will be fixed in version 8
12
+ '''
13
+ def isthere_captcha(response):
14
+ #me when captcha
15
+ html_content = response.text
16
+ html_lower = html_content.lower()
17
+
18
+ # cloudflare turnstile
19
+ if "challenge-platform" in html_content or "cf-challenge" in html_content:
20
+ return True, "Cloudflare Turnstile (Managed Challenge)"
21
+ if "window._cf_chl_opt" in html_content or "cf-ray:" in html_lower:
22
+ return True, "Cloudflare WAF Block Page"
23
+ if "cf-mitigated" in response.headers.get("Server", "").lower():
24
+ return True, "Cloudflare Edge Mitigation"
25
+
26
+ # perimeterx
27
+ if "window._pxappid" in html_lower or "px-captcha" in html_lower:
28
+ return True, "Human Security (PerimeterX) CAPTCHA"
29
+ if "captcha.px-cdn.net" in html_content or "client.perimeterx.net" in html_content:
30
+ return True, "Human Security (PerimeterX) Shield Active"
31
+
32
+ #recaptcha
33
+ if "google.com" in html_lower or "g-recaptcha" in html_lower:
34
+ return True, "Google reCAPTCHA Challenge"
35
+ if "recaptcha.js" in html_lower or "__recaptcha_api" in html_lower:
36
+ return True, "Google reCAPTCHA Script Loaded"
37
+
38
+ #h captcha
39
+ if "hcaptcha.com" in html_content or "h-captcha" in html_lower:
40
+ return True, "hCaptcha Verification Screen"
41
+
42
+ #akamai
43
+ if "akam_bm" in response.cookies or "bm_sz" in response.cookies:
44
+ return True, "Akamai Bot Manager Cookie Block"
45
+ if "_sec_challenge" in html_lower or "akamai-extension" in html_lower:
46
+ return True, "Akamai WAF Challenge Injection"
47
+
48
+ # aws/amazon captcha
49
+ if "aws-waf-token" in html_lower or "awswaf" in html_lower:
50
+ return True, "AWS WAF Token Challenge"
51
+ if "amazon captcha" in html_lower or "amzn-captcha" in html_lower:
52
+ return True, "Amazon Custom CAPTCHA Screen"
53
+
54
+ # incapsula or soemthing
55
+ if "incapsula" in html_lower or "_incap_" in html_lower:
56
+ return True, "Imperva Incapsula Bot Shield"
57
+ if "visid_incap" in response.cookies:
58
+ return True, "Imperva Session Interception"
59
+
60
+ # kasada
61
+ if "kpsdk" in html_lower or "ips.js" in html_lower:
62
+ return True, "Kasada Anti-Bot Handshake"
63
+
64
+ # generic captchas
65
+ if response.status_code in [403, 429]:
66
+ generic_signals = ["captcha", "robot", "automated access", "verify you are human", "checking your browser"]
67
+ for signal in generic_signals:
68
+ if signal in html_lower:
69
+ return True, f"Generic Firewall Block ({signal.title()})"
70
+ return True, f"Unidentified Security Drop (HTTP {response.status_code})"
71
+
72
+ return False, ""
73
+ '''
74
+ HEADER = {
75
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
76
+ "Accept-Language": "en-US, en;q=0.9"
77
+ } #browser header
78
+
79
+ def gethtmlafterload(url):
80
+ with sync_playwright() as p:
81
+ browser = p.chromium.launch(
82
+ headless=True,
83
+ args=[
84
+ "--disable-blink-features=AutomationControlled",
85
+ "--no-sandbox",
86
+ "--disable-infobars"
87
+ ])
88
+ context = browser.new_context(
89
+ user_agent=HEADER['User-Agent'],
90
+ viewport={'width': 1920, 'height': 1080},
91
+ has_touch=True
92
+ )
93
+ page = context.new_page()
94
+ Stealth().apply_stealth_sync(page) #stealth
95
+
96
+ #go to page and get code, using stealth to bypass captchas
97
+ try:
98
+ page.goto(url, wait_until="domcontentloaded", timeout=60000) #wait for page content to download
99
+ page.mouse.move(100, 100)
100
+ page.mouse.move(200, 300)
101
+ page.evaluate("window.scrollTo(0, 500)")
102
+
103
+ page.wait_for_timeout(5000) #wait for page to load downloaded content, and cookie
104
+ mainhtml = page.content()
105
+ cookies = {c['name']: c['value'] for c in context.cookies()}
106
+
107
+ except Exception as e:
108
+ print("Unexpected Error:", e)
109
+ mainhtml, cookies = "", {}
110
+
111
+ browser.close()
112
+ return mainhtml, cookies
113
+
114
+
115
+ def identify_javascript_type(html, headers=None):
116
+ stack = []
117
+ #print(f"\n[DEBUG] HTML Snippet: {html[:1000]}\n")
118
+ # Next.js
119
+ if any(term in html for term in ['data-next-head', 'script id="__NEXT_DATA__"', 'next-head-count', '_next/', '_next/data']):
120
+ stack.append("Next.js")
121
+ # React
122
+ if 'data-reactroot' in html or 'id="root"' in html or 'react-dom' in html.lower():
123
+ stack.append("React")
124
+ # Vue / Angular
125
+ if 'id="app"' in html or 'v-bind' in html: stack.append("Vue.js")
126
+ if 'id="__nuxt"' in html or 'window.__NUXT__' in html: stack.append("Nuxt.js (Vue)")
127
+ if '<app-root' in html or 'ng-version' in html or '_nghost-' in html: stack.append("Angular")
128
+ # Node.js
129
+ if headers:
130
+ powered_by = headers.get('X-Powered-By', '').lower()
131
+ if 'express' in powered_by or 'node' in powered_by:
132
+ stack.append(f"Node.js ({powered_by.capitalize()})")
133
+ # Build Tools
134
+ if any(term in html.lower() for term in ['@vite/client', 'vite-plugin', 'src="/@vite']):
135
+ stack.append("Vite")
136
+ if 'webpack' in html.lower(): stack.append("Webpack")
137
+
138
+ return " + ".join(stack) if stack else "Unknown JS Stack"
139
+
140
+ async def async_rate_test(url, num_reqs=100):
141
+ print(f"\nStarting Rate Limit Test: {num_reqs} requests to {url}")
142
+
143
+ async with requests.AsyncSession(impersonate="chrome120") as session:
144
+ tasks = [session.get(url, headers=HEADER, timeout=10) for _ in range(num_reqs)]
145
+ responses = await asyncio.gather(*tasks, return_exceptions=True)
146
+
147
+ status_counts = {}
148
+ first_limit_at = None
149
+
150
+ for i, res in enumerate(responses):
151
+ request_number = i + 1
152
+ if isinstance(res, Exception):
153
+ status_counts['Error'] = status_counts.get('Error', 0) + 1
154
+ continue
155
+
156
+ code = res.status_code
157
+ status_counts[code] = status_counts.get(code, 0) + 1
158
+
159
+ if code != 200 and first_limit_at is None:
160
+ first_limit_at = (request_number, code)
161
+
162
+ print("\n--- Rate Limit Results ---")
163
+ for code, count in status_counts.items():
164
+ if code == 'Error': continue
165
+ label = "OK" if code == 200 else "LIMITED" if code == 429 else "WAF/FORBIDDEN" if code == 403 else "CRASHED" if code == 500 else "Other"
166
+ print(f" Status {code} ({label}): {count}")
167
+
168
+ if status_counts.get(200, 0) == num_reqs:
169
+ print(f"\nWebsite is potentially vulnerable to DoS or brute-forcing (No rate limit detected after {num_reqs} requests).")
170
+ elif first_limit_at:
171
+ req_num, code = first_limit_at
172
+ if code == 403:
173
+ print(f"\nA WAF (Firewall) likely intercepted the requests (403 Forbidden after {req_num} requests).")
174
+ elif code == 429:
175
+ print(f"\nServer-side rate limiting is active (429 Too Many Requests detected after {req_num} requests).")
176
+ else:
177
+ print(f"\nServer began responding with {code} after {req_num} requests.")
178
+
179
+ def main():
180
+ parser = argparse.ArgumentParser()
181
+ parser.add_argument("target", nargs='?', help="URL")
182
+ parser.add_argument("--ratelimit", nargs='?', const=100, type=int, default=None, help="Number of requests")
183
+ parser.add_argument("--testpath", nargs='?', const='/', type=str, help="Endpoint to test")
184
+ parser.add_argument("--show-404s", action="store_true", help="Show endpoints tested that returned a 404")
185
+ parser.add_argument("--disable-extra-files", action="store_true", help="Disable scanning of extra structural mapping files (robots, sitemaps, manifests, etc.)")
186
+ parser.add_argument("--show-assets", action="store_true", help="Include assets like images and fonts in scan results")
187
+
188
+ args = parser.parse_args()
189
+ if args.testpath and args.ratelimit is None:
190
+ args.ratelimit = 100
191
+
192
+ ignored_extensions = ()
193
+
194
+ # error if the user somehow didn't read the instructions (i've idiotproofed the code ENOUGH)
195
+ if args.testpath and args.ratelimit is None:
196
+ parser.error("--testpath requires the --ratelimit flag.\nIf you want to do a rate limit test, use --ratelimit (number of requests) --testpath (path to test).\nIf not, don't use --ratelimit nor --testpath.")
197
+
198
+ show_dead = args.show_404s
199
+ target = args.target if args.target else input("Enter website (e.g. https://example.com): ").strip()
200
+ if not target.startswith(("http://", "https://")):
201
+ target = "https://" + target
202
+ try:
203
+ response = requests.get(target, headers=HEADER, timeout=5, impersonate="chrome120")
204
+ except requests.exceptions.SSLError:
205
+ if target.startswith("https://"):
206
+ print('[!] HTTPS SSL Error. Trying HTTP...') #becuase later sum http noob
207
+ target = target.replace("https://", "http://")
208
+ try:
209
+ requests.get(target, headers=HEADER, timeout=5, impersonate="chrome120")
210
+ except Exception as e:
211
+ print(f'[!] Target unreachable on HTTP: {e}')
212
+ exit()
213
+ except Exception as e:
214
+ print(f'Target unreachable: {e}')
215
+ exit()
216
+ try:
217
+ st = time.perf_counter()
218
+ uptimeres = requests.get(target, headers=HEADER, timeout=10, impersonate="chrome120")
219
+ et = time.perf_counter()
220
+ restime = et - st
221
+ print(f"Site responded in {round(restime, 2)} seconds.")
222
+
223
+ if restime < 0.5:
224
+ print("Server is very fast.")
225
+ elif restime < 1:
226
+ print("Server is fast.")
227
+ elif restime < 2.5:
228
+ print("Server is average speed.")
229
+ elif restime < 4:
230
+ print("Server is slow.")
231
+ else:
232
+ print("Server is very slow.")
233
+ except requests.exceptions.Timeout:
234
+ print("Server did not respond after 10 seconds.")
235
+
236
+ SENSITIVE_ENDPOINT = {
237
+ "/.env", "/.env.local", "/.env.production", "/.env.development",
238
+ "/.git/config", "/.git/HEAD", "/robots.txt", "/sitemap.xml",
239
+ "/package.json", "/package-lock.json", "/.npmrc", "/.dockerenv",
240
+ "/.gitignore", "/api/health", "/admin", "/login", "/config"
241
+ }
242
+
243
+ results_fromotherfiles = []
244
+ found_paths = set(SENSITIVE_ENDPOINT)
245
+ discovered_in_js = set()
246
+
247
+ if not args.disable_extra_files:
248
+ print("\nFinding paths from map files. (If they exist)")
249
+
250
+ # bobot.txt
251
+ try:
252
+ r_res = requests.get(urljoin(target, "/robots.txt"), headers=HEADER, impersonate="chrome120", timeout=4)
253
+ if r_res.status_code == 200 and "disallow" in r_res.text.lower():
254
+ rules = re.findall(r'(?:Disallow|Allow):\s*(/[a-zA-Z0-9_\-\./{}:|]*)', r_res.text, re.IGNORECASE)
255
+ for rule in rules:
256
+ clean_rule = rule.strip()
257
+ if clean_rule and clean_rule not in ["/", "/*"] and clean_rule not in found_paths:
258
+ found_paths.add(clean_rule)
259
+ results_fromotherfiles.append(f"{clean_rule} [Source: robots.txt]")
260
+ except: pass
261
+
262
+ # sitemap
263
+ try:
264
+ s_res = requests.get(urljoin(target, "/sitemap.xml"), headers=HEADER, impersonate="chrome120", timeout=4)
265
+ if s_res.status_code == 200 and "<loc" in s_res.text.lower():
266
+ locs = re.findall(r'<loc>https?://[^/]+(/[^<]+)</loc>', s_res.text, re.IGNORECASE)
267
+ for loc in locs:
268
+ clean_path = loc.strip()
269
+ if clean_path and clean_path != "/" and clean_path not in found_paths:
270
+ found_paths.add(clean_path)
271
+ results_fromotherfiles.append(f"{clean_path} [Source: sitemap.xml]")
272
+ except: pass
273
+
274
+ # me when manifesto without the o
275
+ try:
276
+ m_res = requests.get(urljoin(target, "/asset-manifest.json"), headers=HEADER, impersonate="chrome120", timeout=4)
277
+ if m_res.status_code == 200 and "{" in m_res.text:
278
+ paths = re.findall(r'["\'](/[a-zA-Z0-9_\-\./]+)["\']', m_res.text)
279
+ for path in paths:
280
+ if path not in found_paths:
281
+ found_paths.add(path)
282
+ results_fromotherfiles.append(f"{path} [Source: asset-manifest.json]")
283
+ except: pass
284
+ for manifest_path in ["/web-manifest.json", "/manifest.json"]:
285
+ try:
286
+ m_url = urljoin(target, manifest_path)
287
+ m_res = requests.get(m_url, headers=HEADER, impersonate="chrome120", timeout=4)
288
+ if m_res.status_code == 200 and "{" in m_res.text:
289
+ paths = re.findall(r'["\'](/[a-zA-Z0-9_\-\./]+)["\']', m_res.text)
290
+ for path in paths:
291
+ if path not in found_paths:
292
+ found_paths.add(path)
293
+ results_fromotherfiles.append(f"{path} [Source: {manifest_path.lstrip('/')}]")
294
+ except: pass
295
+ # service worker lol
296
+ for sw_path in ["/service-worker.js", "/sw.js"]:
297
+ try:
298
+ sw_res = requests.get(urljoin(target, sw_path), headers=HEADER, impersonate="chrome120", timeout=4)
299
+ if sw_res.status_code == 200:
300
+ paths = re.findall(r'["\'`](/[a-zA-Z0-9_\-\./{}:]+)["\'`]', sw_res.text)
301
+ for path in paths:
302
+ if path not in found_paths and not any(path.endswith(ext) for ext in ['.js', '.css']):
303
+ found_paths.add(path)
304
+ results_fromotherfiles.append(f"{path} [Source: {sw_path}]")
305
+ except: pass
306
+
307
+ #openid
308
+ try:
309
+ oidc_res = requests.get(urljoin(target, "/.well-known/openid-configuration"), headers=HEADER, impersonate="chrome120", timeout=4)
310
+ if oidc_res.status_code == 200 and "{" in oidc_res.text:
311
+ paths = re.findall(r'https?://[^/]+(/[^"\']*)', oidc_res.text)
312
+ for path in paths:
313
+ if path not in found_paths:
314
+ found_paths.add(path)
315
+ results_fromotherfiles.append(f"{path} [Source: openid-configuration]")
316
+ except: pass
317
+
318
+ print("\nStarting headless browser to bypass captchas and detect shells with a fake path.")
319
+ main_html, session_cookies = gethtmlafterload(target)
320
+
321
+ fake_path = "/very-fake-page-123456123456abcdefg"
322
+ fake_url = urljoin(target, fake_path)
323
+ try:
324
+ fake_res = requests.get(fake_url, cookies=session_cookies, headers=HEADER, impersonate="chrome120", timeout=10)
325
+ shell_content = fake_res.text
326
+ except:
327
+ shell_content = ""
328
+ try:
329
+ print(f"Detected JS Type: {identify_javascript_type(main_html)}")
330
+ soup = BeautifulSoup(main_html, 'html.parser')
331
+
332
+ # next js code files
333
+ js_files = [urljoin(target, s.get('src')) for s in soup.find_all('script') if s.get('src')]
334
+ js_files.append(urljoin(target, "/_next/static/development/_buildManifest.js"))
335
+ js_files.append(urljoin(target, "/_next/static/runtime/_buildManifest.js"))
336
+
337
+ for script_tag in soup.find_all('script'):
338
+ src = script_tag.get('src')
339
+ if src and not src.startswith(('http://', 'https://')):
340
+ # Use urlparse to strip away parameters and isolate a clean local path
341
+ local_path = urlparse(src).path
342
+ if local_path:
343
+ # Guarantee exactly ONE starting slash, nothing more, nothing less
344
+ clean_path = '/' + local_path.lstrip('/')
345
+ found_paths.add(clean_path)
346
+
347
+ # Gather stylesheets safely without corrupting string slashes
348
+ for link_tag in soup.find_all('link', rel='stylesheet'):
349
+ href = link_tag.get('href')
350
+ if href and not href.startswith(('http://', 'https://')):
351
+ local_path = urlparse(href).path
352
+ if local_path:
353
+ clean_path = '/' + local_path.lstrip('/')
354
+ found_paths.add(clean_path)
355
+ patterns = [
356
+ r'["\'`](/[a-zA-Z0-9_\-\./{}:]*)["\'`]',
357
+ r'(?:path|href|to|post|get|patch|put|delete|head|options)[\s]*[:=\(\|]+[\s]*["\'`](/?[a-zA-Z0-9_\-\./{}:\$]*[\./][a-zA-Z0-9_\-\./{}:\$]*)["\'`]',
358
+ r'[`](https?://[a-zA-Z0-9_\-\./{}:\$]+)[`]'
359
+ ]
360
+
361
+ # check <script> for endpoint too
362
+ inline_scripts = soup.find_all('script')
363
+ for script in inline_scripts:
364
+ if script.string:
365
+ chunks = re.findall(r'["\'](/[a-zA-Z0-9_\-\./]*\.js)["\']', script.string)
366
+ for c in chunks:
367
+ js_files.append(urljoin(target, c))
368
+
369
+ for p in patterns:
370
+ matches = re.findall(p, script.string)
371
+ for m in matches:
372
+ m_clean = re.sub(r'(\$\{.*\}|:[a-zA-Z0-9]+)', '1', m)
373
+ if not m_clean.startswith('/'): m_clean = '/' + m_clean
374
+
375
+ if not m_clean.lower().endswith(ignored_extensions):
376
+ if m_clean in ["/", "//", "///", "/.", "/..", "/..."]:
377
+ continue
378
+ found_paths.add(m_clean)
379
+ discovered_in_js.add(m_clean)
380
+
381
+ # scan all js files
382
+ for js_url in js_files:
383
+ try:
384
+ js_res = requests.get(js_url, headers=HEADER, cookies=session_cookies, timeout=5, impersonate="chrome120")
385
+ if js_res.status_code == 200:
386
+ for p in patterns:
387
+ matches = re.findall(p, js_res.text)
388
+ for m in matches:
389
+ m_clean = re.sub(r'(\$\{.*?\}|:[a-zA-Z0-9]+)', '1', m)
390
+ if not m_clean.startswith('/'): m_clean = '/' + m_clean
391
+
392
+ if not m_clean.lower().endswith(ignored_extensions):
393
+ if m_clean in ["/", "//", "///", "/.", "/..", "/..."]:
394
+ continue
395
+ found_paths.add(m_clean)
396
+ discovered_in_js.add(m_clean)
397
+ except: continue
398
+
399
+ print(f"Total paths to test: {len(found_paths)} ({len(discovered_in_js)} scraped from JS).")
400
+ print("Testing endpoints...")
401
+ results_200, results_dead, results_30x = [], [], []
402
+ results_services, results_ext = [], []
403
+ results_frameworks, results_assets = [], []
404
+
405
+ for path in sorted(found_paths):
406
+ # all external url cuz https: //blahblah
407
+ if "://" in path:
408
+ results_ext.append(path.lstrip('/'))
409
+ continue
410
+
411
+ try:
412
+ parsed_path = urlparse(path)
413
+ target_domain = urlparse(target).netloc
414
+
415
+ # get the base domain (efg.hijk from abcd.efg.hijk)
416
+ def get_base(domain):
417
+ parts = domain.split('.')
418
+ return ".".join(parts[-2:]) if len(parts) > 1 else domain
419
+
420
+ is_external = parsed_path.netloc and get_base(parsed_path.netloc) != get_base(target_domain)
421
+
422
+ if is_external:
423
+ results_ext.append(f"{path.lstrip('/')} [External Reference]")
424
+ continue
425
+
426
+
427
+ r = requests.get(
428
+ urljoin(target, path),
429
+ headers=HEADER,
430
+ cookies=session_cookies,
431
+ timeout=5,
432
+ allow_redirects=False,
433
+ impersonate="chrome120"
434
+ )
435
+
436
+ content_type = r.headers.get("Content-Type", "").lower()
437
+ is_shell = (r.text == shell_content)
438
+ is_home_redirect = (r.text == main_html)
439
+
440
+ # common service and api i think
441
+ service_markers = ["/api", "/v1", "/v2", "socket.io", "engine.io", "/graphql", "/webhook", "/rpc"]
442
+ is_machine_path = any(marker in path.lower() for marker in service_markers)
443
+
444
+ media_extensions = ('.png', '.jpg', '.jpeg', '.svg', '.webp', '.gif', '.ico', '.woff', '.woff2', '.ttf')
445
+ framework_extensions = ('.js', '.css', '.json', '.txt', '.xml', '.map')
446
+
447
+ is_media_asset = path.lower().endswith(media_extensions)
448
+ is_framework_asset = any(path.lower().endswith(ext) for ext in framework_extensions)
449
+
450
+ if r.status_code == 200:
451
+ # if its a service/api path then like service and stuff ykyk
452
+ if is_machine_path:
453
+ results_services.append(f"{path} [Service/API]")
454
+ elif is_shell or is_home_redirect:
455
+ if path in discovered_in_js:
456
+ results_200.append(f"{path} [Client-Side Route, Requires Login]")
457
+ else:
458
+ results_dead.append(f"404 Not Found (React Shell): {path}")
459
+ else:
460
+ if "text/html" in content_type:
461
+ results_200.append(f"{path} [Access no matter what]")
462
+ elif is_media_asset:
463
+ results_assets.append(f"{path}")
464
+ elif is_framework_asset:
465
+ results_frameworks.append(f"{path}")
466
+ else:
467
+ #standard is like js and css
468
+ results_frameworks.append(f"{path} [Non-Standard File]")
469
+
470
+
471
+ elif r.status_code == 400:
472
+ if is_framework_asset:
473
+ results_frameworks.append(f"{path} [Asset Error - 400]")
474
+ # get machine services like socket.io that reject simple GET request. cuz socket stinky.
475
+ if "." in path or "/" in path:
476
+ results_services.append(f"{path} [Potential Service - 400]")
477
+ elif is_machine_path:
478
+ results_services.append(f"{path} [Service/API]") #cuz socket
479
+ else:
480
+ results_dead.append(f"400 Bad Request: {path}")
481
+
482
+ elif r.status_code in [403, 404]:
483
+ results_dead.append(f"{r.status_code} Error: {path}")
484
+ elif str(r.status_code).startswith('3'):
485
+ results_30x.append(f"{path} -> {r.headers.get('Location')}")
486
+ except: continue
487
+
488
+
489
+
490
+ if len(results_200) != 0:
491
+ print("\n---- ENDPOINTS FOUND ----")
492
+ for p in results_200: print(f" {p}")
493
+ else:
494
+ print("\n----NO ENDPOITNS FOUND----")
495
+ if len(results_services) != 0:
496
+ print("\n----SERVICES/APIS USED----")
497
+ for p in results_services: print(f" {p}")
498
+ else:
499
+ print("\n----NO SERVICES/APIS FOUND----")
500
+ if len(results_ext) != 0:
501
+ print("\n----EXTERNAL LINKS----")
502
+ for p in results_ext: print(f" {p}")
503
+ else:
504
+ print("\n----NO EXTERNAL LINKS FOUND----")
505
+ if len(results_frameworks) != 0:
506
+ print("\n----WEBSITE FRAMEWORKS----")
507
+ for p in results_frameworks: print(f" {p}")
508
+ else:
509
+ print("\n----NO WEBSITE FRAMEWORKS FOUND----")
510
+ if len(results_30x) != 0:
511
+ print("\n---- REDIRECTS (301/302/307) ----")
512
+ for p in results_30x: print(f" {p}")
513
+ else:
514
+ print("\n----NO REDIRECTS FOUND----")
515
+ # if result contains stuff
516
+ if not args.disable_extra_files:
517
+ if results_fromotherfiles:
518
+ print("\n---- PATHS FROM OTHER FILES ----")
519
+ for entry in results_fromotherfiles:
520
+ print(f" {entry}")
521
+ else:
522
+ print("\n----NO EXTRA PATHS FOUND FROM OTHER FILES----")
523
+ else:
524
+ pass
525
+ if args.show_assets:
526
+ if len(results_assets) != 0:
527
+ print("\n----WEBSITE ASSETS----")
528
+ for p in results_assets: print(f" {p}")
529
+ else:
530
+ print("\n----NO WEBSITE ASSETS FOUND----")
531
+ else:
532
+ pass
533
+ if show_dead:
534
+ if len(results_dead) != 0:
535
+ print("\n---- INACCESSIBLE (Confirmed 404/403) ----")
536
+ for p in results_dead: print(f" {p}")
537
+ else:
538
+ print("\n----NONE INACCESSIBLE 404/403----")
539
+
540
+ print(f"\n--- Scan Summary ---")
541
+ if args.show_assets:
542
+ print(f"Total Accessible Pages: {len(results_200)}\nTotal Assets: {len(results_assets)}\nTotal Inaccessible: {len(results_dead)}\nTotal Redirects: {len(results_30x)}\nTotal Frameworks: {len(results_frameworks)}")
543
+ else:
544
+ print(f"Total Accessible Pages: {len(results_200)}\nTotal Assets: {len(results_assets)} (Assets are hidden, use --show-assets to show them.)\nTotal Inaccessible: {len(results_dead)}\nTotal Redirects: {len(results_30x)}\nTotal Frameworks: {len(results_frameworks)}")
545
+
546
+
547
+ if args.ratelimit is not None:
548
+ num = args.ratelimit
549
+ test_path = "/" #root dir
550
+ if args.testpath: #idiotproof
551
+ test_path = args.testpath if args.testpath.startswith('/') else '/' + args.testpath
552
+ try:
553
+ check_res = requests.get(urljoin(target, test_path), headers=HEADER, timeout=5, impersonate="chrome120")
554
+ if check_res.status_code in [301, 302, 307, 308, 403, 404]:
555
+ print(f"{test_path} is {check_res.status_code}. Testing on root domain.")
556
+ test_path = "/"
557
+ except:
558
+ test_path = "/"
559
+
560
+ asyncio.run(async_rate_test(urljoin(target, test_path), num))
561
+
562
+ except Exception as e:
563
+ print(f"Main Error: {e}")
564
+
565
+ if __name__ == "__main__":
566
+ main()