synapse-hub 2.0.1 → 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/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 +4 -4
- package/package.json +6 -2
- 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 +121 -9
package/tools/TOOLS_RUNBOOK.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
| **Comprehensive Health & AST Analysis ("analyze project")** | `tools/analyze_project.py` | `python tools/analyze_project.py [target]` (or `agent-qa analyze`).<br/>*Post-Audit:* Stages, commits, and pushes `PROJECT_HEALTH_REPORT.md` & `repo_map.txt` to project Git. |
|
|
14
14
|
| **Action Safety Firewall (Environment Protection)** | `tools/safety_firewall.py` | `python tools/safety_firewall.py --text "Delete" --action click` (or `agent-qa safety-check`) |
|
|
15
15
|
| **API Contract Drift (Breaking Change Detection)** | `tools/api_drift_sentinel.py` | `python tools/api_drift_sentinel.py --old base.json --new curr.json` (or `agent-qa contract-drift`) |
|
|
16
|
+
| **Autonomous API Fuzzing & Chaos Breaker** | `tools/api_fuzzer.py` | `agent-qa fuzz-api --base-url <url> --spec <swagger.json>` (Catches 500, DB leaks, SLA breach) |
|
|
16
17
|
| **FastMCP JSON-RPC Server (Cursor/Claude/Antigravity)** | `tools/mcp_server.py` | `python tools/mcp_server.py` (or `agent-qa mcp-serve`) |
|
|
17
18
|
| **AST Micro-Mutation (Assertion Quality Verification)** | `tools/micro_mutator.py` | `python tools/micro_mutator.py [target]` (or `agent-qa mutate`) |
|
|
18
19
|
| **Self-Healing PR Automator (Trace & Visual Diff Proof)** | `tools/heal_pr_automator.py` | `python tools/heal_pr_automator.py --page P.groovy --old "//a" --new "//b" --trace-zip trace.zip` (or `agent-qa heal-pr`) |
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
| **After healing a test failure** | `tools/cache_resolver.py` | `python tools/cache_resolver.py record-lesson --context "<page>" --error "<sig>" --lesson "<summary>"` |
|
|
34
35
|
| **Generating test scripts from Natural Language Story** | `tools/generate_tc_from_story.py`| `python tools/generate_tc_from_story.py "<story>"` |
|
|
35
36
|
| **Generating E2E matrix from OpenAPI/Swagger** | `tools/generate_api_matrix.py` | `python tools/generate_api_matrix.py --spec <swagger.json>` |
|
|
37
|
+
| **Exporting API tests to Postman Collection v2.1** | `tools/export_postman_collection.py` | `agent-qa export-postman --spec <swagger.json> -o col.json` |
|
|
38
|
+
| **Generating JMeter Performance Test Plan (.jmx)** | `tools/generate_jmx_plan.py` | `agent-qa generate-jmx --spec <swagger.json> --vus 100 -o plan.jmx` |
|
|
36
39
|
| **Auditing Web Accessibility (WCAG 2.2)** | `tools/wcag_audit.py` | `python tools/wcag_audit.py <path_to_html_or_url>` |
|
|
37
40
|
| **0-Token Polyglot Test Transpilation** | `tools/omni_transpiler.py` | `agent-qa transpile <file> --to <target>` (Playwright TS, Python, Java, Katalon, Cypress) |
|
|
38
41
|
| **0-Token Repository Polyglot AST Scan** | `tools/omni_transpiler.py` | `agent-qa omni-scan [path]` (Instant inventory of Pages, Locators, Tests across languages) |
|
|
@@ -233,14 +236,30 @@
|
|
|
233
236
|
---
|
|
234
237
|
|
|
235
238
|
### 15. `tools/visual_som_helper.py`
|
|
236
|
-
- **Purpose:** Multimodal Vision Grounding and Set-of-Marks (SoM) fallback engine. When standard DOM locators fail on HTML5 Canvas, WebGL, or deeply nested Shadow DOM, it overlays numbered bounding box badges on screenshots, resolves target coordinates `(x, y)`, and dispatches physical Playwright mouse clicks (`await page.mouse.click(x, y)`).
|
|
239
|
+
- **Purpose:** Multimodal Vision Grounding and Set-of-Marks (SoM) fallback engine. When standard DOM locators fail on HTML5 Canvas, GIS maps, WebGL charts, or deeply nested Shadow DOM, it autonomously detects interactive visual contours using PIL edge clustering, overlays numbered bounding box badges on screenshots, resolves target coordinates `(x, y)`, caches coordinates via 64-bit dHash in SQLite, and dispatches physical Playwright mouse clicks (`await page.mouse.click(x, y)`).
|
|
237
240
|
- **CLI Syntax:**
|
|
238
241
|
```bash
|
|
242
|
+
# Autonomous Canvas contour detection without DOM
|
|
243
|
+
python tools/visual_som_helper.py --image canvas_map.png --detect-only
|
|
244
|
+
|
|
239
245
|
# Generate Set-of-Marks overlay and resolve target coordinates
|
|
240
246
|
python tools/visual_som_helper.py --image screenshot.png --label "Execute Trade"
|
|
241
247
|
|
|
242
|
-
#
|
|
243
|
-
python tools/visual_som_helper.py --image screenshot.png --
|
|
248
|
+
# Zero-Token Visual Cache lookup (Similarity > 98%)
|
|
249
|
+
python tools/visual_som_helper.py --image screenshot.png --component "TradeButton" --cache-lookup
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### 16. `tools/api_fuzzer.py`
|
|
255
|
+
- **Purpose:** Autonomous runtime API fuzzer and chaos breaker. Ingests OpenAPI 3.x / Swagger 2.0 schemas, generates dynamic boundary, type-inverted, and adversarial fuzzing payloads from `tools/fuzz_payloads.py`, executes live HTTP/S requests against staging/test APIs, and evaluates three-alarm defect Oracle: HTTP 500 crashes, database stacktrace leaks, and SLA latency timeouts (> 3000ms).
|
|
256
|
+
- **CLI Syntax:**
|
|
257
|
+
```bash
|
|
258
|
+
# Execute active fuzzing against staging API
|
|
259
|
+
agent-qa fuzz-api --base-url "https://staging.api.enterprise.com" --spec swagger.json --token "JWT_SECRET"
|
|
260
|
+
|
|
261
|
+
# Air-gapped / deterministic dry-run mode for CI/CD
|
|
262
|
+
agent-qa fuzz-api --dry-run
|
|
244
263
|
```
|
|
245
264
|
|
|
246
265
|
---
|
|
@@ -296,3 +315,35 @@
|
|
|
296
315
|
```bash
|
|
297
316
|
agent-qa ci-hook --file LoginPage.ts --old-loc "//button[@id='old']" --new-loc "//button[@id='new']"
|
|
298
317
|
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### 21. `tools/export_postman_collection.py`
|
|
322
|
+
- **Purpose:** Enterprise OpenAPI/Swagger & HAR to Postman Collection v2.1.0 Exporter. Generates fully structured Postman collections with dynamic pre-request scripts, SLA latency tests, response code assertions, and autonomous bearer token / created entity ID extraction for Newman and CI/CD pipelines.
|
|
323
|
+
- **CLI Syntax:**
|
|
324
|
+
```bash
|
|
325
|
+
# Ingest OpenAPI spec and export Postman collection + companion environment
|
|
326
|
+
agent-qa export-postman --spec swagger.json -o collection.json --env-output env.json
|
|
327
|
+
|
|
328
|
+
# Replay production HAR traffic as Postman requests
|
|
329
|
+
agent-qa export-postman --har session.har -o har_collection.json
|
|
330
|
+
|
|
331
|
+
# Generate sample collection from built-in enterprise schema
|
|
332
|
+
agent-qa export-postman --sample -o sample_collection.json
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### 22. `tools/generate_jmx_plan.py`
|
|
338
|
+
- **Purpose:** Apache JMeter 5.x Performance Test Plan XML (.jmx) Generator. Ingests OpenAPI specifications, Postman Collections, or HAR recordings to output production-ready JMeter load, stress, and spike test plans with configurable VUs, ramp-up schedules, SLA assertions (< 500ms), and dynamic session token propagation.
|
|
339
|
+
- **CLI Syntax:**
|
|
340
|
+
```bash
|
|
341
|
+
# Generate JMeter plan from OpenAPI with 100 Virtual Users for 10 minutes
|
|
342
|
+
agent-qa generate-jmx --spec swagger.json -o load_plan.jmx --vus 100 --duration 600
|
|
343
|
+
|
|
344
|
+
# Convert existing Postman collection into JMeter performance test plan
|
|
345
|
+
agent-qa generate-jmx --postman collection.json -o perf_plan.jmx --vus 50 --ramp-up 20
|
|
346
|
+
|
|
347
|
+
# Run headless JMeter CLI
|
|
348
|
+
jmeter -n -t load_plan.jmx -l results.jtl -e -o ./html_report
|
|
349
|
+
```
|
|
@@ -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.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
|
|
50
|
+
__version: '2.1.0',
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* 1. Smart Action Batching
|