enumendpoint 7.0__tar.gz

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,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,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,162 @@
1
+ # Website Endpoint Scanner and Rate Limit Tester For Websites (Version 7)
2
+
3
+ ## How to run
4
+
5
+ Command to run after installing **(For installation, look for the 'Installation' section.)**:
6
+
7
+ Example commands to run:
8
+
9
+ ```bash
10
+ enumendpoint (domain, e.g. https://example.com) --ratelimit 100 --testpath /app --show-404s --show-assets
11
+ ```
12
+
13
+ Passable arguments:
14
+
15
+ ```bash
16
+ --ratelimit
17
+ --testpath
18
+ --show-404s
19
+ --disable-extra-files
20
+ --show-assets
21
+ ```
22
+
23
+ | Argument | What the argument does |
24
+ | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `--ratelimit` | how many requests to send to server to test. Without this argument, the rate limit test is not performed. |
26
+ | `--testpath` | which endpoint to test for rate limiting. Without this argument, the rate limit test will happen on the root directory ('/'). If an endpoint here returns a 404, it also defaults to the root directory. |
27
+ | `--show-404s` | Show inaccessible endpoints. |
28
+ | `--disable-extra-files` | The script won't scan through extra map files like robots.txt for extra endpoints. |
29
+ | `--show-assets` | Show assets like images that the script finds. |
30
+
31
+ Example use:
32
+
33
+ ```bash
34
+ enumendpoint example.com --ratelimit 100 --testpath /api/v1
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ To install enumendpoint, run the command:
40
+
41
+ ```bash
42
+ python3 -m pip install git+https://github.com/SphericalFlower52811/endpointscanner.git
43
+ ```
44
+
45
+ After that, install chromium on playwright (playwright will be installed when you install endpointscanner):
46
+
47
+ ```bash
48
+ playwright install chromium
49
+ ```
50
+
51
+ ### You may need to create a virtual environment if there is PEP 668.
52
+
53
+ To create a virtual environment named 'myvenv':
54
+
55
+ ```bash
56
+ python3 -m venv myvenv
57
+ ```
58
+
59
+ To activate virtual environment on Mac/Linux:
60
+
61
+ ```bash
62
+ source myvenv/bin/activate
63
+ ```
64
+
65
+ To activate virtual environment on Windows Command Prompt:
66
+
67
+ ```text
68
+ myvenv\Scripts\activate
69
+ ```
70
+
71
+ To activate virtual environment on Windows PowerShell:
72
+
73
+ ```powershell
74
+ myvenv\Scripts\Activate.ps1
75
+ ```
76
+
77
+ ### Alternative (Not Recommended)
78
+
79
+ If you do not want to create a virtual environment, you can run:
80
+
81
+ ```bash
82
+ python3 -m pip install git+https://github.com/SphericalFlower52811/endpointscanner.git --break-system-packages
83
+ ```
84
+
85
+ to install it without PEP 668.
86
+
87
+ **Warning**: Using `--break-system-packages` may corrupt your OS-managed python environment. Proceed entirely at your own risk. The author is not liable for any system damage if you run this.
88
+
89
+ ### Updating script
90
+
91
+ To update the script, you can run:
92
+
93
+ ```bash
94
+ python3 -m pip install --upgrade git+https://github.com/SphericalFlower52811/endpointscanner.git
95
+ ```
96
+
97
+ ## Details
98
+
99
+ I made this a command line tool that you install with the instructions above
100
+
101
+ The command prints out how many endpoints to test, and lists them all out after testing them.
102
+ It can only bypass simple captchas.
103
+ If there is a captcha in the website, it will detect what captcha it is (as long as it is one of the captcha types below)
104
+
105
+ - google recaptcha
106
+ - hcaptcha
107
+ - cloudflare turnstile
108
+ - perimeterX
109
+ - akamai bot manager
110
+ - kasada
111
+ - incapsula
112
+ - amazon captcha
113
+
114
+ If there are signs of a generic captcha in the website, it will say it is a generic captcha.
115
+
116
+ The python code scans for endpoints in websites by looking through all the js files listed in the htmml, and also checks <script></script> tags. It also prints what JS type it uses. It scans for code like get, post etc, href and much more.
117
+
118
+ Types of JS it can detect, but it is a bit buggy and may list the wrong js type.
119
+ Node.js
120
+ React
121
+ Next.js
122
+ Vue
123
+ Angular
124
+ Vite
125
+ Webpack
126
+
127
+ If there is a {id} inside the path, it replaces it with 1 to test the endpoint whether it is a 200 OK, 404/403, or a redirect (30x Header)
128
+
129
+ 404(Soft) means the server incorrectly returns 200 response while giving a 404 page instead.
130
+
131
+ It also tries very sensitive endpoints like .env, .git/config, and a lot more.
132
+
133
+ ai assisted code btw
134
+
135
+ The code prints the website uptime, how many seconds it takes to load and whether it is fast or not.
136
+
137
+ The code can also test for rate limiting in the website by performing an async function to send 100 GET requests to an endpoint the user wants to test.
138
+
139
+ ## Weaknesses
140
+
141
+ - If there are shells in the page, it may mistake some sensitive endpoints as real. If you see sensitive endpoints in the scan, they may not actually be exposed on the website if the website has a shell. (E.g. .gitignore, .env)
142
+ - If there is a login page, the script will either show that all of the pages require login, or label all of them as 403.
143
+
144
+ ## What was added
145
+
146
+ Version 7 added:
147
+
148
+ - Scanning extra map files (e.g. robots.txt, sitemap.xml) for more endpoints
149
+ - Being able to show assets
150
+ - Hiding inaccessible pages by default
151
+
152
+ ## Plans for next version
153
+
154
+ Version 8 is planned to have:
155
+
156
+ - Detecting what captcha was used if it is blocked
157
+ - Proper detection of timeouts
158
+ - Optimisation (maybe, if not in v9)
159
+
160
+ # Legal Disclaimer
161
+
162
+ Note that this tool is strictly meant for **authorised** testing and security research. Running this script on websites where you are not permitted to do so can result in legal action. The author of this script assumes no responsibility for any misuse or legal consequences from running this script. Ensure you have received permission from the owner of the target owner before performing tests or scans on their website.
@@ -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,10 @@
1
+ LICENSE
2
+ README.md
3
+ enumerateendpoint.py
4
+ pyproject.toml
5
+ enumendpoint.egg-info/PKG-INFO
6
+ enumendpoint.egg-info/SOURCES.txt
7
+ enumendpoint.egg-info/dependency_links.txt
8
+ enumendpoint.egg-info/entry_points.txt
9
+ enumendpoint.egg-info/requires.txt
10
+ enumendpoint.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ enumendpoint = enumerateendpoint:main
@@ -0,0 +1,4 @@
1
+ curl_cffi
2
+ beautifulsoup4
3
+ playwright
4
+ playwright-stealth
@@ -0,0 +1 @@
1
+ enumerateendpoint
@@ -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()
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "enumendpoint"
7
+ version = "7.0"
8
+ dependencies = [
9
+ "curl_cffi",
10
+ "beautifulsoup4",
11
+ "playwright",
12
+ "playwright-stealth"
13
+ ]
14
+
15
+ [project.scripts]
16
+ # pyprojectthing
17
+ enumendpoint = "enumerateendpoint:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+