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/cors.py ADDED
@@ -0,0 +1,158 @@
1
+ """CORS misconfiguration scanner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from stryx.utils.evidence import Finding, Severity
6
+ from stryx.utils.http_client import HttpClient
7
+ from stryx.utils.logging import get_logger
8
+
9
+ logger = get_logger("scanner.cors")
10
+
11
+
12
+ class CorsScanner:
13
+ """Scanner for CORS misconfiguration vulnerabilities."""
14
+
15
+ def __init__(self, client: HttpClient):
16
+ self.client = client
17
+
18
+ async def scan(self, base_url: str) -> list[Finding]:
19
+ """Run CORS tests against the target."""
20
+ findings: list[Finding] = []
21
+ logger.info("Running CORS scanner")
22
+
23
+ # Extract domain from base URL
24
+ domain = base_url.split("//")[-1].split("/")[0].split(":")[0]
25
+
26
+ # Test various origin patterns
27
+ test_origins = [
28
+ # Evil domains
29
+ ("https://evil.com", "external evil domain"),
30
+ ("https://attacker.com", "external attacker domain"),
31
+ # Null origin (sandboxed iframes, file:// protocol)
32
+ ("null", "null origin"),
33
+ # Subdomain attacks
34
+ (f"https://{domain}.evil.com", "subdomain takeover"),
35
+ (f"https://evil{domain}", "domain prefix"),
36
+ (f"https://{domain}evil.com", "domain suffix"),
37
+ # HTTP downgrade
38
+ (f"http://{domain}", "HTTP downgrade"),
39
+ # Unicode/Punycode
40
+ (f"https://evil{domain}.com", "homograph domain"),
41
+ ]
42
+
43
+ seen_origins: set[str] = set()
44
+
45
+ for origin, desc in test_origins:
46
+ if origin in seen_origins:
47
+ continue
48
+ seen_origins.add(origin)
49
+
50
+ try:
51
+ response, evidence = await self.client.get(
52
+ base_url,
53
+ headers={"Origin": origin},
54
+ )
55
+
56
+ acao = response.headers.get("access-control-allow-origin", "")
57
+ acac = response.headers.get("access-control-allow-credentials", "")
58
+
59
+ if not acao:
60
+ continue
61
+
62
+ # Check for dangerous patterns
63
+ finding = self._check_origin_reflection(
64
+ origin, acao, acac, desc, evidence
65
+ )
66
+ if finding:
67
+ findings.append(finding)
68
+
69
+ # Check for wildcard with credentials (very dangerous)
70
+ finding = self._check_wildcard_credentials(acao, acac, evidence)
71
+ if finding:
72
+ findings.append(finding)
73
+
74
+ except Exception as e:
75
+ logger.debug(f"CORS test error: {e}")
76
+
77
+ logger.info(f"CORS scanner found {len(findings)} findings")
78
+ return findings
79
+
80
+ def _check_origin_reflection(
81
+ self,
82
+ origin: str,
83
+ acao: str,
84
+ acac: str,
85
+ desc: str,
86
+ evidence,
87
+ ) -> Finding | None:
88
+ """Check if origin is reflected in ACAO header."""
89
+ if acao == origin or acao == "null":
90
+ severity = Severity.MEDIUM
91
+ if acac.lower() == "true":
92
+ severity = Severity.HIGH
93
+
94
+ evidence.confidence = 0.8
95
+ evidence.payload = f"Origin: {origin}"
96
+ return Finding(
97
+ title=f"CORS reflects {desc} origin: {origin}",
98
+ severity=severity,
99
+ evidence=evidence,
100
+ description=(
101
+ f"The server reflects the Origin header '{origin}' in "
102
+ f"Access-Control-Allow-Origin. "
103
+ f"Credentials allowed: {acac}. "
104
+ f"This allows cross-origin requests from {desc}."
105
+ ),
106
+ remediation=(
107
+ "Whitelist specific trusted origins. "
108
+ "Never reflect arbitrary origins when credentials are allowed."
109
+ ),
110
+ cwe="CWE-942",
111
+ owasp="A05:2021 - Security Misconfiguration",
112
+ scanner="cors",
113
+ tags=["cors", "misconfiguration", desc.replace(" ", "-")],
114
+ )
115
+ return None
116
+
117
+ def _check_wildcard_credentials(
118
+ self,
119
+ acao: str,
120
+ acac: str,
121
+ evidence,
122
+ ) -> Finding | None:
123
+ """Check for wildcard ACAO with credentials (very dangerous)."""
124
+ if acao == "*" and acac.lower() == "true":
125
+ evidence.confidence = 0.9
126
+ evidence.payload = "Access-Control-Allow-Origin: *, Credentials: true"
127
+ return Finding(
128
+ title="CORS: wildcard with credentials",
129
+ severity=Severity.CRITICAL,
130
+ evidence=evidence,
131
+ description=(
132
+ "The server returns Access-Control-Allow-Origin: * with "
133
+ "Access-Control-Allow-Credentials: true. This is a critical "
134
+ "misconfiguration that allows any origin to make credentialed requests."
135
+ ),
136
+ remediation=(
137
+ "Remove wildcard CORS or disable credentials. "
138
+ "Whitelist specific trusted origins instead."
139
+ ),
140
+ cwe="CWE-942",
141
+ owasp="A05:2021 - Security Misconfiguration",
142
+ scanner="cors",
143
+ tags=["cors", "wildcard", "credentials"],
144
+ )
145
+ elif acao == "*":
146
+ evidence.confidence = 0.5
147
+ return Finding(
148
+ title="CORS: wildcard Access-Control-Allow-Origin",
149
+ severity=Severity.LOW,
150
+ evidence=evidence,
151
+ description="The server returns Access-Control-Allow-Origin: * for all requests.",
152
+ remediation="Restrict CORS to specific trusted origins.",
153
+ cwe="CWE-942",
154
+ owasp="A05:2021 - Security Misconfiguration",
155
+ scanner="cors",
156
+ tags=["cors", "wildcard"],
157
+ )
158
+ return None
@@ -0,0 +1,296 @@
1
+ """JavaScript dependency scanner.
2
+
3
+ Analyzes frontend JavaScript files to detect known vulnerable libraries
4
+ and versions. Identifies supply chain vulnerabilities in frontend dependencies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import yaml
14
+
15
+ from stryx.utils.evidence import Evidence, Finding, Severity
16
+ from stryx.utils.http_client import HttpClient
17
+ from stryx.utils.logging import get_logger
18
+
19
+ logger = get_logger("scanner.dependencies")
20
+
21
+ # Path to known vulnerabilities database
22
+ VULN_DB_PATH = Path(__file__).parent.parent / "signatures" / "known_vulns.yaml"
23
+
24
+
25
+ def _load_vuln_db() -> dict[str, Any]:
26
+ """Load the known vulnerabilities database."""
27
+ if VULN_DB_PATH.exists():
28
+ try:
29
+ with open(VULN_DB_PATH, encoding="utf-8") as f:
30
+ data = yaml.safe_load(f)
31
+ return data if isinstance(data, dict) else {}
32
+ except Exception:
33
+ logger.warning("Failed to load vulnerability database")
34
+ return {}
35
+
36
+
37
+ def _version_tuple(version_str: str) -> tuple[int, ...]:
38
+ """Parse a version string into a comparable tuple."""
39
+ # Remove pre-release suffixes
40
+ clean = re.split(r'[-+]', version_str)[0]
41
+ parts = []
42
+ for part in clean.split('.'):
43
+ try:
44
+ parts.append(int(part))
45
+ except ValueError:
46
+ break
47
+ return tuple(parts) if parts else (0,)
48
+
49
+
50
+ # Patterns to extract library names and versions from JS files
51
+ VERSION_PATTERNS = [
52
+ # jQuery
53
+ (re.compile(r'jquery[/-](\d+\.\d+\.\d+)', re.I), "jQuery"),
54
+ (re.compile(r'jQuery v(\d+\.\d+\.\d+)', re.I), "jQuery"),
55
+ (re.compile(r'@version (\d+\.\d+\.\d+).*jquery', re.I), "jQuery"),
56
+ # Angular
57
+ (re.compile(r'angular[/-](\d+\.\d+\.\d+)', re.I), "Angular"),
58
+ (re.compile(r'angular\.js v(\d+\.\d+\.\d+)', re.I), "Angular"),
59
+ (re.compile(r'@version (\d+\.\d+\.\d+).*angular', re.I), "Angular"),
60
+ # React
61
+ (re.compile(r'react[/-](\d+\.\d+\.\d+)', re.I), "React"),
62
+ (re.compile(r'React v(\d+\.\d+\.\d+)', re.I), "React"),
63
+ # Vue
64
+ (re.compile(r'vue[/-](\d+\.\d+\.\d+)', re.I), "Vue.js"),
65
+ (re.compile(r'Vue\.js v(\d+\.\d+\.\d+)', re.I), "Vue.js"),
66
+ # Lodash
67
+ (re.compile(r'lodash[/-](\d+\.\d+\.\d+)', re.I), "Lodash"),
68
+ (re.compile(r'@version (\d+\.\d+\.\d+).*lodash', re.I), "Lodash"),
69
+ # Bootstrap
70
+ (re.compile(r'bootstrap[/-](\d+\.\d+\.\d+)', re.I), "Bootstrap"),
71
+ (re.compile(r'Bootstrap v(\d+\.\d+\.\d+)', re.I), "Bootstrap"),
72
+ # Moment.js
73
+ (re.compile(r'moment[/-](\d+\.\d+\.\d+)', re.I), "Moment.js"),
74
+ (re.compile(r'@version (\d+\.\d+\.\d+).*moment', re.I), "Moment.js"),
75
+ # Underscore
76
+ (re.compile(r'underscore[/-](\d+\.\d+\.\d+)', re.I), "Underscore.js"),
77
+ (re.compile(r'@version (\d+\.\d+\.\d+).*underscore', re.I), "Underscore.js"),
78
+ # Backbone
79
+ (re.compile(r'backbone[/-](\d+\.\d+\.\d+)', re.I), "Backbone.js"),
80
+ # Ember
81
+ (re.compile(r'ember[/-](\d+\.\d+\.\d+)', re.I), "Ember.js"),
82
+ # Mootools
83
+ (re.compile(r'mootools[/-](\d+\.\d+\.\d+)', re.I), "MooTools"),
84
+ # Dojo
85
+ (re.compile(r'dojo[/-](\d+\.\d+\.\d+)', re.I), "Dojo"),
86
+ # ExtJS
87
+ (re.compile(r'extjs[/-](\d+\.\d+\.\d+)', re.I), "ExtJS"),
88
+ # Prototype
89
+ (re.compile(r'prototype[/-](\d+\.\d+\.\d+)', re.I), "Prototype.js"),
90
+ # Three.js
91
+ (re.compile(r'three[/-]js[/-](\d+\.\d+\.\d+)', re.I), "Three.js"),
92
+ (re.compile(r'three\.js v(\d+\.\d+\.\d+)', re.I), "Three.js"),
93
+ # D3
94
+ (re.compile(r'd3[/-](\d+\.\d+\.\d+)', re.I), "D3.js"),
95
+ (re.compile(r'd3\.js v(\d+\.\d+\.\d+)', re.I), "D3.js"),
96
+ # Chart.js
97
+ (re.compile(r'chart[/-]js[/-](\d+\.\d+\.\d+)', re.I), "Chart.js"),
98
+ # Lodash (already covered above, but CDN pattern)
99
+ (re.compile(r'cdn.*lodash.*?/(\d+\.\d+\.\d+)/lodash', re.I), "Lodash"),
100
+ # jQuery CDN
101
+ (re.compile(r'cdn.*jquery.*?/(\d+\.\d+\.\d+)/jquery', re.I), "jQuery"),
102
+ # Generic version comment pattern
103
+ (re.compile(r'@version\s+(\d+\.\d+\.\d+)'), "Unknown"),
104
+ (re.compile(r'v(\d+\.\d+\.\d+)\s'), "Unknown"),
105
+ ]
106
+
107
+ # Patterns to detect library name from file path
108
+ LIBRARY_PATH_PATTERNS = [
109
+ (re.compile(r'/jquery[-.]?(\d+\.\d+\.\d+)', re.I), "jQuery"),
110
+ (re.compile(r'/angular[-.]?(\d+\.\d+\.\d+)', re.I), "Angular"),
111
+ (re.compile(r'/react[-.]?(\d+\.\d+\.\d+)', re.I), "React"),
112
+ (re.compile(r'/vue[-.]?(\d+\.\d+\.\d+)', re.I), "Vue.js"),
113
+ (re.compile(r'/lodash[-.]?(\d+\.\d+\.\d+)', re.I), "Lodash"),
114
+ (re.compile(r'/bootstrap[-.]?(\d+\.\d+\.\d+)', re.I), "Bootstrap"),
115
+ (re.compile(r'/moment[-.]?(\d+\.\d+\.\d+)', re.I), "Moment.js"),
116
+ (re.compile(r'/underscore[-.]?(\d+\.\d+\.\d+)', re.I), "Underscore.js"),
117
+ ]
118
+
119
+
120
+ class DependencyScanner:
121
+ """Scanner for vulnerable JavaScript dependencies."""
122
+
123
+ def __init__(self, client: HttpClient):
124
+ self.client = client
125
+ self._vuln_db = _load_vuln_db()
126
+
127
+ async def scan(
128
+ self,
129
+ endpoints: list[str],
130
+ base_url: str,
131
+ ) -> list[Finding]:
132
+ """Scan for vulnerable JavaScript dependencies."""
133
+ findings: list[Finding] = []
134
+ logger.info("Running dependency scanner")
135
+
136
+ seen_libraries: set[str] = set()
137
+
138
+ # Scan each endpoint for JS files
139
+ for endpoint in endpoints[:5]: # Limit to 5 pages
140
+ try:
141
+ response, _ = await self.client.get(endpoint)
142
+ if response.status_code != 200:
143
+ continue
144
+
145
+ # Extract script sources
146
+ script_srcs = self._extract_script_sources(response.text, endpoint)
147
+
148
+ for js_url, js_content in script_srcs:
149
+ # Detect libraries
150
+ libraries = self._detect_libraries(js_content, js_url)
151
+
152
+ for lib_name, version in libraries:
153
+ lib_key = f"{lib_name}@{version}"
154
+ if lib_key in seen_libraries:
155
+ continue
156
+ seen_libraries.add(lib_key)
157
+
158
+ # Check against vulnerability database
159
+ vulns = self._check_vulnerabilities(lib_name, version)
160
+ for vuln in vulns:
161
+ findings.append(Finding(
162
+ title=f"Vulnerable library: {lib_name} {version} - {vuln['cve']}",
163
+ severity=Severity(vuln.get("severity", "high")),
164
+ evidence=Evidence(
165
+ request_method="GET",
166
+ request_url=js_url,
167
+ response_status=200,
168
+ response_body=f"Library: {lib_name}, Version: {version}",
169
+ response_snippet=f"CVE: {vuln['cve']}, Description: {vuln.get('description', 'N/A')}",
170
+ confidence=0.9,
171
+ ),
172
+ description=(
173
+ f"The JavaScript library {lib_name} version {version} is installed. "
174
+ f"This version has known vulnerabilities: {vuln['cve']}. "
175
+ f"{vuln.get('description', '')}"
176
+ ),
177
+ remediation=(
178
+ f"Update {lib_name} to version {vuln.get('fixed_version', 'latest')} "
179
+ f"or later. See {vuln.get('reference', 'N/A')} for details."
180
+ ),
181
+ cwe=vuln.get("cwe", "CWE-1395"),
182
+ owasp="A06:2021 - Vulnerable and Outdated Components",
183
+ endpoint=endpoint,
184
+ scanner="dependencies",
185
+ tags=["supply-chain", "vulnerable-dependency", lib_name.lower()],
186
+ ))
187
+
188
+ except Exception as e:
189
+ logger.debug(f"Dependency scan error on {endpoint}: {e}")
190
+
191
+ logger.info(f"Dependency scanner found {len(findings)} findings")
192
+ return findings
193
+
194
+ def _extract_script_sources(
195
+ self, html: str, base_url: str
196
+ ) -> list[tuple[str, str]]:
197
+ """Extract script tag sources from HTML."""
198
+ from urllib.parse import urljoin
199
+
200
+ results = []
201
+ pattern = re.compile(
202
+ r'<script[^>]+src=["\']([^"\']+)["\']',
203
+ re.IGNORECASE,
204
+ )
205
+
206
+ for match in pattern.finditer(html):
207
+ src = match.group(1)
208
+ if src.startswith(("data:", "javascript:", "blob:")):
209
+ continue
210
+
211
+ full_url = urljoin(base_url, src)
212
+ # Only fetch same-origin scripts
213
+ if full_url.startswith(base_url):
214
+ results.append((full_url, "")) # Content fetched lazily
215
+
216
+ return results[:10] # Limit to 10 scripts
217
+
218
+ def _detect_libraries(self, js_content: str, js_url: str) -> list[tuple[str, str]]:
219
+ """Detect library names and versions from JS content or URL."""
220
+ libraries: list[tuple[str, str]] = []
221
+
222
+ # Try version patterns on content
223
+ for pattern, lib_name in VERSION_PATTERNS:
224
+ match = pattern.search(js_content)
225
+ if match:
226
+ version = match.group(1)
227
+ if lib_name != "Unknown" or js_url:
228
+ libraries.append((lib_name if lib_name != "Unknown" else js_url, version))
229
+
230
+ # Try path patterns on URL
231
+ for pattern, lib_name in LIBRARY_PATH_PATTERNS:
232
+ match = pattern.search(js_url)
233
+ if match:
234
+ version = match.group(1)
235
+ libraries.append((lib_name, version))
236
+
237
+ return libraries
238
+
239
+ def _check_vulnerabilities(
240
+ self, library_name: str, version_str: str
241
+ ) -> list[dict[str, Any]]:
242
+ """Check a library version against the vulnerability database."""
243
+ vulns: list[dict[str, Any]] = []
244
+ version = _version_tuple(version_str)
245
+
246
+ # Check built-in vulnerability database
247
+ lib_vulns = self._vuln_db.get(library_name.lower(), {})
248
+ for cve, vuln_data in lib_vulns.items():
249
+ affected_range = vuln_data.get("affected", "")
250
+ fixed_version = vuln_data.get("fixed_version", "")
251
+
252
+ if self._version_in_range(version, affected_range, fixed_version):
253
+ vulns.append({
254
+ "cve": cve,
255
+ "severity": vuln_data.get("severity", "high"),
256
+ "description": vuln_data.get("description", ""),
257
+ "fixed_version": fixed_version,
258
+ "reference": vuln_data.get("reference", ""),
259
+ "cwe": vuln_data.get("cwe", "CWE-1395"),
260
+ })
261
+
262
+ return vulns
263
+
264
+ def _version_in_range(
265
+ self, version: tuple[int, ...], affected: str, fixed: str
266
+ ) -> bool:
267
+ """Check if a version falls within an affected range."""
268
+ try:
269
+ if fixed:
270
+ fixed_tuple = _version_tuple(fixed)
271
+ return version < fixed_tuple
272
+ if affected:
273
+ # Parse range like "<4.17.21" or ">=1.0.0,<2.0.0"
274
+ parts = affected.split(",")
275
+ for part in parts:
276
+ part = part.strip()
277
+ if part.startswith("<"):
278
+ upper = _version_tuple(part[1:])
279
+ if version >= upper:
280
+ return False
281
+ elif part.startswith(">="):
282
+ lower = _version_tuple(part[2:])
283
+ if version < lower:
284
+ return False
285
+ elif part.startswith(">"):
286
+ lower = _version_tuple(part[1:])
287
+ if version <= lower:
288
+ return False
289
+ elif part.startswith("<="):
290
+ upper = _version_tuple(part[1:])
291
+ if version > upper:
292
+ return False
293
+ return True
294
+ except Exception:
295
+ pass
296
+ return False