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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""SARIF 2.1.0 report generator for CI/CD integration.
|
|
2
|
+
|
|
3
|
+
Generates Static Analysis Results Interchange Format (SARIF) reports
|
|
4
|
+
compatible with GitHub Security tab, Azure DevOps, and other SARIF consumers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from stryx.utils.evidence import Finding, Severity
|
|
14
|
+
from stryx.utils.logging import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger("reports.sarif")
|
|
17
|
+
|
|
18
|
+
# SARIF severity mapping
|
|
19
|
+
SEVERITY_MAP = {
|
|
20
|
+
"critical": {"level": "error", "score": 1.0},
|
|
21
|
+
"high": {"level": "error", "score": 0.8},
|
|
22
|
+
"medium": {"level": "warning", "score": 0.5},
|
|
23
|
+
"low": {"level": "note", "score": 0.2},
|
|
24
|
+
"info": {"level": "none", "score": 0.0},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SarifReport:
|
|
29
|
+
"""Generates SARIF 2.1.0 compliant reports."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
target_url: str,
|
|
34
|
+
findings: list[Finding],
|
|
35
|
+
scan_time: str | None = None,
|
|
36
|
+
):
|
|
37
|
+
self.target_url = target_url
|
|
38
|
+
self.findings = findings
|
|
39
|
+
self.scan_time = scan_time or datetime.now(timezone.utc).isoformat()
|
|
40
|
+
|
|
41
|
+
def generate(self) -> dict[str, Any]:
|
|
42
|
+
"""Generate SARIF report as a dictionary."""
|
|
43
|
+
# Build rules from findings
|
|
44
|
+
rules: list[dict[str, Any]] = []
|
|
45
|
+
rule_ids: set[str] = set()
|
|
46
|
+
|
|
47
|
+
for finding in self.findings:
|
|
48
|
+
rule_id = self._get_rule_id(finding)
|
|
49
|
+
if rule_id not in rule_ids:
|
|
50
|
+
rule_ids.add(rule_id)
|
|
51
|
+
rules.append(self._build_rule(finding, rule_id))
|
|
52
|
+
|
|
53
|
+
# Build results
|
|
54
|
+
results = [self._build_result(f) for f in self.findings]
|
|
55
|
+
|
|
56
|
+
sarif = {
|
|
57
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
58
|
+
"version": "2.1.0",
|
|
59
|
+
"runs": [
|
|
60
|
+
{
|
|
61
|
+
"tool": {
|
|
62
|
+
"driver": {
|
|
63
|
+
"name": "STRYX",
|
|
64
|
+
"version": "0.1.0",
|
|
65
|
+
"semanticVersion": "0.1.0",
|
|
66
|
+
"informationUri": "https://github.com/medusa-Security/stryx",
|
|
67
|
+
"rules": rules,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"artifacts": [
|
|
71
|
+
{
|
|
72
|
+
"location": {
|
|
73
|
+
"uri": self.target_url,
|
|
74
|
+
"uriBaseId": "%SRCROOT%",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"results": results,
|
|
79
|
+
"invocations": [
|
|
80
|
+
{
|
|
81
|
+
"startTimeUtc": self.scan_time,
|
|
82
|
+
"toolExecutionNotifications": [],
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sarif
|
|
90
|
+
|
|
91
|
+
def to_json(self, indent: int = 2) -> str:
|
|
92
|
+
"""Generate SARIF report as JSON string."""
|
|
93
|
+
return json.dumps(self.generate(), indent=indent, default=str)
|
|
94
|
+
|
|
95
|
+
def save(self, path: str) -> None:
|
|
96
|
+
"""Save SARIF report to file."""
|
|
97
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
98
|
+
f.write(self.to_json())
|
|
99
|
+
logger.info(f"SARIF report saved to {path}")
|
|
100
|
+
|
|
101
|
+
def _get_rule_id(self, finding: Finding) -> str:
|
|
102
|
+
"""Generate a stable rule ID for a finding."""
|
|
103
|
+
# Use CWE if available
|
|
104
|
+
if finding.cwe:
|
|
105
|
+
cwe_num = finding.cwe.replace("CWE-", "")
|
|
106
|
+
return f"stryx-cwe-{cwe_num}"
|
|
107
|
+
|
|
108
|
+
# Use scanner + title hash
|
|
109
|
+
import hashlib
|
|
110
|
+
title_hash = hashlib.md5(finding.title.encode()).hexdigest()[:8]
|
|
111
|
+
return f"stryx-{finding.scanner}-{title_hash}"
|
|
112
|
+
|
|
113
|
+
def _build_rule(self, finding: Finding, rule_id: str) -> dict[str, Any]:
|
|
114
|
+
"""Build a SARIF rule definition."""
|
|
115
|
+
rule: dict[str, Any] = {
|
|
116
|
+
"id": rule_id,
|
|
117
|
+
"name": finding.title[:80],
|
|
118
|
+
"shortDescription": {
|
|
119
|
+
"text": finding.title,
|
|
120
|
+
},
|
|
121
|
+
"fullDescription": {
|
|
122
|
+
"text": finding.description or finding.title,
|
|
123
|
+
},
|
|
124
|
+
"helpUri": "",
|
|
125
|
+
"properties": {
|
|
126
|
+
"tags": finding.tags if hasattr(finding, 'tags') and finding.tags else [],
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Add CWE reference
|
|
131
|
+
if finding.cwe:
|
|
132
|
+
cwe_num = finding.cwe.replace("CWE-", "")
|
|
133
|
+
rule["helpUri"] = f"https://cwe.mitre.org/data/definitions/{cwe_num}.html"
|
|
134
|
+
rule["properties"]["tags"].append(f"security")
|
|
135
|
+
rule["properties"]["tags"].append(f"external/cwe/cwe-{cwe_num}")
|
|
136
|
+
|
|
137
|
+
# Add OWASP reference
|
|
138
|
+
if finding.owasp:
|
|
139
|
+
rule["properties"]["owasp"] = finding.owasp
|
|
140
|
+
|
|
141
|
+
return rule
|
|
142
|
+
|
|
143
|
+
def _build_result(self, finding: Finding) -> dict[str, Any]:
|
|
144
|
+
"""Build a SARIF result for a finding."""
|
|
145
|
+
sev_data = SEVERITY_MAP.get(finding.severity.value, SEVERITY_MAP["info"])
|
|
146
|
+
|
|
147
|
+
result: dict[str, Any] = {
|
|
148
|
+
"ruleId": self._get_rule_id(finding),
|
|
149
|
+
"level": sev_data["level"],
|
|
150
|
+
"message": {
|
|
151
|
+
"text": finding.description or finding.title,
|
|
152
|
+
},
|
|
153
|
+
"locations": [
|
|
154
|
+
{
|
|
155
|
+
"physicalLocation": {
|
|
156
|
+
"artifactLocation": {
|
|
157
|
+
"uri": finding.endpoint or self.target_url,
|
|
158
|
+
},
|
|
159
|
+
"region": {
|
|
160
|
+
"startLine": 1,
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"properties": {
|
|
166
|
+
"severity": finding.severity.value,
|
|
167
|
+
"confidence": finding.evidence.confidence,
|
|
168
|
+
"scanner": finding.scanner,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Add CWE/OWASP to properties
|
|
173
|
+
if finding.cwe:
|
|
174
|
+
result["properties"]["cwe"] = finding.cwe
|
|
175
|
+
if finding.owasp:
|
|
176
|
+
result["properties"]["owasp"] = finding.owasp
|
|
177
|
+
|
|
178
|
+
# Add remediation as help text
|
|
179
|
+
if finding.remediation:
|
|
180
|
+
result["fixes"] = [
|
|
181
|
+
{
|
|
182
|
+
"description": {
|
|
183
|
+
"text": finding.remediation,
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
# Add request/response evidence
|
|
189
|
+
if finding.evidence:
|
|
190
|
+
evidence_parts = []
|
|
191
|
+
evidence_parts.append(f"Request: {finding.evidence.request_method} {finding.evidence.request_url}")
|
|
192
|
+
evidence_parts.append(f"Status: {finding.evidence.response_status}")
|
|
193
|
+
if finding.evidence.payload:
|
|
194
|
+
evidence_parts.append(f"Payload: {finding.evidence.payload}")
|
|
195
|
+
if finding.evidence.response_snippet:
|
|
196
|
+
evidence_parts.append(f"Response: {finding.evidence.response_snippet[:200]}")
|
|
197
|
+
|
|
198
|
+
result["properties"]["evidence"] = "\n".join(evidence_parts)
|
|
199
|
+
|
|
200
|
+
return result
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>STRYX Security Report - {{ target }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.6; }
|
|
10
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
11
|
+
.header { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 24px; margin-bottom: 24px; }
|
|
12
|
+
.header h1 { color: #58a6ff; font-size: 24px; margin-bottom: 8px; }
|
|
13
|
+
.header .meta { color: #8b949e; font-size: 14px; }
|
|
14
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
15
|
+
.summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; text-align: center; }
|
|
16
|
+
.summary-card .count { font-size: 32px; font-weight: bold; }
|
|
17
|
+
.summary-card .label { color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
|
18
|
+
.critical { color: #f85149; }
|
|
19
|
+
.high { color: #f0883e; }
|
|
20
|
+
.medium { color: #d29922; }
|
|
21
|
+
.low { color: #58a6ff; }
|
|
22
|
+
.info { color: #8b949e; }
|
|
23
|
+
.finding { background: #161b22; border: 1px solid #30363d; border-radius: 8px; margin-bottom: 16px; overflow: hidden; }
|
|
24
|
+
.finding-header { padding: 16px; border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; align-items: center; }
|
|
25
|
+
.finding-title { font-size: 16px; font-weight: 600; }
|
|
26
|
+
.severity-badge { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; text-transform: uppercase; }
|
|
27
|
+
.severity-critical { background: #f85149; color: #fff; }
|
|
28
|
+
.severity-high { background: #f0883e; color: #fff; }
|
|
29
|
+
.severity-medium { background: #d29922; color: #fff; }
|
|
30
|
+
.severity-low { background: #58a6ff; color: #fff; }
|
|
31
|
+
.severity-info { background: #8b949e; color: #fff; }
|
|
32
|
+
.finding-body { padding: 16px; }
|
|
33
|
+
.finding-section { margin-bottom: 12px; }
|
|
34
|
+
.finding-section h3 { color: #8b949e; font-size: 12px; text-transform: uppercase; margin-bottom: 4px; }
|
|
35
|
+
.finding-section p { margin: 0; }
|
|
36
|
+
.tags { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
|
|
37
|
+
.tag { background: #21262d; color: #8b949e; padding: 2px 8px; border-radius: 12px; font-size: 11px; }
|
|
38
|
+
.evidence-block { background: #0d1117; border: 1px solid #30363d; border-radius: 4px; padding: 12px; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
|
39
|
+
.evidence-header { color: #58a6ff; margin-bottom: 8px; }
|
|
40
|
+
.evidence-label { color: #8b949e; }
|
|
41
|
+
.chain { background: #161b22; border: 1px solid #30363d; border-radius: 8px; margin-bottom: 16px; padding: 16px; }
|
|
42
|
+
.chain h2 { color: #f0883e; margin-bottom: 12px; font-size: 18px; }
|
|
43
|
+
.chain-meta { color: #8b949e; margin-bottom: 12px; }
|
|
44
|
+
.chain-step { padding: 8px 12px; border-left: 3px solid #30363d; margin-bottom: 8px; background: #0d1117; border-radius: 0 4px 4px 0; }
|
|
45
|
+
.chain-step-title { font-weight: 600; color: #c9d1d9; }
|
|
46
|
+
.chain-step-desc { color: #8b949e; font-size: 13px; margin-top: 4px; }
|
|
47
|
+
.footer { text-align: center; color: #8b949e; padding: 24px; font-size: 12px; border-top: 1px solid #30363d; margin-top: 24px; }
|
|
48
|
+
.no-findings { text-align: center; padding: 40px; color: #3fb950; font-size: 18px; }
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<div class="container">
|
|
53
|
+
<div class="header">
|
|
54
|
+
<h1>🛡️ STRYX Security Report</h1>
|
|
55
|
+
<div class="meta">
|
|
56
|
+
<p><strong>Target:</strong> {{ target }}</p>
|
|
57
|
+
<p><strong>Generated:</strong> {{ generated_at }}</p>
|
|
58
|
+
<p><strong>Tool:</strong> STRYX v0.1.0 - AI-Powered DAST</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="summary">
|
|
63
|
+
<div class="summary-card">
|
|
64
|
+
<div class="count">{{ total_findings }}</div>
|
|
65
|
+
<div class="label">Total Findings</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="summary-card">
|
|
68
|
+
<div class="count critical">{{ critical_count }}</div>
|
|
69
|
+
<div class="label">Critical</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="summary-card">
|
|
72
|
+
<div class="count high">{{ high_count }}</div>
|
|
73
|
+
<div class="label">High</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="summary-card">
|
|
76
|
+
<div class="count medium">{{ medium_count }}</div>
|
|
77
|
+
<div class="label">Medium</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="summary-card">
|
|
80
|
+
<div class="count low">{{ low_count }}</div>
|
|
81
|
+
<div class="label">Low</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{% if findings %}
|
|
86
|
+
<h2 style="color: #c9d1d9; margin-bottom: 16px;">Findings ({{ findings|length }})</h2>
|
|
87
|
+
|
|
88
|
+
{% for finding in findings %}
|
|
89
|
+
<div class="finding">
|
|
90
|
+
<div class="finding-header">
|
|
91
|
+
<span class="finding-title">{{ finding.title }}</span>
|
|
92
|
+
<span class="severity-badge severity-{{ finding.severity }}">{{ finding.severity }}</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="finding-body">
|
|
95
|
+
<div class="finding-section">
|
|
96
|
+
<h3>Endpoint</h3>
|
|
97
|
+
<p><code>{{ finding.endpoint }}</code></p>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="finding-section">
|
|
100
|
+
<h3>Description</h3>
|
|
101
|
+
<p>{{ finding.description }}</p>
|
|
102
|
+
</div>
|
|
103
|
+
{% if finding.remediation %}
|
|
104
|
+
<div class="finding-section">
|
|
105
|
+
<h3>Remediation</h3>
|
|
106
|
+
<p>{{ finding.remediation }}</p>
|
|
107
|
+
</div>
|
|
108
|
+
{% endif %}
|
|
109
|
+
{% if finding.cwe or finding.owasp %}
|
|
110
|
+
<div class="finding-section">
|
|
111
|
+
<h3>References</h3>
|
|
112
|
+
<p>
|
|
113
|
+
{% if finding.cwe %}<strong>CWE:</strong> {{ finding.cwe }}{% endif %}
|
|
114
|
+
{% if finding.cwe and finding.owasp %} | {% endif %}
|
|
115
|
+
{% if finding.owasp %}<strong>OWASP:</strong> {{ finding.owasp }}{% endif %}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
{% endif %}
|
|
119
|
+
<div class="finding-section">
|
|
120
|
+
<h3>Evidence</h3>
|
|
121
|
+
<div class="evidence-block"><span class="evidence-header">{{ finding.evidence.request_method }} {{ finding.evidence.request_url }}</span>
|
|
122
|
+
<span class="evidence-label">Status:</span> {{ finding.evidence.response_status }}
|
|
123
|
+
<span class="evidence-label">Confidence:</span> {{ "%.0f"|format(finding.evidence.confidence * 100) }}%
|
|
124
|
+
{% if finding.evidence.payload %}<span class="evidence-label">Payload:</span> {{ finding.evidence.payload }}{% endif %}
|
|
125
|
+
|
|
126
|
+
<span class="evidence-label">Response Snippet:</span>
|
|
127
|
+
{{ finding.evidence.response_snippet[:500] }}</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
{% endfor %}
|
|
132
|
+
{% else %}
|
|
133
|
+
<div class="no-findings">✅ No security findings detected</div>
|
|
134
|
+
{% endif %}
|
|
135
|
+
|
|
136
|
+
{% if attack_chains %}
|
|
137
|
+
<h2 style="color: #f0883e; margin: 24px 0 16px;">⚔️ Attack Chains ({{ attack_chains|length }})</h2>
|
|
138
|
+
|
|
139
|
+
{% for chain in attack_chains %}
|
|
140
|
+
<div class="chain">
|
|
141
|
+
<h2>{{ chain.name }}</h2>
|
|
142
|
+
<div class="chain-meta">
|
|
143
|
+
<p><strong>Impact:</strong> {{ chain.total_impact }}</p>
|
|
144
|
+
<p><strong>Estimated Severity:</strong> {{ chain.estimated_severity }}</p>
|
|
145
|
+
</div>
|
|
146
|
+
{% for step in chain.steps %}
|
|
147
|
+
<div class="chain-step">
|
|
148
|
+
<div class="chain-step-title">Step {{ loop.index }}: {{ step.title }}</div>
|
|
149
|
+
<div class="chain-step-desc">{{ step.description }}</div>
|
|
150
|
+
</div>
|
|
151
|
+
{% endfor %}
|
|
152
|
+
</div>
|
|
153
|
+
{% endfor %}
|
|
154
|
+
{% endif %}
|
|
155
|
+
|
|
156
|
+
<div class="footer">
|
|
157
|
+
<p>STRYX v0.1.0 - AI-Powered Dynamic Application Security Testing</p>
|
|
158
|
+
<p>Built by Akhilesh Varma under Medusa Security</p>
|
|
159
|
+
<p>Generated {{ generated_at }}</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Terminal report generator using Rich."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from stryx.attacks.attack_chain import AttackChain
|
|
9
|
+
from stryx.utils.evidence import Finding
|
|
10
|
+
from stryx.utils.logging import console as stryx_console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TerminalReport:
|
|
14
|
+
"""Generates rich terminal output for scan results."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
target_url: str,
|
|
19
|
+
findings: list[Finding],
|
|
20
|
+
attack_chains: list[AttackChain] | None = None,
|
|
21
|
+
):
|
|
22
|
+
self.target_url = target_url
|
|
23
|
+
self.findings = findings
|
|
24
|
+
self.attack_chains = attack_chains or []
|
|
25
|
+
self.console = stryx_console
|
|
26
|
+
|
|
27
|
+
def display(self) -> None:
|
|
28
|
+
"""Display the terminal report."""
|
|
29
|
+
self._display_summary()
|
|
30
|
+
self._display_findings_table()
|
|
31
|
+
self._display_findings_detail()
|
|
32
|
+
self._display_attack_chains()
|
|
33
|
+
|
|
34
|
+
def _display_summary(self) -> None:
|
|
35
|
+
"""Display summary panel."""
|
|
36
|
+
severity_counts: dict[str, int] = {}
|
|
37
|
+
for f in self.findings:
|
|
38
|
+
sev = f.severity.value
|
|
39
|
+
severity_counts[sev] = severity_counts.get(sev, 0) + 1
|
|
40
|
+
|
|
41
|
+
summary_parts = []
|
|
42
|
+
for sev in ["critical", "high", "medium", "low", "info"]:
|
|
43
|
+
count = severity_counts.get(sev, 0)
|
|
44
|
+
if count > 0:
|
|
45
|
+
summary_parts.append(f"[severity.{sev}]{sev.upper()}: {count}[/]")
|
|
46
|
+
|
|
47
|
+
summary = " | ".join(summary_parts) if summary_parts else "No findings"
|
|
48
|
+
|
|
49
|
+
self.console.print(Panel(
|
|
50
|
+
f"[bold]Target:[/] {self.target_url}\n"
|
|
51
|
+
f"[bold]Total Findings:[/] {len(self.findings)}\n"
|
|
52
|
+
f"[bold]Breakdown:[/] {summary}",
|
|
53
|
+
title="[bold]STRYX Scan Results[/]",
|
|
54
|
+
border_style="cyan",
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
def _display_findings_table(self) -> None:
|
|
58
|
+
"""Display findings in a table."""
|
|
59
|
+
if not self.findings:
|
|
60
|
+
self.console.print("[green]No findings detected.[/]")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
table = Table(title="Findings", show_lines=True)
|
|
64
|
+
table.add_column("#", style="dim", width=4)
|
|
65
|
+
table.add_column("Severity", width=10)
|
|
66
|
+
table.add_column("Title", min_width=30)
|
|
67
|
+
table.add_column("Endpoint", min_width=20)
|
|
68
|
+
table.add_column("Confidence", width=10)
|
|
69
|
+
|
|
70
|
+
for i, f in enumerate(self.findings, 1):
|
|
71
|
+
severity_colors = {
|
|
72
|
+
"critical": "bold red",
|
|
73
|
+
"high": "red",
|
|
74
|
+
"medium": "yellow",
|
|
75
|
+
"low": "blue",
|
|
76
|
+
"info": "dim",
|
|
77
|
+
}
|
|
78
|
+
color = severity_colors.get(f.severity.value, "white")
|
|
79
|
+
|
|
80
|
+
table.add_row(
|
|
81
|
+
str(i),
|
|
82
|
+
f"[{color}]{f.severity.value.upper()}[/]",
|
|
83
|
+
f.title,
|
|
84
|
+
f.endpoint[:50] + ("..." if len(f.endpoint) > 50 else ""),
|
|
85
|
+
f"{f.evidence.confidence:.0%}",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.console.print(table)
|
|
89
|
+
|
|
90
|
+
def _display_findings_detail(self) -> None:
|
|
91
|
+
"""Display detailed findings."""
|
|
92
|
+
if not self.findings:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
self.console.print("\n[bold]Detailed Findings:[/]\n")
|
|
96
|
+
|
|
97
|
+
for i, f in enumerate(self.findings, 1):
|
|
98
|
+
severity_colors = {
|
|
99
|
+
"critical": "bold red",
|
|
100
|
+
"high": "red",
|
|
101
|
+
"medium": "yellow",
|
|
102
|
+
"low": "blue",
|
|
103
|
+
"info": "dim",
|
|
104
|
+
}
|
|
105
|
+
color = severity_colors.get(f.severity.value, "white")
|
|
106
|
+
|
|
107
|
+
detail = (
|
|
108
|
+
f"[bold]{f.title}[/]\n"
|
|
109
|
+
f" Severity: [{color}]{f.severity.value.upper()}[/] | "
|
|
110
|
+
f"CWE: {f.cwe} | Scanner: {f.scanner}\n"
|
|
111
|
+
f" Endpoint: {f.endpoint}\n"
|
|
112
|
+
f" Description: {f.description}\n"
|
|
113
|
+
f" Remediation: {f.remediation}\n"
|
|
114
|
+
f" Evidence: {f.evidence.request_method} {f.evidence.request_url} "
|
|
115
|
+
f"-> {f.evidence.response_status}"
|
|
116
|
+
)
|
|
117
|
+
if f.evidence.payload:
|
|
118
|
+
detail += f"\n Payload: {f.evidence.payload}"
|
|
119
|
+
if f.evidence.response_snippet:
|
|
120
|
+
detail += f"\n Response: {f.evidence.response_snippet[:200]}"
|
|
121
|
+
|
|
122
|
+
self.console.print(Panel(detail, border_style=color))
|
|
123
|
+
|
|
124
|
+
def _display_attack_chains(self) -> None:
|
|
125
|
+
"""Display attack chains."""
|
|
126
|
+
if not self.attack_chains:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
self.console.print("\n[bold]Attack Chains:[/]\n")
|
|
130
|
+
|
|
131
|
+
for chain in self.attack_chains:
|
|
132
|
+
chain_text = f"[bold]{chain.name}[/]\n"
|
|
133
|
+
chain_text += f"Impact: {chain.total_impact}\n"
|
|
134
|
+
chain_text += f"Severity: {chain.estimated_severity.upper()}\n\n"
|
|
135
|
+
for j, step in enumerate(chain.steps, 1):
|
|
136
|
+
chain_text += f" {j}. {step.finding.title} - {step.description}\n"
|
|
137
|
+
|
|
138
|
+
self.console.print(Panel(chain_text, border_style="magenta"))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Scanner modules for STRYX."""
|
|
2
|
+
|
|
3
|
+
from stryx.scanners.auth import AuthScanner
|
|
4
|
+
from stryx.scanners.authorization import AuthorizationScanner
|
|
5
|
+
from stryx.scanners.cors import CorsScanner
|
|
6
|
+
from stryx.scanners.fuzz import FuzzScanner
|
|
7
|
+
from stryx.scanners.graphql import GraphQLScanner
|
|
8
|
+
from stryx.scanners.injection import InjectionScanner
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AuthScanner",
|
|
12
|
+
"AuthorizationScanner",
|
|
13
|
+
"InjectionScanner",
|
|
14
|
+
"FuzzScanner",
|
|
15
|
+
"CorsScanner",
|
|
16
|
+
"GraphQLScanner",
|
|
17
|
+
]
|