aiptx 2.0.2__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.
Potentially problematic release.
This version of aiptx might be problematic. Click here for more details.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +24 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +960 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +321 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +288 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +85 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2284 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +44 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +201 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aiptx-2.0.2.dist-info/METADATA +324 -0
- aiptx-2.0.2.dist-info/RECORD +165 -0
- aiptx-2.0.2.dist-info/WHEEL +5 -0
- aiptx-2.0.2.dist-info/entry_points.txt +7 -0
- aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI/Swagger Security Fuzzer
|
|
3
|
+
|
|
4
|
+
Comprehensive REST API security testing based on OpenAPI specifications:
|
|
5
|
+
- Automatic endpoint discovery from OpenAPI/Swagger specs
|
|
6
|
+
- Parameter fuzzing (path, query, header, body)
|
|
7
|
+
- Authentication bypass testing
|
|
8
|
+
- BOLA/IDOR detection
|
|
9
|
+
- Mass assignment vulnerabilities
|
|
10
|
+
- Rate limiting bypass
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
- OWASP API Security Top 10
|
|
14
|
+
- https://swagger.io/specification/
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from aipt_v2.tools.api_security import OpenAPIFuzzer
|
|
18
|
+
|
|
19
|
+
fuzzer = OpenAPIFuzzer("https://api.target.com", spec_path="openapi.yaml")
|
|
20
|
+
findings = await fuzzer.fuzz()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import random
|
|
27
|
+
import string
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import List, Dict, Any, Optional, Union
|
|
32
|
+
from urllib.parse import urljoin, urlencode
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import aiohttp
|
|
36
|
+
except ImportError:
|
|
37
|
+
aiohttp = None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
import yaml
|
|
41
|
+
except ImportError:
|
|
42
|
+
yaml = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class OpenAPIConfig:
|
|
47
|
+
"""OpenAPI fuzzer configuration."""
|
|
48
|
+
base_url: str
|
|
49
|
+
spec_path: Optional[str] = None
|
|
50
|
+
spec_url: Optional[str] = None
|
|
51
|
+
spec_data: Optional[Dict] = None
|
|
52
|
+
|
|
53
|
+
# Authentication
|
|
54
|
+
auth_token: str = ""
|
|
55
|
+
auth_header: str = "Authorization"
|
|
56
|
+
api_key: str = ""
|
|
57
|
+
api_key_header: str = "X-API-Key"
|
|
58
|
+
|
|
59
|
+
# Fuzzing options
|
|
60
|
+
fuzz_parameters: bool = True
|
|
61
|
+
fuzz_bodies: bool = True
|
|
62
|
+
test_bola: bool = True # Broken Object Level Authorization
|
|
63
|
+
test_mass_assignment: bool = True
|
|
64
|
+
test_rate_limit: bool = True
|
|
65
|
+
test_auth_bypass: bool = True
|
|
66
|
+
|
|
67
|
+
# Limits
|
|
68
|
+
max_requests_per_endpoint: int = 10
|
|
69
|
+
timeout: int = 30
|
|
70
|
+
delay_ms: int = 100 # Delay between requests
|
|
71
|
+
|
|
72
|
+
# Headers
|
|
73
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class OpenAPIFinding:
|
|
78
|
+
"""OpenAPI security finding."""
|
|
79
|
+
vulnerability: str
|
|
80
|
+
severity: str
|
|
81
|
+
endpoint: str
|
|
82
|
+
method: str
|
|
83
|
+
description: str
|
|
84
|
+
evidence: str
|
|
85
|
+
remediation: str
|
|
86
|
+
parameter: str = ""
|
|
87
|
+
payload: str = ""
|
|
88
|
+
response_code: int = 0
|
|
89
|
+
timestamp: str = ""
|
|
90
|
+
cwe: str = ""
|
|
91
|
+
|
|
92
|
+
def __post_init__(self):
|
|
93
|
+
if not self.timestamp:
|
|
94
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class OpenAPIEndpoint:
|
|
99
|
+
"""Parsed OpenAPI endpoint."""
|
|
100
|
+
path: str
|
|
101
|
+
method: str
|
|
102
|
+
operation_id: str
|
|
103
|
+
summary: str
|
|
104
|
+
parameters: List[Dict]
|
|
105
|
+
request_body: Optional[Dict]
|
|
106
|
+
responses: Dict
|
|
107
|
+
security: List[Dict]
|
|
108
|
+
tags: List[str]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class OpenAPIFuzzResult:
|
|
113
|
+
"""Result of OpenAPI fuzzing."""
|
|
114
|
+
base_url: str
|
|
115
|
+
status: str
|
|
116
|
+
started_at: str
|
|
117
|
+
finished_at: str
|
|
118
|
+
duration: float
|
|
119
|
+
endpoints_tested: int
|
|
120
|
+
requests_made: int
|
|
121
|
+
findings: List[OpenAPIFinding]
|
|
122
|
+
spec_info: Dict[str, Any]
|
|
123
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class OpenAPIFuzzer:
|
|
127
|
+
"""
|
|
128
|
+
OpenAPI/Swagger Security Fuzzer.
|
|
129
|
+
|
|
130
|
+
Parses OpenAPI specifications and performs comprehensive
|
|
131
|
+
security testing on discovered endpoints.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
# Fuzzing payloads by type
|
|
135
|
+
SQLI_PAYLOADS = ["'", "\"", "' OR '1'='1", "1; DROP TABLE users--", "admin'--"]
|
|
136
|
+
XSS_PAYLOADS = ["<script>alert(1)</script>", "javascript:alert(1)", "<img onerror=alert(1)>"]
|
|
137
|
+
PATH_TRAVERSAL = ["../../../etc/passwd", "..\\..\\..\\windows\\system32\\config\\sam"]
|
|
138
|
+
COMMAND_INJECTION = ["; ls -la", "| cat /etc/passwd", "`whoami`", "$(id)"]
|
|
139
|
+
NOSQL_PAYLOADS = ['{"$gt": ""}', '{"$ne": null}', '{"$regex": ".*"}']
|
|
140
|
+
|
|
141
|
+
# BOLA test IDs
|
|
142
|
+
BOLA_IDS = ["1", "0", "-1", "admin", "999999", "../1", "1 OR 1=1"]
|
|
143
|
+
|
|
144
|
+
def __init__(self, base_url: str, config: Optional[OpenAPIConfig] = None, **kwargs):
|
|
145
|
+
"""
|
|
146
|
+
Initialize OpenAPI fuzzer.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
base_url: Base API URL
|
|
150
|
+
config: Fuzzer configuration
|
|
151
|
+
**kwargs: Additional config options
|
|
152
|
+
"""
|
|
153
|
+
self.base_url = base_url.rstrip("/")
|
|
154
|
+
self.config = config or OpenAPIConfig(
|
|
155
|
+
base_url=base_url,
|
|
156
|
+
spec_path=kwargs.get("spec_path"),
|
|
157
|
+
spec_url=kwargs.get("spec_url")
|
|
158
|
+
)
|
|
159
|
+
self.spec: Dict = {}
|
|
160
|
+
self.endpoints: List[OpenAPIEndpoint] = []
|
|
161
|
+
self.findings: List[OpenAPIFinding] = []
|
|
162
|
+
self.requests_made = 0
|
|
163
|
+
|
|
164
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
165
|
+
"""Build request headers."""
|
|
166
|
+
headers = {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"Accept": "application/json",
|
|
169
|
+
"User-Agent": "AIPTX-OpenAPI-Fuzzer/1.0"
|
|
170
|
+
}
|
|
171
|
+
headers.update(self.config.headers)
|
|
172
|
+
|
|
173
|
+
if self.config.auth_token:
|
|
174
|
+
headers[self.config.auth_header] = f"Bearer {self.config.auth_token}"
|
|
175
|
+
|
|
176
|
+
if self.config.api_key:
|
|
177
|
+
headers[self.config.api_key_header] = self.config.api_key
|
|
178
|
+
|
|
179
|
+
return headers
|
|
180
|
+
|
|
181
|
+
async def load_spec(self) -> bool:
|
|
182
|
+
"""Load and parse OpenAPI specification."""
|
|
183
|
+
spec_data = None
|
|
184
|
+
|
|
185
|
+
# Try loading from data
|
|
186
|
+
if self.config.spec_data:
|
|
187
|
+
spec_data = self.config.spec_data
|
|
188
|
+
|
|
189
|
+
# Try loading from file
|
|
190
|
+
elif self.config.spec_path:
|
|
191
|
+
path = Path(self.config.spec_path)
|
|
192
|
+
if path.exists():
|
|
193
|
+
content = path.read_text()
|
|
194
|
+
if path.suffix in [".yaml", ".yml"]:
|
|
195
|
+
if yaml:
|
|
196
|
+
spec_data = yaml.safe_load(content)
|
|
197
|
+
else:
|
|
198
|
+
raise ImportError("PyYAML required for YAML specs. Install with: pip install pyyaml")
|
|
199
|
+
else:
|
|
200
|
+
spec_data = json.loads(content)
|
|
201
|
+
|
|
202
|
+
# Try loading from URL
|
|
203
|
+
elif self.config.spec_url:
|
|
204
|
+
if aiohttp:
|
|
205
|
+
try:
|
|
206
|
+
async with aiohttp.ClientSession() as session:
|
|
207
|
+
async with session.get(
|
|
208
|
+
self.config.spec_url,
|
|
209
|
+
headers=self._get_headers(),
|
|
210
|
+
ssl=False
|
|
211
|
+
) as response:
|
|
212
|
+
text = await response.text()
|
|
213
|
+
if "yaml" in self.config.spec_url or "yml" in self.config.spec_url:
|
|
214
|
+
if yaml:
|
|
215
|
+
spec_data = yaml.safe_load(text)
|
|
216
|
+
else:
|
|
217
|
+
spec_data = json.loads(text)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"[!] Error loading spec from URL: {e}")
|
|
220
|
+
|
|
221
|
+
# Try common spec locations
|
|
222
|
+
if not spec_data:
|
|
223
|
+
common_paths = [
|
|
224
|
+
"/openapi.json", "/swagger.json", "/api-docs",
|
|
225
|
+
"/openapi.yaml", "/swagger.yaml",
|
|
226
|
+
"/v2/api-docs", "/v3/api-docs"
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
if aiohttp:
|
|
230
|
+
async with aiohttp.ClientSession() as session:
|
|
231
|
+
for path in common_paths:
|
|
232
|
+
try:
|
|
233
|
+
url = urljoin(self.base_url, path)
|
|
234
|
+
async with session.get(url, ssl=False, timeout=10) as response:
|
|
235
|
+
if response.status == 200:
|
|
236
|
+
text = await response.text()
|
|
237
|
+
try:
|
|
238
|
+
spec_data = json.loads(text)
|
|
239
|
+
print(f"[*] Found OpenAPI spec at {path}")
|
|
240
|
+
break
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
if yaml:
|
|
243
|
+
spec_data = yaml.safe_load(text)
|
|
244
|
+
print(f"[*] Found OpenAPI spec at {path}")
|
|
245
|
+
break
|
|
246
|
+
except Exception:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
if spec_data:
|
|
250
|
+
self.spec = spec_data
|
|
251
|
+
self._parse_endpoints()
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _parse_endpoints(self):
|
|
257
|
+
"""Parse endpoints from OpenAPI spec."""
|
|
258
|
+
self.endpoints = []
|
|
259
|
+
|
|
260
|
+
# Get paths from spec
|
|
261
|
+
paths = self.spec.get("paths", {})
|
|
262
|
+
|
|
263
|
+
for path, methods in paths.items():
|
|
264
|
+
for method, operation in methods.items():
|
|
265
|
+
if method.upper() not in ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Parse parameters
|
|
269
|
+
parameters = []
|
|
270
|
+
params = operation.get("parameters", []) + methods.get("parameters", [])
|
|
271
|
+
for param in params:
|
|
272
|
+
# Handle $ref
|
|
273
|
+
if "$ref" in param:
|
|
274
|
+
ref_path = param["$ref"].split("/")[-1]
|
|
275
|
+
param = self.spec.get("components", {}).get("parameters", {}).get(ref_path, param)
|
|
276
|
+
|
|
277
|
+
parameters.append({
|
|
278
|
+
"name": param.get("name", ""),
|
|
279
|
+
"in": param.get("in", "query"),
|
|
280
|
+
"required": param.get("required", False),
|
|
281
|
+
"schema": param.get("schema", {}),
|
|
282
|
+
"type": param.get("schema", {}).get("type", "string")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
# Parse request body
|
|
286
|
+
request_body = None
|
|
287
|
+
if "requestBody" in operation:
|
|
288
|
+
rb = operation["requestBody"]
|
|
289
|
+
content = rb.get("content", {})
|
|
290
|
+
if "application/json" in content:
|
|
291
|
+
schema = content["application/json"].get("schema", {})
|
|
292
|
+
request_body = {
|
|
293
|
+
"required": rb.get("required", False),
|
|
294
|
+
"schema": schema
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
endpoint = OpenAPIEndpoint(
|
|
298
|
+
path=path,
|
|
299
|
+
method=method.upper(),
|
|
300
|
+
operation_id=operation.get("operationId", ""),
|
|
301
|
+
summary=operation.get("summary", ""),
|
|
302
|
+
parameters=parameters,
|
|
303
|
+
request_body=request_body,
|
|
304
|
+
responses=operation.get("responses", {}),
|
|
305
|
+
security=operation.get("security", []),
|
|
306
|
+
tags=operation.get("tags", [])
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
self.endpoints.append(endpoint)
|
|
310
|
+
|
|
311
|
+
async def _send_request(
|
|
312
|
+
self,
|
|
313
|
+
method: str,
|
|
314
|
+
path: str,
|
|
315
|
+
params: Optional[Dict] = None,
|
|
316
|
+
body: Optional[Dict] = None,
|
|
317
|
+
headers: Optional[Dict] = None
|
|
318
|
+
) -> Dict[str, Any]:
|
|
319
|
+
"""Send HTTP request and return response."""
|
|
320
|
+
if aiohttp is None:
|
|
321
|
+
raise ImportError("aiohttp required. Install with: pip install aiohttp")
|
|
322
|
+
|
|
323
|
+
url = urljoin(self.base_url, path)
|
|
324
|
+
if params:
|
|
325
|
+
url = f"{url}?{urlencode(params)}"
|
|
326
|
+
|
|
327
|
+
req_headers = self._get_headers()
|
|
328
|
+
if headers:
|
|
329
|
+
req_headers.update(headers)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
await asyncio.sleep(self.config.delay_ms / 1000)
|
|
333
|
+
self.requests_made += 1
|
|
334
|
+
|
|
335
|
+
async with aiohttp.ClientSession() as session:
|
|
336
|
+
async with session.request(
|
|
337
|
+
method,
|
|
338
|
+
url,
|
|
339
|
+
json=body if body else None,
|
|
340
|
+
headers=req_headers,
|
|
341
|
+
timeout=aiohttp.ClientTimeout(total=self.config.timeout),
|
|
342
|
+
ssl=False
|
|
343
|
+
) as response:
|
|
344
|
+
text = await response.text()
|
|
345
|
+
try:
|
|
346
|
+
data = json.loads(text) if text else {}
|
|
347
|
+
except json.JSONDecodeError:
|
|
348
|
+
data = {"raw": text}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"status": response.status,
|
|
352
|
+
"headers": dict(response.headers),
|
|
353
|
+
"data": data,
|
|
354
|
+
"url": str(response.url)
|
|
355
|
+
}
|
|
356
|
+
except Exception as e:
|
|
357
|
+
return {"error": str(e)}
|
|
358
|
+
|
|
359
|
+
async def fuzz_endpoint(self, endpoint: OpenAPIEndpoint) -> List[OpenAPIFinding]:
|
|
360
|
+
"""Fuzz a single endpoint."""
|
|
361
|
+
findings = []
|
|
362
|
+
|
|
363
|
+
# Fuzz path parameters
|
|
364
|
+
path = endpoint.path
|
|
365
|
+
path_params = re.findall(r"\{(\w+)\}", path)
|
|
366
|
+
|
|
367
|
+
for param in path_params:
|
|
368
|
+
for payload in self.SQLI_PAYLOADS + self.PATH_TRAVERSAL:
|
|
369
|
+
test_path = path.replace(f"{{{param}}}", payload)
|
|
370
|
+
response = await self._send_request(endpoint.method, test_path)
|
|
371
|
+
|
|
372
|
+
if self._check_sqli_response(response):
|
|
373
|
+
findings.append(OpenAPIFinding(
|
|
374
|
+
vulnerability="SQL Injection in Path Parameter",
|
|
375
|
+
severity="critical",
|
|
376
|
+
endpoint=endpoint.path,
|
|
377
|
+
method=endpoint.method,
|
|
378
|
+
parameter=param,
|
|
379
|
+
payload=payload,
|
|
380
|
+
description=f"Path parameter '{param}' appears vulnerable to SQL injection",
|
|
381
|
+
evidence=f"Response indicates SQL error or unexpected behavior",
|
|
382
|
+
response_code=response.get("status", 0),
|
|
383
|
+
remediation="Use parameterized queries and input validation",
|
|
384
|
+
cwe="CWE-89"
|
|
385
|
+
))
|
|
386
|
+
break
|
|
387
|
+
|
|
388
|
+
# Fuzz query parameters
|
|
389
|
+
if self.config.fuzz_parameters:
|
|
390
|
+
for param in endpoint.parameters:
|
|
391
|
+
if param["in"] == "query":
|
|
392
|
+
for payload in self.SQLI_PAYLOADS + self.XSS_PAYLOADS:
|
|
393
|
+
path_with_id = self._replace_path_params(endpoint.path)
|
|
394
|
+
response = await self._send_request(
|
|
395
|
+
endpoint.method,
|
|
396
|
+
path_with_id,
|
|
397
|
+
params={param["name"]: payload}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if self._check_sqli_response(response):
|
|
401
|
+
findings.append(OpenAPIFinding(
|
|
402
|
+
vulnerability="SQL Injection in Query Parameter",
|
|
403
|
+
severity="critical",
|
|
404
|
+
endpoint=endpoint.path,
|
|
405
|
+
method=endpoint.method,
|
|
406
|
+
parameter=param["name"],
|
|
407
|
+
payload=payload,
|
|
408
|
+
description=f"Query parameter '{param['name']}' vulnerable to injection",
|
|
409
|
+
evidence="Response indicates injection vulnerability",
|
|
410
|
+
response_code=response.get("status", 0),
|
|
411
|
+
remediation="Validate and sanitize all input",
|
|
412
|
+
cwe="CWE-89"
|
|
413
|
+
))
|
|
414
|
+
break
|
|
415
|
+
|
|
416
|
+
if self._check_xss_response(response, payload):
|
|
417
|
+
findings.append(OpenAPIFinding(
|
|
418
|
+
vulnerability="Reflected XSS in Query Parameter",
|
|
419
|
+
severity="high",
|
|
420
|
+
endpoint=endpoint.path,
|
|
421
|
+
method=endpoint.method,
|
|
422
|
+
parameter=param["name"],
|
|
423
|
+
payload=payload,
|
|
424
|
+
description=f"Query parameter '{param['name']}' reflects XSS payload",
|
|
425
|
+
evidence="XSS payload reflected in response",
|
|
426
|
+
response_code=response.get("status", 0),
|
|
427
|
+
remediation="Encode output and validate input",
|
|
428
|
+
cwe="CWE-79"
|
|
429
|
+
))
|
|
430
|
+
break
|
|
431
|
+
|
|
432
|
+
# Fuzz request body
|
|
433
|
+
if self.config.fuzz_bodies and endpoint.request_body:
|
|
434
|
+
findings.extend(await self._fuzz_body(endpoint))
|
|
435
|
+
|
|
436
|
+
return findings
|
|
437
|
+
|
|
438
|
+
def _replace_path_params(self, path: str) -> str:
|
|
439
|
+
"""Replace path parameters with test values."""
|
|
440
|
+
return re.sub(r"\{(\w+)\}", "1", path)
|
|
441
|
+
|
|
442
|
+
def _check_sqli_response(self, response: Dict) -> bool:
|
|
443
|
+
"""Check if response indicates SQL injection."""
|
|
444
|
+
if "error" in response:
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
data_str = json.dumps(response.get("data", {})).lower()
|
|
448
|
+
sql_indicators = [
|
|
449
|
+
"sql syntax", "mysql", "postgresql", "sqlite", "oracle",
|
|
450
|
+
"syntax error", "unclosed quotation", "unterminated",
|
|
451
|
+
"ORA-", "PG::", "SQLSTATE", "SQL Server"
|
|
452
|
+
]
|
|
453
|
+
return any(ind.lower() in data_str for ind in sql_indicators)
|
|
454
|
+
|
|
455
|
+
def _check_xss_response(self, response: Dict, payload: str) -> bool:
|
|
456
|
+
"""Check if XSS payload is reflected."""
|
|
457
|
+
if "error" in response:
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
data_str = json.dumps(response.get("data", {}))
|
|
461
|
+
return payload in data_str
|
|
462
|
+
|
|
463
|
+
async def _fuzz_body(self, endpoint: OpenAPIEndpoint) -> List[OpenAPIFinding]:
|
|
464
|
+
"""Fuzz request body parameters."""
|
|
465
|
+
findings = []
|
|
466
|
+
|
|
467
|
+
if not endpoint.request_body:
|
|
468
|
+
return findings
|
|
469
|
+
|
|
470
|
+
schema = endpoint.request_body.get("schema", {})
|
|
471
|
+
properties = schema.get("properties", {})
|
|
472
|
+
|
|
473
|
+
for prop_name, prop_schema in properties.items():
|
|
474
|
+
prop_type = prop_schema.get("type", "string")
|
|
475
|
+
|
|
476
|
+
# Test injection in string fields
|
|
477
|
+
if prop_type == "string":
|
|
478
|
+
for payload in self.SQLI_PAYLOADS[:3]:
|
|
479
|
+
body = {prop_name: payload}
|
|
480
|
+
path = self._replace_path_params(endpoint.path)
|
|
481
|
+
response = await self._send_request(endpoint.method, path, body=body)
|
|
482
|
+
|
|
483
|
+
if self._check_sqli_response(response):
|
|
484
|
+
findings.append(OpenAPIFinding(
|
|
485
|
+
vulnerability="SQL Injection in Request Body",
|
|
486
|
+
severity="critical",
|
|
487
|
+
endpoint=endpoint.path,
|
|
488
|
+
method=endpoint.method,
|
|
489
|
+
parameter=prop_name,
|
|
490
|
+
payload=payload,
|
|
491
|
+
description=f"Body parameter '{prop_name}' vulnerable to SQL injection",
|
|
492
|
+
evidence="SQL error detected in response",
|
|
493
|
+
response_code=response.get("status", 0),
|
|
494
|
+
remediation="Use parameterized queries",
|
|
495
|
+
cwe="CWE-89"
|
|
496
|
+
))
|
|
497
|
+
break
|
|
498
|
+
|
|
499
|
+
return findings
|
|
500
|
+
|
|
501
|
+
async def test_bola(self) -> List[OpenAPIFinding]:
|
|
502
|
+
"""Test for Broken Object Level Authorization (BOLA/IDOR)."""
|
|
503
|
+
findings = []
|
|
504
|
+
|
|
505
|
+
for endpoint in self.endpoints:
|
|
506
|
+
# Look for ID parameters in path
|
|
507
|
+
if "{id}" in endpoint.path or any("{" in endpoint.path for _ in [1]):
|
|
508
|
+
for test_id in self.BOLA_IDS:
|
|
509
|
+
path = re.sub(r"\{[^}]+\}", test_id, endpoint.path)
|
|
510
|
+
response = await self._send_request(endpoint.method, path)
|
|
511
|
+
|
|
512
|
+
if response.get("status") == 200 and "error" not in response:
|
|
513
|
+
data = response.get("data", {})
|
|
514
|
+
if data and data != {"raw": ""}:
|
|
515
|
+
findings.append(OpenAPIFinding(
|
|
516
|
+
vulnerability="Potential BOLA/IDOR",
|
|
517
|
+
severity="high",
|
|
518
|
+
endpoint=endpoint.path,
|
|
519
|
+
method=endpoint.method,
|
|
520
|
+
payload=test_id,
|
|
521
|
+
description=f"Endpoint may be vulnerable to BOLA with ID: {test_id}",
|
|
522
|
+
evidence=f"Received 200 OK for ID: {test_id}",
|
|
523
|
+
response_code=200,
|
|
524
|
+
remediation="Implement proper authorization checks for all resources",
|
|
525
|
+
cwe="CWE-639"
|
|
526
|
+
))
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
return findings
|
|
530
|
+
|
|
531
|
+
async def test_auth_bypass(self) -> List[OpenAPIFinding]:
|
|
532
|
+
"""Test for authentication bypass."""
|
|
533
|
+
findings = []
|
|
534
|
+
|
|
535
|
+
# Save current auth
|
|
536
|
+
orig_token = self.config.auth_token
|
|
537
|
+
orig_key = self.config.api_key
|
|
538
|
+
|
|
539
|
+
# Test without auth
|
|
540
|
+
self.config.auth_token = ""
|
|
541
|
+
self.config.api_key = ""
|
|
542
|
+
|
|
543
|
+
for endpoint in self.endpoints:
|
|
544
|
+
if endpoint.security: # Should require auth
|
|
545
|
+
path = self._replace_path_params(endpoint.path)
|
|
546
|
+
response = await self._send_request(endpoint.method, path)
|
|
547
|
+
|
|
548
|
+
if response.get("status") == 200:
|
|
549
|
+
findings.append(OpenAPIFinding(
|
|
550
|
+
vulnerability="Authentication Bypass",
|
|
551
|
+
severity="critical",
|
|
552
|
+
endpoint=endpoint.path,
|
|
553
|
+
method=endpoint.method,
|
|
554
|
+
description="Endpoint accessible without authentication",
|
|
555
|
+
evidence=f"Received 200 OK without auth token",
|
|
556
|
+
response_code=200,
|
|
557
|
+
remediation="Enforce authentication on all protected endpoints",
|
|
558
|
+
cwe="CWE-306"
|
|
559
|
+
))
|
|
560
|
+
|
|
561
|
+
# Restore auth
|
|
562
|
+
self.config.auth_token = orig_token
|
|
563
|
+
self.config.api_key = orig_key
|
|
564
|
+
|
|
565
|
+
return findings
|
|
566
|
+
|
|
567
|
+
async def test_rate_limiting(self) -> List[OpenAPIFinding]:
|
|
568
|
+
"""Test for missing rate limiting."""
|
|
569
|
+
findings = []
|
|
570
|
+
|
|
571
|
+
# Pick a GET endpoint
|
|
572
|
+
get_endpoints = [e for e in self.endpoints if e.method == "GET"]
|
|
573
|
+
if not get_endpoints:
|
|
574
|
+
return findings
|
|
575
|
+
|
|
576
|
+
endpoint = get_endpoints[0]
|
|
577
|
+
path = self._replace_path_params(endpoint.path)
|
|
578
|
+
|
|
579
|
+
# Send rapid requests
|
|
580
|
+
success_count = 0
|
|
581
|
+
for _ in range(20):
|
|
582
|
+
response = await self._send_request("GET", path)
|
|
583
|
+
if response.get("status") == 200:
|
|
584
|
+
success_count += 1
|
|
585
|
+
elif response.get("status") == 429:
|
|
586
|
+
return findings # Rate limiting detected
|
|
587
|
+
|
|
588
|
+
if success_count >= 18:
|
|
589
|
+
findings.append(OpenAPIFinding(
|
|
590
|
+
vulnerability="Missing Rate Limiting",
|
|
591
|
+
severity="medium",
|
|
592
|
+
endpoint=endpoint.path,
|
|
593
|
+
method="GET",
|
|
594
|
+
description="API lacks rate limiting protection",
|
|
595
|
+
evidence=f"{success_count}/20 rapid requests succeeded",
|
|
596
|
+
response_code=200,
|
|
597
|
+
remediation="Implement rate limiting to prevent abuse",
|
|
598
|
+
cwe="CWE-770"
|
|
599
|
+
))
|
|
600
|
+
|
|
601
|
+
return findings
|
|
602
|
+
|
|
603
|
+
async def test_mass_assignment(self) -> List[OpenAPIFinding]:
|
|
604
|
+
"""Test for mass assignment vulnerabilities."""
|
|
605
|
+
findings = []
|
|
606
|
+
|
|
607
|
+
# Look for POST/PUT/PATCH endpoints
|
|
608
|
+
for endpoint in self.endpoints:
|
|
609
|
+
if endpoint.method not in ["POST", "PUT", "PATCH"]:
|
|
610
|
+
continue
|
|
611
|
+
if not endpoint.request_body:
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
# Try adding extra fields
|
|
615
|
+
extra_fields = {
|
|
616
|
+
"role": "admin",
|
|
617
|
+
"isAdmin": True,
|
|
618
|
+
"admin": True,
|
|
619
|
+
"permissions": ["admin"],
|
|
620
|
+
"is_superuser": True,
|
|
621
|
+
"privilege": "admin"
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
path = self._replace_path_params(endpoint.path)
|
|
625
|
+
|
|
626
|
+
for field_name, field_value in extra_fields.items():
|
|
627
|
+
body = {field_name: field_value}
|
|
628
|
+
response = await self._send_request(endpoint.method, path, body=body)
|
|
629
|
+
|
|
630
|
+
if response.get("status") in [200, 201]:
|
|
631
|
+
data = response.get("data", {})
|
|
632
|
+
if isinstance(data, dict) and field_name in str(data):
|
|
633
|
+
findings.append(OpenAPIFinding(
|
|
634
|
+
vulnerability="Potential Mass Assignment",
|
|
635
|
+
severity="high",
|
|
636
|
+
endpoint=endpoint.path,
|
|
637
|
+
method=endpoint.method,
|
|
638
|
+
parameter=field_name,
|
|
639
|
+
description=f"API accepts undocumented field: {field_name}",
|
|
640
|
+
evidence=f"Field '{field_name}' was accepted in request",
|
|
641
|
+
response_code=response.get("status", 0),
|
|
642
|
+
remediation="Implement allowlist for acceptable fields",
|
|
643
|
+
cwe="CWE-915"
|
|
644
|
+
))
|
|
645
|
+
|
|
646
|
+
return findings
|
|
647
|
+
|
|
648
|
+
async def fuzz(self) -> OpenAPIFuzzResult:
|
|
649
|
+
"""
|
|
650
|
+
Run full OpenAPI fuzzing scan.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
OpenAPIFuzzResult with all findings
|
|
654
|
+
"""
|
|
655
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
656
|
+
start_time = asyncio.get_event_loop().time()
|
|
657
|
+
|
|
658
|
+
# Load spec
|
|
659
|
+
if not self.spec:
|
|
660
|
+
spec_loaded = await self.load_spec()
|
|
661
|
+
if not spec_loaded:
|
|
662
|
+
return OpenAPIFuzzResult(
|
|
663
|
+
base_url=self.base_url,
|
|
664
|
+
status="failed",
|
|
665
|
+
started_at=started_at,
|
|
666
|
+
finished_at=datetime.now(timezone.utc).isoformat(),
|
|
667
|
+
duration=0,
|
|
668
|
+
endpoints_tested=0,
|
|
669
|
+
requests_made=0,
|
|
670
|
+
findings=[],
|
|
671
|
+
spec_info={},
|
|
672
|
+
metadata={"error": "Could not load OpenAPI specification"}
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
findings = []
|
|
676
|
+
|
|
677
|
+
# Fuzz each endpoint
|
|
678
|
+
for endpoint in self.endpoints:
|
|
679
|
+
endpoint_findings = await self.fuzz_endpoint(endpoint)
|
|
680
|
+
findings.extend(endpoint_findings)
|
|
681
|
+
|
|
682
|
+
# Run additional tests
|
|
683
|
+
if self.config.test_bola:
|
|
684
|
+
findings.extend(await self.test_bola())
|
|
685
|
+
|
|
686
|
+
if self.config.test_auth_bypass:
|
|
687
|
+
findings.extend(await self.test_auth_bypass())
|
|
688
|
+
|
|
689
|
+
if self.config.test_rate_limit:
|
|
690
|
+
findings.extend(await self.test_rate_limiting())
|
|
691
|
+
|
|
692
|
+
if self.config.test_mass_assignment:
|
|
693
|
+
findings.extend(await self.test_mass_assignment())
|
|
694
|
+
|
|
695
|
+
finished_at = datetime.now(timezone.utc).isoformat()
|
|
696
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
697
|
+
|
|
698
|
+
# Spec info
|
|
699
|
+
spec_info = {
|
|
700
|
+
"title": self.spec.get("info", {}).get("title", ""),
|
|
701
|
+
"version": self.spec.get("info", {}).get("version", ""),
|
|
702
|
+
"openapi_version": self.spec.get("openapi", self.spec.get("swagger", "")),
|
|
703
|
+
"endpoints_count": len(self.endpoints),
|
|
704
|
+
"servers": self.spec.get("servers", [])
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return OpenAPIFuzzResult(
|
|
708
|
+
base_url=self.base_url,
|
|
709
|
+
status="completed",
|
|
710
|
+
started_at=started_at,
|
|
711
|
+
finished_at=finished_at,
|
|
712
|
+
duration=duration,
|
|
713
|
+
endpoints_tested=len(self.endpoints),
|
|
714
|
+
requests_made=self.requests_made,
|
|
715
|
+
findings=findings,
|
|
716
|
+
spec_info=spec_info,
|
|
717
|
+
metadata={
|
|
718
|
+
"config": {
|
|
719
|
+
"fuzz_parameters": self.config.fuzz_parameters,
|
|
720
|
+
"fuzz_bodies": self.config.fuzz_bodies,
|
|
721
|
+
"test_bola": self.config.test_bola,
|
|
722
|
+
"test_auth_bypass": self.config.test_auth_bypass
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
# Convenience function
|
|
729
|
+
async def fuzz_openapi(
|
|
730
|
+
base_url: str,
|
|
731
|
+
spec_path: Optional[str] = None,
|
|
732
|
+
spec_url: Optional[str] = None,
|
|
733
|
+
auth_token: Optional[str] = None,
|
|
734
|
+
full_scan: bool = True
|
|
735
|
+
) -> OpenAPIFuzzResult:
|
|
736
|
+
"""
|
|
737
|
+
Quick OpenAPI fuzzing scan.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
base_url: Base API URL
|
|
741
|
+
spec_path: Path to OpenAPI spec file
|
|
742
|
+
spec_url: URL to OpenAPI spec
|
|
743
|
+
auth_token: Bearer token for authentication
|
|
744
|
+
full_scan: Run all tests if True
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
OpenAPIFuzzResult
|
|
748
|
+
"""
|
|
749
|
+
config = OpenAPIConfig(
|
|
750
|
+
base_url=base_url,
|
|
751
|
+
spec_path=spec_path,
|
|
752
|
+
spec_url=spec_url,
|
|
753
|
+
auth_token=auth_token or "",
|
|
754
|
+
test_bola=full_scan,
|
|
755
|
+
test_mass_assignment=full_scan,
|
|
756
|
+
test_rate_limit=full_scan,
|
|
757
|
+
test_auth_bypass=full_scan
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
fuzzer = OpenAPIFuzzer(base_url, config)
|
|
761
|
+
return await fuzzer.fuzz()
|