stryx-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stryx/__init__.py +35 -0
- stryx/ai/__init__.py +5 -0
- stryx/ai/attack_planner.py +199 -0
- stryx/ai/payload_generator.py +212 -0
- stryx/ai/prompts.py +85 -0
- stryx/ai/providers.py +249 -0
- stryx/attacks/__init__.py +6 -0
- stryx/attacks/attack_chain.py +111 -0
- stryx/attacks/replay.py +186 -0
- stryx/auth/__init__.py +1 -0
- stryx/auth/session_manager.py +335 -0
- stryx/cli.py +675 -0
- stryx/comparison/__init__.py +1 -0
- stryx/comparison/differ.py +255 -0
- stryx/config/__init__.py +6 -0
- stryx/config/default_config.yaml +12 -0
- stryx/config/loader.py +111 -0
- stryx/config/schema.py +80 -0
- stryx/crawler/__init__.py +5 -0
- stryx/crawler/discovery.py +178 -0
- stryx/crawler/graphql_discovery.py +86 -0
- stryx/crawler/html_crawler.py +294 -0
- stryx/crawler/js_endpoints.py +165 -0
- stryx/crawler/openapi.py +76 -0
- stryx/crawler/sitemap.py +94 -0
- stryx/orchestrator.py +347 -0
- stryx/payloads/cmdi.txt +31 -0
- stryx/payloads/header_injection.txt +15 -0
- stryx/payloads/ldap.txt +19 -0
- stryx/payloads/nosqli.txt +21 -0
- stryx/payloads/open_redirect.txt +23 -0
- stryx/payloads/path_traversal.txt +26 -0
- stryx/payloads/sqli.txt +31 -0
- stryx/payloads/ssrf.txt +30 -0
- stryx/payloads/ssti.txt +21 -0
- stryx/payloads/xss.txt +48 -0
- stryx/payloads/xxe.txt +35 -0
- stryx/plugins/__init__.py +5 -0
- stryx/plugins/base.py +85 -0
- stryx/policy/__init__.py +1 -0
- stryx/policy/engine.py +201 -0
- stryx/py.typed +0 -0
- stryx/reports/__init__.py +5 -0
- stryx/reports/generator.py +122 -0
- stryx/reports/json_report.py +72 -0
- stryx/reports/markdown_report.py +101 -0
- stryx/reports/sarif_report.py +200 -0
- stryx/reports/templates/report.html.j2 +163 -0
- stryx/reports/terminal_report.py +138 -0
- stryx/scanners/__init__.py +17 -0
- stryx/scanners/auth.py +444 -0
- stryx/scanners/authorization.py +220 -0
- stryx/scanners/blind.py +328 -0
- stryx/scanners/cloud_ssrf.py +208 -0
- stryx/scanners/cors.py +158 -0
- stryx/scanners/dependencies.py +296 -0
- stryx/scanners/disclosure.py +339 -0
- stryx/scanners/fuzz.py +391 -0
- stryx/scanners/graphql.py +309 -0
- stryx/scanners/injection.py +511 -0
- stryx/scanners/race.py +257 -0
- stryx/signatures/framework_fingerprints.yaml +122 -0
- stryx/signatures/known_vulns.yaml +210 -0
- stryx/utils/__init__.py +7 -0
- stryx/utils/evidence.py +109 -0
- stryx/utils/http_client.py +159 -0
- stryx/utils/logging.py +51 -0
- stryx/utils/rate_limiter.py +37 -0
- stryx_cli-0.1.0.dist-info/METADATA +236 -0
- stryx_cli-0.1.0.dist-info/RECORD +74 -0
- stryx_cli-0.1.0.dist-info/WHEEL +5 -0
- stryx_cli-0.1.0.dist-info/entry_points.txt +2 -0
- stryx_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
- stryx_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Information disclosure scanner.
|
|
2
|
+
|
|
3
|
+
Detects server version leaks, debug endpoints, sensitive file exposure,
|
|
4
|
+
stack traces, and secrets/credentials in responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from stryx.utils.evidence import Evidence, Finding, Severity
|
|
12
|
+
from stryx.utils.http_client import HttpClient
|
|
13
|
+
from stryx.utils.logging import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger("scanner.disclosure")
|
|
16
|
+
|
|
17
|
+
# Sensitive paths to check
|
|
18
|
+
SENSITIVE_PATHS = [
|
|
19
|
+
("/.env", "Environment variables file"),
|
|
20
|
+
("/.git/HEAD", "Git repository exposure"),
|
|
21
|
+
("/.git/config", "Git config exposure"),
|
|
22
|
+
("/robots.txt", "Robots.txt with hidden paths"),
|
|
23
|
+
("/sitemap.xml", "Sitemap with hidden endpoints"),
|
|
24
|
+
("/server-info", "Apache server info"),
|
|
25
|
+
("/server-status", "Apache server status"),
|
|
26
|
+
("/phpinfo.php", "PHP information disclosure"),
|
|
27
|
+
("/info.php", "PHP information disclosure"),
|
|
28
|
+
("/debug", "Debug endpoint exposed"),
|
|
29
|
+
("/debug/vars", "Debug variables exposed"),
|
|
30
|
+
("/debug/health", "Debug health endpoint"),
|
|
31
|
+
("/trace.axd", "ASP.NET trace handler"),
|
|
32
|
+
("/elmah.axd", "ELMAH error log"),
|
|
33
|
+
("/actuator", "Spring Boot Actuator"),
|
|
34
|
+
("/actuator/env", "Spring Boot environment"),
|
|
35
|
+
("/actuator/health", "Spring Boot health"),
|
|
36
|
+
("/actuator/configprops", "Spring Boot config properties"),
|
|
37
|
+
("/swagger-ui.html", "Swagger UI exposed"),
|
|
38
|
+
("/swagger.json", "Swagger API spec exposed"),
|
|
39
|
+
("/api-docs", "API documentation exposed"),
|
|
40
|
+
("/graphql", "GraphQL endpoint exposed"),
|
|
41
|
+
("/.well-known/security.txt", "Security contact info"),
|
|
42
|
+
("/crossdomain.xml", "Cross-domain policy"),
|
|
43
|
+
("/clientaccesspolicy.xml", "Silverlight cross-domain"),
|
|
44
|
+
("/backup.zip", "Backup file exposed"),
|
|
45
|
+
("/backup.sql", "Database backup exposed"),
|
|
46
|
+
("/dump.sql", "Database dump exposed"),
|
|
47
|
+
("/db.sql", "Database file exposed"),
|
|
48
|
+
("/config.yml", "Configuration file exposed"),
|
|
49
|
+
("/config.json", "Configuration file exposed"),
|
|
50
|
+
("/.htaccess", "Apache config exposed"),
|
|
51
|
+
("/web.config", "IIS config exposed"),
|
|
52
|
+
("/wp-config.php.bak", "WordPress config backup"),
|
|
53
|
+
("/.DS_Store", "macOS directory metadata"),
|
|
54
|
+
("/Thumbs.db", "Windows thumbnail cache"),
|
|
55
|
+
("/.svn/entries", "SVN repository exposed"),
|
|
56
|
+
("/.hg/dirstate", "Mercurial repository exposed"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Headers that leak information
|
|
60
|
+
LEAKED_HEADERS = {
|
|
61
|
+
"Server": "Server version disclosed",
|
|
62
|
+
"X-Powered-By": "Technology stack disclosed",
|
|
63
|
+
"X-AspNet-Version": "ASP.NET version disclosed",
|
|
64
|
+
"X-AspNetMvc-Version": "ASP.NET MVC version disclosed",
|
|
65
|
+
"X-Runtime": "Framework runtime disclosed",
|
|
66
|
+
"X-Generator": "CMS generator disclosed",
|
|
67
|
+
"X-Debug-Token": "Debug token exposed",
|
|
68
|
+
"X-Debug-Token-Link": "Debug token link exposed",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Secret patterns (regex)
|
|
72
|
+
SECRET_PATTERNS = [
|
|
73
|
+
(re.compile(r'(?i)(?:api[_-]?key|apikey)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{20,})'), "API key detected"),
|
|
74
|
+
(re.compile(r'(?i)(?:secret[_-]?key|secretkey)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{20,})'), "Secret key detected"),
|
|
75
|
+
(re.compile(r'(?i)(?:password|passwd|pwd)\s*[=:]\s*["\']([^"\']{8,})'), "Password detected"),
|
|
76
|
+
(re.compile(r'(?i)(?:aws[_-]?access[_-]?key[_-]?id)\s*[=:]\s*["\']?(AKIA[0-9A-Z]{16})'), "AWS access key detected"),
|
|
77
|
+
(re.compile(r'(?i)(?:aws[_-]?secret[_-]?access[_-]?key)\s*[=:]\s*["\']?([a-zA-Z0-9/+=]{40})'), "AWS secret key detected"),
|
|
78
|
+
(re.compile(r'(?i)(?:ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36}'), "GitHub token detected"),
|
|
79
|
+
(re.compile(r'(?i)sk-[a-zA-Z0-9]{20,}'), "OpenAI/Stripe API key detected"),
|
|
80
|
+
(re.compile(r'(?i)sk_live_[a-zA-Z0-9]{20,}'), "Stripe live key detected"),
|
|
81
|
+
(re.compile(r'(?i)sk_test_[a-zA-Z0-9]{20,}'), "Stripe test key detected"),
|
|
82
|
+
(re.compile(r'(?i)(?:jdbc|mysql|postgres|mongodb)://[^\s"\']+'), "Database connection string detected"),
|
|
83
|
+
(re.compile(r'(?i)-----BEGIN (?:RSA |EC )?PRIVATE KEY-----'), "Private key detected"),
|
|
84
|
+
(re.compile(r'(?i)(?:jwt[_-]?secret)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{16,})'), "JWT secret detected"),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Stack trace indicators
|
|
88
|
+
STACK_TRACE_PATTERNS = [
|
|
89
|
+
re.compile(r'(?i)traceback \(most recent call last\)', re.MULTILINE),
|
|
90
|
+
re.compile(r'(?i)at\s+[\w.$]+\([\w.]+:\d+\)', re.MULTILINE), # Java/.NET stack traces
|
|
91
|
+
re.compile(r'(?i)File "[^"]+", line \d+', re.MULTILINE), # Python stack traces
|
|
92
|
+
re.compile(r'(?i)Exception in thread', re.MULTILINE),
|
|
93
|
+
re.compile(r'(?i)Uncaught exception', re.MULTILINE),
|
|
94
|
+
re.compile(r'(?i)Stack Trace:', re.MULTILINE),
|
|
95
|
+
re.compile(r'(?i)Internal Server Error.*<pre.*>', re.DOTALL),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DisclosureScanner:
|
|
100
|
+
"""Scanner for information disclosure vulnerabilities."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, client: HttpClient):
|
|
103
|
+
self.client = client
|
|
104
|
+
|
|
105
|
+
async def scan(
|
|
106
|
+
self,
|
|
107
|
+
endpoints: list[str],
|
|
108
|
+
base_url: str,
|
|
109
|
+
) -> list[Finding]:
|
|
110
|
+
"""Run information disclosure tests."""
|
|
111
|
+
findings: list[Finding] = []
|
|
112
|
+
logger.info("Running information disclosure scanner")
|
|
113
|
+
|
|
114
|
+
# Test sensitive paths
|
|
115
|
+
path_findings = await self._test_sensitive_paths(base_url)
|
|
116
|
+
findings.extend(path_findings)
|
|
117
|
+
|
|
118
|
+
# Test header leaks on main endpoints
|
|
119
|
+
header_findings = await self._test_header_leaks(endpoints[:5])
|
|
120
|
+
findings.extend(header_findings)
|
|
121
|
+
|
|
122
|
+
# Test for stack traces via error triggering
|
|
123
|
+
trace_findings = await self._test_error_disclosure(endpoints[:5])
|
|
124
|
+
findings.extend(trace_findings)
|
|
125
|
+
|
|
126
|
+
# Test for secrets in responses
|
|
127
|
+
secret_findings = await self._test_secret_disclosure(endpoints[:5])
|
|
128
|
+
findings.extend(secret_findings)
|
|
129
|
+
|
|
130
|
+
logger.info(f"Disclosure scanner found {len(findings)} findings")
|
|
131
|
+
return findings
|
|
132
|
+
|
|
133
|
+
async def _test_sensitive_paths(self, base_url: str) -> list[Finding]:
|
|
134
|
+
"""Test for exposed sensitive files and endpoints."""
|
|
135
|
+
findings: list[Finding] = []
|
|
136
|
+
|
|
137
|
+
for path, description in SENSITIVE_PATHS:
|
|
138
|
+
try:
|
|
139
|
+
url = f"{base_url}{path}"
|
|
140
|
+
response, evidence = await self.client.get(url)
|
|
141
|
+
|
|
142
|
+
if response.status_code == 200:
|
|
143
|
+
# Verify it's not a generic 404 page
|
|
144
|
+
body_lower = response.text.lower()
|
|
145
|
+
if "not found" in body_lower or "404" in body_lower:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Determine severity based on path
|
|
149
|
+
severity = Severity.MEDIUM
|
|
150
|
+
if path in ("/.env", "/.git/HEAD", "/.git/config", "/backup.sql",
|
|
151
|
+
"/dump.sql", "/db.sql", "/web.config", "/wp-config.php.bak"):
|
|
152
|
+
severity = Severity.CRITICAL
|
|
153
|
+
elif path in ("/actuator/env", "/actuator/configprops", "/phpinfo.php",
|
|
154
|
+
"/info.php", "/trace.axd", "/elmah.axd"):
|
|
155
|
+
severity = Severity.HIGH
|
|
156
|
+
elif path in ("/server-info", "/server-status", "/debug",
|
|
157
|
+
"/swagger-ui.html", "/swagger.json", "/api-docs"):
|
|
158
|
+
severity = Severity.MEDIUM
|
|
159
|
+
|
|
160
|
+
evidence.confidence = 0.9
|
|
161
|
+
findings.append(Finding(
|
|
162
|
+
title=f"Sensitive path exposed: {path}",
|
|
163
|
+
severity=severity,
|
|
164
|
+
evidence=evidence,
|
|
165
|
+
description=f"{description}. The path {path} is accessible and returns data.",
|
|
166
|
+
remediation=f"Restrict access to {path} or remove it from production.",
|
|
167
|
+
cwe="CWE-200",
|
|
168
|
+
owasp="A01:2021 - Broken Access Control",
|
|
169
|
+
endpoint=url,
|
|
170
|
+
scanner="disclosure",
|
|
171
|
+
tags=["information-disclosure", "sensitive-path"],
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
except Exception:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
return findings
|
|
178
|
+
|
|
179
|
+
async def _test_header_leaks(self, endpoints: list[str]) -> list[Finding]:
|
|
180
|
+
"""Test for information-leaking HTTP headers."""
|
|
181
|
+
findings: list[Finding] = []
|
|
182
|
+
seen_headers: set[str] = set()
|
|
183
|
+
|
|
184
|
+
for endpoint in endpoints:
|
|
185
|
+
try:
|
|
186
|
+
response, _ = await self.client.get(endpoint)
|
|
187
|
+
|
|
188
|
+
for header_name, description in LEAKED_HEADERS.items():
|
|
189
|
+
if header_name in response.headers and header_name not in seen_headers:
|
|
190
|
+
seen_headers.add(header_name)
|
|
191
|
+
value = response.headers[header_name]
|
|
192
|
+
|
|
193
|
+
# Determine severity
|
|
194
|
+
severity = Severity.LOW
|
|
195
|
+
if header_name in ("X-Debug-Token", "X-Debug-Token-Link"):
|
|
196
|
+
severity = Severity.HIGH
|
|
197
|
+
elif header_name in ("Server", "X-Powered-By"):
|
|
198
|
+
severity = Severity.INFO
|
|
199
|
+
|
|
200
|
+
findings.append(Finding(
|
|
201
|
+
title=f"Information leak via {header_name} header",
|
|
202
|
+
severity=severity,
|
|
203
|
+
evidence=Evidence(
|
|
204
|
+
request_method="GET",
|
|
205
|
+
request_url=endpoint,
|
|
206
|
+
response_status=response.status_code,
|
|
207
|
+
response_headers=dict(response.headers),
|
|
208
|
+
response_body=f"{header_name}: {value}",
|
|
209
|
+
confidence=0.95,
|
|
210
|
+
),
|
|
211
|
+
description=f"{description}: {header_name}: {value}",
|
|
212
|
+
remediation=f"Remove or obfuscate the {header_name} header.",
|
|
213
|
+
cwe="CWE-200",
|
|
214
|
+
owasp="A05:2021 - Security Misconfiguration",
|
|
215
|
+
endpoint=endpoint,
|
|
216
|
+
scanner="disclosure",
|
|
217
|
+
tags=["information-disclosure", "header-leak"],
|
|
218
|
+
))
|
|
219
|
+
|
|
220
|
+
except Exception:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
return findings
|
|
224
|
+
|
|
225
|
+
async def _test_error_disclosure(self, endpoints: list[str]) -> list[Finding]:
|
|
226
|
+
"""Test for stack traces and error disclosure."""
|
|
227
|
+
findings: list[Finding] = []
|
|
228
|
+
|
|
229
|
+
# Payloads that may trigger error responses
|
|
230
|
+
error_triggers = [
|
|
231
|
+
"'",
|
|
232
|
+
"NULL",
|
|
233
|
+
"{{7*7}}",
|
|
234
|
+
"${7*7}",
|
|
235
|
+
"<%7*7%>",
|
|
236
|
+
"\\",
|
|
237
|
+
"%00",
|
|
238
|
+
"../../../etc/passwd",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
for endpoint in endpoints:
|
|
242
|
+
for trigger in error_triggers[:4]:
|
|
243
|
+
try:
|
|
244
|
+
# Try as query parameter
|
|
245
|
+
if "?" in endpoint:
|
|
246
|
+
test_url = f"{endpoint}&q={trigger}"
|
|
247
|
+
else:
|
|
248
|
+
test_url = f"{endpoint}?q={trigger}"
|
|
249
|
+
|
|
250
|
+
response, _ = await self.client.get(test_url)
|
|
251
|
+
|
|
252
|
+
# Check for stack traces
|
|
253
|
+
for pattern in STACK_TRACE_PATTERNS:
|
|
254
|
+
if pattern.search(response.text):
|
|
255
|
+
findings.append(Finding(
|
|
256
|
+
title="Stack trace / error disclosure",
|
|
257
|
+
severity=Severity.MEDIUM,
|
|
258
|
+
evidence=Evidence(
|
|
259
|
+
request_method="GET",
|
|
260
|
+
request_url=test_url,
|
|
261
|
+
response_status=response.status_code,
|
|
262
|
+
response_body=response.text[:1000],
|
|
263
|
+
response_snippet=response.text[:500],
|
|
264
|
+
payload=trigger,
|
|
265
|
+
confidence=0.85,
|
|
266
|
+
),
|
|
267
|
+
description=(
|
|
268
|
+
"The application returns stack traces or detailed error messages "
|
|
269
|
+
"when triggered with malicious input. This leaks internal implementation details."
|
|
270
|
+
),
|
|
271
|
+
remediation=(
|
|
272
|
+
"Implement custom error pages. Disable debug mode in production. "
|
|
273
|
+
"Use structured logging instead of exposing errors to users."
|
|
274
|
+
),
|
|
275
|
+
cwe="CWE-209",
|
|
276
|
+
owasp="A05:2021 - Security Misconfiguration",
|
|
277
|
+
endpoint=endpoint,
|
|
278
|
+
scanner="disclosure",
|
|
279
|
+
tags=["information-disclosure", "stack-trace"],
|
|
280
|
+
))
|
|
281
|
+
break # One finding per endpoint
|
|
282
|
+
|
|
283
|
+
except Exception:
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
return findings
|
|
287
|
+
|
|
288
|
+
async def _test_secret_disclosure(self, endpoints: list[str]) -> list[Finding]:
|
|
289
|
+
"""Scan responses for leaked secrets and credentials."""
|
|
290
|
+
findings: list[Finding] = []
|
|
291
|
+
seen_secrets: set[str] = set()
|
|
292
|
+
|
|
293
|
+
for endpoint in endpoints:
|
|
294
|
+
try:
|
|
295
|
+
response, _ = await self.client.get(endpoint)
|
|
296
|
+
|
|
297
|
+
for pattern, description in SECRET_PATTERNS:
|
|
298
|
+
matches = pattern.findall(response.text)
|
|
299
|
+
for match in matches:
|
|
300
|
+
# Deduplicate
|
|
301
|
+
secret_key = f"{description}:{match[:20]}"
|
|
302
|
+
if secret_key in seen_secrets:
|
|
303
|
+
continue
|
|
304
|
+
seen_secrets.add(secret_key)
|
|
305
|
+
|
|
306
|
+
# Mask the secret in evidence
|
|
307
|
+
masked = match[:4] + "*" * (len(match) - 8) + match[-4:] if len(match) > 8 else "****"
|
|
308
|
+
|
|
309
|
+
findings.append(Finding(
|
|
310
|
+
title=f"Secret/credential exposed: {description}",
|
|
311
|
+
severity=Severity.HIGH,
|
|
312
|
+
evidence=Evidence(
|
|
313
|
+
request_method="GET",
|
|
314
|
+
request_url=endpoint,
|
|
315
|
+
response_status=response.status_code,
|
|
316
|
+
response_body=response.text[:500],
|
|
317
|
+
response_snippet=f"Pattern matched: {masked}",
|
|
318
|
+
confidence=0.8,
|
|
319
|
+
),
|
|
320
|
+
description=(
|
|
321
|
+
f"{description} found in HTTP response. "
|
|
322
|
+
f"The value appears to be: {masked}"
|
|
323
|
+
),
|
|
324
|
+
remediation=(
|
|
325
|
+
"Remove secrets from code and responses. "
|
|
326
|
+
"Use environment variables or secret management services. "
|
|
327
|
+
"Rotate the exposed credentials immediately."
|
|
328
|
+
),
|
|
329
|
+
cwe="CWE-798",
|
|
330
|
+
owasp="A07:2021 - Identification and Authentication Failures",
|
|
331
|
+
endpoint=endpoint,
|
|
332
|
+
scanner="disclosure",
|
|
333
|
+
tags=["information-disclosure", "secret-leak"],
|
|
334
|
+
))
|
|
335
|
+
|
|
336
|
+
except Exception:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
return findings
|