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.
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/lessons-learned.md +1 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +8 -5
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +13 -0
- package/README.md +172 -29
- package/package.json +9 -5
- package/skills_manifest.json +260 -260
- package/tools/TOOLS_RUNBOOK.md +54 -3
- package/tools/api_fuzzer.py +468 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +211 -0
- package/tools/cache_resolver.py +128 -0
- package/tools/cli.py +41 -1
- package/tools/export_postman_collection.py +606 -0
- package/tools/flaky_detector.py +180 -14
- package/tools/generate_jmx_plan.py +486 -0
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +56 -0
- package/tools/network_mock_shield.py +141 -0
- package/tools/repo_map.json +1 -1
- package/tools/repo_map.txt +1 -1
- package/tools/visual_som_helper.py +128 -9
|
@@ -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())
|
package/tools/browser_helpers.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
(function () {
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
if (window.__qa && window.__qa.__version === '2.
|
|
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.
|
|
50
|
+
__version: '2.1.0',
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* 1. Smart Action Batching
|