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.
Files changed (74) hide show
  1. stryx/__init__.py +35 -0
  2. stryx/ai/__init__.py +5 -0
  3. stryx/ai/attack_planner.py +199 -0
  4. stryx/ai/payload_generator.py +212 -0
  5. stryx/ai/prompts.py +85 -0
  6. stryx/ai/providers.py +249 -0
  7. stryx/attacks/__init__.py +6 -0
  8. stryx/attacks/attack_chain.py +111 -0
  9. stryx/attacks/replay.py +186 -0
  10. stryx/auth/__init__.py +1 -0
  11. stryx/auth/session_manager.py +335 -0
  12. stryx/cli.py +675 -0
  13. stryx/comparison/__init__.py +1 -0
  14. stryx/comparison/differ.py +255 -0
  15. stryx/config/__init__.py +6 -0
  16. stryx/config/default_config.yaml +12 -0
  17. stryx/config/loader.py +111 -0
  18. stryx/config/schema.py +80 -0
  19. stryx/crawler/__init__.py +5 -0
  20. stryx/crawler/discovery.py +178 -0
  21. stryx/crawler/graphql_discovery.py +86 -0
  22. stryx/crawler/html_crawler.py +294 -0
  23. stryx/crawler/js_endpoints.py +165 -0
  24. stryx/crawler/openapi.py +76 -0
  25. stryx/crawler/sitemap.py +94 -0
  26. stryx/orchestrator.py +347 -0
  27. stryx/payloads/cmdi.txt +31 -0
  28. stryx/payloads/header_injection.txt +15 -0
  29. stryx/payloads/ldap.txt +19 -0
  30. stryx/payloads/nosqli.txt +21 -0
  31. stryx/payloads/open_redirect.txt +23 -0
  32. stryx/payloads/path_traversal.txt +26 -0
  33. stryx/payloads/sqli.txt +31 -0
  34. stryx/payloads/ssrf.txt +30 -0
  35. stryx/payloads/ssti.txt +21 -0
  36. stryx/payloads/xss.txt +48 -0
  37. stryx/payloads/xxe.txt +35 -0
  38. stryx/plugins/__init__.py +5 -0
  39. stryx/plugins/base.py +85 -0
  40. stryx/policy/__init__.py +1 -0
  41. stryx/policy/engine.py +201 -0
  42. stryx/py.typed +0 -0
  43. stryx/reports/__init__.py +5 -0
  44. stryx/reports/generator.py +122 -0
  45. stryx/reports/json_report.py +72 -0
  46. stryx/reports/markdown_report.py +101 -0
  47. stryx/reports/sarif_report.py +200 -0
  48. stryx/reports/templates/report.html.j2 +163 -0
  49. stryx/reports/terminal_report.py +138 -0
  50. stryx/scanners/__init__.py +17 -0
  51. stryx/scanners/auth.py +444 -0
  52. stryx/scanners/authorization.py +220 -0
  53. stryx/scanners/blind.py +328 -0
  54. stryx/scanners/cloud_ssrf.py +208 -0
  55. stryx/scanners/cors.py +158 -0
  56. stryx/scanners/dependencies.py +296 -0
  57. stryx/scanners/disclosure.py +339 -0
  58. stryx/scanners/fuzz.py +391 -0
  59. stryx/scanners/graphql.py +309 -0
  60. stryx/scanners/injection.py +511 -0
  61. stryx/scanners/race.py +257 -0
  62. stryx/signatures/framework_fingerprints.yaml +122 -0
  63. stryx/signatures/known_vulns.yaml +210 -0
  64. stryx/utils/__init__.py +7 -0
  65. stryx/utils/evidence.py +109 -0
  66. stryx/utils/http_client.py +159 -0
  67. stryx/utils/logging.py +51 -0
  68. stryx/utils/rate_limiter.py +37 -0
  69. stryx_cli-0.1.0.dist-info/METADATA +236 -0
  70. stryx_cli-0.1.0.dist-info/RECORD +74 -0
  71. stryx_cli-0.1.0.dist-info/WHEEL +5 -0
  72. stryx_cli-0.1.0.dist-info/entry_points.txt +2 -0
  73. stryx_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
  74. 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