botversion-sdk 1.1.7__tar.gz → 2.0.0__tar.gz
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.
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/PKG-INFO +1 -1
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk/__init__.py +68 -81
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk/client.py +8 -3
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk/interceptor.py +245 -1
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk/scanner.py +1 -327
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk.egg-info/PKG-INFO +1 -1
- botversion_sdk-2.0.0/botversion_sdk.egg-info/SOURCES.txt +10 -0
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/pyproject.toml +1 -4
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/setup.py +1 -9
- botversion_sdk-1.1.7/botversion_sdk/cli/__init__.py +0 -0
- botversion_sdk-1.1.7/botversion_sdk/cli/detector.py +0 -1157
- botversion_sdk-1.1.7/botversion_sdk/cli/generator.py +0 -169
- botversion_sdk-1.1.7/botversion_sdk/cli/init.py +0 -673
- botversion_sdk-1.1.7/botversion_sdk/cli/prompts.py +0 -155
- botversion_sdk-1.1.7/botversion_sdk/cli/writer.py +0 -724
- botversion_sdk-1.1.7/botversion_sdk.egg-info/SOURCES.txt +0 -17
- botversion_sdk-1.1.7/botversion_sdk.egg-info/entry_points.txt +0 -2
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk.egg-info/dependency_links.txt +0 -0
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/botversion_sdk.egg-info/top_level.txt +0 -0
- {botversion_sdk-1.1.7 → botversion_sdk-2.0.0}/setup.cfg +0 -0
|
@@ -5,7 +5,7 @@ import builtins
|
|
|
5
5
|
import os
|
|
6
6
|
|
|
7
7
|
from .client import BotVersionClient
|
|
8
|
-
from .scanner import scan_routes
|
|
8
|
+
from .scanner import scan_routes
|
|
9
9
|
from .interceptor import (
|
|
10
10
|
attach_fastapi_interceptor,
|
|
11
11
|
attach_flask_interceptor,
|
|
@@ -20,12 +20,64 @@ from .interceptor import (
|
|
|
20
20
|
attach_cherrypy_interceptor,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
def _classify_installation(framework):
|
|
24
|
+
"""
|
|
25
|
+
Python/PHP SDKs only ever run on a backend server — there is no such
|
|
26
|
+
thing as a Python or PHP frontend project. So unlike the JS SDK, this
|
|
27
|
+
is a simple two-way classification: either a supported backend
|
|
28
|
+
framework was detected, or it wasn't.
|
|
29
|
+
"""
|
|
30
|
+
if framework:
|
|
31
|
+
return "backend-only"
|
|
32
|
+
return "unknown"
|
|
33
|
+
|
|
23
34
|
_initialized = False
|
|
24
35
|
_client = None
|
|
25
36
|
_options = {}
|
|
26
37
|
_app = None
|
|
27
38
|
|
|
28
39
|
|
|
40
|
+
def _run_full_scan(app, framework, cwd, client, debug=False):
|
|
41
|
+
"""
|
|
42
|
+
Runs a full scan (backend endpoints + frontend routes) and reports
|
|
43
|
+
results to the platform. This used to run automatically on server
|
|
44
|
+
start / first request. Now it only runs when the BotVersion dashboard's
|
|
45
|
+
"Scan" button sends a request to this SDK's scan-trigger endpoint
|
|
46
|
+
(see interceptor.py). This fixes scans not happening on redeploys
|
|
47
|
+
(e.g. Vercel/serverless) since there's no "restart" event to hook into.
|
|
48
|
+
"""
|
|
49
|
+
result = {
|
|
50
|
+
"endpoint_count": 0,
|
|
51
|
+
"route_count": 0,
|
|
52
|
+
"project_type": None,
|
|
53
|
+
"detected_backend": framework,
|
|
54
|
+
"detected_frontend": None,
|
|
55
|
+
"sdk_language": "python",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
endpoints = []
|
|
60
|
+
if app is not None:
|
|
61
|
+
endpoints = scan_routes(app, framework)
|
|
62
|
+
elif framework == "django":
|
|
63
|
+
endpoints = scan_routes(None, "django")
|
|
64
|
+
|
|
65
|
+
result["endpoint_count"] = len(endpoints)
|
|
66
|
+
|
|
67
|
+
if endpoints:
|
|
68
|
+
client.register_endpoints_now(endpoints)
|
|
69
|
+
|
|
70
|
+
except Exception:
|
|
71
|
+
if debug:
|
|
72
|
+
import traceback
|
|
73
|
+
traceback.print_exc()
|
|
74
|
+
|
|
75
|
+
result["detected_frontend"] = None
|
|
76
|
+
result["project_type"] = _classify_installation(framework)
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
29
81
|
def init(app=None, api_key=None, **options):
|
|
30
82
|
"""
|
|
31
83
|
Initialize the BotVersion SDK.
|
|
@@ -52,10 +104,16 @@ def init(app=None, api_key=None, **options):
|
|
|
52
104
|
# Re-attach interceptor after hot reload
|
|
53
105
|
framework = _detect_framework(app)
|
|
54
106
|
if framework and _client:
|
|
107
|
+
_cwd = _options.get("cwd", os.getcwd())
|
|
108
|
+
_debug = _options.get("debug", False)
|
|
55
109
|
interceptor_options = {
|
|
56
110
|
"exclude": _options.get("exclude", []),
|
|
57
111
|
"api_prefix": _options.get("api_prefix", None),
|
|
58
|
-
"debug":
|
|
112
|
+
"debug": _debug,
|
|
113
|
+
"scan_secret": _options.get("api_key"),
|
|
114
|
+
"on_scan_requested": lambda: _run_full_scan(
|
|
115
|
+
app, framework, _cwd, _client, _debug
|
|
116
|
+
),
|
|
59
117
|
}
|
|
60
118
|
if framework == "fastapi":
|
|
61
119
|
attach_fastapi_interceptor(app, _client, interceptor_options)
|
|
@@ -94,6 +152,8 @@ def init(app=None, api_key=None, **options):
|
|
|
94
152
|
|
|
95
153
|
if not framework:
|
|
96
154
|
_initialized = False
|
|
155
|
+
if debug:
|
|
156
|
+
print("[botversion] No supported framework detected. SDK not initialized.")
|
|
97
157
|
return
|
|
98
158
|
|
|
99
159
|
_client = BotVersionClient({
|
|
@@ -108,10 +168,16 @@ def init(app=None, api_key=None, **options):
|
|
|
108
168
|
builtins._botversion_client = _client
|
|
109
169
|
builtins._botversion_options = _options
|
|
110
170
|
|
|
171
|
+
cwd = options.get("cwd", os.getcwd())
|
|
172
|
+
|
|
111
173
|
interceptor_options = {
|
|
112
174
|
"exclude": options.get("exclude", []),
|
|
113
175
|
"api_prefix": options.get("api_prefix", None),
|
|
114
176
|
"debug": debug,
|
|
177
|
+
"scan_secret": api_key,
|
|
178
|
+
"on_scan_requested": lambda: _run_full_scan(
|
|
179
|
+
app, framework, cwd, _client, debug
|
|
180
|
+
),
|
|
115
181
|
}
|
|
116
182
|
|
|
117
183
|
# ── Attach runtime interceptor ───────────────────────────────────────────
|
|
@@ -141,85 +207,6 @@ def init(app=None, api_key=None, **options):
|
|
|
141
207
|
else:
|
|
142
208
|
return
|
|
143
209
|
|
|
144
|
-
# ── Static scan (delayed 500ms — let app finish registering routes) ──────
|
|
145
|
-
def _run_scan():
|
|
146
|
-
try:
|
|
147
|
-
endpoints = []
|
|
148
|
-
|
|
149
|
-
if app is not None:
|
|
150
|
-
endpoints = scan_routes(app, framework)
|
|
151
|
-
|
|
152
|
-
elif framework == "django":
|
|
153
|
-
endpoints = scan_routes(None, "django")
|
|
154
|
-
|
|
155
|
-
# For any other framework with no app object, skip backend scan
|
|
156
|
-
# but still continue to frontend scan below
|
|
157
|
-
|
|
158
|
-
if endpoints:
|
|
159
|
-
_client.register_endpoints_now(endpoints)
|
|
160
|
-
|
|
161
|
-
except Exception as e:
|
|
162
|
-
if debug:
|
|
163
|
-
import traceback
|
|
164
|
-
traceback.print_exc()
|
|
165
|
-
|
|
166
|
-
cwd = options.get("cwd", os.getcwd())
|
|
167
|
-
route_patterns = scan_frontend_routes(cwd)
|
|
168
|
-
if route_patterns:
|
|
169
|
-
_client.register_route_patterns(route_patterns)
|
|
170
|
-
|
|
171
|
-
if framework == "flask":
|
|
172
|
-
with app.app_context():
|
|
173
|
-
t = threading.Thread(target=_run_scan, daemon=False)
|
|
174
|
-
t.start()
|
|
175
|
-
t.join(timeout=15)
|
|
176
|
-
app._botversion_scanned = True
|
|
177
|
-
|
|
178
|
-
@app.after_request
|
|
179
|
-
def _botversion_first_scan(response):
|
|
180
|
-
if not getattr(app, '_botversion_scanned', False):
|
|
181
|
-
app._botversion_scanned = True
|
|
182
|
-
t = threading.Thread(target=_run_scan, daemon=False)
|
|
183
|
-
t.start()
|
|
184
|
-
t.join(timeout=15)
|
|
185
|
-
return response
|
|
186
|
-
|
|
187
|
-
elif framework == "fastapi":
|
|
188
|
-
@app.on_event("startup")
|
|
189
|
-
async def _botversion_startup_scan():
|
|
190
|
-
if not getattr(app, '_botversion_scanned', False):
|
|
191
|
-
app._botversion_scanned = True
|
|
192
|
-
t = threading.Thread(target=_run_scan, daemon=False)
|
|
193
|
-
t.start()
|
|
194
|
-
t.join(timeout=15)
|
|
195
|
-
|
|
196
|
-
@app.middleware("http")
|
|
197
|
-
async def _botversion_first_scan(request, call_next):
|
|
198
|
-
if not getattr(app, '_botversion_scanned', False):
|
|
199
|
-
app._botversion_scanned = True
|
|
200
|
-
t = threading.Thread(target=_run_scan, daemon=False)
|
|
201
|
-
t.start()
|
|
202
|
-
t.join(timeout=15)
|
|
203
|
-
return await call_next(request)
|
|
204
|
-
|
|
205
|
-
elif framework == "django":
|
|
206
|
-
try:
|
|
207
|
-
from django.apps import apps
|
|
208
|
-
if apps.ready:
|
|
209
|
-
t = threading.Thread(target=_run_scan, daemon=False)
|
|
210
|
-
t.start()
|
|
211
|
-
t.join(timeout=15)
|
|
212
|
-
except Exception:
|
|
213
|
-
t = threading.Timer(3.0, _run_scan)
|
|
214
|
-
t.daemon = False
|
|
215
|
-
t.start()
|
|
216
|
-
|
|
217
|
-
elif framework in ("starlette", "sanic", "falcon", "bottle",
|
|
218
|
-
"aiohttp", "tornado", "pyramid", "cherrypy"):
|
|
219
|
-
t = threading.Timer(3.0, _run_scan)
|
|
220
|
-
t.daemon = False
|
|
221
|
-
t.start()
|
|
222
|
-
|
|
223
210
|
|
|
224
211
|
def get_endpoints():
|
|
225
212
|
"""Get all registered endpoints for this workspace."""
|
|
@@ -89,13 +89,14 @@ class BotVersionClient:
|
|
|
89
89
|
|
|
90
90
|
# ── Register frontend route patterns ─────────────────────────────────────────
|
|
91
91
|
|
|
92
|
-
def register_route_patterns(self, patterns):
|
|
93
|
-
if not patterns:
|
|
92
|
+
def register_route_patterns(self, patterns, classification=None):
|
|
93
|
+
if not patterns and not classification:
|
|
94
94
|
return
|
|
95
95
|
try:
|
|
96
96
|
self._post("/api/sdk/register-route-patterns", {
|
|
97
97
|
"workspaceKey": self.api_key,
|
|
98
|
-
"patterns": patterns,
|
|
98
|
+
"patterns": patterns or [],
|
|
99
|
+
"classification": classification,
|
|
99
100
|
})
|
|
100
101
|
except Exception as e:
|
|
101
102
|
if self.debug:
|
|
@@ -121,6 +122,7 @@ class BotVersionClient:
|
|
|
121
122
|
headers={
|
|
122
123
|
"Content-Type": "application/json",
|
|
123
124
|
"Content-Length": str(len(body)),
|
|
125
|
+
"X-BotVersion-SDK": "1.0.0-python",
|
|
124
126
|
},
|
|
125
127
|
)
|
|
126
128
|
|
|
@@ -150,6 +152,9 @@ class BotVersionClient:
|
|
|
150
152
|
req = urllib.request.Request(
|
|
151
153
|
url,
|
|
152
154
|
method="GET",
|
|
155
|
+
headers={
|
|
156
|
+
"X-BotVersion-SDK": "1.0.0-python",
|
|
157
|
+
},
|
|
153
158
|
)
|
|
154
159
|
|
|
155
160
|
try:
|
|
@@ -212,11 +212,34 @@ def attach_fastapi_interceptor(app, client, options):
|
|
|
212
212
|
from starlette.requests import Request
|
|
213
213
|
import json as _json
|
|
214
214
|
|
|
215
|
+
from starlette.responses import JSONResponse
|
|
216
|
+
|
|
215
217
|
class BotVersionMiddleware(BaseHTTPMiddleware):
|
|
216
218
|
async def dispatch(self, request: Request, call_next):
|
|
217
219
|
path = request.url.path
|
|
218
220
|
method = request.method.upper()
|
|
219
221
|
|
|
222
|
+
# ── Scan trigger from BotVersion dashboard ───────────────
|
|
223
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
224
|
+
provided_key = request.headers.get("x-botversion-scan-key", "")
|
|
225
|
+
if (
|
|
226
|
+
options.get("scan_secret")
|
|
227
|
+
and provided_key == options["scan_secret"]
|
|
228
|
+
and callable(options.get("on_scan_requested"))
|
|
229
|
+
):
|
|
230
|
+
try:
|
|
231
|
+
result = options["on_scan_requested"]()
|
|
232
|
+
return JSONResponse(
|
|
233
|
+
{"success": True, "result": result}, status_code=200
|
|
234
|
+
)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return JSONResponse(
|
|
237
|
+
{"success": False, "error": str(e)}, status_code=500
|
|
238
|
+
)
|
|
239
|
+
return JSONResponse(
|
|
240
|
+
{"success": False, "error": "Unauthorized"}, status_code=401
|
|
241
|
+
)
|
|
242
|
+
|
|
220
243
|
response = await call_next(request)
|
|
221
244
|
|
|
222
245
|
if not should_ignore(path, options.get("exclude")):
|
|
@@ -259,6 +282,21 @@ def attach_flask_interceptor(app, client, options):
|
|
|
259
282
|
path = flask_request.path
|
|
260
283
|
method = flask_request.method.upper()
|
|
261
284
|
|
|
285
|
+
# ── Scan trigger from BotVersion dashboard ───────────────────
|
|
286
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
287
|
+
provided_key = flask_request.headers.get("x-botversion-scan-key", "")
|
|
288
|
+
if (
|
|
289
|
+
options.get("scan_secret")
|
|
290
|
+
and provided_key == options["scan_secret"]
|
|
291
|
+
and callable(options.get("on_scan_requested"))
|
|
292
|
+
):
|
|
293
|
+
try:
|
|
294
|
+
result = options["on_scan_requested"]()
|
|
295
|
+
return {"success": True, "result": result}, 200
|
|
296
|
+
except Exception as e:
|
|
297
|
+
return {"success": False, "error": str(e)}, 500
|
|
298
|
+
return {"success": False, "error": "Unauthorized"}, 401
|
|
299
|
+
|
|
262
300
|
if should_ignore(path, options.get("exclude")):
|
|
263
301
|
return
|
|
264
302
|
if options.get("api_prefix") and not path.startswith(options["api_prefix"]):
|
|
@@ -296,6 +334,23 @@ class BotVersionDjangoMiddleware:
|
|
|
296
334
|
def __call__(self, request):
|
|
297
335
|
path = request.path
|
|
298
336
|
method = request.method.upper()
|
|
337
|
+
options = self.__class__._options
|
|
338
|
+
|
|
339
|
+
# ── Scan trigger from BotVersion dashboard ───────────────────────
|
|
340
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
341
|
+
provided_key = request.META.get("HTTP_X_BOTVERSION_SCAN_KEY", "")
|
|
342
|
+
from django.http import JsonResponse
|
|
343
|
+
if (
|
|
344
|
+
options.get("scan_secret")
|
|
345
|
+
and provided_key == options["scan_secret"]
|
|
346
|
+
and callable(options.get("on_scan_requested"))
|
|
347
|
+
):
|
|
348
|
+
try:
|
|
349
|
+
result = options["on_scan_requested"]()
|
|
350
|
+
return JsonResponse({"success": True, "result": result}, status=200)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
return JsonResponse({"success": False, "error": str(e)}, status=500)
|
|
353
|
+
return JsonResponse({"success": False, "error": "Unauthorized"}, status=401)
|
|
299
354
|
|
|
300
355
|
if not should_ignore(path, self.__class__._options.get("exclude")):
|
|
301
356
|
if not self.__class__._options.get("api_prefix") or path.startswith(self.__class__._options["api_prefix"]):
|
|
@@ -353,11 +408,34 @@ def attach_starlette_interceptor(app, client, options):
|
|
|
353
408
|
from starlette.requests import Request
|
|
354
409
|
import json as _json
|
|
355
410
|
|
|
411
|
+
from starlette.responses import JSONResponse
|
|
412
|
+
|
|
356
413
|
class BotVersionStarletteMiddleware(BaseHTTPMiddleware):
|
|
357
414
|
async def dispatch(self, request: Request, call_next):
|
|
358
415
|
path = request.url.path
|
|
359
416
|
method = request.method.upper()
|
|
360
417
|
|
|
418
|
+
# ── Scan trigger from BotVersion dashboard ───────────────
|
|
419
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
420
|
+
provided_key = request.headers.get("x-botversion-scan-key", "")
|
|
421
|
+
if (
|
|
422
|
+
options.get("scan_secret")
|
|
423
|
+
and provided_key == options["scan_secret"]
|
|
424
|
+
and callable(options.get("on_scan_requested"))
|
|
425
|
+
):
|
|
426
|
+
try:
|
|
427
|
+
result = options["on_scan_requested"]()
|
|
428
|
+
return JSONResponse(
|
|
429
|
+
{"success": True, "result": result}, status_code=200
|
|
430
|
+
)
|
|
431
|
+
except Exception as e:
|
|
432
|
+
return JSONResponse(
|
|
433
|
+
{"success": False, "error": str(e)}, status_code=500
|
|
434
|
+
)
|
|
435
|
+
return JSONResponse(
|
|
436
|
+
{"success": False, "error": "Unauthorized"}, status_code=401
|
|
437
|
+
)
|
|
438
|
+
|
|
361
439
|
response = await call_next(request)
|
|
362
440
|
|
|
363
441
|
if not should_ignore(path, options.get("exclude")):
|
|
@@ -395,6 +473,22 @@ def attach_sanic_interceptor(app, client, options):
|
|
|
395
473
|
path = request.path
|
|
396
474
|
method = request.method.upper()
|
|
397
475
|
|
|
476
|
+
# ── Scan trigger from BotVersion dashboard ───────────────────
|
|
477
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
478
|
+
from sanic.response import json as sanic_json
|
|
479
|
+
provided_key = request.headers.get("x-botversion-scan-key", "")
|
|
480
|
+
if (
|
|
481
|
+
options.get("scan_secret")
|
|
482
|
+
and provided_key == options["scan_secret"]
|
|
483
|
+
and callable(options.get("on_scan_requested"))
|
|
484
|
+
):
|
|
485
|
+
try:
|
|
486
|
+
result = options["on_scan_requested"]()
|
|
487
|
+
return sanic_json({"success": True, "result": result}, status=200)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
return sanic_json({"success": False, "error": str(e)}, status=500)
|
|
490
|
+
return sanic_json({"success": False, "error": "Unauthorized"}, status=401)
|
|
491
|
+
|
|
398
492
|
if should_ignore(path, options.get("exclude")):
|
|
399
493
|
return
|
|
400
494
|
if options.get("api_prefix") and not path.startswith(options["api_prefix"]):
|
|
@@ -428,6 +522,27 @@ def attach_falcon_interceptor(app, client, options):
|
|
|
428
522
|
path = req.path
|
|
429
523
|
method = req.method.upper()
|
|
430
524
|
|
|
525
|
+
# ── Scan trigger from BotVersion dashboard ───────────────
|
|
526
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
527
|
+
provided_key = req.get_header("x-botversion-scan-key") or ""
|
|
528
|
+
if (
|
|
529
|
+
options.get("scan_secret")
|
|
530
|
+
and provided_key == options["scan_secret"]
|
|
531
|
+
and callable(options.get("on_scan_requested"))
|
|
532
|
+
):
|
|
533
|
+
try:
|
|
534
|
+
result = options["on_scan_requested"]()
|
|
535
|
+
resp.media = {"success": True, "result": result}
|
|
536
|
+
resp.status = 200
|
|
537
|
+
except Exception as e:
|
|
538
|
+
resp.media = {"success": False, "error": str(e)}
|
|
539
|
+
resp.status = 500
|
|
540
|
+
else:
|
|
541
|
+
resp.media = {"success": False, "error": "Unauthorized"}
|
|
542
|
+
resp.status = 401
|
|
543
|
+
resp.complete = True
|
|
544
|
+
return
|
|
545
|
+
|
|
431
546
|
if should_ignore(path, options.get("exclude")):
|
|
432
547
|
return
|
|
433
548
|
if options.get("api_prefix") and not path.startswith(options["api_prefix"]):
|
|
@@ -460,12 +575,41 @@ def attach_falcon_interceptor(app, client, options):
|
|
|
460
575
|
|
|
461
576
|
def attach_bottle_interceptor(app, client, options):
|
|
462
577
|
try:
|
|
463
|
-
from bottle import request as bottle_request
|
|
578
|
+
from bottle import request as bottle_request, HTTPResponse
|
|
464
579
|
|
|
465
580
|
def botversion_bottle_interceptor():
|
|
466
581
|
path = bottle_request.path
|
|
467
582
|
method = bottle_request.method.upper()
|
|
468
583
|
|
|
584
|
+
# ── Scan trigger from BotVersion dashboard ────────────────────
|
|
585
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
586
|
+
provided_key = bottle_request.headers.get("x-botversion-scan-key", "")
|
|
587
|
+
if (
|
|
588
|
+
options.get("scan_secret")
|
|
589
|
+
and provided_key == options["scan_secret"]
|
|
590
|
+
and callable(options.get("on_scan_requested"))
|
|
591
|
+
):
|
|
592
|
+
try:
|
|
593
|
+
result = options["on_scan_requested"]()
|
|
594
|
+
raise HTTPResponse(
|
|
595
|
+
body=json.dumps({"success": True, "result": result}),
|
|
596
|
+
status=200,
|
|
597
|
+
headers={"Content-Type": "application/json"},
|
|
598
|
+
)
|
|
599
|
+
except HTTPResponse:
|
|
600
|
+
raise
|
|
601
|
+
except Exception as e:
|
|
602
|
+
raise HTTPResponse(
|
|
603
|
+
body=json.dumps({"success": False, "error": str(e)}),
|
|
604
|
+
status=500,
|
|
605
|
+
headers={"Content-Type": "application/json"},
|
|
606
|
+
)
|
|
607
|
+
raise HTTPResponse(
|
|
608
|
+
body=json.dumps({"success": False, "error": "Unauthorized"}),
|
|
609
|
+
status=401,
|
|
610
|
+
headers={"Content-Type": "application/json"},
|
|
611
|
+
)
|
|
612
|
+
|
|
469
613
|
if should_ignore(path, options.get("exclude")):
|
|
470
614
|
return
|
|
471
615
|
if options.get("api_prefix") and not path.startswith(options["api_prefix"]):
|
|
@@ -503,6 +647,28 @@ def attach_aiohttp_interceptor(app, client, options):
|
|
|
503
647
|
path = request.path
|
|
504
648
|
method = request.method.upper()
|
|
505
649
|
|
|
650
|
+
# ── Scan trigger from BotVersion dashboard ───────────────────
|
|
651
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
652
|
+
from aiohttp import web
|
|
653
|
+
provided_key = request.headers.get("x-botversion-scan-key", "")
|
|
654
|
+
if (
|
|
655
|
+
options.get("scan_secret")
|
|
656
|
+
and provided_key == options["scan_secret"]
|
|
657
|
+
and callable(options.get("on_scan_requested"))
|
|
658
|
+
):
|
|
659
|
+
try:
|
|
660
|
+
result = options["on_scan_requested"]()
|
|
661
|
+
return web.json_response(
|
|
662
|
+
{"success": True, "result": result}, status=200
|
|
663
|
+
)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
return web.json_response(
|
|
666
|
+
{"success": False, "error": str(e)}, status=500
|
|
667
|
+
)
|
|
668
|
+
return web.json_response(
|
|
669
|
+
{"success": False, "error": "Unauthorized"}, status=401
|
|
670
|
+
)
|
|
671
|
+
|
|
506
672
|
response = await handler(request)
|
|
507
673
|
|
|
508
674
|
if not should_ignore(path, options.get("exclude")):
|
|
@@ -548,6 +714,41 @@ def attach_tornado_interceptor(app, client, options):
|
|
|
548
714
|
import tornado.web
|
|
549
715
|
import json as _json
|
|
550
716
|
|
|
717
|
+
original_prepare = tornado.web.RequestHandler.prepare
|
|
718
|
+
|
|
719
|
+
def patched_prepare(self):
|
|
720
|
+
try:
|
|
721
|
+
path = self.request.path
|
|
722
|
+
method = self.request.method.upper()
|
|
723
|
+
|
|
724
|
+
# ── Scan trigger from BotVersion dashboard ───────────────
|
|
725
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
726
|
+
provided_key = self.request.headers.get("x-botversion-scan-key", "")
|
|
727
|
+
if (
|
|
728
|
+
options.get("scan_secret")
|
|
729
|
+
and provided_key == options["scan_secret"]
|
|
730
|
+
and callable(options.get("on_scan_requested"))
|
|
731
|
+
):
|
|
732
|
+
try:
|
|
733
|
+
result = options["on_scan_requested"]()
|
|
734
|
+
self.set_status(200)
|
|
735
|
+
self.set_header("Content-Type", "application/json")
|
|
736
|
+
self.finish(_json.dumps({"success": True, "result": result}))
|
|
737
|
+
except Exception as e:
|
|
738
|
+
self.set_status(500)
|
|
739
|
+
self.set_header("Content-Type", "application/json")
|
|
740
|
+
self.finish(_json.dumps({"success": False, "error": str(e)}))
|
|
741
|
+
else:
|
|
742
|
+
self.set_status(401)
|
|
743
|
+
self.set_header("Content-Type", "application/json")
|
|
744
|
+
self.finish(_json.dumps({"success": False, "error": "Unauthorized"}))
|
|
745
|
+
return
|
|
746
|
+
except Exception:
|
|
747
|
+
pass
|
|
748
|
+
return original_prepare(self)
|
|
749
|
+
|
|
750
|
+
tornado.web.RequestHandler.prepare = patched_prepare
|
|
751
|
+
|
|
551
752
|
original_finish = tornado.web.RequestHandler.finish
|
|
552
753
|
|
|
553
754
|
def patched_finish(self, chunk=None):
|
|
@@ -592,6 +793,25 @@ _pyramid_options = {}
|
|
|
592
793
|
|
|
593
794
|
def botversion_pyramid_tween_factory(handler, registry):
|
|
594
795
|
def botversion_tween(request):
|
|
796
|
+
path = request.path
|
|
797
|
+
method = request.method.upper()
|
|
798
|
+
|
|
799
|
+
# ── Scan trigger from BotVersion dashboard ────────────────────────
|
|
800
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
801
|
+
from pyramid.response import Response
|
|
802
|
+
provided_key = request.headers.get("x-botversion-scan-key", "")
|
|
803
|
+
if (
|
|
804
|
+
_pyramid_options.get("scan_secret")
|
|
805
|
+
and provided_key == _pyramid_options["scan_secret"]
|
|
806
|
+
and callable(_pyramid_options.get("on_scan_requested"))
|
|
807
|
+
):
|
|
808
|
+
try:
|
|
809
|
+
result = _pyramid_options["on_scan_requested"]()
|
|
810
|
+
return Response(json_body={"success": True, "result": result}, status=200)
|
|
811
|
+
except Exception as e:
|
|
812
|
+
return Response(json_body={"success": False, "error": str(e)}, status=500)
|
|
813
|
+
return Response(json_body={"success": False, "error": "Unauthorized"}, status=401)
|
|
814
|
+
|
|
595
815
|
response = handler(request)
|
|
596
816
|
try:
|
|
597
817
|
import json as _json
|
|
@@ -645,6 +865,30 @@ def attach_cherrypy_interceptor(app, client, options):
|
|
|
645
865
|
path = request.path_info
|
|
646
866
|
method = request.method.upper()
|
|
647
867
|
|
|
868
|
+
# ── Scan trigger from BotVersion dashboard ────────────────────
|
|
869
|
+
if path == "/__botversion/scan" and method == "POST":
|
|
870
|
+
provided_key = request.headers.get("x-botversion-scan-key", "")
|
|
871
|
+
if (
|
|
872
|
+
options.get("scan_secret")
|
|
873
|
+
and provided_key == options["scan_secret"]
|
|
874
|
+
and callable(options.get("on_scan_requested"))
|
|
875
|
+
):
|
|
876
|
+
try:
|
|
877
|
+
result = options["on_scan_requested"]()
|
|
878
|
+
cherrypy.response.status = 200
|
|
879
|
+
body = _json.dumps({"success": True, "result": result})
|
|
880
|
+
except Exception as e:
|
|
881
|
+
cherrypy.response.status = 500
|
|
882
|
+
body = _json.dumps({"success": False, "error": str(e)})
|
|
883
|
+
else:
|
|
884
|
+
cherrypy.response.status = 401
|
|
885
|
+
body = _json.dumps({"success": False, "error": "Unauthorized"})
|
|
886
|
+
|
|
887
|
+
cherrypy.response.headers["Content-Type"] = "application/json"
|
|
888
|
+
cherrypy.response.body = body.encode("utf-8")
|
|
889
|
+
cherrypy.request.handler = None
|
|
890
|
+
return
|
|
891
|
+
|
|
648
892
|
if should_ignore(path, options.get("exclude")):
|
|
649
893
|
return
|
|
650
894
|
if options.get("api_prefix") and not path.startswith(options["api_prefix"]):
|