synapse-hub 2.0.0 → 2.1.0

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.
@@ -0,0 +1,468 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Enterprise Autonomous API Fuzzing & Chaos Breaker
5
+ -------------------------------------------------
6
+ Zero-dependency, active runtime API mutator and resilience tester.
7
+ Ingests OpenAPI 3.x / Swagger 2.0 specifications (or endpoint catalogs),
8
+ generates dynamic boundary, type-inverted, and adversarial fuzzing payloads,
9
+ and executes live HTTP/S requests against staging/test APIs.
10
+
11
+ Three-Alarm Oracle Detection:
12
+ 1. HTTP 500 / 502 / 503 Internal Server Error (Backend crash / unhandled fault)
13
+ 2. Database & Framework Stacktrace Leaks (SQL injection reflection, Django/Spring/Postgres traces)
14
+ 3. SLA Latency Breach (> 3000ms response time - potential ReDoS or lock contention)
15
+ """
16
+
17
+ import os
18
+ import re
19
+ import sys
20
+ import json
21
+ import time
22
+ import ssl
23
+ import urllib.request
24
+ import urllib.error
25
+ import urllib.parse
26
+ import argparse
27
+ from typing import Dict, Any, List, Optional, Tuple
28
+
29
+ # Enforce UTF-8 stdout encoding for Windows/macOS/Linux compatibility
30
+ if hasattr(sys.stdout, 'reconfigure'):
31
+ try:
32
+ sys.stdout.reconfigure(encoding='utf-8')
33
+ sys.stderr.reconfigure(encoding='utf-8')
34
+ except Exception:
35
+ pass
36
+
37
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
38
+ HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
39
+ REPORTS_DIR = os.path.join(HUB_ROOT, "reports")
40
+
41
+ if SCRIPT_DIR not in sys.path:
42
+ sys.path.insert(0, SCRIPT_DIR)
43
+
44
+ try:
45
+ from fuzz_payloads import get_fuzz_payloads, get_field_fuzz_scenarios
46
+ except ImportError:
47
+ get_fuzz_payloads = None
48
+ get_field_fuzz_scenarios = None
49
+
50
+ # Stacktrace and DB leak pattern signatures
51
+ STACKTRACE_LEAK_PATTERNS = [
52
+ re.compile(r'Traceback \(most recent call last\):', re.IGNORECASE),
53
+ re.compile(r'Exception in thread "[^"]+"', re.IGNORECASE),
54
+ re.compile(r'\b(org\.springframework|org\.hibernate|java\.lang\.NullPointerException)\b', re.IGNORECASE),
55
+ re.compile(r'\b(psycopg2\.(OperationalError|ProgrammingError)|PSQLException)\b', re.IGNORECASE),
56
+ re.compile(r'\b(mysql_error|mysqli_sql_exception|pymysql\.err)\b', re.IGNORECASE),
57
+ re.compile(r'\b(sqlite3\.OperationalError|SQLiteException)\b', re.IGNORECASE),
58
+ re.compile(r'\b(django\.db\.utils|sqlalchemy\.exc)\b', re.IGNORECASE),
59
+ re.compile(r'SQLSTATE\[[0-9A-Z]{5}\]', re.IGNORECASE),
60
+ re.compile(r'syntax error at or near\s+"', re.IGNORECASE),
61
+ re.compile(r'ODBC SQL Server Driver', re.IGNORECASE),
62
+ ]
63
+
64
+ GREEN = "\033[92m"
65
+ RED = "\033[91m"
66
+ YELLOW = "\033[93m"
67
+ CYAN = "\033[96m"
68
+ BOLD = "\033[1m"
69
+ RESET = "\033[0m"
70
+
71
+
72
+ class ApiFuzzer:
73
+ """Autonomous runtime API fuzzer with three-alarm defect Oracle."""
74
+
75
+ def __init__(
76
+ self,
77
+ base_url: str,
78
+ spec_path_or_url: Optional[str] = None,
79
+ headers: Optional[Dict[str, str]] = None,
80
+ timeout_sec: float = 10.0,
81
+ sla_ms: float = 3000.0,
82
+ verify_ssl: bool = False
83
+ ):
84
+ self.base_url = base_url.rstrip("/")
85
+ self.spec_path = spec_path_or_url
86
+ self.custom_headers = headers or {"Content-Type": "application/json", "User-Agent": "AgentQA-Fuzzer/2.0"}
87
+ self.timeout_sec = timeout_sec
88
+ self.sla_ms = sla_ms
89
+ self.verify_ssl = verify_ssl
90
+
91
+ # Prepare SSL context (support self-signed staging certificates)
92
+ self.ssl_context = ssl.create_default_context()
93
+ if not verify_ssl:
94
+ self.ssl_context.check_hostname = False
95
+ self.ssl_context.verify_mode = ssl.CERT_NONE
96
+
97
+ def load_spec(self) -> Dict[str, Any]:
98
+ """Loads OpenAPI or Swagger specification from file or URL."""
99
+ if not self.spec_path:
100
+ # Fallback default health and mock endpoints
101
+ return {
102
+ "paths": {
103
+ "/api/v1/users": {
104
+ "post": {
105
+ "summary": "Create user",
106
+ "requestBody": {
107
+ "content": {
108
+ "application/json": {
109
+ "schema": {
110
+ "type": "object",
111
+ "properties": {
112
+ "username": {"type": "string", "maxLength": 50},
113
+ "age": {"type": "integer", "minimum": 1, "maximum": 120},
114
+ "email": {"type": "string"}
115
+ },
116
+ "required": ["username"]
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ if self.spec_path.startswith(("http://", "https://")):
127
+ req = urllib.request.Request(self.spec_path, headers={"User-Agent": "AgentQA-Fuzzer/2.0"})
128
+ with urllib.request.urlopen(req, context=self.ssl_context, timeout=10.0) as resp:
129
+ raw = resp.read().decode("utf-8")
130
+ return json.loads(raw)
131
+ else:
132
+ if not os.path.exists(self.spec_path):
133
+ raise FileNotFoundError(f"OpenAPI spec not found: {self.spec_path}")
134
+ with open(self.spec_path, "r", encoding="utf-8") as f:
135
+ return json.load(f)
136
+
137
+ def extract_fuzz_targets(self, spec: Dict[str, Any]) -> List[Dict[str, Any]]:
138
+ """Extracts actionable API routes and property schemas from spec."""
139
+ targets = []
140
+ paths = spec.get("paths", {})
141
+ for path, path_item in paths.items():
142
+ for method, details in path_item.items():
143
+ if method.lower() not in ("get", "post", "put", "patch", "delete"):
144
+ continue
145
+
146
+ req_body = details.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema", {})
147
+ props = req_body.get("properties", {})
148
+ required = req_body.get("required", [])
149
+
150
+ targets.append({
151
+ "path": path,
152
+ "method": method.upper(),
153
+ "summary": details.get("summary", f"{method.upper()} {path}"),
154
+ "parameters": details.get("parameters", []),
155
+ "properties": props,
156
+ "required": required
157
+ })
158
+ return targets
159
+
160
+ def generate_mutations(self, target: Dict[str, Any]) -> List[Dict[str, Any]]:
161
+ """Generates dynamic mutation vectors for query params and request body."""
162
+ mutations = []
163
+ props = target.get("properties", {})
164
+ method = target["method"]
165
+ path = target["path"]
166
+
167
+ # Base valid skeleton
168
+ valid_body: Dict[str, Any] = {}
169
+ for p_name, p_schema in props.items():
170
+ t = p_schema.get("type", "string")
171
+ if t == "string":
172
+ valid_body[p_name] = f"valid_{p_name}"
173
+ elif t in ("integer", "number"):
174
+ valid_body[p_name] = 10
175
+ elif t == "boolean":
176
+ valid_body[p_name] = True
177
+ else:
178
+ valid_body[p_name] = "test"
179
+
180
+ # 1. Baseline Happy-Path
181
+ mutations.append({
182
+ "name": f"{method} {path} - Baseline Happy-Path",
183
+ "type": "BASELINE",
184
+ "path": path,
185
+ "method": method,
186
+ "body": valid_body,
187
+ "headers": self.custom_headers
188
+ })
189
+
190
+ # 2. Field Fuzzing Payloads
191
+ for p_name, p_schema in props.items():
192
+ p_type = p_schema.get("type", "string")
193
+
194
+ # A. Extreme Buffer Stress (10,000 chars)
195
+ mut_body = dict(valid_body)
196
+ mut_body[p_name] = "A" * 10000
197
+ mutations.append({
198
+ "name": f"{method} {path} - Buffer Stress on '{p_name}'",
199
+ "type": "BUFFER_STRESS",
200
+ "path": path,
201
+ "method": method,
202
+ "body": mut_body,
203
+ "headers": self.custom_headers
204
+ })
205
+
206
+ # B. SQLi Polyglot Probe
207
+ mut_body = dict(valid_body)
208
+ mut_body[p_name] = "' OR '1'='1' -- /* injected */"
209
+ mutations.append({
210
+ "name": f"{method} {path} - SQLi Polyglot Probe on '{p_name}'",
211
+ "type": "SQLI_INJECTION",
212
+ "path": path,
213
+ "method": method,
214
+ "body": mut_body,
215
+ "headers": self.custom_headers
216
+ })
217
+
218
+ # C. Unicode BiDi / RTL Trojan Source
219
+ mut_body = dict(valid_body)
220
+ mut_body[p_name] = "\u202E\u0041\u0042\u0043_AUTO_TX"
221
+ mutations.append({
222
+ "name": f"{method} {path} - Unicode BiDi RTL Override on '{p_name}'",
223
+ "type": "BIDI_UNICODE",
224
+ "path": path,
225
+ "method": method,
226
+ "body": mut_body,
227
+ "headers": self.custom_headers
228
+ })
229
+
230
+ # D. Null Byte Injection
231
+ mut_body = dict(valid_body)
232
+ mut_body[p_name] = f"data\x00{p_name}.json"
233
+ mutations.append({
234
+ "name": f"{method} {path} - Null Byte Injection on '{p_name}'",
235
+ "type": "NULL_BYTE",
236
+ "path": path,
237
+ "method": method,
238
+ "body": mut_body,
239
+ "headers": self.custom_headers
240
+ })
241
+
242
+ # E. Numeric Boundaries (if integer)
243
+ if p_type in ("integer", "number"):
244
+ mut_body = dict(valid_body)
245
+ mut_body[p_name] = 2147483648 # 32-bit overflow
246
+ mutations.append({
247
+ "name": f"{method} {path} - 32-bit Integer Overflow on '{p_name}'",
248
+ "type": "NUMERIC_OVERFLOW",
249
+ "path": path,
250
+ "method": method,
251
+ "body": mut_body,
252
+ "headers": self.custom_headers
253
+ })
254
+
255
+ # F. Type Inversion (String where integer expected or vice versa)
256
+ mut_body = dict(valid_body)
257
+ mut_body[p_name] = 99999 if p_type == "string" else "NOT_A_NUMBER"
258
+ mutations.append({
259
+ "name": f"{method} {path} - Type Mutation on '{p_name}'",
260
+ "type": "TYPE_MUTATION",
261
+ "path": path,
262
+ "method": method,
263
+ "body": mut_body,
264
+ "headers": self.custom_headers
265
+ })
266
+
267
+ # 3. Auth Bypass Vector (Remove Authorization Header)
268
+ mut_headers = {k: v for k, v in self.custom_headers.items() if k.lower() != "authorization"}
269
+ mutations.append({
270
+ "name": f"{method} {path} - Auth Token Omission Probe",
271
+ "type": "AUTH_OMISSION",
272
+ "path": path,
273
+ "method": method,
274
+ "body": valid_body,
275
+ "headers": mut_headers
276
+ })
277
+
278
+ return mutations
279
+
280
+ def execute_request(self, mutation: Dict[str, Any], dry_run: bool = False) -> Dict[str, Any]:
281
+ """Dispatches HTTP mutation and evaluates the three-alarm Oracle."""
282
+ url = f"{self.base_url}{mutation['path']}"
283
+ method = mutation["method"]
284
+ body_data = None
285
+ if method in ("POST", "PUT", "PATCH") and mutation.get("body") is not None:
286
+ body_data = json.dumps(mutation["body"]).encode("utf-8")
287
+
288
+ if dry_run:
289
+ # Deterministic offline mock for CI & local checks
290
+ return {
291
+ "name": mutation["name"],
292
+ "type": mutation["type"],
293
+ "url": url,
294
+ "method": method,
295
+ "status_code": 400 if mutation["type"] != "BASELINE" else 200,
296
+ "duration_ms": 42.5,
297
+ "verdict": "PASS",
298
+ "alarms": [],
299
+ "response_preview": '{"status": "handled_by_dry_run"}'
300
+ }
301
+
302
+ req = urllib.request.Request(
303
+ url=url,
304
+ data=body_data,
305
+ headers=mutation["headers"],
306
+ method=method
307
+ )
308
+
309
+ start_time = time.time()
310
+ status_code = 0
311
+ response_body = ""
312
+ duration_ms = 0.0
313
+
314
+ try:
315
+ with urllib.request.urlopen(req, context=self.ssl_context, timeout=self.timeout_sec) as resp:
316
+ status_code = resp.getcode()
317
+ response_body = resp.read().decode("utf-8", errors="replace")
318
+ except urllib.error.HTTPError as he:
319
+ status_code = he.code
320
+ try:
321
+ response_body = he.read().decode("utf-8", errors="replace")
322
+ except Exception:
323
+ response_body = str(he)
324
+ except urllib.error.URLError as ue:
325
+ status_code = -1
326
+ response_body = str(ue.reason)
327
+ except Exception as e:
328
+ status_code = -2
329
+ response_body = str(e)
330
+
331
+ duration_ms = round((time.time() - start_time) * 1000, 2)
332
+
333
+ # ----------------------------------------------------
334
+ # THREE-ALARM DEFECT ORACLE EVALUATION
335
+ # ----------------------------------------------------
336
+ alarms = []
337
+
338
+ # Alarm 1: HTTP 500 / 502 / 503 Internal Server Error
339
+ if status_code in (500, 502, 503):
340
+ alarms.append(f"HTTP_{status_code}_SERVER_CRASH: Unhandled backend exception / server panic")
341
+
342
+ # Alarm 2: Database / Framework Stacktrace Leak
343
+ for pattern in STACKTRACE_LEAK_PATTERNS:
344
+ if pattern.search(response_body):
345
+ matched = pattern.pattern
346
+ alarms.append(f"STACKTRACE_LEAK: Response leaks backend internals matching '{matched}'")
347
+ break
348
+
349
+ # Alarm 3: SLA Latency Breach (> 3000ms)
350
+ if duration_ms > self.sla_ms:
351
+ alarms.append(f"SLA_TIMEOUT: Response took {duration_ms}ms (threshold: {self.sla_ms}ms), possible ReDoS or DB lock")
352
+
353
+ # Alarm 4: Auth Bypass (OMISSION succeeded with 200/201)
354
+ if mutation["type"] == "AUTH_OMISSION" and status_code in (200, 201):
355
+ alarms.append("AUTH_BYPASS_VULNERABILITY: Endpoint returned 200 OK without Authorization header")
356
+
357
+ verdict = "FAIL" if alarms else "PASS"
358
+
359
+ return {
360
+ "name": mutation["name"],
361
+ "type": mutation["type"],
362
+ "url": url,
363
+ "method": method,
364
+ "status_code": status_code,
365
+ "duration_ms": duration_ms,
366
+ "verdict": verdict,
367
+ "alarms": alarms,
368
+ "response_preview": response_body[:300]
369
+ }
370
+
371
+ def run_fuzz_suite(self, dry_run: bool = False) -> Dict[str, Any]:
372
+ """Runs the complete fuzzing suite against all extracted targets."""
373
+ spec = self.load_spec()
374
+ targets = self.extract_fuzz_targets(spec)
375
+
376
+ all_mutations = []
377
+ for t in targets:
378
+ all_mutations.extend(self.generate_mutations(t))
379
+
380
+ results = []
381
+ total_failures = 0
382
+
383
+ print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
384
+ print(f"{BOLD}{CYAN} ENTERPRISE AUTONOMOUS API FUZZING & CHAOS BREAKER {RESET}")
385
+ print(f"{BOLD}{CYAN}======================================================================{RESET}")
386
+ print(f" Target Base URL : {self.base_url}")
387
+ print(f" Spec Source : {self.spec_path or 'Default Archetype'}")
388
+ print(f" Total Endpoints : {len(targets)}")
389
+ print(f" Total Mutations : {len(all_mutations)}")
390
+ print(f" SLA Threshold : {self.sla_ms} ms")
391
+ print(f" Mode : {'DRY-RUN (Air-Gapped / Deterministic)' if dry_run else 'LIVE STAGING ATTACK'}\n")
392
+
393
+ for idx, m in enumerate(all_mutations, start=1):
394
+ res = self.execute_request(m, dry_run=dry_run)
395
+ results.append(res)
396
+
397
+ status_str = f"{GREEN}PASS{RESET}" if res["verdict"] == "PASS" else f"{RED}ALARM{RESET}"
398
+ sys.stdout.write(f" [{idx:03d}/{len(all_mutations):03d}] {status_str} | HTTP {res['status_code']} ({res['duration_ms']}ms) | {m['name']}\n")
399
+ if res["alarms"]:
400
+ total_failures += 1
401
+ for alarm in res["alarms"]:
402
+ print(f" 👉 {RED}{BOLD}[DEFECT]{RESET} {alarm}")
403
+
404
+ scorecard = {
405
+ "base_url": self.base_url,
406
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
407
+ "total_mutations": len(all_mutations),
408
+ "passed": len(all_mutations) - total_failures,
409
+ "failed": total_failures,
410
+ "sla_ms": self.sla_ms,
411
+ "dry_run": dry_run,
412
+ "results": results
413
+ }
414
+
415
+ # Print Summary Scorecard
416
+ print(f"\n{BOLD}======================================================================{RESET}")
417
+ print(f"{BOLD} API FUZZING ORACLE SCORECARD {RESET}")
418
+ print(f"{BOLD}======================================================================{RESET}")
419
+ print(f" Mutations Executed: {scorecard['total_mutations']}")
420
+ print(f" Clean Handled : {GREEN}{scorecard['passed']}{RESET}")
421
+ print(f" Defects Detected : {RED if total_failures > 0 else GREEN}{scorecard['failed']}{RESET}")
422
+ if total_failures == 0:
423
+ print(f" Verdict : {GREEN}[PASS] API resilient against input fuzzing & chaos.{RESET}")
424
+ else:
425
+ print(f" Verdict : {RED}[FAIL] {total_failures} defect alarms detected. Backend fixes required!{RESET}")
426
+ print(f"======================================================================\n")
427
+
428
+ # Save Report
429
+ os.makedirs(REPORTS_DIR, exist_ok=True)
430
+ report_path = os.path.join(REPORTS_DIR, "api_fuzz_report.json")
431
+ with open(report_path, "w", encoding="utf-8") as f:
432
+ json.dump(scorecard, f, indent=2)
433
+ print(f"Full telemetry report saved to: {report_path}")
434
+
435
+ return scorecard
436
+
437
+
438
+ def main():
439
+ parser = argparse.ArgumentParser(description="Enterprise Autonomous API Fuzzer & Chaos Breaker")
440
+ parser.add_argument("--base-url", default="http://localhost:8080", help="Base URL of target API server")
441
+ parser.add_argument("--spec", default=None, help="Path or URL to OpenAPI/Swagger JSON specification")
442
+ parser.add_argument("--token", default=None, help="Bearer token for authenticated endpoints")
443
+ parser.add_argument("--sla-ms", type=float, default=3000.0, help="SLA latency ceiling in ms (default: 3000ms)")
444
+ parser.add_argument("--dry-run", action="store_true", help="Simulate mutation generation without live requests")
445
+ parser.add_argument("--json", action="store_true", help="Output summary in JSON format")
446
+
447
+ args = parser.parse_args()
448
+
449
+ headers = {"Content-Type": "application/json", "User-Agent": "AgentQA-Fuzzer/2.0"}
450
+ if args.token:
451
+ headers["Authorization"] = f"Bearer {args.token}"
452
+
453
+ fuzzer = ApiFuzzer(
454
+ base_url=args.base_url,
455
+ spec_path_or_url=args.spec,
456
+ headers=headers,
457
+ sla_ms=args.sla_ms
458
+ )
459
+
460
+ scorecard = fuzzer.run_fuzz_suite(dry_run=args.dry_run)
461
+ if args.json:
462
+ print(json.dumps(scorecard, indent=2))
463
+
464
+ return 1 if scorecard["failed"] > 0 else 0
465
+
466
+
467
+ if __name__ == "__main__":
468
+ sys.exit(main())
@@ -13,7 +13,7 @@
13
13
  (function () {
14
14
  'use strict';
15
15
 
16
- if (window.__qa && window.__qa.__version === '2.0.0') {
16
+ if (window.__qa && window.__qa.__version === '2.1.0') {
17
17
  return window.__qa;
18
18
  }
19
19
 
@@ -47,7 +47,7 @@
47
47
  }
48
48
 
49
49
  const qa = {
50
- __version: '2.0.0',
50
+ __version: '2.1.0',
51
51
 
52
52
  /**
53
53
  * 1. Smart Action Batching