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/utils/evidence.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Evidence and Finding data models -- the core data types for all scan results.
|
|
2
|
+
|
|
3
|
+
Every scanner MUST produce Findings with populated Evidence. This is enforced
|
|
4
|
+
at the data-model level: constructing a Finding without Evidence raises ValueError.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Severity(StrEnum):
|
|
15
|
+
"""Severity levels for findings."""
|
|
16
|
+
|
|
17
|
+
CRITICAL = "critical"
|
|
18
|
+
HIGH = "high"
|
|
19
|
+
MEDIUM = "medium"
|
|
20
|
+
LOW = "low"
|
|
21
|
+
INFO = "info"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Evidence:
|
|
26
|
+
"""Evidence supporting a security finding.
|
|
27
|
+
|
|
28
|
+
Every Finding MUST contain a non-empty Evidence. This ensures no
|
|
29
|
+
heuristic-only findings are reported without supporting data.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
request_method: str
|
|
33
|
+
request_url: str
|
|
34
|
+
request_headers: dict[str, str] = field(default_factory=dict)
|
|
35
|
+
request_body: str | None = None
|
|
36
|
+
response_status: int = 0
|
|
37
|
+
response_headers: dict[str, str] = field(default_factory=dict)
|
|
38
|
+
response_body: str = ""
|
|
39
|
+
response_snippet: str = ""
|
|
40
|
+
payload: str = ""
|
|
41
|
+
confidence: float = 0.0 # 0.0 to 1.0
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if not self.request_method or not self.request_url:
|
|
45
|
+
raise ValueError("Evidence must have request_method and request_url")
|
|
46
|
+
if not self.response_snippet and self.response_body:
|
|
47
|
+
# Auto-generate snippet from response body
|
|
48
|
+
self.response_snippet = self.response_body[:500]
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
"""Convert to dictionary for serialization."""
|
|
52
|
+
return {
|
|
53
|
+
"request_method": self.request_method,
|
|
54
|
+
"request_url": self.request_url,
|
|
55
|
+
"request_headers": self.request_headers,
|
|
56
|
+
"request_body": self.request_body,
|
|
57
|
+
"response_status": self.response_status,
|
|
58
|
+
"response_headers": self.response_headers,
|
|
59
|
+
"response_body": self.response_body[:2000], # cap for storage
|
|
60
|
+
"response_snippet": self.response_snippet,
|
|
61
|
+
"payload": self.payload,
|
|
62
|
+
"confidence": self.confidence,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Finding:
|
|
68
|
+
"""A security finding with mandatory evidence.
|
|
69
|
+
|
|
70
|
+
This is the universal output type for all scanners. No scanner may emit
|
|
71
|
+
a Finding without populated Evidence -- this is enforced by __post_init__.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
title: str
|
|
75
|
+
severity: Severity
|
|
76
|
+
evidence: Evidence
|
|
77
|
+
description: str = ""
|
|
78
|
+
remediation: str = ""
|
|
79
|
+
cwe: str = ""
|
|
80
|
+
owasp: str = ""
|
|
81
|
+
endpoint: str = ""
|
|
82
|
+
scanner: str = ""
|
|
83
|
+
tags: list[str] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
def __post_init__(self) -> None:
|
|
86
|
+
if not self.evidence:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Finding '{self.title}' has no evidence. "
|
|
89
|
+
"All findings MUST include supporting Evidence."
|
|
90
|
+
)
|
|
91
|
+
if self.evidence.confidence < 0.0 or self.evidence.confidence > 1.0:
|
|
92
|
+
raise ValueError("Confidence must be between 0.0 and 1.0")
|
|
93
|
+
if not self.endpoint:
|
|
94
|
+
self.endpoint = self.evidence.request_url
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict[str, Any]:
|
|
97
|
+
"""Convert to dictionary for JSON serialization."""
|
|
98
|
+
return {
|
|
99
|
+
"title": self.title,
|
|
100
|
+
"severity": self.severity.value,
|
|
101
|
+
"description": self.description,
|
|
102
|
+
"remediation": self.remediation,
|
|
103
|
+
"cwe": self.cwe,
|
|
104
|
+
"owasp": self.owasp,
|
|
105
|
+
"endpoint": self.endpoint,
|
|
106
|
+
"scanner": self.scanner,
|
|
107
|
+
"tags": self.tags,
|
|
108
|
+
"evidence": self.evidence.to_dict(),
|
|
109
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Async HTTP client wrapper with session cookies, rate limiting, and evidence collection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from stryx.utils.evidence import Evidence
|
|
10
|
+
from stryx.utils.rate_limiter import RateLimiter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HttpClient:
|
|
14
|
+
"""Async HTTP client with session cookie jar, rate limiting, and evidence collection.
|
|
15
|
+
|
|
16
|
+
Maintains cookies across requests (Set-Cookie responses update the jar)
|
|
17
|
+
so authenticated sessions persist naturally.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
timeout: int = 10,
|
|
23
|
+
proxy: str | None = None,
|
|
24
|
+
rate_limit: int | None = None,
|
|
25
|
+
headers: dict[str, str] | None = None,
|
|
26
|
+
cookies: str | None = None,
|
|
27
|
+
):
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
self.proxy = proxy
|
|
30
|
+
self.base_headers = headers or {}
|
|
31
|
+
self.rate_limiter = RateLimiter(rate_limit) if rate_limit else None
|
|
32
|
+
self._cookie_jar: dict[str, str] = {}
|
|
33
|
+
|
|
34
|
+
# Parse initial cookies from string
|
|
35
|
+
if cookies:
|
|
36
|
+
for pair in cookies.split(";"):
|
|
37
|
+
if "=" in pair:
|
|
38
|
+
k, v = pair.split("=", 1)
|
|
39
|
+
self._cookie_jar[k.strip()] = v.strip()
|
|
40
|
+
|
|
41
|
+
def _format_cookies(self) -> str:
|
|
42
|
+
"""Format cookie jar as Cookie header string."""
|
|
43
|
+
return "; ".join(f"{k}={v}" for k, v in self._cookie_jar.items())
|
|
44
|
+
|
|
45
|
+
def _update_cookies_from_response(self, response: httpx.Response) -> None:
|
|
46
|
+
"""Extract Set-Cookie headers and update the jar."""
|
|
47
|
+
for cookie in response.cookies.items():
|
|
48
|
+
self._cookie_jar[cookie[0]] = cookie[1]
|
|
49
|
+
|
|
50
|
+
async def request(
|
|
51
|
+
self,
|
|
52
|
+
method: str,
|
|
53
|
+
url: str,
|
|
54
|
+
headers: dict[str, str] | None = None,
|
|
55
|
+
body: str | None = None,
|
|
56
|
+
json_data: dict | None = None,
|
|
57
|
+
cookies: dict[str, str] | None = None,
|
|
58
|
+
follow_redirects: bool = True,
|
|
59
|
+
) -> tuple[httpx.Response, Evidence]:
|
|
60
|
+
"""Make an HTTP request and return response with Evidence.
|
|
61
|
+
|
|
62
|
+
Cookies from the jar are sent automatically. Set-Cookie responses
|
|
63
|
+
update the jar for subsequent requests (session persistence).
|
|
64
|
+
"""
|
|
65
|
+
if self.rate_limiter:
|
|
66
|
+
await self.rate_limiter.acquire()
|
|
67
|
+
|
|
68
|
+
merged_headers = dict(self.base_headers)
|
|
69
|
+
if headers:
|
|
70
|
+
merged_headers.update(headers)
|
|
71
|
+
|
|
72
|
+
# Merge explicit cookies into jar
|
|
73
|
+
if cookies:
|
|
74
|
+
self._cookie_jar.update(cookies)
|
|
75
|
+
|
|
76
|
+
request_headers = dict(merged_headers)
|
|
77
|
+
|
|
78
|
+
# Add cookies from jar
|
|
79
|
+
cookie_str = self._format_cookies()
|
|
80
|
+
if cookie_str:
|
|
81
|
+
merged_headers["Cookie"] = cookie_str
|
|
82
|
+
|
|
83
|
+
kwargs: dict[str, Any] = {
|
|
84
|
+
"timeout": httpx.Timeout(self.timeout),
|
|
85
|
+
"follow_redirects": follow_redirects,
|
|
86
|
+
"verify": False,
|
|
87
|
+
}
|
|
88
|
+
if self.proxy:
|
|
89
|
+
kwargs["proxy"] = self.proxy
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
async with httpx.AsyncClient(**kwargs) as client:
|
|
93
|
+
response = await client.request(
|
|
94
|
+
method=method,
|
|
95
|
+
url=url,
|
|
96
|
+
headers=merged_headers,
|
|
97
|
+
content=body,
|
|
98
|
+
json=json_data,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Update cookie jar from response
|
|
102
|
+
self._update_cookies_from_response(response)
|
|
103
|
+
|
|
104
|
+
response_headers = dict(response.headers)
|
|
105
|
+
response_text = response.text
|
|
106
|
+
|
|
107
|
+
evidence = Evidence(
|
|
108
|
+
request_method=method,
|
|
109
|
+
request_url=url,
|
|
110
|
+
request_headers=request_headers,
|
|
111
|
+
request_body=body,
|
|
112
|
+
response_status=response.status_code,
|
|
113
|
+
response_headers=response_headers,
|
|
114
|
+
response_body=response_text,
|
|
115
|
+
response_snippet=response_text[:500],
|
|
116
|
+
confidence=0.0,
|
|
117
|
+
)
|
|
118
|
+
return response, evidence
|
|
119
|
+
|
|
120
|
+
except httpx.RequestError as e:
|
|
121
|
+
evidence = Evidence(
|
|
122
|
+
request_method=method,
|
|
123
|
+
request_url=url,
|
|
124
|
+
request_headers=request_headers,
|
|
125
|
+
request_body=body,
|
|
126
|
+
response_status=0,
|
|
127
|
+
response_body=str(e),
|
|
128
|
+
response_snippet=str(e)[:500],
|
|
129
|
+
confidence=0.0,
|
|
130
|
+
)
|
|
131
|
+
error_response = httpx.Response(
|
|
132
|
+
status_code=0,
|
|
133
|
+
request=httpx.Request(method=method, url=url),
|
|
134
|
+
)
|
|
135
|
+
return error_response, evidence
|
|
136
|
+
|
|
137
|
+
async def get(self, url: str, **kwargs: Any) -> tuple[httpx.Response, Evidence]:
|
|
138
|
+
"""Send GET request."""
|
|
139
|
+
return await self.request("GET", url, **kwargs)
|
|
140
|
+
|
|
141
|
+
async def post(self, url: str, **kwargs: Any) -> tuple[httpx.Response, Evidence]:
|
|
142
|
+
"""Send POST request."""
|
|
143
|
+
return await self.request("POST", url, **kwargs)
|
|
144
|
+
|
|
145
|
+
async def put(self, url: str, **kwargs: Any) -> tuple[httpx.Response, Evidence]:
|
|
146
|
+
"""Send PUT request."""
|
|
147
|
+
return await self.request("PUT", url, **kwargs)
|
|
148
|
+
|
|
149
|
+
async def delete(self, url: str, **kwargs: Any) -> tuple[httpx.Response, Evidence]:
|
|
150
|
+
"""Send DELETE request."""
|
|
151
|
+
return await self.request("DELETE", url, **kwargs)
|
|
152
|
+
|
|
153
|
+
async def head(self, url: str, **kwargs: Any) -> tuple[httpx.Response, Evidence]:
|
|
154
|
+
"""Send HEAD request."""
|
|
155
|
+
return await self.request("HEAD", url, **kwargs)
|
|
156
|
+
|
|
157
|
+
async def patch(self, url: str, **kwargs: Any) -> tuple[httpx.Response, Evidence]:
|
|
158
|
+
"""Send PATCH request."""
|
|
159
|
+
return await self.request("PATCH", url, **kwargs)
|
stryx/utils/logging.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Logging configuration for STRYX using Rich."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.logging import RichHandler
|
|
9
|
+
from rich.theme import Theme
|
|
10
|
+
|
|
11
|
+
# Custom theme for STRYX
|
|
12
|
+
STRYX_THEME = Theme({
|
|
13
|
+
"info": "cyan",
|
|
14
|
+
"warning": "yellow",
|
|
15
|
+
"error": "bold red",
|
|
16
|
+
"critical": "bold white on red",
|
|
17
|
+
"success": "bold green",
|
|
18
|
+
"severity.critical": "bold red",
|
|
19
|
+
"severity.high": "red",
|
|
20
|
+
"severity.medium": "yellow",
|
|
21
|
+
"severity.low": "blue",
|
|
22
|
+
"severity.info": "dim",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
console = Console(theme=STRYX_THEME)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_logging(level: str = "INFO") -> logging.Logger:
|
|
29
|
+
"""Configure Rich-based logging for STRYX."""
|
|
30
|
+
logger = logging.getLogger("stryx")
|
|
31
|
+
if logger.handlers:
|
|
32
|
+
return logger
|
|
33
|
+
|
|
34
|
+
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
35
|
+
|
|
36
|
+
handler = RichHandler(
|
|
37
|
+
console=console,
|
|
38
|
+
show_path=False,
|
|
39
|
+
show_time=True,
|
|
40
|
+
markup=True,
|
|
41
|
+
rich_tracebacks=True,
|
|
42
|
+
)
|
|
43
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
44
|
+
logger.addHandler(handler)
|
|
45
|
+
|
|
46
|
+
return logger
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_logger(name: str = "stryx") -> logging.Logger:
|
|
50
|
+
"""Get a named logger under the stryx namespace."""
|
|
51
|
+
return logging.getLogger(f"stryx.{name}" if name != "stryx" else "stryx")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Async rate limiter for controlling request frequency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from collections import deque
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RateLimiter:
|
|
11
|
+
"""Token-bucket style async rate limiter.
|
|
12
|
+
|
|
13
|
+
Limits to `max_requests` requests per `window_seconds` period.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, max_requests: int = 20, window_seconds: float = 1.0):
|
|
17
|
+
self.max_requests = max_requests
|
|
18
|
+
self.window_seconds = window_seconds
|
|
19
|
+
self._timestamps: deque[float] = deque()
|
|
20
|
+
self._lock = asyncio.Lock()
|
|
21
|
+
|
|
22
|
+
async def acquire(self) -> None:
|
|
23
|
+
"""Wait until a request slot is available."""
|
|
24
|
+
async with self._lock:
|
|
25
|
+
now = time.monotonic()
|
|
26
|
+
# Remove expired timestamps
|
|
27
|
+
while self._timestamps and self._timestamps[0] <= now - self.window_seconds:
|
|
28
|
+
self._timestamps.popleft()
|
|
29
|
+
|
|
30
|
+
if len(self._timestamps) >= self.max_requests:
|
|
31
|
+
# Wait until the oldest request expires
|
|
32
|
+
wait_time = self._timestamps[0] + self.window_seconds - now
|
|
33
|
+
if wait_time > 0:
|
|
34
|
+
await asyncio.sleep(wait_time)
|
|
35
|
+
self._timestamps.popleft()
|
|
36
|
+
|
|
37
|
+
self._timestamps.append(time.monotonic())
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stryx-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-Powered Dynamic Application Security Testing (DAST) -- part of the MEDUSA security platform
|
|
5
|
+
Author-email: Akhilesh Varma <akhverm@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Medusa-Security/stryx
|
|
8
|
+
Project-URL: Repository, https://github.com/Medusa-Security/stryx
|
|
9
|
+
Project-URL: Issues, https://github.com/Medusa-Security/stryx/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/Medusa-Security/stryx/blob/main/docs/USAGE.md
|
|
11
|
+
Project-URL: Changelog, https://github.com/Medusa-Security/stryx/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: security,dast,dynamic-analysis,vulnerability-scanner,penetration-testing,web-security,api-security,owasp,sast,application-security
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Framework :: AsyncIO
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: click<9,>=8.1
|
|
31
|
+
Requires-Dist: rich<14,>=13.0
|
|
32
|
+
Requires-Dist: httpx[socks]<1,>=0.25
|
|
33
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
34
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
35
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
36
|
+
Requires-Dist: openai<2,>=1.0
|
|
37
|
+
Requires-Dist: anthropic<1,>=0.25
|
|
38
|
+
Requires-Dist: groq<1,>=0.4
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
44
|
+
Requires-Dist: fastapi>=0.100; extra == "dev"
|
|
45
|
+
Requires-Dist: uvicorn>=0.20; extra == "dev"
|
|
46
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
47
|
+
Provides-Extra: xml
|
|
48
|
+
Requires-Dist: lxml<6,>=4.9; extra == "xml"
|
|
49
|
+
Provides-Extra: all
|
|
50
|
+
Requires-Dist: stryx[xml]; extra == "all"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# STRYX
|
|
54
|
+
|
|
55
|
+
**AI-Powered Dynamic Application Security Testing (DAST)**
|
|
56
|
+
|
|
57
|
+
[](LICENSE)
|
|
58
|
+
[](https://www.python.org/downloads/)
|
|
59
|
+
[](https://github.com/medusa-Security/stryx/actions)
|
|
60
|
+
|
|
61
|
+
STRYX is a developer-first, AI-assisted DAST engine that crawls, maps, and tests the live attack surface of web applications. It discovers hidden endpoints, simulates real attacker behavior, and detects exploitable vulnerabilities with minimal false positives. Every finding carries full evidence (request, response, status code, payload, confidence score) -- no heuristic-only results.
|
|
62
|
+
|
|
63
|
+
**Design philosophy: high signal, low noise.**
|
|
64
|
+
|
|
65
|
+
## How It Fits Into MEDUSA
|
|
66
|
+
|
|
67
|
+
- **Remy** -- SAST, analyzes source code pre-deployment.
|
|
68
|
+
- **STRYX** -- DAST, validates a running application's live attack surface and exploitability.
|
|
69
|
+
- **MEDUSA** -- Aggregates findings from both, correlates static and runtime issues, estimates business impact, provides centralized security management.
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
CLI -> Configuration -> Attack Orchestrator -> { Endpoint Discovery }
|
|
75
|
+
|
|
|
76
|
+
Authentication Engine
|
|
77
|
+
Authorization Engine
|
|
78
|
+
Injection Engine
|
|
79
|
+
API Fuzzer
|
|
80
|
+
AI Attack Planner
|
|
81
|
+
Evidence Collector
|
|
82
|
+
Report Generator
|
|
83
|
+
|
|
|
84
|
+
Target Application
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# From PyPI
|
|
91
|
+
pip install stryx
|
|
92
|
+
|
|
93
|
+
# From source
|
|
94
|
+
git clone https://github.com/medusa-Security/stryx.git
|
|
95
|
+
cd stryx
|
|
96
|
+
pip install -e .
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quickstart
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Run a full scan
|
|
103
|
+
stryx scan http://localhost:8000
|
|
104
|
+
|
|
105
|
+
# Output:
|
|
106
|
+
# STRYX Scan Results
|
|
107
|
+
# Target: http://localhost:8000
|
|
108
|
+
# Total Findings: 3
|
|
109
|
+
# Breakdown: CRITICAL: 1 | HIGH: 1 | MEDIUM: 1
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Core Modules
|
|
113
|
+
|
|
114
|
+
| Module | Purpose |
|
|
115
|
+
|--------|---------|
|
|
116
|
+
| Endpoint Discovery | Crawls targets, extracts APIs from OpenAPI/Swagger, sitemaps, JS files, GraphQL |
|
|
117
|
+
| Authentication Scanner | Tests missing auth, weak JWTs, session fixation, cookie security |
|
|
118
|
+
| Authorization Scanner | Tests IDOR, privilege escalation, admin access, multi-tenant escape |
|
|
119
|
+
| Injection Engine | SQL, NoSQL, command, SSRF, path traversal, XXE, SSTI, LDAP injection |
|
|
120
|
+
| API Fuzzer | Parameter mutation, boundary values, type confusion, nested JSON |
|
|
121
|
+
| CORS Scanner | Detects misconfigured CORS policies and origin reflection |
|
|
122
|
+
| GraphQL Scanner | Tests introspection exposure, query depth limiting |
|
|
123
|
+
| AI Attack Planner | Constructs multi-step attack chains across findings |
|
|
124
|
+
| Evidence Engine | Enforces evidence requirements on every finding |
|
|
125
|
+
|
|
126
|
+
## CLI Reference
|
|
127
|
+
|
|
128
|
+
### Commands
|
|
129
|
+
|
|
130
|
+
| Command | Description |
|
|
131
|
+
|---------|-------------|
|
|
132
|
+
| `stryx scan <url>` | Run full security scan |
|
|
133
|
+
| `stryx crawl <url>` | Crawl and discover endpoints |
|
|
134
|
+
| `stryx auth <url>` | Run authentication tests |
|
|
135
|
+
| `stryx fuzz <url>` | Run API fuzzing tests |
|
|
136
|
+
| `stryx report <url>` | Generate reports |
|
|
137
|
+
| `stryx config` | View/update configuration |
|
|
138
|
+
| `stryx providers` | List supported AI providers |
|
|
139
|
+
| `stryx update` | Update STRYX |
|
|
140
|
+
|
|
141
|
+
### Flags
|
|
142
|
+
|
|
143
|
+
| Flag | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `--deep` | Enable deep scanning |
|
|
146
|
+
| `--json <file>` | Output JSON report |
|
|
147
|
+
| `--html <file>` | Output HTML report |
|
|
148
|
+
| `--markdown <file>` | Output Markdown report |
|
|
149
|
+
| `--threads <n>` | Concurrent threads (1-200) |
|
|
150
|
+
| `--timeout <s>` | HTTP timeout in seconds |
|
|
151
|
+
| `--headers <json>` | Custom headers |
|
|
152
|
+
| `--cookies <str>` | Authentication cookies |
|
|
153
|
+
| `--proxy <url>` | HTTP proxy |
|
|
154
|
+
| `--wordlist <file>` | Custom wordlist |
|
|
155
|
+
| `--rate <n>` | Requests per second limit |
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
provider: groq
|
|
161
|
+
model: llama-3.3-70b-versatile
|
|
162
|
+
threads: 20
|
|
163
|
+
timeout: 10
|
|
164
|
+
crawl_depth: 5
|
|
165
|
+
respect_robots: false
|
|
166
|
+
ai_attack_planning: true
|
|
167
|
+
modules:
|
|
168
|
+
auth: true
|
|
169
|
+
authorization: true
|
|
170
|
+
injection: true
|
|
171
|
+
fuzzing: true
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Supported AI providers: Groq, OpenAI, Anthropic, OpenRouter, Ollama, XAI, NVIDIA NIM.
|
|
175
|
+
|
|
176
|
+
## Sample Report
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
+--------------------------------------------------------------------+
|
|
180
|
+
| STRYX Scan Results |
|
|
181
|
+
| Target: http://localhost:8000 |
|
|
182
|
+
| Total Findings: 3 |
|
|
183
|
+
| Breakdown: CRITICAL: 1 | HIGH: 1 | MEDIUM: 1 |
|
|
184
|
+
+--------------------------------------------------------------------+
|
|
185
|
+
|
|
186
|
+
[#] [CRITICAL] Unauthenticated access to /admin
|
|
187
|
+
Endpoint: http://localhost:8000/admin
|
|
188
|
+
CWE: CWE-306 | Scanner: auth
|
|
189
|
+
Confidence: 70%
|
|
190
|
+
Evidence: GET http://localhost:8000/admin -> 200
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Roadmap
|
|
194
|
+
|
|
195
|
+
### v0.1 (this build)
|
|
196
|
+
|
|
197
|
+
- [ ] HTTP crawler
|
|
198
|
+
- [ ] Endpoint discovery
|
|
199
|
+
- [ ] Authentication scanning
|
|
200
|
+
- [ ] Authorization testing
|
|
201
|
+
- [ ] Basic injection testing
|
|
202
|
+
- [ ] JSON/HTML reports
|
|
203
|
+
|
|
204
|
+
### v0.5
|
|
205
|
+
|
|
206
|
+
- [ ] Browser automation
|
|
207
|
+
- [ ] GraphQL support
|
|
208
|
+
- [ ] Multi-threaded fuzzing
|
|
209
|
+
- [ ] AI attack planning
|
|
210
|
+
- [ ] Custom payloads
|
|
211
|
+
- [ ] Plugin SDK
|
|
212
|
+
|
|
213
|
+
### v1.0
|
|
214
|
+
|
|
215
|
+
- [ ] Autonomous attack chaining
|
|
216
|
+
- [ ] Headless browser exploitation
|
|
217
|
+
- [ ] CI/CD integration
|
|
218
|
+
- [ ] Distributed scanning
|
|
219
|
+
- [ ] Cloud dashboards (MEDUSA integration)
|
|
220
|
+
- [ ] AI-powered exploit reasoning
|
|
221
|
+
|
|
222
|
+
## Contributing
|
|
223
|
+
|
|
224
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
225
|
+
|
|
226
|
+
## Security Policy
|
|
227
|
+
|
|
228
|
+
See [SECURITY.md](SECURITY.md) for reporting vulnerabilities in STRYX itself.
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
Apache License 2.0. See [LICENSE](LICENSE).
|
|
233
|
+
|
|
234
|
+
## Author
|
|
235
|
+
|
|
236
|
+
Built by Akhilesh Varma (ak495867) under Medusa Security.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
stryx/__init__.py,sha256=S_izSu_FrzL4EFF0E2_Nd_hNYoTBgxL4zpdAVygT55A,1156
|
|
2
|
+
stryx/cli.py,sha256=Et1hU9UhYQVTYkmaYs6kqhHrlgM0LmQQg3TvGyiLDl8,24111
|
|
3
|
+
stryx/orchestrator.py,sha256=hdERxQQ64MR-7nZHeHqI0s854yARRj04sGuIKIFkm_0,13485
|
|
4
|
+
stryx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
stryx/ai/__init__.py,sha256=N4agk2G5KbVO9_uGLJyzjkGVIOOzmv-McDCYFY7fKhc,132
|
|
6
|
+
stryx/ai/attack_planner.py,sha256=BhEo3EtyZDEreHPulysJYC6E6aHusrXJ3XufghNFzgI,7331
|
|
7
|
+
stryx/ai/payload_generator.py,sha256=F88a9nHzCXkIhnVXhJdM4PA655Mhx4FwnP2q7Z9oH7U,6705
|
|
8
|
+
stryx/ai/prompts.py,sha256=Ip8tvSygFDfZeXjewxCoa39PJ93JnndzNAlAnoZSq-Y,2931
|
|
9
|
+
stryx/ai/providers.py,sha256=bO5tbN0wriRUGwlJCqYSFfVnExIW31vyp3xxHTbuJFM,9067
|
|
10
|
+
stryx/attacks/__init__.py,sha256=O44X9KOBHdXnFbpakiZihAncrrSqmiJ-fxeRdoH6sHQ,193
|
|
11
|
+
stryx/attacks/attack_chain.py,sha256=0zHEAAbeMiKty5SleVTJv9cE_VJBpkG2mRxE87r0Jh0,4123
|
|
12
|
+
stryx/attacks/replay.py,sha256=wbNFPv1ZswXxAnbKkvfuwIytOgPY8wxyTB7SrYaz0Fc,7024
|
|
13
|
+
stryx/auth/__init__.py,sha256=QgMP1kRWoVXpyzJzQaR6bsLOi5w8_fyckwUanAALE5k,55
|
|
14
|
+
stryx/auth/session_manager.py,sha256=wtXoabxwe_hJ9YdVZh9jj_kSa8I2-CRAK9YjrtdFSnM,12803
|
|
15
|
+
stryx/comparison/__init__.py,sha256=sGfy6rwPOo9bd5oV0Rs2KDPKYBS6Ehr4FXHknyY3a_Y,67
|
|
16
|
+
stryx/comparison/differ.py,sha256=koUzauoMiZifVyDpnPXhvXUEJlwTXsUZ--l0n9rOe18,8895
|
|
17
|
+
stryx/config/__init__.py,sha256=R_mHNoQOEqY61KAnI-btysnSzpBm4_rwayR11DwgKAY,219
|
|
18
|
+
stryx/config/default_config.yaml,sha256=HkaAX1F5QL1xM1lrvIdbL95kMDG8HKGXMFqQA6ypPuI,206
|
|
19
|
+
stryx/config/loader.py,sha256=KSgzJO03-xAzBJXKXhWO4-ytAu6Oj2RI4VDdkW_Mg7U,3864
|
|
20
|
+
stryx/config/schema.py,sha256=LtBY0oT0DbHorcGgjk8GdxCb8DedUWkpXediOewdX7s,2642
|
|
21
|
+
stryx/crawler/__init__.py,sha256=7aX59WiOKvIJhiu-IviCvUSEyjpTBfoBDY48U0i8BzE,171
|
|
22
|
+
stryx/crawler/discovery.py,sha256=BwgRNpcY7mBajldqIIV5bVBat29T7BVRRE5BiqZizxU,6223
|
|
23
|
+
stryx/crawler/graphql_discovery.py,sha256=wJfK_QSjFkkKLUPWxJsRmhmxV2g33ndNfJH7gstVOMU,2590
|
|
24
|
+
stryx/crawler/html_crawler.py,sha256=DGkPucwBX3xkqpX1t9YzEVJJqyo-TP-1eDj_zZhBVF0,10395
|
|
25
|
+
stryx/crawler/js_endpoints.py,sha256=AVKrS5m-D636xGgYxXAgWveVk8ulGFJ-pB25qy3Fp8I,6463
|
|
26
|
+
stryx/crawler/openapi.py,sha256=tus4jYhezh9pItFHXshEiwKwfRlo61NErUZLf47YSxA,2466
|
|
27
|
+
stryx/crawler/sitemap.py,sha256=yiR1i2DnYrFeNuzk-gfTFr0DSzSEOWVdL_8SOFCXRak,3214
|
|
28
|
+
stryx/payloads/cmdi.txt,sha256=hMMuUgc2_aOOPEEegyAXN4q_fvEh-I0k6HHbZFKteow,462
|
|
29
|
+
stryx/payloads/header_injection.txt,sha256=mohHBpMdv6xGWDn62SzS3gXMdmX4Oow7rbTbzAc1AXI,599
|
|
30
|
+
stryx/payloads/ldap.txt,sha256=FvyGR4mUbl6wpWkCWLUPp237nS8pLLs5pc5-IhOOwNY,351
|
|
31
|
+
stryx/payloads/nosqli.txt,sha256=hpBlSHImpoLRErpk5JPLC8v0x-CjSgppPHrJHUf6mBU,617
|
|
32
|
+
stryx/payloads/open_redirect.txt,sha256=AkudcGqfeLxA37_l0mr-QNbqh3mXHjPGF2CpvaYTMVE,424
|
|
33
|
+
stryx/payloads/path_traversal.txt,sha256=sTld--0__8f_QPvAdbMt3zK9sMXx-rJSLjdVg6DMV_c,647
|
|
34
|
+
stryx/payloads/sqli.txt,sha256=ctDMdoakCn8WbIqkBIcopZLtUcrerEtObxyaOd_-j4c,936
|
|
35
|
+
stryx/payloads/ssrf.txt,sha256=sfXyI856nZ4_udEr6OuJmMOM_1JJ_dAAZMXTTY3jO7Q,637
|
|
36
|
+
stryx/payloads/ssti.txt,sha256=x4g7N8KGp0MJXmO3pWYwcYTyjRwDITkxnt9OqKvMkvM,813
|
|
37
|
+
stryx/payloads/xss.txt,sha256=Co0_Q7H9VDj4Ahem7QQriqmZ12VXTP4QtU5vhLBLvf0,1814
|
|
38
|
+
stryx/payloads/xxe.txt,sha256=NkzyZ3aHjMK6Osf4zbhwMkKmMGoHksgZQh3Gz_Oq5Vg,1395
|
|
39
|
+
stryx/plugins/__init__.py,sha256=ypeTW9tc68QqwcnpG09_xl2w6W2Zt6nYsJfP73gVFE0,178
|
|
40
|
+
stryx/plugins/base.py,sha256=6Wz7HR2gUk5p1a37FV2iiFdsm4qgKhdiys12i51WqN4,2335
|
|
41
|
+
stryx/policy/__init__.py,sha256=qoY4uVnwSqVw7bjo8t6st54i8NXizsek714KzseNj0M,45
|
|
42
|
+
stryx/policy/engine.py,sha256=mEOVlF15mQ2EFDvjW-KwxMqGZ47ICNRIj6GA9bGt5Y8,6733
|
|
43
|
+
stryx/reports/__init__.py,sha256=jUHz6kg7J_5h78SAS71FAWUFZ-PTqp09SEaDfgGImFM,127
|
|
44
|
+
stryx/reports/generator.py,sha256=Z7iZJVvYYexj8y6M947rH2cHWPhlvAkfALlT3dielqI,4245
|
|
45
|
+
stryx/reports/json_report.py,sha256=L2dFNkuXImvEN66EreYuMmIs-wdp-ELPp4sbLFZloko,2458
|
|
46
|
+
stryx/reports/markdown_report.py,sha256=oYthgmWR25gJR_j7CqJQ6CUqhfWAgVNAD_J3LkRVmsw,3750
|
|
47
|
+
stryx/reports/sarif_report.py,sha256=FavrrnlppE1Ke4093kRl0dVYDsDscgiXtGWmfeBoPI8,6910
|
|
48
|
+
stryx/reports/terminal_report.py,sha256=fsl634ZLrC5X8QLemmVv_NQa9XoNyKo_AQL_sEp37Oo,4893
|
|
49
|
+
stryx/reports/templates/report.html.j2,sha256=UD3YAdly4uJKCXO3ro2tAm083cjZVSPw6R-UIEjYKuw,8480
|
|
50
|
+
stryx/scanners/__init__.py,sha256=0Hmecn71GmRO3Di53ty4IrZE_6jB43GWOWndUnY-8_Q,478
|
|
51
|
+
stryx/scanners/auth.py,sha256=xh51nxCl9a8CXP3qEbf7JUdIDsB2a-Qzf1tOo_oYmec,17078
|
|
52
|
+
stryx/scanners/authorization.py,sha256=Hl_I2LmlPfpVgp4ZIcVA1nk72qcBqp_SLBssF235rmQ,8113
|
|
53
|
+
stryx/scanners/blind.py,sha256=M3h3ELkefwbpkqv0dUTzJn3P63_TyUvxgva1fzGpPEU,14009
|
|
54
|
+
stryx/scanners/cloud_ssrf.py,sha256=McINdyoaKRYu5Hv3yOxKFk_ppissdiQV0N5GfChNNDk,8137
|
|
55
|
+
stryx/scanners/cors.py,sha256=Y5PC6lrLtwpYi3mVsHgF3aipDbGIBXPq9xBGhNRWzTw,5869
|
|
56
|
+
stryx/scanners/dependencies.py,sha256=V1ly0-3TsQYA4LcVLdvDm2Zvr7CkizCUlQxzeBcET5k,12191
|
|
57
|
+
stryx/scanners/disclosure.py,sha256=FWjYgUoSRTKkosTmT8Pygi0xqoSkrs4aJ82Ys1QfhaU,15342
|
|
58
|
+
stryx/scanners/fuzz.py,sha256=vkrSDmFtiymUPTm1g_42kCsl9jp2BoCqc9i40I5SJiI,15279
|
|
59
|
+
stryx/scanners/graphql.py,sha256=Ux71PFtPfF_IREdvOYH7vSdkgSRqkH6wF9tmm7EZtsU,12244
|
|
60
|
+
stryx/scanners/injection.py,sha256=Xoi0BUNMXTX4_wSlYg9gPgcR3F5Wcdw-AW7_ujw0y8o,19129
|
|
61
|
+
stryx/scanners/race.py,sha256=lTX1utTq8Fd78YfEN6jOBKdTZcAEOkFJbq8pIPR2gu4,9894
|
|
62
|
+
stryx/signatures/framework_fingerprints.yaml,sha256=pdh_upEFMh7trgRCkqDHeN7aiPDLX5mN8iUgpN5qBcw,2479
|
|
63
|
+
stryx/signatures/known_vulns.yaml,sha256=WW4hqJKHp-E-MGK3RcjcW2_tYIBCnpxsybe7xiYya3g,6309
|
|
64
|
+
stryx/utils/__init__.py,sha256=43l-UFX8ifc9dF7DSRUoMyD1kSxqCQRnLjyd-sL3f70,245
|
|
65
|
+
stryx/utils/evidence.py,sha256=WbcqPh_GKbuEibP97qKL8jiy6nYj02ulMBQ4W5efAZA,3598
|
|
66
|
+
stryx/utils/http_client.py,sha256=Mmd2OLGwwPxqdf8D4aDzLttzgpunEKDyUgdR3d8kyOY,5612
|
|
67
|
+
stryx/utils/logging.py,sha256=ttmZBCrBO9HEfxyH4_IoFRoKZpPx2kRIPVKZauYoeNs,1304
|
|
68
|
+
stryx/utils/rate_limiter.py,sha256=L6H_xq-KYapoh4WRpOK08gT-irRnNKbj2Dcy138QXic,1251
|
|
69
|
+
stryx_cli-0.1.0.dist-info/licenses/LICENSE,sha256=Ey3jWLx-ooBQMHASeVds3kmg6D6Ecakbg76Sev2_bPc,10765
|
|
70
|
+
stryx_cli-0.1.0.dist-info/METADATA,sha256=jiNW8yYaDXNabi5-e6b2fKaMlZrtQVN3efIR4giXTh4,8345
|
|
71
|
+
stryx_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
72
|
+
stryx_cli-0.1.0.dist-info/entry_points.txt,sha256=yD7annfJT3nYuGQAQ2LmhByIZtTcl68xGXgH3eX7vaQ,41
|
|
73
|
+
stryx_cli-0.1.0.dist-info/top_level.txt,sha256=IAEyYP-Fs3MI8cybsIUFdFESepI90IKy7xqmkabIyzI,6
|
|
74
|
+
stryx_cli-0.1.0.dist-info/RECORD,,
|