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/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """STRYX - AI-Powered Dynamic Application Security Testing (DAST).
2
+
3
+ Part of the MEDUSA security platform:
4
+ - Remy (SAST) + STRYX (DAST) + MEDUSA (Aggregator)
5
+
6
+ Features:
7
+ - 16 scanner modules (injection, auth, authorization, fuzz, CORS, GraphQL,
8
+ blind, disclosure, race, cloud SSRF, dependencies, and more)
9
+ - AI-powered attack chain planning and payload generation
10
+ - Recursive HTML crawler with depth limiting
11
+ - Session state machine for authenticated scanning
12
+ - SARIF 2.1.0 output for CI/CD integration
13
+ - Policy engine for security quality gates
14
+ - Baseline comparison for regression tracking
15
+ - Multi-format reports (Terminal, JSON, HTML, Markdown, SARIF)
16
+
17
+ Usage:
18
+ stryx scan https://target.com
19
+ stryx scan https://target.com --deep --sarif results.sarif
20
+ stryx scan https://target.com --policy policy.yaml
21
+ """
22
+
23
+ __version__ = "0.1.0"
24
+ __author__ = "Akhilesh Varma"
25
+ __license__ = "Apache-2.0"
26
+ __description__ = "AI-Powered Dynamic Application Security Testing (DAST)"
27
+ __url__ = "https://github.com/medusa-Security/stryx"
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "__author__",
32
+ "__license__",
33
+ "__description__",
34
+ "__url__",
35
+ ]
stryx/ai/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """AI-powered attack planning modules for STRYX."""
2
+
3
+ from stryx.ai.attack_planner import AttackPlanner
4
+
5
+ __all__ = ["AttackPlanner"]
@@ -0,0 +1,199 @@
1
+ """AI-powered attack planner.
2
+
3
+ Uses AI providers to analyze findings and construct attack chains.
4
+ Falls back to rule-based chain building if AI is unavailable.
5
+ Generates per-finding remediation via AI when available.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ from stryx.ai.prompts import (
13
+ ATTACK_CHAIN_PROMPT,
14
+ REMEDIATION_PROMPT,
15
+ SYSTEM_PROMPT,
16
+ format_findings_for_prompt,
17
+ )
18
+ from stryx.ai.providers import AIProvider, get_provider
19
+ from stryx.attacks.attack_chain import AttackChain, ChainBuilder
20
+ from stryx.utils.evidence import Finding
21
+ from stryx.utils.logging import get_logger
22
+
23
+ logger = get_logger("ai.attack_planner")
24
+
25
+
26
+ class AttackPlanner:
27
+ """AI-assisted attack chain planner."""
28
+
29
+ def __init__(
30
+ self,
31
+ provider_name: str = "groq",
32
+ model: str | None = None,
33
+ api_key: str | None = None,
34
+ ):
35
+ self.provider_name = provider_name
36
+ self.model = model
37
+ self.api_key = api_key
38
+ self._provider: AIProvider | None = None
39
+
40
+ def _get_provider(self) -> AIProvider | None:
41
+ """Get or create the AI provider."""
42
+ if self._provider is None:
43
+ try:
44
+ self._provider = get_provider(
45
+ self.provider_name,
46
+ api_key=self.api_key,
47
+ model=self.model,
48
+ )
49
+ except Exception as e:
50
+ logger.warning(f"Failed to initialize AI provider: {e}")
51
+ return None
52
+ return self._provider
53
+
54
+ async def plan_attack_chains(self, findings: list[Finding]) -> list[AttackChain]:
55
+ """Plan attack chains using AI or rule-based fallback."""
56
+ if not findings:
57
+ logger.info("No findings to analyze")
58
+ return []
59
+
60
+ # Always build rule-based chains as a baseline
61
+ builder = ChainBuilder(findings)
62
+ chains = builder.build_chains()
63
+
64
+ # Try AI-enhanced planning
65
+ provider = self._get_provider()
66
+ if provider and self.api_key:
67
+ try:
68
+ ai_chains = await self._ai_plan_chains(findings, provider)
69
+ if ai_chains:
70
+ chains.extend(ai_chains)
71
+ logger.info(f"AI generated {len(ai_chains)} additional attack chains")
72
+ except Exception as e:
73
+ logger.warning(f"AI attack planning failed, using rule-based chains: {e}")
74
+
75
+ # Generate AI remediation for findings
76
+ try:
77
+ await self._generate_remediation(findings, provider)
78
+ except Exception as e:
79
+ logger.warning(f"AI remediation generation failed: {e}")
80
+ else:
81
+ logger.info("AI provider not configured, using rule-based attack chains only")
82
+
83
+ return chains
84
+
85
+ async def _generate_remediation(
86
+ self, findings: list[Finding], provider: AIProvider
87
+ ) -> None:
88
+ """Generate AI-powered remediation for each finding."""
89
+ logger.info(f"Generating remediation for {len(findings)} findings")
90
+
91
+ # Process findings in batches to avoid token limits
92
+ batch_size = 10
93
+ for i in range(0, len(findings), batch_size):
94
+ batch = findings[i:i + batch_size]
95
+
96
+ for finding in batch:
97
+ if finding.remediation:
98
+ continue # Skip if already has remediation
99
+
100
+ try:
101
+ prompt = REMEDIATION_PROMPT.format(
102
+ title=finding.title,
103
+ severity=finding.severity.value,
104
+ cwe=finding.cwe,
105
+ endpoint=finding.endpoint,
106
+ description=finding.description,
107
+ )
108
+
109
+ response = await provider.generate(prompt, SYSTEM_PROMPT)
110
+ if not response:
111
+ continue
112
+
113
+ # Parse response
114
+ response = response.strip()
115
+ if response.startswith("```"):
116
+ response = response.split("\n", 1)[1]
117
+ if response.endswith("```"):
118
+ response = response[:-3]
119
+
120
+ data = json.loads(response)
121
+ if isinstance(data, dict):
122
+ fix = data.get("fix", "")
123
+ if fix:
124
+ finding.remediation = fix
125
+ logger.debug(f"Generated remediation for: {finding.title}")
126
+
127
+ except (json.JSONDecodeError, ValueError, Exception) as e:
128
+ logger.debug(f"Failed to generate remediation for {finding.title}: {e}")
129
+ continue
130
+
131
+ async def _ai_plan_chains(
132
+ self, findings: list[Finding], provider: AIProvider
133
+ ) -> list[AttackChain]:
134
+ """Use AI to generate attack chains."""
135
+ findings_data = [f.to_dict() for f in findings]
136
+ findings_text = format_findings_for_prompt(findings_data)
137
+
138
+ prompt = ATTACK_CHAIN_PROMPT.format(findings_json=findings_text)
139
+
140
+ response = await provider.generate(prompt, SYSTEM_PROMPT)
141
+ if not response:
142
+ return []
143
+
144
+ # Parse AI response
145
+ try:
146
+ # Try to extract JSON from response
147
+ response = response.strip()
148
+ if response.startswith("```"):
149
+ response = response.split("\n", 1)[1]
150
+ if response.endswith("```"):
151
+ response = response[:-3]
152
+
153
+ chains_data = json.loads(response)
154
+ if not isinstance(chains_data, list):
155
+ return []
156
+
157
+ chains: list[AttackChain] = []
158
+ for chain_data in chains_data:
159
+ if not isinstance(chain_data, dict):
160
+ continue
161
+
162
+ chain = AttackChain(
163
+ name=chain_data.get("name", "AI-Generated Chain"),
164
+ total_impact=chain_data.get("total_impact", ""),
165
+ estimated_severity=chain_data.get("estimated_severity", "high"),
166
+ )
167
+
168
+ for step_data in chain_data.get("steps", []):
169
+ if isinstance(step_data, dict):
170
+ # Find matching finding
171
+ matching_finding = self._find_matching_finding(
172
+ findings, step_data.get("title", "")
173
+ )
174
+ if matching_finding:
175
+ chain.add_step(
176
+ matching_finding,
177
+ step_data.get("description", ""),
178
+ step_data.get("impact", ""),
179
+ )
180
+
181
+ if chain.steps:
182
+ chains.append(chain)
183
+
184
+ return chains
185
+
186
+ except (json.JSONDecodeError, ValueError) as e:
187
+ logger.warning(f"Failed to parse AI response: {e}")
188
+ return []
189
+
190
+ def _find_matching_finding(
191
+ self, findings: list[Finding], title: str
192
+ ) -> Finding | None:
193
+ """Find a finding matching the given title."""
194
+ title_lower = title.lower()
195
+ for f in findings:
196
+ if title_lower in f.title.lower() or f.title.lower() in title_lower:
197
+ return f
198
+ # Fallback: return first finding
199
+ return findings[0] if findings else None
@@ -0,0 +1,212 @@
1
+ """AI-powered smart payload generator.
2
+
3
+ Uses LLMs to generate context-aware attack payloads that bypass WAFs
4
+ and target specific frameworks. Falls back to static payloads when
5
+ AI is unavailable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from stryx.utils.logging import get_logger
14
+
15
+ logger = get_logger("ai.payload_generator")
16
+
17
+
18
+ # Static fallback payloads by category
19
+ FALLBACK_PAYLOADS: dict[str, list[str]] = {
20
+ "sqli": [
21
+ "' OR '1'='1'--",
22
+ "' UNION SELECT NULL--",
23
+ "1' AND SLEEP(3)--",
24
+ "1; DROP TABLE users--",
25
+ "' OR 1=1#",
26
+ ],
27
+ "xss": [
28
+ "<script>alert(1)</script>",
29
+ "<img src=x onerror=alert(1)>",
30
+ "<svg onload=alert(1)>",
31
+ "javascript:alert(1)",
32
+ "'-alert(1)-'",
33
+ ],
34
+ "ssrf": [
35
+ "http://127.0.0.1:80/",
36
+ "http://169.254.169.254/latest/meta-data/",
37
+ "http://localhost:8080/",
38
+ "http://[::1]/",
39
+ "http://0177.0.0.1/",
40
+ ],
41
+ "cmdi": [
42
+ "; cat /etc/passwd",
43
+ "| id",
44
+ "`whoami`",
45
+ "$(cat /etc/passwd)",
46
+ "; sleep 5",
47
+ ],
48
+ "path_traversal": [
49
+ "../../../etc/passwd",
50
+ "..%2f..%2f..%2fetc/passwd",
51
+ "....//....//....//etc/passwd",
52
+ "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd",
53
+ ],
54
+ "ssti": [
55
+ "{{7*7}}",
56
+ "${7*7}",
57
+ "<%= 7*7 %>",
58
+ "#{7*7}",
59
+ "{{config.items()}}",
60
+ ],
61
+ }
62
+
63
+
64
+ PAYLOAD_GENERATION_PROMPT = """You are an expert penetration tester. Generate {count} security testing payloads for the following scenario:
65
+
66
+ Category: {category}
67
+ Framework: {framework}
68
+ Target endpoint: {endpoint}
69
+ WAF detected: {waf}
70
+ Previous payload that worked: {previous_payload}
71
+
72
+ Generate payloads that:
73
+ 1. Are specific to the {framework} framework
74
+ 2. Bypass common WAF rules if {waf} is detected
75
+ 3. Are realistic and would be used by a real attacker
76
+ 4. Cover different encoding and bypass techniques
77
+
78
+ Return ONLY a JSON array of payload strings, no other text.
79
+ Example: ["payload1", "payload2", "payload3"]
80
+ """
81
+
82
+
83
+ class PayloadGenerator:
84
+ """Generates context-aware attack payloads using AI."""
85
+
86
+ def __init__(self, ai_provider: Any = None):
87
+ """
88
+ Args:
89
+ ai_provider: An AI provider instance with a generate() method.
90
+ If None, uses fallback static payloads.
91
+ """
92
+ self.ai_provider = ai_provider
93
+ self._cache: dict[str, list[str]] = {}
94
+
95
+ async def generate(
96
+ self,
97
+ category: str,
98
+ framework: str = "unknown",
99
+ endpoint: str = "",
100
+ waf: str = "none",
101
+ previous_payload: str = "",
102
+ count: int = 10,
103
+ ) -> list[str]:
104
+ """Generate payloads for a specific category and context.
105
+
106
+ Args:
107
+ category: Payload category (sqli, xss, ssrf, cmdi, etc.)
108
+ framework: Detected framework (e.g., "Express", "Django", "Flask")
109
+ endpoint: Target endpoint URL
110
+ waf: Detected WAF name (e.g., "Cloudflare", "ModSecurity")
111
+ previous_payload: A payload that previously worked
112
+ count: Number of payloads to generate
113
+
114
+ Returns:
115
+ List of payload strings
116
+ """
117
+ # Check cache
118
+ cache_key = f"{category}:{framework}:{waf}"
119
+ if cache_key in self._cache:
120
+ return self._cache[cache_key][:count]
121
+
122
+ # Try AI generation
123
+ if self.ai_provider:
124
+ try:
125
+ payloads = await self._generate_with_ai(
126
+ category, framework, endpoint, waf, previous_payload, count
127
+ )
128
+ if payloads:
129
+ self._cache[cache_key] = payloads
130
+ return payloads
131
+ except Exception as e:
132
+ logger.debug(f"AI payload generation failed: {e}")
133
+
134
+ # Fallback to static payloads
135
+ payloads = self._get_fallback_payloads(category)
136
+ self._cache[cache_key] = payloads
137
+ return payloads[:count]
138
+
139
+ async def _generate_with_ai(
140
+ self,
141
+ category: str,
142
+ framework: str,
143
+ endpoint: str,
144
+ waf: str,
145
+ previous_payload: str,
146
+ count: int,
147
+ ) -> list[str]:
148
+ """Generate payloads using the AI provider."""
149
+ prompt = PAYLOAD_GENERATION_PROMPT.format(
150
+ count=count,
151
+ category=category,
152
+ framework=framework,
153
+ endpoint=endpoint or "N/A",
154
+ waf=waf,
155
+ previous_payload=previous_payload or "None",
156
+ )
157
+
158
+ response = await self.ai_provider.generate(
159
+ prompt=prompt,
160
+ system_prompt="You are a security testing expert. Return only valid JSON arrays.",
161
+ )
162
+
163
+ # Parse response
164
+ try:
165
+ # Try to extract JSON array from response
166
+ text = response.strip()
167
+ if text.startswith("["):
168
+ payloads = json.loads(text)
169
+ else:
170
+ # Try to find JSON array in the response
171
+ start = text.find("[")
172
+ end = text.rfind("]") + 1
173
+ if start != -1 and end > start:
174
+ payloads = json.loads(text[start:end])
175
+ else:
176
+ return []
177
+
178
+ if isinstance(payloads, list):
179
+ return [str(p) for p in payloads if isinstance(p, str)][:count]
180
+ except (json.JSONDecodeError, TypeError):
181
+ pass
182
+
183
+ return []
184
+
185
+ def _get_fallback_payloads(self, category: str) -> list[str]:
186
+ """Get static fallback payloads for a category."""
187
+ return FALLBACK_PAYLOADS.get(category, [])
188
+
189
+ def detect_waf(self, response_headers: dict[str, str], response_body: str = "") -> str:
190
+ """Detect if a WAF is present based on response patterns."""
191
+ waf_signatures = {
192
+ "Cloudflare": ["cf-ray", "cf-cache-status", "cloudflare"],
193
+ "Akamai": ["x-akamai-transformed", "akamai"],
194
+ "AWS WAF": ["x-amzn-waf", "aws"],
195
+ "ModSecurity": ["mod_security", "modsecurity"],
196
+ "Incapsula": ["x-iinfo", "incap_ses", "incapsula"],
197
+ "Sucuri": ["x-sucuri-id", "sucuri"],
198
+ "Barracuda": ["x-barracuda", "barracuda"],
199
+ "F5 BIG-IP": ["x-cnection", "bigip"],
200
+ "FortiWeb": ["x-fortiwaf", "fortiweb"],
201
+ "Imperva": ["x-cdn", "imperva"],
202
+ }
203
+
204
+ headers_str = json.dumps(response_headers).lower()
205
+ body_lower = response_body.lower()
206
+
207
+ for waf_name, signatures in waf_signatures.items():
208
+ for sig in signatures:
209
+ if sig.lower() in headers_str or sig.lower() in body_lower:
210
+ return waf_name
211
+
212
+ return "none"
stryx/ai/prompts.py ADDED
@@ -0,0 +1,85 @@
1
+ """Prompt templates for the AI attack planner."""
2
+
3
+ SYSTEM_PROMPT = """You are an expert application security analyst specializing in
4
+ Dynamic Application Security Testing (DAST). Your role is to analyze scan findings
5
+ and construct realistic attack chains that demonstrate how individual vulnerabilities
6
+ can be combined to achieve significant impact.
7
+
8
+ Rules:
9
+ 1. Only propose attack chains that are supported by the actual findings provided.
10
+ 2. Each chain must have a clear sequence of steps.
11
+ 3. Estimate the realistic impact of each chain.
12
+ 4. Return your response as strict JSON matching the schema provided.
13
+ 5. Do not fabricate vulnerabilities not present in the findings.
14
+ 6. Prioritize chains with highest real-world impact.
15
+ 7. Consider the relationships between findings (e.g., auth bypass enables IDOR)."""
16
+
17
+ ATTACK_CHAIN_PROMPT = """Analyze the following security scan findings and construct
18
+ realistic attack chains. Each chain should show how individual vulnerabilities can
19
+ be combined for greater impact.
20
+
21
+ Findings:
22
+ {findings_json}
23
+
24
+ Return a JSON array of attack chains with this exact schema:
25
+ [
26
+ {{
27
+ "name": "Chain name describing the attack path",
28
+ "steps": [
29
+ {{
30
+ "title": "Step title",
31
+ "description": "What this step achieves",
32
+ "severity": "critical|high|medium|low",
33
+ "endpoint": "affected endpoint"
34
+ }}
35
+ ],
36
+ "total_impact": "Description of the overall impact",
37
+ "estimated_severity": "critical|high|medium|low",
38
+ "rationale": "Why this chain is realistic and actionable"
39
+ }}
40
+ ]
41
+
42
+ Focus on chains that:
43
+ 1. Combine authentication/authorization issues with injection or data exposure
44
+ 2. Demonstrate privilege escalation paths
45
+ 3. Show data exfiltration scenarios
46
+ 4. Reveal denial-of-service opportunities
47
+
48
+ Return ONLY the JSON array, no other text."""
49
+
50
+ REMEDIATION_PROMPT = """Based on the following finding, provide a detailed
51
+ remediation recommendation:
52
+
53
+ Title: {title}
54
+ Severity: {severity}
55
+ CWE: {cwe}
56
+ Endpoint: {endpoint}
57
+ Description: {description}
58
+
59
+ Provide:
60
+ 1. A specific, actionable fix
61
+ 2. Code example if applicable
62
+ 3. Additional hardening recommendations
63
+ 4. Testing steps to verify the fix
64
+
65
+ Return as JSON:
66
+ {{
67
+ "fix": "Specific remediation steps",
68
+ "code_example": "Code example if applicable",
69
+ "hardening": ["Additional recommendations"],
70
+ "testing": ["Steps to verify the fix"]
71
+ }}"""
72
+
73
+
74
+ def format_findings_for_prompt(findings: list[dict]) -> str:
75
+ """Format findings list into a string for the AI prompt."""
76
+ formatted = []
77
+ for i, f in enumerate(findings, 1):
78
+ formatted.append(
79
+ f"{i}. [{f.get('severity', 'unknown').upper()}] {f.get('title', 'Unknown')}\n"
80
+ f" Endpoint: {f.get('endpoint', 'N/A')}\n"
81
+ f" Description: {f.get('description', 'N/A')}\n"
82
+ f" CWE: {f.get('cwe', 'N/A')}\n"
83
+ f" Tags: {', '.join(f.get('tags', []))}\n"
84
+ )
85
+ return "\n".join(formatted)