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
stryx/scanners/fuzz.py ADDED
@@ -0,0 +1,391 @@
1
+ """API fuzzer module.
2
+
3
+ Mutates parameters, headers, cookies, JSON bodies, and query strings
4
+ to test for type confusion, boundary values, integer overflow, missing
5
+ validation, invalid encodings, deeply nested JSON, and large payloads.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+ from urllib.parse import parse_qs, urlencode, urlparse
13
+
14
+ from stryx.utils.evidence import Evidence, Finding, Severity
15
+ from stryx.utils.http_client import HttpClient
16
+ from stryx.utils.logging import get_logger
17
+
18
+ logger = get_logger("scanner.fuzz")
19
+
20
+ # Fuzz payloads for different mutation types
21
+ FUZZ_PAYLOADS = {
22
+ "integer": [
23
+ "0", "-1", "1", "999999999999", "2147483647", "2147483648",
24
+ "-2147483648", "-2147483649", "0x7FFFFFFF", "0x80000000",
25
+ "99999999999999999999", "1.5", "NaN", "Infinity",
26
+ ],
27
+ "string": [
28
+ "", "a", "a" * 1000, "a" * 10000,
29
+ "'", "\"", "\\", "\\'", "\\\"",
30
+ "<script>alert(1)</script>",
31
+ "{{7*7}}", "${7*7}",
32
+ "%00", "%0a", "%0d%0a",
33
+ "true", "false", "null", "undefined",
34
+ "[]", "{}", "[[]]",
35
+ ],
36
+ "boundary": [
37
+ "0", "-0", "+0", "0.0", "-0.0",
38
+ "1.7976931348623157E+308",
39
+ "4.9E-324",
40
+ "1e999", "-1e999",
41
+ "99999999999999999999999999",
42
+ ],
43
+ "encoding": [
44
+ "%00", "%0a", "%0d", "%0d%0a",
45
+ "%ef%bb%bf", "%c0%80", "%e0%80%80",
46
+ "\\u0000", "\\n", "\\r",
47
+ "\x00", "\n", "\r\n",
48
+ ],
49
+ "nested_json": [
50
+ json.dumps({"a": {"b": {"c": {"d": {"e": "f"}}}}}),
51
+ json.dumps({"a": [1, [2, [3, [4]]]]}),
52
+ json.dumps({"__proto__": {"admin": True}}),
53
+ json.dumps({"constructor": {"prototype": {"admin": True}}}),
54
+ ],
55
+ }
56
+
57
+
58
+ class FuzzScanner:
59
+ """Scanner for API fuzzing and input validation."""
60
+
61
+ def __init__(self, client: HttpClient):
62
+ self.client = client
63
+
64
+ async def scan(
65
+ self,
66
+ endpoints: list[str],
67
+ base_url: str,
68
+ tests: list[str] | None = None,
69
+ ) -> list[Finding]:
70
+ """Run fuzzing tests against endpoints.
71
+
72
+ Args:
73
+ endpoints: List of endpoint URLs to test.
74
+ base_url: Base URL of the target.
75
+ tests: List of test types to run (None = all).
76
+ Valid: parameters, body, headers, cookies, multipart.
77
+ """
78
+ findings: list[Finding] = []
79
+ logger.info("Running fuzz scanner")
80
+
81
+ all_tests = ["parameters", "body", "headers", "cookies", "multipart"]
82
+ active_tests = tests or all_tests
83
+
84
+ for endpoint in endpoints:
85
+ try:
86
+ if "parameters" in active_tests:
87
+ param_findings = await self._fuzz_parameters(endpoint)
88
+ findings.extend(param_findings)
89
+
90
+ if "body" in active_tests:
91
+ body_findings = await self._fuzz_body(endpoint)
92
+ findings.extend(body_findings)
93
+
94
+ if "headers" in active_tests:
95
+ header_findings = await self._fuzz_headers(endpoint)
96
+ findings.extend(header_findings)
97
+
98
+ if "cookies" in active_tests:
99
+ cookie_findings = await self._fuzz_cookies(endpoint)
100
+ findings.extend(cookie_findings)
101
+
102
+ if "multipart" in active_tests:
103
+ multipart_findings = await self._fuzz_multipart(endpoint)
104
+ findings.extend(multipart_findings)
105
+
106
+ except Exception as e:
107
+ logger.debug(f"Fuzz error on {endpoint}: {e}")
108
+
109
+ logger.info(f"Fuzz scanner found {len(findings)} findings")
110
+ return findings
111
+
112
+ async def _fuzz_parameters(self, endpoint: str) -> list[Finding]:
113
+ """Fuzz query parameters of an endpoint."""
114
+ findings: list[Finding] = []
115
+ parsed = urlparse(endpoint)
116
+ params = parse_qs(parsed.query)
117
+
118
+ if not params:
119
+ return findings
120
+
121
+ for param_name, param_values in params.items():
122
+ original_value = param_values[0] if param_values else ""
123
+
124
+ # Determine fuzz category based on original value
125
+ categories = _guess_param_type(original_value)
126
+
127
+ for category in categories:
128
+ for payload in FUZZ_PAYLOADS.get(category, [])[:8]:
129
+ try:
130
+ test_params = dict(params)
131
+ test_params[param_name] = [payload]
132
+ test_url = (
133
+ f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
134
+ f"?{urlencode(test_params, doseq=True)}"
135
+ )
136
+
137
+ response, evidence = await self.client.get(test_url)
138
+
139
+ # Check for error-based indicators
140
+ if self._detect_fuzz_issue(response, payload, evidence):
141
+ evidence.confidence = 0.6
142
+ evidence.payload = payload
143
+ findings.append(Finding(
144
+ title=f"Fuzzing issue in parameter '{param_name}' ({category})",
145
+ severity=Severity.MEDIUM,
146
+ evidence=evidence,
147
+ description=(
148
+ f"Fuzzing parameter '{param_name}' with {category} "
149
+ f"payload caused an unexpected response."
150
+ ),
151
+ remediation="Validate and sanitize all input parameters.",
152
+ cwe="CWE-20",
153
+ owasp="A03:2021 - Injection",
154
+ scanner="fuzz",
155
+ tags=["fuzzing", "input-validation"],
156
+ ))
157
+ break # One finding per category per param
158
+ except Exception:
159
+ continue
160
+
161
+ return findings
162
+
163
+ async def _fuzz_body(self, endpoint: str) -> list[Finding]:
164
+ """Fuzz the request body."""
165
+ findings: list[Finding] = []
166
+
167
+ # Try nested JSON
168
+ for payload in FUZZ_PAYLOADS["nested_json"][:3]:
169
+ try:
170
+ response, evidence = await self.client.post(
171
+ endpoint,
172
+ body=payload,
173
+ headers={"Content-Type": "application/json"},
174
+ )
175
+ if self._detect_fuzz_issue(response, payload, evidence):
176
+ evidence.confidence = 0.5
177
+ evidence.payload = payload
178
+ findings.append(Finding(
179
+ title="Potential prototype pollution or nested JSON issue",
180
+ severity=Severity.MEDIUM,
181
+ evidence=evidence,
182
+ description="Deeply nested or prototype-polluting JSON payload caused an unexpected response.",
183
+ remediation="Validate JSON structure depth and reject prototype-polluting keys.",
184
+ cwe="CWE-1321",
185
+ owasp="A03:2021 - Injection",
186
+ scanner="fuzz",
187
+ tags=["fuzzing", "prototype-pollution"],
188
+ ))
189
+ break
190
+ except Exception:
191
+ continue
192
+
193
+ return findings
194
+
195
+ def _detect_fuzz_issue(
196
+ self, response: Any, payload: str, evidence: Evidence
197
+ ) -> bool:
198
+ """Detect if a fuzzing payload caused an issue."""
199
+ status = response.status_code
200
+ body = response.text.lower() if hasattr(response, "text") else ""
201
+
202
+ # Error indicators
203
+ error_patterns = [
204
+ "traceback", "exception", "error",
205
+ "internal server error", "500",
206
+ "stack trace", "debug",
207
+ "unhandled", "panic",
208
+ ]
209
+
210
+ if status >= 500:
211
+ return True
212
+
213
+ return any(p in body for p in error_patterns)
214
+
215
+ async def _fuzz_headers(self, endpoint: str) -> list[Finding]:
216
+ """Fuzz HTTP headers for injection and bypass."""
217
+ findings: list[Finding] = []
218
+
219
+ # Headers to fuzz
220
+ fuzz_headers = [
221
+ "User-Agent", "X-Forwarded-For", "X-Real-IP",
222
+ "X-Original-URL", "X-Rewrite-URL", "X-Custom-IP-Authorization",
223
+ "X-Forwarded-Host", "X-Host", "X-Remote-Addr",
224
+ "Content-Type", "Accept", "Authorization",
225
+ ]
226
+
227
+ # Payloads for header fuzzing
228
+ header_payloads = [
229
+ "<script>alert(1)</script>",
230
+ "{{7*7}}",
231
+ "' OR '1'='1",
232
+ "../../etc/passwd",
233
+ "A" * 1000, # Long header
234
+ "%0d%0a%0d%0a", # CRLF injection
235
+ "\x00", # Null byte
236
+ ]
237
+
238
+ for header_name in fuzz_headers:
239
+ for payload in header_payloads[:5]: # Limit payloads
240
+ try:
241
+ response, evidence = await self.client.get(
242
+ endpoint,
243
+ headers={header_name: payload},
244
+ )
245
+ if self._detect_fuzz_issue(response, payload, evidence):
246
+ evidence.confidence = 0.6
247
+ evidence.payload = payload
248
+ findings.append(Finding(
249
+ title=f"Fuzzing issue in header '{header_name}'",
250
+ severity=Severity.MEDIUM,
251
+ evidence=evidence,
252
+ description=(
253
+ f"Fuzzing header '{header_name}' with payload "
254
+ f"caused an unexpected response."
255
+ ),
256
+ remediation="Validate and sanitize all header values.",
257
+ cwe="CWE-20",
258
+ owasp="A03:2021 - Injection",
259
+ scanner="fuzz",
260
+ tags=["fuzzing", "header-injection"],
261
+ ))
262
+ break # One finding per header
263
+ except Exception:
264
+ continue
265
+
266
+ return findings
267
+
268
+ async def _fuzz_cookies(self, endpoint: str) -> list[Finding]:
269
+ """Fuzz cookies for injection and session manipulation."""
270
+ findings: list[Finding] = []
271
+
272
+ # Cookie names to fuzz
273
+ cookie_names = [
274
+ "session_id", "token", "user_id", "role", "admin",
275
+ "debug", "test", "language", "theme",
276
+ ]
277
+
278
+ # Payloads for cookie fuzzing
279
+ cookie_payloads = [
280
+ "admin",
281
+ "true",
282
+ "1",
283
+ "<script>alert(1)</script>",
284
+ "' OR '1'='1",
285
+ "../../etc/passwd",
286
+ "A" * 500, # Long cookie
287
+ "%0d%0a", # CRLF injection
288
+ ]
289
+
290
+ for cookie_name in cookie_names:
291
+ for payload in cookie_payloads[:5]:
292
+ try:
293
+ response, evidence = await self.client.get(
294
+ endpoint,
295
+ cookies={cookie_name: payload},
296
+ )
297
+ if self._detect_fuzz_issue(response, payload, evidence):
298
+ evidence.confidence = 0.6
299
+ evidence.payload = f"{cookie_name}={payload}"
300
+ findings.append(Finding(
301
+ title=f"Fuzzing issue in cookie '{cookie_name}'",
302
+ severity=Severity.MEDIUM,
303
+ evidence=evidence,
304
+ description=(
305
+ f"Fuzzing cookie '{cookie_name}' with payload "
306
+ f"caused an unexpected response."
307
+ ),
308
+ remediation="Validate and sanitize all cookie values.",
309
+ cwe="CWE-20",
310
+ owasp="A03:2021 - Injection",
311
+ scanner="fuzz",
312
+ tags=["fuzzing", "cookie-injection"],
313
+ ))
314
+ break # One finding per cookie
315
+ except Exception:
316
+ continue
317
+
318
+ return findings
319
+
320
+ async def _fuzz_multipart(self, endpoint: str) -> list[Finding]:
321
+ """Fuzz multipart form-data uploads."""
322
+ findings: list[Finding] = []
323
+
324
+ # Payloads for multipart fuzzing
325
+ multipart_payloads = [
326
+ # Normal file with XSS content
327
+ ("test.txt", b"<script>alert(1)</script>"),
328
+ # Oversized filename
329
+ ("A" * 1000 + ".txt", b"test"),
330
+ # Path traversal in filename
331
+ ("../../etc/passwd", b"test"),
332
+ # Null byte in filename
333
+ ("test\x00.txt", b"test"),
334
+ # Deeply nested JSON
335
+ ("test.json", b'{"a":{"b":{"c":{"d":"e"}}}}'),
336
+ # Prototype pollution
337
+ ("test.json", b'{"__proto__":{"admin":true}}'),
338
+ ]
339
+
340
+ for filename, content in multipart_payloads:
341
+ try:
342
+ # Build multipart form-data manually
343
+ body = (
344
+ f"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n"
345
+ f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
346
+ f"Content-Type: application/octet-stream\r\n\r\n"
347
+ ).encode() + content + b"\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n"
348
+
349
+ response, evidence = await self.client.post(
350
+ endpoint,
351
+ body=body.decode("latin-1"),
352
+ headers={
353
+ "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
354
+ },
355
+ )
356
+ if self._detect_fuzz_issue(response, filename, evidence):
357
+ evidence.confidence = 0.5
358
+ evidence.payload = f"filename={filename}"
359
+ findings.append(Finding(
360
+ title=f"Fuzzing issue in multipart upload: {filename}",
361
+ severity=Severity.MEDIUM,
362
+ evidence=evidence,
363
+ description=(
364
+ f"Multipart upload with filename '{filename}' "
365
+ f"caused an unexpected response."
366
+ ),
367
+ remediation="Validate and sanitize file uploads.",
368
+ cwe="CWE-20",
369
+ owasp="A03:2021 - Injection",
370
+ scanner="fuzz",
371
+ tags=["fuzzing", "multipart", "file-upload"],
372
+ ))
373
+ break # One finding for multipart
374
+ except Exception:
375
+ continue
376
+
377
+ return findings
378
+
379
+
380
+ def _guess_param_type(value: str) -> list[str]:
381
+ """Guess the type of a parameter value."""
382
+ categories = ["string"]
383
+
384
+ if value.isdigit() or (value.startswith("-") and value[1:].isdigit()):
385
+ categories.append("integer")
386
+ categories.append("boundary")
387
+ elif value.replace(".", "").replace("-", "").isdigit():
388
+ categories.append("boundary")
389
+
390
+ categories.extend(["encoding", "nested_json"])
391
+ return categories