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.

Files changed (165) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +24 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/ptt.py +406 -0
  8. aipt_v2/agents/state.py +168 -0
  9. aipt_v2/app.py +960 -0
  10. aipt_v2/browser/__init__.py +31 -0
  11. aipt_v2/browser/automation.py +458 -0
  12. aipt_v2/browser/crawler.py +453 -0
  13. aipt_v2/cli.py +321 -0
  14. aipt_v2/compliance/__init__.py +71 -0
  15. aipt_v2/compliance/compliance_report.py +449 -0
  16. aipt_v2/compliance/framework_mapper.py +424 -0
  17. aipt_v2/compliance/nist_mapping.py +345 -0
  18. aipt_v2/compliance/owasp_mapping.py +330 -0
  19. aipt_v2/compliance/pci_mapping.py +297 -0
  20. aipt_v2/config.py +288 -0
  21. aipt_v2/core/__init__.py +43 -0
  22. aipt_v2/core/agent.py +630 -0
  23. aipt_v2/core/llm.py +395 -0
  24. aipt_v2/core/memory.py +305 -0
  25. aipt_v2/core/ptt.py +329 -0
  26. aipt_v2/database/__init__.py +14 -0
  27. aipt_v2/database/models.py +232 -0
  28. aipt_v2/database/repository.py +384 -0
  29. aipt_v2/docker/__init__.py +23 -0
  30. aipt_v2/docker/builder.py +260 -0
  31. aipt_v2/docker/manager.py +222 -0
  32. aipt_v2/docker/sandbox.py +371 -0
  33. aipt_v2/evasion/__init__.py +58 -0
  34. aipt_v2/evasion/request_obfuscator.py +272 -0
  35. aipt_v2/evasion/tls_fingerprint.py +285 -0
  36. aipt_v2/evasion/ua_rotator.py +301 -0
  37. aipt_v2/evasion/waf_bypass.py +439 -0
  38. aipt_v2/execution/__init__.py +23 -0
  39. aipt_v2/execution/executor.py +302 -0
  40. aipt_v2/execution/parser.py +544 -0
  41. aipt_v2/execution/terminal.py +337 -0
  42. aipt_v2/health.py +437 -0
  43. aipt_v2/intelligence/__init__.py +85 -0
  44. aipt_v2/intelligence/auth.py +520 -0
  45. aipt_v2/intelligence/chaining.py +775 -0
  46. aipt_v2/intelligence/cve_aipt.py +334 -0
  47. aipt_v2/intelligence/cve_info.py +1111 -0
  48. aipt_v2/intelligence/rag.py +239 -0
  49. aipt_v2/intelligence/scope.py +442 -0
  50. aipt_v2/intelligence/searchers/__init__.py +5 -0
  51. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  52. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  53. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  54. aipt_v2/intelligence/tools.json +443 -0
  55. aipt_v2/intelligence/triage.py +670 -0
  56. aipt_v2/interface/__init__.py +5 -0
  57. aipt_v2/interface/cli.py +230 -0
  58. aipt_v2/interface/main.py +501 -0
  59. aipt_v2/interface/tui.py +1276 -0
  60. aipt_v2/interface/utils.py +583 -0
  61. aipt_v2/llm/__init__.py +39 -0
  62. aipt_v2/llm/config.py +26 -0
  63. aipt_v2/llm/llm.py +514 -0
  64. aipt_v2/llm/memory.py +214 -0
  65. aipt_v2/llm/request_queue.py +89 -0
  66. aipt_v2/llm/utils.py +89 -0
  67. aipt_v2/models/__init__.py +15 -0
  68. aipt_v2/models/findings.py +295 -0
  69. aipt_v2/models/phase_result.py +224 -0
  70. aipt_v2/models/scan_config.py +207 -0
  71. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  72. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  73. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  74. aipt_v2/monitoring/prometheus.yml +60 -0
  75. aipt_v2/orchestration/__init__.py +52 -0
  76. aipt_v2/orchestration/pipeline.py +398 -0
  77. aipt_v2/orchestration/progress.py +300 -0
  78. aipt_v2/orchestration/scheduler.py +296 -0
  79. aipt_v2/orchestrator.py +2284 -0
  80. aipt_v2/payloads/__init__.py +27 -0
  81. aipt_v2/payloads/cmdi.py +150 -0
  82. aipt_v2/payloads/sqli.py +263 -0
  83. aipt_v2/payloads/ssrf.py +204 -0
  84. aipt_v2/payloads/templates.py +222 -0
  85. aipt_v2/payloads/traversal.py +166 -0
  86. aipt_v2/payloads/xss.py +204 -0
  87. aipt_v2/prompts/__init__.py +60 -0
  88. aipt_v2/proxy/__init__.py +29 -0
  89. aipt_v2/proxy/history.py +352 -0
  90. aipt_v2/proxy/interceptor.py +452 -0
  91. aipt_v2/recon/__init__.py +44 -0
  92. aipt_v2/recon/dns.py +241 -0
  93. aipt_v2/recon/osint.py +367 -0
  94. aipt_v2/recon/subdomain.py +372 -0
  95. aipt_v2/recon/tech_detect.py +311 -0
  96. aipt_v2/reports/__init__.py +17 -0
  97. aipt_v2/reports/generator.py +313 -0
  98. aipt_v2/reports/html_report.py +378 -0
  99. aipt_v2/runtime/__init__.py +44 -0
  100. aipt_v2/runtime/base.py +30 -0
  101. aipt_v2/runtime/docker.py +401 -0
  102. aipt_v2/runtime/local.py +346 -0
  103. aipt_v2/runtime/tool_server.py +205 -0
  104. aipt_v2/scanners/__init__.py +28 -0
  105. aipt_v2/scanners/base.py +273 -0
  106. aipt_v2/scanners/nikto.py +244 -0
  107. aipt_v2/scanners/nmap.py +402 -0
  108. aipt_v2/scanners/nuclei.py +273 -0
  109. aipt_v2/scanners/web.py +454 -0
  110. aipt_v2/scripts/security_audit.py +366 -0
  111. aipt_v2/telemetry/__init__.py +7 -0
  112. aipt_v2/telemetry/tracer.py +347 -0
  113. aipt_v2/terminal/__init__.py +28 -0
  114. aipt_v2/terminal/executor.py +400 -0
  115. aipt_v2/terminal/sandbox.py +350 -0
  116. aipt_v2/tools/__init__.py +44 -0
  117. aipt_v2/tools/active_directory/__init__.py +78 -0
  118. aipt_v2/tools/active_directory/ad_config.py +238 -0
  119. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  120. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  121. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  122. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  123. aipt_v2/tools/agents_graph/__init__.py +19 -0
  124. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  125. aipt_v2/tools/api_security/__init__.py +76 -0
  126. aipt_v2/tools/api_security/api_discovery.py +608 -0
  127. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  128. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  129. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  130. aipt_v2/tools/browser/__init__.py +5 -0
  131. aipt_v2/tools/browser/browser_actions.py +238 -0
  132. aipt_v2/tools/browser/browser_instance.py +535 -0
  133. aipt_v2/tools/browser/tab_manager.py +344 -0
  134. aipt_v2/tools/cloud/__init__.py +70 -0
  135. aipt_v2/tools/cloud/cloud_config.py +273 -0
  136. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  137. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  138. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  139. aipt_v2/tools/executor.py +307 -0
  140. aipt_v2/tools/parser.py +408 -0
  141. aipt_v2/tools/proxy/__init__.py +5 -0
  142. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  143. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  144. aipt_v2/tools/registry.py +196 -0
  145. aipt_v2/tools/scanners/__init__.py +343 -0
  146. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  147. aipt_v2/tools/scanners/burp_tool.py +631 -0
  148. aipt_v2/tools/scanners/config.py +156 -0
  149. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  150. aipt_v2/tools/scanners/zap_tool.py +612 -0
  151. aipt_v2/tools/terminal/__init__.py +5 -0
  152. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  153. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  154. aipt_v2/tools/terminal/terminal_session.py +449 -0
  155. aipt_v2/tools/tool_processing.py +108 -0
  156. aipt_v2/utils/__init__.py +17 -0
  157. aipt_v2/utils/logging.py +201 -0
  158. aipt_v2/utils/model_manager.py +187 -0
  159. aipt_v2/utils/searchers/__init__.py +269 -0
  160. aiptx-2.0.2.dist-info/METADATA +324 -0
  161. aiptx-2.0.2.dist-info/RECORD +165 -0
  162. aiptx-2.0.2.dist-info/WHEEL +5 -0
  163. aiptx-2.0.2.dist-info/entry_points.txt +7 -0
  164. aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
  165. 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()