aiptx 2.0.7__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.
- 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 +46 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/exploit_agent.py +688 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +957 -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 +2933 -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 +341 -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 +194 -0
- aipt_v2/intelligence/adaptation.py +474 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/correlation.py +536 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/knowledge_graph.py +590 -0
- aipt_v2/intelligence/learning.py +626 -0
- aipt_v2/intelligence/llm_analyzer.py +502 -0
- aipt_v2/intelligence/llm_tool_selector.py +518 -0
- aipt_v2/intelligence/payload_generator.py +562 -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/interactive_shell.py +559 -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/local_tool_installer.py +1467 -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 +2427 -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 +53 -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/runtime/vps.py +830 -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/setup_wizard.py +941 -0
- aipt_v2/skills/__init__.py +80 -0
- aipt_v2/skills/agents/__init__.py +14 -0
- aipt_v2/skills/agents/api_tester.py +706 -0
- aipt_v2/skills/agents/base.py +477 -0
- aipt_v2/skills/agents/code_review.py +459 -0
- aipt_v2/skills/agents/security_agent.py +336 -0
- aipt_v2/skills/agents/web_pentest.py +818 -0
- aipt_v2/skills/prompts/__init__.py +647 -0
- aipt_v2/system_detector.py +539 -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 +202 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aipt_v2/verify_install.py +793 -0
- aiptx-2.0.7.dist-info/METADATA +345 -0
- aiptx-2.0.7.dist-info/RECORD +187 -0
- aiptx-2.0.7.dist-info/WHEEL +5 -0
- aiptx-2.0.7.dist-info/entry_points.txt +7 -0
- aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Security Testing Agent - AI-powered REST API security assessment.
|
|
3
|
+
|
|
4
|
+
Tests APIs for:
|
|
5
|
+
- Injection vulnerabilities (SQLi, NoSQLi, Command Injection)
|
|
6
|
+
- Broken authentication and authorization (BOLA, BFLA)
|
|
7
|
+
- Data exposure
|
|
8
|
+
- Rate limiting bypass
|
|
9
|
+
- Mass assignment
|
|
10
|
+
- SSRF
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
from urllib.parse import urljoin, urlparse
|
|
17
|
+
|
|
18
|
+
import structlog
|
|
19
|
+
|
|
20
|
+
from aipt_v2.skills.agents.base import (
|
|
21
|
+
AgentConfig,
|
|
22
|
+
AgentResult,
|
|
23
|
+
BaseSecurityAgent,
|
|
24
|
+
Finding,
|
|
25
|
+
Severity,
|
|
26
|
+
VulnCategory,
|
|
27
|
+
register_tool,
|
|
28
|
+
)
|
|
29
|
+
from aipt_v2.skills.prompts import SkillPrompts
|
|
30
|
+
|
|
31
|
+
logger = structlog.get_logger()
|
|
32
|
+
|
|
33
|
+
# HTTP client for API testing
|
|
34
|
+
_http_client = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_http_client():
|
|
38
|
+
"""Get or create HTTP client."""
|
|
39
|
+
global _http_client
|
|
40
|
+
if _http_client is None:
|
|
41
|
+
import httpx
|
|
42
|
+
_http_client = httpx.AsyncClient(
|
|
43
|
+
timeout=30.0,
|
|
44
|
+
follow_redirects=True,
|
|
45
|
+
verify=False # Allow self-signed certs for testing
|
|
46
|
+
)
|
|
47
|
+
return _http_client
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Register API testing tools
|
|
51
|
+
@register_tool(
|
|
52
|
+
name="http_request",
|
|
53
|
+
description="Send an HTTP request to test an API endpoint",
|
|
54
|
+
parameters={
|
|
55
|
+
"method": {"type": "string", "description": "HTTP method (GET, POST, PUT, DELETE, PATCH)"},
|
|
56
|
+
"url": {"type": "string", "description": "Full URL to request"},
|
|
57
|
+
"headers": {"type": "object", "description": "Optional headers dict"},
|
|
58
|
+
"body": {"type": "string", "description": "Optional request body (JSON string)"},
|
|
59
|
+
"params": {"type": "object", "description": "Optional query parameters"}
|
|
60
|
+
},
|
|
61
|
+
category="api_test"
|
|
62
|
+
)
|
|
63
|
+
async def http_request(
|
|
64
|
+
method: str,
|
|
65
|
+
url: str,
|
|
66
|
+
headers: Optional[Dict[str, str]] = None,
|
|
67
|
+
body: Optional[str] = None,
|
|
68
|
+
params: Optional[Dict[str, str]] = None
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Send an HTTP request."""
|
|
71
|
+
try:
|
|
72
|
+
client = get_http_client()
|
|
73
|
+
|
|
74
|
+
# Parse body if JSON string
|
|
75
|
+
json_body = None
|
|
76
|
+
if body:
|
|
77
|
+
try:
|
|
78
|
+
json_body = json.loads(body)
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
response = await client.request(
|
|
83
|
+
method=method.upper(),
|
|
84
|
+
url=url,
|
|
85
|
+
headers=headers,
|
|
86
|
+
json=json_body if json_body else None,
|
|
87
|
+
content=body if body and not json_body else None,
|
|
88
|
+
params=params
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Build response summary
|
|
92
|
+
result = f"""HTTP {method.upper()} {url}
|
|
93
|
+
Status: {response.status_code}
|
|
94
|
+
Headers: {dict(response.headers)}
|
|
95
|
+
|
|
96
|
+
Response Body:
|
|
97
|
+
{response.text[:5000]}"""
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return f"Request failed: {str(e)}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@register_tool(
|
|
106
|
+
name="parse_openapi",
|
|
107
|
+
description="Parse an OpenAPI/Swagger specification to discover endpoints",
|
|
108
|
+
parameters={
|
|
109
|
+
"spec_url_or_content": {"type": "string", "description": "URL to OpenAPI spec or the spec content itself"}
|
|
110
|
+
},
|
|
111
|
+
category="api_test"
|
|
112
|
+
)
|
|
113
|
+
async def parse_openapi(spec_url_or_content: str) -> str:
|
|
114
|
+
"""Parse OpenAPI specification."""
|
|
115
|
+
try:
|
|
116
|
+
import yaml
|
|
117
|
+
|
|
118
|
+
# Try to fetch if URL
|
|
119
|
+
if spec_url_or_content.startswith(('http://', 'https://')):
|
|
120
|
+
client = get_http_client()
|
|
121
|
+
response = await client.get(spec_url_or_content)
|
|
122
|
+
content = response.text
|
|
123
|
+
else:
|
|
124
|
+
content = spec_url_or_content
|
|
125
|
+
|
|
126
|
+
# Parse YAML or JSON
|
|
127
|
+
try:
|
|
128
|
+
spec = yaml.safe_load(content)
|
|
129
|
+
except yaml.YAMLError:
|
|
130
|
+
spec = json.loads(content)
|
|
131
|
+
|
|
132
|
+
# Extract endpoints
|
|
133
|
+
endpoints = []
|
|
134
|
+
paths = spec.get('paths', {})
|
|
135
|
+
|
|
136
|
+
for path, methods in paths.items():
|
|
137
|
+
for method, details in methods.items():
|
|
138
|
+
if method in ['get', 'post', 'put', 'delete', 'patch']:
|
|
139
|
+
params = []
|
|
140
|
+
for param in details.get('parameters', []):
|
|
141
|
+
params.append(f"{param.get('name')} ({param.get('in', 'query')})")
|
|
142
|
+
|
|
143
|
+
endpoint = {
|
|
144
|
+
'path': path,
|
|
145
|
+
'method': method.upper(),
|
|
146
|
+
'summary': details.get('summary', ''),
|
|
147
|
+
'parameters': params,
|
|
148
|
+
'security': details.get('security', [])
|
|
149
|
+
}
|
|
150
|
+
endpoints.append(endpoint)
|
|
151
|
+
|
|
152
|
+
# Format output
|
|
153
|
+
output = f"OpenAPI Spec: {spec.get('info', {}).get('title', 'Unknown')}\n"
|
|
154
|
+
output += f"Version: {spec.get('info', {}).get('version', 'Unknown')}\n"
|
|
155
|
+
output += f"Base URL: {spec.get('servers', [{}])[0].get('url', 'Not specified')}\n\n"
|
|
156
|
+
output += f"Endpoints ({len(endpoints)}):\n"
|
|
157
|
+
|
|
158
|
+
for ep in endpoints:
|
|
159
|
+
output += f"\n{ep['method']} {ep['path']}"
|
|
160
|
+
if ep['summary']:
|
|
161
|
+
output += f" - {ep['summary']}"
|
|
162
|
+
if ep['parameters']:
|
|
163
|
+
output += f"\n Parameters: {', '.join(ep['parameters'])}"
|
|
164
|
+
if ep['security']:
|
|
165
|
+
output += f"\n Security: {ep['security']}"
|
|
166
|
+
|
|
167
|
+
return output
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return f"Failed to parse OpenAPI spec: {str(e)}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@register_tool(
|
|
174
|
+
name="fuzz_parameter",
|
|
175
|
+
description="Fuzz a parameter with various payloads",
|
|
176
|
+
parameters={
|
|
177
|
+
"base_url": {"type": "string", "description": "Base URL of the endpoint"},
|
|
178
|
+
"method": {"type": "string", "description": "HTTP method"},
|
|
179
|
+
"param_name": {"type": "string", "description": "Parameter name to fuzz"},
|
|
180
|
+
"param_location": {"type": "string", "description": "Location: query, body, header, path"},
|
|
181
|
+
"payloads": {"type": "array", "description": "List of payloads to test"},
|
|
182
|
+
"headers": {"type": "object", "description": "Optional headers"}
|
|
183
|
+
},
|
|
184
|
+
category="api_test"
|
|
185
|
+
)
|
|
186
|
+
async def fuzz_parameter(
|
|
187
|
+
base_url: str,
|
|
188
|
+
method: str,
|
|
189
|
+
param_name: str,
|
|
190
|
+
param_location: str,
|
|
191
|
+
payloads: List[str],
|
|
192
|
+
headers: Optional[Dict[str, str]] = None
|
|
193
|
+
) -> str:
|
|
194
|
+
"""Fuzz a parameter with various payloads."""
|
|
195
|
+
try:
|
|
196
|
+
client = get_http_client()
|
|
197
|
+
results = []
|
|
198
|
+
|
|
199
|
+
for payload in payloads[:20]: # Limit to 20 payloads
|
|
200
|
+
try:
|
|
201
|
+
url = base_url
|
|
202
|
+
req_headers = headers.copy() if headers else {}
|
|
203
|
+
body = None
|
|
204
|
+
params = None
|
|
205
|
+
|
|
206
|
+
if param_location == "query":
|
|
207
|
+
params = {param_name: payload}
|
|
208
|
+
elif param_location == "body":
|
|
209
|
+
body = json.dumps({param_name: payload})
|
|
210
|
+
req_headers["Content-Type"] = "application/json"
|
|
211
|
+
elif param_location == "header":
|
|
212
|
+
req_headers[param_name] = payload
|
|
213
|
+
elif param_location == "path":
|
|
214
|
+
url = base_url.replace(f"{{{param_name}}}", payload)
|
|
215
|
+
|
|
216
|
+
response = await client.request(
|
|
217
|
+
method=method.upper(),
|
|
218
|
+
url=url,
|
|
219
|
+
headers=req_headers,
|
|
220
|
+
content=body,
|
|
221
|
+
params=params
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Check for interesting responses
|
|
225
|
+
interesting = False
|
|
226
|
+
indicators = []
|
|
227
|
+
|
|
228
|
+
# Error indicators
|
|
229
|
+
if response.status_code >= 500:
|
|
230
|
+
interesting = True
|
|
231
|
+
indicators.append("Server Error")
|
|
232
|
+
if "error" in response.text.lower():
|
|
233
|
+
interesting = True
|
|
234
|
+
indicators.append("Error in response")
|
|
235
|
+
if "exception" in response.text.lower():
|
|
236
|
+
interesting = True
|
|
237
|
+
indicators.append("Exception disclosed")
|
|
238
|
+
if "sql" in response.text.lower():
|
|
239
|
+
interesting = True
|
|
240
|
+
indicators.append("SQL-related")
|
|
241
|
+
|
|
242
|
+
results.append({
|
|
243
|
+
"payload": payload,
|
|
244
|
+
"status": response.status_code,
|
|
245
|
+
"length": len(response.text),
|
|
246
|
+
"interesting": interesting,
|
|
247
|
+
"indicators": indicators,
|
|
248
|
+
"response_preview": response.text[:200] if interesting else ""
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# Small delay between requests
|
|
252
|
+
await asyncio.sleep(0.1)
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
results.append({
|
|
256
|
+
"payload": payload,
|
|
257
|
+
"error": str(e)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
# Format results
|
|
261
|
+
output = f"Fuzzing {param_name} ({param_location}) on {method} {base_url}\n\n"
|
|
262
|
+
|
|
263
|
+
interesting_results = [r for r in results if r.get("interesting")]
|
|
264
|
+
if interesting_results:
|
|
265
|
+
output += "=== INTERESTING RESULTS ===\n"
|
|
266
|
+
for r in interesting_results:
|
|
267
|
+
output += f"\nPayload: {r['payload']}\n"
|
|
268
|
+
output += f"Status: {r['status']}, Length: {r['length']}\n"
|
|
269
|
+
output += f"Indicators: {', '.join(r['indicators'])}\n"
|
|
270
|
+
if r.get('response_preview'):
|
|
271
|
+
output += f"Preview: {r['response_preview']}\n"
|
|
272
|
+
|
|
273
|
+
output += f"\n=== ALL RESULTS ({len(results)} payloads) ===\n"
|
|
274
|
+
for r in results:
|
|
275
|
+
if 'error' in r:
|
|
276
|
+
output += f"Payload: {r['payload']} - Error: {r['error']}\n"
|
|
277
|
+
else:
|
|
278
|
+
output += f"Payload: {r['payload']} - Status: {r['status']}, Length: {r['length']}\n"
|
|
279
|
+
|
|
280
|
+
return output
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return f"Fuzzing failed: {str(e)}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
import asyncio
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@register_tool(
|
|
290
|
+
name="test_authentication",
|
|
291
|
+
description="Test API authentication mechanisms",
|
|
292
|
+
parameters={
|
|
293
|
+
"base_url": {"type": "string", "description": "API base URL"},
|
|
294
|
+
"auth_endpoint": {"type": "string", "description": "Authentication endpoint (e.g., /auth/login)"},
|
|
295
|
+
"test_credentials": {"type": "array", "description": "List of username:password pairs to test"}
|
|
296
|
+
},
|
|
297
|
+
category="api_test"
|
|
298
|
+
)
|
|
299
|
+
async def test_authentication(
|
|
300
|
+
base_url: str,
|
|
301
|
+
auth_endpoint: str,
|
|
302
|
+
test_credentials: List[str]
|
|
303
|
+
) -> str:
|
|
304
|
+
"""Test authentication mechanisms."""
|
|
305
|
+
try:
|
|
306
|
+
client = get_http_client()
|
|
307
|
+
url = urljoin(base_url, auth_endpoint)
|
|
308
|
+
results = []
|
|
309
|
+
|
|
310
|
+
for cred in test_credentials[:10]: # Limit to 10 attempts
|
|
311
|
+
try:
|
|
312
|
+
username, password = cred.split(":", 1)
|
|
313
|
+
|
|
314
|
+
# Try common auth payload formats
|
|
315
|
+
payloads = [
|
|
316
|
+
{"username": username, "password": password},
|
|
317
|
+
{"email": username, "password": password},
|
|
318
|
+
{"user": username, "pass": password},
|
|
319
|
+
{"login": username, "password": password},
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
for payload in payloads:
|
|
323
|
+
response = await client.post(url, json=payload)
|
|
324
|
+
|
|
325
|
+
# Check if authentication succeeded
|
|
326
|
+
success_indicators = ["token", "jwt", "session", "access_token", "auth"]
|
|
327
|
+
is_success = any(ind in response.text.lower() for ind in success_indicators)
|
|
328
|
+
is_success = is_success or response.status_code == 200
|
|
329
|
+
|
|
330
|
+
results.append({
|
|
331
|
+
"credential": cred,
|
|
332
|
+
"payload_format": list(payload.keys()),
|
|
333
|
+
"status": response.status_code,
|
|
334
|
+
"success": is_success,
|
|
335
|
+
"response_preview": response.text[:300]
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
if is_success:
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
await asyncio.sleep(0.2) # Rate limiting
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
results.append({"credential": cred, "error": str(e)})
|
|
345
|
+
|
|
346
|
+
# Format output
|
|
347
|
+
output = f"Authentication Testing: {url}\n\n"
|
|
348
|
+
|
|
349
|
+
successful = [r for r in results if r.get("success")]
|
|
350
|
+
if successful:
|
|
351
|
+
output += "=== SUCCESSFUL AUTHENTICATIONS ===\n"
|
|
352
|
+
for r in successful:
|
|
353
|
+
output += f"Credential: {r['credential']}\n"
|
|
354
|
+
output += f"Response: {r['response_preview']}\n\n"
|
|
355
|
+
|
|
356
|
+
output += f"\n=== ALL RESULTS ({len(results)} attempts) ===\n"
|
|
357
|
+
for r in results:
|
|
358
|
+
if 'error' in r:
|
|
359
|
+
output += f"{r['credential']}: Error - {r['error']}\n"
|
|
360
|
+
else:
|
|
361
|
+
status = "SUCCESS" if r['success'] else "FAILED"
|
|
362
|
+
output += f"{r['credential']}: {status} (HTTP {r['status']})\n"
|
|
363
|
+
|
|
364
|
+
return output
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return f"Auth testing failed: {str(e)}"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@register_tool(
|
|
371
|
+
name="test_authorization",
|
|
372
|
+
description="Test for authorization bypass (IDOR/BOLA)",
|
|
373
|
+
parameters={
|
|
374
|
+
"url": {"type": "string", "description": "URL with object ID to test"},
|
|
375
|
+
"auth_header": {"type": "string", "description": "Authorization header value"},
|
|
376
|
+
"test_ids": {"type": "array", "description": "List of IDs to test access to"}
|
|
377
|
+
},
|
|
378
|
+
category="api_test"
|
|
379
|
+
)
|
|
380
|
+
async def test_authorization(
|
|
381
|
+
url: str,
|
|
382
|
+
auth_header: str,
|
|
383
|
+
test_ids: List[str]
|
|
384
|
+
) -> str:
|
|
385
|
+
"""Test for authorization bypass."""
|
|
386
|
+
try:
|
|
387
|
+
client = get_http_client()
|
|
388
|
+
results = []
|
|
389
|
+
|
|
390
|
+
headers = {"Authorization": auth_header}
|
|
391
|
+
|
|
392
|
+
for test_id in test_ids[:20]: # Limit to 20 IDs
|
|
393
|
+
try:
|
|
394
|
+
# Replace ID placeholder in URL
|
|
395
|
+
test_url = url.replace("{id}", test_id).replace(":id", test_id)
|
|
396
|
+
|
|
397
|
+
response = await client.get(test_url, headers=headers)
|
|
398
|
+
|
|
399
|
+
results.append({
|
|
400
|
+
"id": test_id,
|
|
401
|
+
"url": test_url,
|
|
402
|
+
"status": response.status_code,
|
|
403
|
+
"length": len(response.text),
|
|
404
|
+
"accessible": response.status_code == 200,
|
|
405
|
+
"response_preview": response.text[:200] if response.status_code == 200 else ""
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
await asyncio.sleep(0.1)
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
results.append({"id": test_id, "error": str(e)})
|
|
412
|
+
|
|
413
|
+
# Analyze for IDOR
|
|
414
|
+
accessible = [r for r in results if r.get("accessible")]
|
|
415
|
+
|
|
416
|
+
output = f"Authorization Testing: {url}\n\n"
|
|
417
|
+
|
|
418
|
+
if len(accessible) > 1:
|
|
419
|
+
output += "⚠️ POTENTIAL IDOR DETECTED - Multiple resources accessible\n\n"
|
|
420
|
+
|
|
421
|
+
output += f"=== ACCESSIBLE RESOURCES ({len(accessible)}) ===\n"
|
|
422
|
+
for r in accessible:
|
|
423
|
+
output += f"ID: {r['id']} - Length: {r['length']}\n"
|
|
424
|
+
if r.get('response_preview'):
|
|
425
|
+
output += f"Preview: {r['response_preview']}\n\n"
|
|
426
|
+
|
|
427
|
+
output += f"\n=== ALL RESULTS ({len(results)} IDs tested) ===\n"
|
|
428
|
+
for r in results:
|
|
429
|
+
if 'error' in r:
|
|
430
|
+
output += f"ID {r['id']}: Error - {r['error']}\n"
|
|
431
|
+
else:
|
|
432
|
+
status = "ACCESSIBLE" if r['accessible'] else "DENIED"
|
|
433
|
+
output += f"ID {r['id']}: {status} (HTTP {r['status']})\n"
|
|
434
|
+
|
|
435
|
+
return output
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
return f"Authorization testing failed: {str(e)}"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@register_tool(
|
|
442
|
+
name="report_api_finding",
|
|
443
|
+
description="Report an API security vulnerability",
|
|
444
|
+
parameters={
|
|
445
|
+
"title": {"type": "string", "description": "Title of the vulnerability"},
|
|
446
|
+
"severity": {"type": "string", "description": "Severity level"},
|
|
447
|
+
"endpoint": {"type": "string", "description": "Affected endpoint"},
|
|
448
|
+
"method": {"type": "string", "description": "HTTP method"},
|
|
449
|
+
"description": {"type": "string", "description": "Detailed description"},
|
|
450
|
+
"request": {"type": "string", "description": "Example malicious request"},
|
|
451
|
+
"response": {"type": "string", "description": "Response showing vulnerability"},
|
|
452
|
+
"remediation": {"type": "string", "description": "How to fix"}
|
|
453
|
+
},
|
|
454
|
+
category="api_test"
|
|
455
|
+
)
|
|
456
|
+
async def report_api_finding(
|
|
457
|
+
title: str,
|
|
458
|
+
severity: str,
|
|
459
|
+
endpoint: str,
|
|
460
|
+
method: str,
|
|
461
|
+
description: str,
|
|
462
|
+
request: str,
|
|
463
|
+
response: str,
|
|
464
|
+
remediation: str
|
|
465
|
+
) -> str:
|
|
466
|
+
"""Report an API security finding."""
|
|
467
|
+
return f"""API Security Finding Recorded:
|
|
468
|
+
Title: {title}
|
|
469
|
+
Severity: {severity}
|
|
470
|
+
Endpoint: {method} {endpoint}
|
|
471
|
+
Description: {description}
|
|
472
|
+
Request: {request[:500]}
|
|
473
|
+
Response: {response[:500]}
|
|
474
|
+
Remediation: {remediation}
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
API_TEST_SYSTEM_PROMPT = """You are an expert API security tester specializing in REST API penetration testing.
|
|
479
|
+
|
|
480
|
+
## EXPERTISE AREAS
|
|
481
|
+
- OWASP API Security Top 10
|
|
482
|
+
- Authentication/Authorization attacks (BOLA, BFLA)
|
|
483
|
+
- Injection attacks (SQLi, NoSQLi, Command Injection)
|
|
484
|
+
- Mass assignment vulnerabilities
|
|
485
|
+
- Rate limiting bypass
|
|
486
|
+
- Information disclosure
|
|
487
|
+
|
|
488
|
+
## TESTING METHODOLOGY
|
|
489
|
+
|
|
490
|
+
### 1. Reconnaissance
|
|
491
|
+
- Parse OpenAPI/Swagger specs if available
|
|
492
|
+
- Enumerate endpoints and parameters
|
|
493
|
+
- Identify authentication mechanisms
|
|
494
|
+
- Map data models and relationships
|
|
495
|
+
|
|
496
|
+
### 2. Authentication Testing
|
|
497
|
+
- Test for weak credentials
|
|
498
|
+
- Check token handling (JWT vulnerabilities, session issues)
|
|
499
|
+
- Test password reset flows
|
|
500
|
+
- Check for authentication bypass
|
|
501
|
+
|
|
502
|
+
### 3. Authorization Testing (BOLA/BFLA)
|
|
503
|
+
- Test accessing other users' resources (horizontal escalation)
|
|
504
|
+
- Test accessing admin functions (vertical escalation)
|
|
505
|
+
- Check for IDOR vulnerabilities
|
|
506
|
+
- Test function-level access control
|
|
507
|
+
|
|
508
|
+
### 4. Injection Testing
|
|
509
|
+
- SQL injection in all parameters
|
|
510
|
+
- NoSQL injection (MongoDB operators)
|
|
511
|
+
- Command injection
|
|
512
|
+
- Template injection
|
|
513
|
+
|
|
514
|
+
### 5. Data Handling
|
|
515
|
+
- Test for mass assignment
|
|
516
|
+
- Check for sensitive data in responses
|
|
517
|
+
- Test file upload functionality
|
|
518
|
+
- Check for rate limiting
|
|
519
|
+
|
|
520
|
+
## PAYLOADS BY VULNERABILITY
|
|
521
|
+
|
|
522
|
+
### SQL Injection
|
|
523
|
+
- ' OR '1'='1
|
|
524
|
+
- 1; DROP TABLE--
|
|
525
|
+
- ' UNION SELECT NULL--
|
|
526
|
+
|
|
527
|
+
### NoSQL Injection
|
|
528
|
+
- {"$gt": ""}
|
|
529
|
+
- {"$ne": null}
|
|
530
|
+
- {"$where": "sleep(5000)"}
|
|
531
|
+
|
|
532
|
+
### Command Injection
|
|
533
|
+
- ; id
|
|
534
|
+
- | cat /etc/passwd
|
|
535
|
+
- `whoami`
|
|
536
|
+
|
|
537
|
+
### Mass Assignment
|
|
538
|
+
- Add admin: true to requests
|
|
539
|
+
- Add role: admin to user creation
|
|
540
|
+
- Include hidden fields
|
|
541
|
+
|
|
542
|
+
## OUTPUT FORMAT
|
|
543
|
+
Use report_api_finding for each vulnerability discovered with:
|
|
544
|
+
- Clear title and severity
|
|
545
|
+
- Affected endpoint and method
|
|
546
|
+
- Full request/response evidence
|
|
547
|
+
- Specific remediation steps
|
|
548
|
+
|
|
549
|
+
Be aggressive and thorough. Test every endpoint. Check every parameter."""
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class APITestAgent(BaseSecurityAgent):
|
|
553
|
+
"""
|
|
554
|
+
AI-powered API security testing agent.
|
|
555
|
+
|
|
556
|
+
Performs comprehensive security testing of REST APIs including:
|
|
557
|
+
- Authentication and authorization testing
|
|
558
|
+
- Injection vulnerability testing
|
|
559
|
+
- Business logic testing
|
|
560
|
+
- Rate limiting and DoS testing
|
|
561
|
+
|
|
562
|
+
Usage:
|
|
563
|
+
agent = APITestAgent(base_url="https://api.example.com")
|
|
564
|
+
result = await agent.run()
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
def __init__(
|
|
568
|
+
self,
|
|
569
|
+
base_url: str,
|
|
570
|
+
config: Optional[AgentConfig] = None,
|
|
571
|
+
openapi_spec: Optional[str] = None,
|
|
572
|
+
auth_token: Optional[str] = None,
|
|
573
|
+
headers: Optional[Dict[str, str]] = None
|
|
574
|
+
):
|
|
575
|
+
"""
|
|
576
|
+
Initialize the API testing agent.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
base_url: Base URL of the API to test
|
|
580
|
+
config: Agent configuration
|
|
581
|
+
openapi_spec: Path or URL to OpenAPI/Swagger spec
|
|
582
|
+
auth_token: Authentication token for API access
|
|
583
|
+
headers: Additional headers to include in requests
|
|
584
|
+
"""
|
|
585
|
+
super().__init__(config)
|
|
586
|
+
self.base_url = base_url.rstrip('/')
|
|
587
|
+
self.openapi_spec = openapi_spec
|
|
588
|
+
self.auth_token = auth_token
|
|
589
|
+
self.headers = headers or {}
|
|
590
|
+
|
|
591
|
+
if auth_token:
|
|
592
|
+
self.headers["Authorization"] = f"Bearer {auth_token}"
|
|
593
|
+
|
|
594
|
+
def get_system_prompt(self) -> str:
|
|
595
|
+
"""Get the API testing system prompt."""
|
|
596
|
+
return API_TEST_SYSTEM_PROMPT
|
|
597
|
+
|
|
598
|
+
def get_tools(self) -> List[Dict[str, Any]]:
|
|
599
|
+
"""Get tools available for API testing."""
|
|
600
|
+
return [
|
|
601
|
+
{
|
|
602
|
+
"name": "http_request",
|
|
603
|
+
"description": "Send an HTTP request to test an API endpoint",
|
|
604
|
+
"parameters": {
|
|
605
|
+
"method": {"type": "string", "description": "HTTP method"},
|
|
606
|
+
"url": {"type": "string", "description": "Full URL"},
|
|
607
|
+
"headers": {"type": "object", "description": "Optional headers"},
|
|
608
|
+
"body": {"type": "string", "description": "Optional JSON body"},
|
|
609
|
+
"params": {"type": "object", "description": "Query parameters"}
|
|
610
|
+
},
|
|
611
|
+
"required": ["method", "url"]
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"name": "parse_openapi",
|
|
615
|
+
"description": "Parse OpenAPI/Swagger specification",
|
|
616
|
+
"parameters": {
|
|
617
|
+
"spec_url_or_content": {"type": "string", "description": "URL or content of spec"}
|
|
618
|
+
},
|
|
619
|
+
"required": ["spec_url_or_content"]
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"name": "fuzz_parameter",
|
|
623
|
+
"description": "Fuzz a parameter with payloads",
|
|
624
|
+
"parameters": {
|
|
625
|
+
"base_url": {"type": "string"},
|
|
626
|
+
"method": {"type": "string"},
|
|
627
|
+
"param_name": {"type": "string"},
|
|
628
|
+
"param_location": {"type": "string"},
|
|
629
|
+
"payloads": {"type": "array"},
|
|
630
|
+
"headers": {"type": "object"}
|
|
631
|
+
},
|
|
632
|
+
"required": ["base_url", "method", "param_name", "param_location", "payloads"]
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
"name": "test_authentication",
|
|
636
|
+
"description": "Test authentication mechanisms",
|
|
637
|
+
"parameters": {
|
|
638
|
+
"base_url": {"type": "string"},
|
|
639
|
+
"auth_endpoint": {"type": "string"},
|
|
640
|
+
"test_credentials": {"type": "array"}
|
|
641
|
+
},
|
|
642
|
+
"required": ["base_url", "auth_endpoint", "test_credentials"]
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
"name": "test_authorization",
|
|
646
|
+
"description": "Test for IDOR/BOLA vulnerabilities",
|
|
647
|
+
"parameters": {
|
|
648
|
+
"url": {"type": "string"},
|
|
649
|
+
"auth_header": {"type": "string"},
|
|
650
|
+
"test_ids": {"type": "array"}
|
|
651
|
+
},
|
|
652
|
+
"required": ["url", "auth_header", "test_ids"]
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"name": "report_api_finding",
|
|
656
|
+
"description": "Report an API security finding",
|
|
657
|
+
"parameters": {
|
|
658
|
+
"title": {"type": "string"},
|
|
659
|
+
"severity": {"type": "string"},
|
|
660
|
+
"endpoint": {"type": "string"},
|
|
661
|
+
"method": {"type": "string"},
|
|
662
|
+
"description": {"type": "string"},
|
|
663
|
+
"request": {"type": "string"},
|
|
664
|
+
"response": {"type": "string"},
|
|
665
|
+
"remediation": {"type": "string"}
|
|
666
|
+
},
|
|
667
|
+
"required": ["title", "severity", "endpoint", "method", "description", "request", "response", "remediation"]
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
async def run(self, initial_message: Optional[str] = None) -> AgentResult:
|
|
672
|
+
"""
|
|
673
|
+
Run the API security test.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
initial_message: Optional additional instructions
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
AgentResult with all security findings
|
|
680
|
+
"""
|
|
681
|
+
message = f"""Perform comprehensive API security testing on: {self.base_url}
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
if self.openapi_spec:
|
|
685
|
+
message += f"OpenAPI Specification available at: {self.openapi_spec}\nStart by parsing the OpenAPI spec to discover all endpoints.\n\n"
|
|
686
|
+
else:
|
|
687
|
+
message += "No OpenAPI spec provided. Start by discovering endpoints through common paths and responses.\n\n"
|
|
688
|
+
|
|
689
|
+
if self.headers:
|
|
690
|
+
message += f"Use these headers for authenticated requests: {json.dumps(self.headers)}\n\n"
|
|
691
|
+
|
|
692
|
+
message += """Testing priorities:
|
|
693
|
+
1. Map all endpoints and parameters
|
|
694
|
+
2. Test authentication mechanisms
|
|
695
|
+
3. Check for authorization bypass (BOLA/IDOR)
|
|
696
|
+
4. Test injection vulnerabilities
|
|
697
|
+
5. Check for information disclosure
|
|
698
|
+
6. Test rate limiting
|
|
699
|
+
|
|
700
|
+
"""
|
|
701
|
+
if initial_message:
|
|
702
|
+
message += initial_message
|
|
703
|
+
|
|
704
|
+
message += "\n\nBegin testing now."
|
|
705
|
+
|
|
706
|
+
return await super().run(message)
|