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,622 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphQL Security Scanner
|
|
3
|
+
|
|
4
|
+
Comprehensive GraphQL API security testing including:
|
|
5
|
+
- Introspection query detection
|
|
6
|
+
- Query depth attacks (DoS)
|
|
7
|
+
- Batch query attacks
|
|
8
|
+
- Field suggestion brute-force
|
|
9
|
+
- SQL/NoSQL injection via GraphQL
|
|
10
|
+
- Authorization bypass
|
|
11
|
+
- Information disclosure
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
|
|
15
|
+
- https://graphql.security/
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
from aipt_v2.tools.api_security import GraphQLScanner
|
|
19
|
+
|
|
20
|
+
scanner = GraphQLScanner("https://api.target.com/graphql")
|
|
21
|
+
findings = await scanner.scan()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from typing import List, Dict, Any, Optional
|
|
30
|
+
from urllib.parse import urlparse
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import aiohttp
|
|
34
|
+
except ImportError:
|
|
35
|
+
aiohttp = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GraphQLConfig:
|
|
40
|
+
"""GraphQL scanner configuration."""
|
|
41
|
+
endpoint: str
|
|
42
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
43
|
+
cookies: Dict[str, str] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
# Test options
|
|
46
|
+
test_introspection: bool = True
|
|
47
|
+
test_depth_attack: bool = True
|
|
48
|
+
test_batch_attack: bool = True
|
|
49
|
+
test_field_suggestions: bool = True
|
|
50
|
+
test_injection: bool = True
|
|
51
|
+
test_dos: bool = False # Disabled by default (potentially harmful)
|
|
52
|
+
|
|
53
|
+
# Limits
|
|
54
|
+
max_depth: int = 10
|
|
55
|
+
batch_size: int = 10
|
|
56
|
+
timeout: int = 30
|
|
57
|
+
|
|
58
|
+
# Authentication
|
|
59
|
+
auth_token: str = ""
|
|
60
|
+
auth_header: str = "Authorization"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class GraphQLFinding:
|
|
65
|
+
"""GraphQL security finding."""
|
|
66
|
+
vulnerability: str
|
|
67
|
+
severity: str # critical, high, medium, low, info
|
|
68
|
+
description: str
|
|
69
|
+
evidence: str
|
|
70
|
+
remediation: str
|
|
71
|
+
endpoint: str
|
|
72
|
+
timestamp: str = ""
|
|
73
|
+
cwe: str = ""
|
|
74
|
+
|
|
75
|
+
def __post_init__(self):
|
|
76
|
+
if not self.timestamp:
|
|
77
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class GraphQLScanResult:
|
|
82
|
+
"""Result of GraphQL security scan."""
|
|
83
|
+
endpoint: str
|
|
84
|
+
status: str
|
|
85
|
+
started_at: str
|
|
86
|
+
finished_at: str
|
|
87
|
+
duration: float
|
|
88
|
+
findings: List[GraphQLFinding]
|
|
89
|
+
schema_info: Dict[str, Any]
|
|
90
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class GraphQLScanner:
|
|
94
|
+
"""
|
|
95
|
+
GraphQL API Security Scanner.
|
|
96
|
+
|
|
97
|
+
Tests GraphQL endpoints for common security vulnerabilities
|
|
98
|
+
including introspection exposure, DoS vectors, and injection attacks.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# Standard introspection query
|
|
102
|
+
INTROSPECTION_QUERY = """
|
|
103
|
+
query IntrospectionQuery {
|
|
104
|
+
__schema {
|
|
105
|
+
queryType { name }
|
|
106
|
+
mutationType { name }
|
|
107
|
+
subscriptionType { name }
|
|
108
|
+
types {
|
|
109
|
+
kind
|
|
110
|
+
name
|
|
111
|
+
description
|
|
112
|
+
fields(includeDeprecated: true) {
|
|
113
|
+
name
|
|
114
|
+
description
|
|
115
|
+
args {
|
|
116
|
+
name
|
|
117
|
+
description
|
|
118
|
+
type { kind name }
|
|
119
|
+
defaultValue
|
|
120
|
+
}
|
|
121
|
+
type { kind name ofType { kind name } }
|
|
122
|
+
isDeprecated
|
|
123
|
+
deprecationReason
|
|
124
|
+
}
|
|
125
|
+
inputFields {
|
|
126
|
+
name
|
|
127
|
+
description
|
|
128
|
+
type { kind name }
|
|
129
|
+
defaultValue
|
|
130
|
+
}
|
|
131
|
+
interfaces { kind name }
|
|
132
|
+
enumValues(includeDeprecated: true) {
|
|
133
|
+
name
|
|
134
|
+
description
|
|
135
|
+
isDeprecated
|
|
136
|
+
deprecationReason
|
|
137
|
+
}
|
|
138
|
+
possibleTypes { kind name }
|
|
139
|
+
}
|
|
140
|
+
directives {
|
|
141
|
+
name
|
|
142
|
+
description
|
|
143
|
+
locations
|
|
144
|
+
args {
|
|
145
|
+
name
|
|
146
|
+
description
|
|
147
|
+
type { kind name }
|
|
148
|
+
defaultValue
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
# Partial introspection queries (sometimes full is blocked)
|
|
156
|
+
PARTIAL_INTROSPECTION = """
|
|
157
|
+
query { __schema { types { name } } }
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
# Type introspection
|
|
161
|
+
TYPE_INTROSPECTION = """
|
|
162
|
+
query { __type(name: "Query") { name fields { name } } }
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# Field suggestion payloads
|
|
166
|
+
FIELD_SUGGESTIONS = [
|
|
167
|
+
"user", "users", "admin", "admins", "login", "me", "profile",
|
|
168
|
+
"account", "accounts", "password", "token", "secret", "key",
|
|
169
|
+
"credential", "auth", "session", "config", "setting", "flag",
|
|
170
|
+
"debug", "test", "internal", "private", "hidden", "system"
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# SQL injection payloads for GraphQL
|
|
174
|
+
SQLI_PAYLOADS = [
|
|
175
|
+
"' OR '1'='1",
|
|
176
|
+
'" OR "1"="1',
|
|
177
|
+
"1 OR 1=1",
|
|
178
|
+
"'; DROP TABLE users; --",
|
|
179
|
+
"1' AND '1'='1",
|
|
180
|
+
"admin'--",
|
|
181
|
+
"1; SELECT * FROM users--"
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# NoSQL injection payloads
|
|
185
|
+
NOSQL_PAYLOADS = [
|
|
186
|
+
'{"$gt": ""}',
|
|
187
|
+
'{"$ne": null}',
|
|
188
|
+
'{"$regex": ".*"}',
|
|
189
|
+
'{"$where": "1==1"}'
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
def __init__(self, endpoint: str, config: Optional[GraphQLConfig] = None):
|
|
193
|
+
"""
|
|
194
|
+
Initialize GraphQL scanner.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
endpoint: GraphQL endpoint URL
|
|
198
|
+
config: Scanner configuration
|
|
199
|
+
"""
|
|
200
|
+
self.endpoint = endpoint
|
|
201
|
+
self.config = config or GraphQLConfig(endpoint=endpoint)
|
|
202
|
+
self.findings: List[GraphQLFinding] = []
|
|
203
|
+
self.schema = None
|
|
204
|
+
|
|
205
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
206
|
+
"""Build request headers."""
|
|
207
|
+
headers = {
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
"Accept": "application/json",
|
|
210
|
+
"User-Agent": "AIPTX-GraphQL-Scanner/1.0"
|
|
211
|
+
}
|
|
212
|
+
headers.update(self.config.headers)
|
|
213
|
+
|
|
214
|
+
if self.config.auth_token:
|
|
215
|
+
headers[self.config.auth_header] = f"Bearer {self.config.auth_token}"
|
|
216
|
+
|
|
217
|
+
return headers
|
|
218
|
+
|
|
219
|
+
async def _send_query(self, query: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
|
|
220
|
+
"""Send GraphQL query and return response."""
|
|
221
|
+
if aiohttp is None:
|
|
222
|
+
raise ImportError("aiohttp is required for GraphQL scanning. Install with: pip install aiohttp")
|
|
223
|
+
|
|
224
|
+
payload = {"query": query}
|
|
225
|
+
if variables:
|
|
226
|
+
payload["variables"] = variables
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
async with aiohttp.ClientSession() as session:
|
|
230
|
+
async with session.post(
|
|
231
|
+
self.endpoint,
|
|
232
|
+
json=payload,
|
|
233
|
+
headers=self._get_headers(),
|
|
234
|
+
cookies=self.config.cookies,
|
|
235
|
+
timeout=aiohttp.ClientTimeout(total=self.config.timeout),
|
|
236
|
+
ssl=False # Allow self-signed certs
|
|
237
|
+
) as response:
|
|
238
|
+
text = await response.text()
|
|
239
|
+
try:
|
|
240
|
+
return {"status": response.status, "data": json.loads(text)}
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
return {"status": response.status, "data": text, "raw": True}
|
|
243
|
+
except Exception as e:
|
|
244
|
+
return {"error": str(e)}
|
|
245
|
+
|
|
246
|
+
async def test_introspection(self) -> List[GraphQLFinding]:
|
|
247
|
+
"""Test if introspection is enabled."""
|
|
248
|
+
findings = []
|
|
249
|
+
|
|
250
|
+
# Test full introspection
|
|
251
|
+
response = await self._send_query(self.INTROSPECTION_QUERY)
|
|
252
|
+
|
|
253
|
+
if "error" not in response:
|
|
254
|
+
data = response.get("data", {})
|
|
255
|
+
if isinstance(data, dict) and "data" in data:
|
|
256
|
+
schema_data = data.get("data", {}).get("__schema")
|
|
257
|
+
if schema_data:
|
|
258
|
+
self.schema = schema_data
|
|
259
|
+
|
|
260
|
+
# Count exposed types
|
|
261
|
+
types = schema_data.get("types", [])
|
|
262
|
+
custom_types = [t for t in types if not t.get("name", "").startswith("__")]
|
|
263
|
+
|
|
264
|
+
findings.append(GraphQLFinding(
|
|
265
|
+
vulnerability="GraphQL Introspection Enabled",
|
|
266
|
+
severity="medium",
|
|
267
|
+
description=f"Full introspection query is enabled, exposing {len(custom_types)} custom types",
|
|
268
|
+
evidence=f"Exposed types: {', '.join([t.get('name') for t in custom_types[:10]])}...",
|
|
269
|
+
remediation="Disable introspection in production or implement authentication for introspection queries",
|
|
270
|
+
endpoint=self.endpoint,
|
|
271
|
+
cwe="CWE-200"
|
|
272
|
+
))
|
|
273
|
+
|
|
274
|
+
# Check for sensitive types
|
|
275
|
+
sensitive_patterns = ["user", "admin", "auth", "password", "token", "secret", "key"]
|
|
276
|
+
sensitive_types = [t for t in custom_types
|
|
277
|
+
if any(p in t.get("name", "").lower() for p in sensitive_patterns)]
|
|
278
|
+
|
|
279
|
+
if sensitive_types:
|
|
280
|
+
findings.append(GraphQLFinding(
|
|
281
|
+
vulnerability="Sensitive Types Exposed via Introspection",
|
|
282
|
+
severity="high",
|
|
283
|
+
description=f"Introspection reveals {len(sensitive_types)} potentially sensitive types",
|
|
284
|
+
evidence=f"Sensitive types: {', '.join([t.get('name') for t in sensitive_types])}",
|
|
285
|
+
remediation="Review exposed types and implement field-level authorization",
|
|
286
|
+
endpoint=self.endpoint,
|
|
287
|
+
cwe="CWE-200"
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
return findings
|
|
291
|
+
|
|
292
|
+
# Try partial introspection
|
|
293
|
+
response = await self._send_query(self.PARTIAL_INTROSPECTION)
|
|
294
|
+
if "error" not in response:
|
|
295
|
+
data = response.get("data", {})
|
|
296
|
+
if isinstance(data, dict) and "__schema" in str(data):
|
|
297
|
+
findings.append(GraphQLFinding(
|
|
298
|
+
vulnerability="GraphQL Partial Introspection Enabled",
|
|
299
|
+
severity="low",
|
|
300
|
+
description="Partial introspection query is allowed",
|
|
301
|
+
evidence="__schema query returned type information",
|
|
302
|
+
remediation="Disable all introspection queries in production",
|
|
303
|
+
endpoint=self.endpoint,
|
|
304
|
+
cwe="CWE-200"
|
|
305
|
+
))
|
|
306
|
+
|
|
307
|
+
return findings
|
|
308
|
+
|
|
309
|
+
async def test_depth_attack(self) -> List[GraphQLFinding]:
|
|
310
|
+
"""Test for query depth attack vulnerability (DoS)."""
|
|
311
|
+
findings = []
|
|
312
|
+
|
|
313
|
+
# Build nested query
|
|
314
|
+
def build_nested_query(depth: int) -> str:
|
|
315
|
+
query = "query {"
|
|
316
|
+
indent = " "
|
|
317
|
+
for i in range(depth):
|
|
318
|
+
query += f"\n{indent * (i + 1)}__typename"
|
|
319
|
+
if i < depth - 1:
|
|
320
|
+
query += "\n" + indent * (i + 1) + "... on Query {"
|
|
321
|
+
for i in range(depth - 1, 0, -1):
|
|
322
|
+
query += "\n" + indent * i + "}"
|
|
323
|
+
query += "\n}"
|
|
324
|
+
return query
|
|
325
|
+
|
|
326
|
+
# Test increasing depths
|
|
327
|
+
for depth in [5, 10, 15]:
|
|
328
|
+
nested_query = build_nested_query(depth)
|
|
329
|
+
response = await self._send_query(nested_query)
|
|
330
|
+
|
|
331
|
+
if "error" not in response and response.get("status") == 200:
|
|
332
|
+
data = response.get("data", {})
|
|
333
|
+
if "errors" not in data:
|
|
334
|
+
if depth >= 10:
|
|
335
|
+
findings.append(GraphQLFinding(
|
|
336
|
+
vulnerability="GraphQL Query Depth Attack",
|
|
337
|
+
severity="medium",
|
|
338
|
+
description=f"Server accepts queries with depth {depth}, allowing DoS attacks",
|
|
339
|
+
evidence=f"Nested query with depth {depth} was accepted",
|
|
340
|
+
remediation="Implement query depth limiting (recommended max: 5-7)",
|
|
341
|
+
endpoint=self.endpoint,
|
|
342
|
+
cwe="CWE-400"
|
|
343
|
+
))
|
|
344
|
+
break
|
|
345
|
+
|
|
346
|
+
return findings
|
|
347
|
+
|
|
348
|
+
async def test_batch_attack(self) -> List[GraphQLFinding]:
|
|
349
|
+
"""Test for batch query attack vulnerability."""
|
|
350
|
+
findings = []
|
|
351
|
+
|
|
352
|
+
# Build batch query
|
|
353
|
+
batch_query = " ".join([f"q{i}: __typename" for i in range(self.config.batch_size)])
|
|
354
|
+
query = f"query {{ {batch_query} }}"
|
|
355
|
+
|
|
356
|
+
response = await self._send_query(query)
|
|
357
|
+
|
|
358
|
+
if "error" not in response and response.get("status") == 200:
|
|
359
|
+
data = response.get("data", {})
|
|
360
|
+
if isinstance(data, dict) and "data" in data:
|
|
361
|
+
result_data = data.get("data", {})
|
|
362
|
+
if isinstance(result_data, dict) and len(result_data) >= self.config.batch_size:
|
|
363
|
+
findings.append(GraphQLFinding(
|
|
364
|
+
vulnerability="GraphQL Batch Query Attack",
|
|
365
|
+
severity="medium",
|
|
366
|
+
description=f"Server accepts batch queries with {self.config.batch_size}+ aliases",
|
|
367
|
+
evidence=f"Batch query with {self.config.batch_size} aliases was accepted",
|
|
368
|
+
remediation="Implement query complexity limiting and alias restrictions",
|
|
369
|
+
endpoint=self.endpoint,
|
|
370
|
+
cwe="CWE-400"
|
|
371
|
+
))
|
|
372
|
+
|
|
373
|
+
# Test array batching
|
|
374
|
+
batch_payload = [
|
|
375
|
+
{"query": "{ __typename }"}
|
|
376
|
+
for _ in range(self.config.batch_size)
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
if aiohttp:
|
|
380
|
+
try:
|
|
381
|
+
async with aiohttp.ClientSession() as session:
|
|
382
|
+
async with session.post(
|
|
383
|
+
self.endpoint,
|
|
384
|
+
json=batch_payload,
|
|
385
|
+
headers=self._get_headers(),
|
|
386
|
+
timeout=aiohttp.ClientTimeout(total=self.config.timeout),
|
|
387
|
+
ssl=False
|
|
388
|
+
) as response:
|
|
389
|
+
if response.status == 200:
|
|
390
|
+
data = await response.json()
|
|
391
|
+
if isinstance(data, list) and len(data) >= self.config.batch_size:
|
|
392
|
+
findings.append(GraphQLFinding(
|
|
393
|
+
vulnerability="GraphQL Array Batching Enabled",
|
|
394
|
+
severity="medium",
|
|
395
|
+
description="Server accepts array-based batch queries",
|
|
396
|
+
evidence=f"Array batch with {self.config.batch_size} queries was accepted",
|
|
397
|
+
remediation="Disable array batching or implement strict rate limiting",
|
|
398
|
+
endpoint=self.endpoint,
|
|
399
|
+
cwe="CWE-400"
|
|
400
|
+
))
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
return findings
|
|
405
|
+
|
|
406
|
+
async def test_field_suggestions(self) -> List[GraphQLFinding]:
|
|
407
|
+
"""Test for field suggestion information disclosure."""
|
|
408
|
+
findings = []
|
|
409
|
+
discovered_fields = []
|
|
410
|
+
|
|
411
|
+
for field_name in self.FIELD_SUGGESTIONS:
|
|
412
|
+
query = f"query {{ {field_name} }}"
|
|
413
|
+
response = await self._send_query(query)
|
|
414
|
+
|
|
415
|
+
if "error" not in response:
|
|
416
|
+
data = response.get("data", {})
|
|
417
|
+
if isinstance(data, dict):
|
|
418
|
+
errors = data.get("errors", [])
|
|
419
|
+
for error in errors:
|
|
420
|
+
message = error.get("message", "")
|
|
421
|
+
# Check for field suggestions in error
|
|
422
|
+
if "Did you mean" in message or "suggestions" in message.lower():
|
|
423
|
+
# Extract suggested fields
|
|
424
|
+
suggestions = re.findall(r'"([^"]+)"', message)
|
|
425
|
+
discovered_fields.extend(suggestions)
|
|
426
|
+
|
|
427
|
+
if discovered_fields:
|
|
428
|
+
unique_fields = list(set(discovered_fields))
|
|
429
|
+
findings.append(GraphQLFinding(
|
|
430
|
+
vulnerability="GraphQL Field Suggestion Disclosure",
|
|
431
|
+
severity="low",
|
|
432
|
+
description=f"Error messages reveal {len(unique_fields)} valid field names",
|
|
433
|
+
evidence=f"Discovered fields: {', '.join(unique_fields[:10])}",
|
|
434
|
+
remediation="Disable field suggestions in production or use generic error messages",
|
|
435
|
+
endpoint=self.endpoint,
|
|
436
|
+
cwe="CWE-200"
|
|
437
|
+
))
|
|
438
|
+
|
|
439
|
+
return findings
|
|
440
|
+
|
|
441
|
+
async def test_injection(self) -> List[GraphQLFinding]:
|
|
442
|
+
"""Test for SQL/NoSQL injection via GraphQL arguments."""
|
|
443
|
+
findings = []
|
|
444
|
+
|
|
445
|
+
# Test SQL injection
|
|
446
|
+
for payload in self.SQLI_PAYLOADS:
|
|
447
|
+
query = f'query {{ user(id: "{payload}") {{ id }} }}'
|
|
448
|
+
response = await self._send_query(query)
|
|
449
|
+
|
|
450
|
+
if "error" not in response:
|
|
451
|
+
data = response.get("data", {})
|
|
452
|
+
if isinstance(data, dict):
|
|
453
|
+
# Check for SQL error patterns
|
|
454
|
+
response_str = json.dumps(data).lower()
|
|
455
|
+
sql_errors = ["sql", "syntax", "mysql", "postgresql", "sqlite", "oracle"]
|
|
456
|
+
if any(err in response_str for err in sql_errors):
|
|
457
|
+
findings.append(GraphQLFinding(
|
|
458
|
+
vulnerability="Potential SQL Injection via GraphQL",
|
|
459
|
+
severity="critical",
|
|
460
|
+
description="GraphQL argument appears vulnerable to SQL injection",
|
|
461
|
+
evidence=f"Payload: {payload} triggered SQL-related error",
|
|
462
|
+
remediation="Use parameterized queries and input validation",
|
|
463
|
+
endpoint=self.endpoint,
|
|
464
|
+
cwe="CWE-89"
|
|
465
|
+
))
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
# Test NoSQL injection
|
|
469
|
+
for payload in self.NOSQL_PAYLOADS:
|
|
470
|
+
query = f'query {{ user(filter: {payload}) {{ id }} }}'
|
|
471
|
+
response = await self._send_query(query)
|
|
472
|
+
|
|
473
|
+
if "error" not in response:
|
|
474
|
+
data = response.get("data", {})
|
|
475
|
+
response_str = json.dumps(data).lower()
|
|
476
|
+
nosql_errors = ["mongodb", "mongoose", "objectid", "bson"]
|
|
477
|
+
if any(err in response_str for err in nosql_errors):
|
|
478
|
+
findings.append(GraphQLFinding(
|
|
479
|
+
vulnerability="Potential NoSQL Injection via GraphQL",
|
|
480
|
+
severity="critical",
|
|
481
|
+
description="GraphQL argument appears vulnerable to NoSQL injection",
|
|
482
|
+
evidence=f"Payload triggered NoSQL-related response",
|
|
483
|
+
remediation="Validate and sanitize all input before database queries",
|
|
484
|
+
endpoint=self.endpoint,
|
|
485
|
+
cwe="CWE-943"
|
|
486
|
+
))
|
|
487
|
+
break
|
|
488
|
+
|
|
489
|
+
return findings
|
|
490
|
+
|
|
491
|
+
async def test_authorization_bypass(self) -> List[GraphQLFinding]:
|
|
492
|
+
"""Test for authorization bypass via alias abuse."""
|
|
493
|
+
findings = []
|
|
494
|
+
|
|
495
|
+
# Try to access potentially restricted fields via aliases
|
|
496
|
+
sensitive_queries = [
|
|
497
|
+
'query { admin: user(role: "admin") { id email } }',
|
|
498
|
+
'query { allUsers: users(limit: 1000) { id email role } }',
|
|
499
|
+
'query { config: systemConfig { debugMode apiKeys } }',
|
|
500
|
+
'query { me: currentUser { id role permissions } }'
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
for query in sensitive_queries:
|
|
504
|
+
response = await self._send_query(query)
|
|
505
|
+
|
|
506
|
+
if "error" not in response:
|
|
507
|
+
data = response.get("data", {})
|
|
508
|
+
if isinstance(data, dict):
|
|
509
|
+
result = data.get("data", {})
|
|
510
|
+
if result and "errors" not in data:
|
|
511
|
+
# Check if we got actual data
|
|
512
|
+
if any(result.values()):
|
|
513
|
+
findings.append(GraphQLFinding(
|
|
514
|
+
vulnerability="Potential Authorization Bypass",
|
|
515
|
+
severity="high",
|
|
516
|
+
description="Query returned data that may require authorization",
|
|
517
|
+
evidence=f"Query '{query[:50]}...' returned data without proper auth check",
|
|
518
|
+
remediation="Implement field-level authorization and access control",
|
|
519
|
+
endpoint=self.endpoint,
|
|
520
|
+
cwe="CWE-862"
|
|
521
|
+
))
|
|
522
|
+
|
|
523
|
+
return findings
|
|
524
|
+
|
|
525
|
+
async def scan(self) -> GraphQLScanResult:
|
|
526
|
+
"""
|
|
527
|
+
Run full GraphQL security scan.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
GraphQLScanResult with all findings
|
|
531
|
+
"""
|
|
532
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
533
|
+
start_time = asyncio.get_event_loop().time()
|
|
534
|
+
|
|
535
|
+
findings = []
|
|
536
|
+
|
|
537
|
+
# Run enabled tests
|
|
538
|
+
if self.config.test_introspection:
|
|
539
|
+
findings.extend(await self.test_introspection())
|
|
540
|
+
|
|
541
|
+
if self.config.test_depth_attack:
|
|
542
|
+
findings.extend(await self.test_depth_attack())
|
|
543
|
+
|
|
544
|
+
if self.config.test_batch_attack:
|
|
545
|
+
findings.extend(await self.test_batch_attack())
|
|
546
|
+
|
|
547
|
+
if self.config.test_field_suggestions:
|
|
548
|
+
findings.extend(await self.test_field_suggestions())
|
|
549
|
+
|
|
550
|
+
if self.config.test_injection:
|
|
551
|
+
findings.extend(await self.test_injection())
|
|
552
|
+
|
|
553
|
+
# Authorization bypass test
|
|
554
|
+
findings.extend(await self.test_authorization_bypass())
|
|
555
|
+
|
|
556
|
+
finished_at = datetime.now(timezone.utc).isoformat()
|
|
557
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
558
|
+
|
|
559
|
+
# Build schema info summary
|
|
560
|
+
schema_info = {}
|
|
561
|
+
if self.schema:
|
|
562
|
+
types = self.schema.get("types", [])
|
|
563
|
+
schema_info = {
|
|
564
|
+
"total_types": len(types),
|
|
565
|
+
"query_type": self.schema.get("queryType", {}).get("name"),
|
|
566
|
+
"mutation_type": self.schema.get("mutationType", {}).get("name"),
|
|
567
|
+
"subscription_type": self.schema.get("subscriptionType", {}).get("name"),
|
|
568
|
+
"custom_types": len([t for t in types if not t.get("name", "").startswith("__")])
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return GraphQLScanResult(
|
|
572
|
+
endpoint=self.endpoint,
|
|
573
|
+
status="completed",
|
|
574
|
+
started_at=started_at,
|
|
575
|
+
finished_at=finished_at,
|
|
576
|
+
duration=duration,
|
|
577
|
+
findings=findings,
|
|
578
|
+
schema_info=schema_info,
|
|
579
|
+
metadata={
|
|
580
|
+
"tests_run": sum([
|
|
581
|
+
self.config.test_introspection,
|
|
582
|
+
self.config.test_depth_attack,
|
|
583
|
+
self.config.test_batch_attack,
|
|
584
|
+
self.config.test_field_suggestions,
|
|
585
|
+
self.config.test_injection
|
|
586
|
+
])
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# Convenience function
|
|
592
|
+
async def scan_graphql(
|
|
593
|
+
endpoint: str,
|
|
594
|
+
auth_token: Optional[str] = None,
|
|
595
|
+
headers: Optional[Dict[str, str]] = None,
|
|
596
|
+
full_scan: bool = True
|
|
597
|
+
) -> GraphQLScanResult:
|
|
598
|
+
"""
|
|
599
|
+
Quick GraphQL security scan.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
endpoint: GraphQL endpoint URL
|
|
603
|
+
auth_token: Bearer token for authentication
|
|
604
|
+
headers: Additional headers
|
|
605
|
+
full_scan: Run all tests if True
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
GraphQLScanResult
|
|
609
|
+
"""
|
|
610
|
+
config = GraphQLConfig(
|
|
611
|
+
endpoint=endpoint,
|
|
612
|
+
auth_token=auth_token or "",
|
|
613
|
+
headers=headers or {},
|
|
614
|
+
test_introspection=True,
|
|
615
|
+
test_depth_attack=full_scan,
|
|
616
|
+
test_batch_attack=full_scan,
|
|
617
|
+
test_field_suggestions=full_scan,
|
|
618
|
+
test_injection=full_scan
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
scanner = GraphQLScanner(endpoint, config)
|
|
622
|
+
return await scanner.scan()
|