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
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,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)
|