clue-api 1.0.1.dev53__tar.gz → 1.0.1.dev58__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.
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/PKG-INFO +1 -1
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/__init__.py +319 -43
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/interactive.py +6 -3
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/pyproject.toml +3 -21
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/LICENSE +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/README.md +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/.gitignore +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/base.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/actions.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/auth.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/configs.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/registration.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/api/v1/static.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/app.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/cache/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/classification.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/classification.yml +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/dict_utils.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/exceptions.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/forge.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/json_utils.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/list_utils.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/logging/audit.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/logging/format.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/regex.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/str_utils.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/swagger.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/common/uid.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/config.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/constants/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/constants/supported_types.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/error.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/extensions/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/extensions/config.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/gunicorn_config.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/healthz.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/helper/discover.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/helper/headers.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/helper/oauth.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/actions.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/config.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/fetchers.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/graph.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/model_list.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/network.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/results/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/results/base.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/results/graph.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/results/image.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/results/status.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/results/validation.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/selector.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/models/validators.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/patched.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/models.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/plugin/utils.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/py.typed +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/security/__init__.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/security/obo.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/security/utils.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/action_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/auth_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/config_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/jwt_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/lookup_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/type_service.py +0 -0
- {clue_api-1.0.1.dev53 → clue_api-1.0.1.dev58}/clue/services/user_service.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import ipaddress
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
@@ -8,6 +9,7 @@ from urllib import parse as ul
|
|
|
8
9
|
|
|
9
10
|
import gevent
|
|
10
11
|
import gevent.pool
|
|
12
|
+
from dotenv import load_dotenv
|
|
11
13
|
from flask import Flask, Response, jsonify, make_response, request
|
|
12
14
|
from flask.globals import _cv_request
|
|
13
15
|
from gevent import Greenlet
|
|
@@ -17,6 +19,7 @@ from clue.cache import Cache
|
|
|
17
19
|
from clue.common.exceptions import (
|
|
18
20
|
AuthenticationException,
|
|
19
21
|
ClueException,
|
|
22
|
+
ClueValueError,
|
|
20
23
|
InvalidDataException,
|
|
21
24
|
NotFoundException,
|
|
22
25
|
TimeoutException,
|
|
@@ -37,11 +40,40 @@ from clue.plugin.helpers.token import get_username
|
|
|
37
40
|
from clue.plugin.models import BulkEntry
|
|
38
41
|
from clue.plugin.utils import Params
|
|
39
42
|
|
|
43
|
+
# Load environment variables from .env file if present
|
|
44
|
+
load_dotenv()
|
|
45
|
+
|
|
46
|
+
# List of function names that can be overridden using the @plugin.use decorator
|
|
47
|
+
# These functions define the core plugin behavior and can be customized per plugin
|
|
48
|
+
OVERRIDABLE_FUNCTIONS = [
|
|
49
|
+
"enrich", # Main enrichment function for processing selectors
|
|
50
|
+
"alternate_bulk_lookup", # Alternative bulk enrichment implementation
|
|
51
|
+
"liveness", # Kubernetes liveness probe endpoint
|
|
52
|
+
"readyness", # Kubernetes readiness probe endpoint
|
|
53
|
+
"run_action", # Function to execute plugin actions
|
|
54
|
+
"run_fetcher", # Function to execute plugin fetchers
|
|
55
|
+
"setup_actions", # Runtime action definition generation
|
|
56
|
+
"validate_token", # Custom authentication token validation
|
|
57
|
+
]
|
|
58
|
+
|
|
40
59
|
|
|
41
60
|
def default_validate_token():
|
|
42
|
-
"""A default validation function that
|
|
61
|
+
"""A default validation function that extracts Bearer tokens from the Authorization header.
|
|
62
|
+
|
|
63
|
+
This function is provided as a reference implementation but is not used by default.
|
|
64
|
+
Plugin developers can use this as a starting point for their own token validation.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
tuple[str | None, str | None]: A tuple containing (token, error_message).
|
|
68
|
+
- If successful: (extracted_token, None)
|
|
69
|
+
- If failed: (None, error_description)
|
|
70
|
+
|
|
71
|
+
Note:
|
|
72
|
+
Expects Authorization header format: "Bearer <token>"
|
|
73
|
+
"""
|
|
43
74
|
token = request.headers.get("Authorization", None, type=str)
|
|
44
75
|
if token and " " in token:
|
|
76
|
+
# Split "Bearer <token>" and extract the token part
|
|
45
77
|
token = token.split()[1]
|
|
46
78
|
|
|
47
79
|
if token:
|
|
@@ -51,21 +83,46 @@ def default_validate_token():
|
|
|
51
83
|
|
|
52
84
|
|
|
53
85
|
def liveness(**_):
|
|
54
|
-
"Default liveness probe
|
|
86
|
+
"""Default liveness probe for Kubernetes health checks.
|
|
87
|
+
|
|
88
|
+
This endpoint indicates whether the application is running and alive.
|
|
89
|
+
Returns a simple "OK" response with 200 status code.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Response: Flask response with "OK" message
|
|
93
|
+
"""
|
|
55
94
|
return make_response("OK")
|
|
56
95
|
|
|
57
96
|
|
|
58
97
|
def readyness(**_):
|
|
59
|
-
"Default
|
|
98
|
+
"""Default readiness probe for Kubernetes health checks.
|
|
99
|
+
|
|
100
|
+
This endpoint indicates whether the application is ready to serve traffic.
|
|
101
|
+
Returns a simple "OK" response with 200 status code.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Response: Flask response with "OK" message
|
|
105
|
+
"""
|
|
60
106
|
return make_response("OK")
|
|
61
107
|
|
|
62
108
|
|
|
63
109
|
def build_default_logger() -> logging.Logger:
|
|
64
|
-
"Configure a default logger
|
|
110
|
+
"""Configure a default logger with standard Clue formatting when none is provided.
|
|
111
|
+
|
|
112
|
+
Creates a logger with INFO level that outputs to console using the standard
|
|
113
|
+
Clue log format and date format for consistency across all plugins.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
logging.Logger: Configured logger instance ready for use
|
|
117
|
+
|
|
118
|
+
Note:
|
|
119
|
+
Uses logger name "clue.plugin.default" to distinguish from user-provided loggers
|
|
120
|
+
"""
|
|
65
121
|
logger = logging.getLogger("clue.plugin.default")
|
|
66
122
|
logger.setLevel(logging.INFO)
|
|
67
123
|
console = logging.StreamHandler()
|
|
68
124
|
console.setLevel(logging.INFO)
|
|
125
|
+
# Apply standard Clue log formatting for consistency
|
|
69
126
|
console.setFormatter(logging.Formatter(CLUE_LOG_FORMAT, CLUE_DATE_FORMAT))
|
|
70
127
|
logger.addHandler(console)
|
|
71
128
|
|
|
@@ -223,7 +280,7 @@ class CluePlugin:
|
|
|
223
280
|
actions: list[Action] = [],
|
|
224
281
|
alternate_bulk_lookup: Callable[[list[dict[str, str]], Params], dict[str, dict[str, BulkEntry]]] | None = None,
|
|
225
282
|
cache_timeout: int = 5 * 60, # five minute timeout
|
|
226
|
-
classification: str = os.environ.get("CLASSIFICATION",
|
|
283
|
+
classification: str | None = os.environ.get("CLASSIFICATION", None),
|
|
227
284
|
enable_apm: bool = False,
|
|
228
285
|
enable_cache: Union[bool, Literal["redis"], Literal["local"]] = True,
|
|
229
286
|
enrich: Callable[[str, str, Params, str | None], Union[list[QueryEntry], QueryEntry]] | None = None,
|
|
@@ -235,7 +292,7 @@ class CluePlugin:
|
|
|
235
292
|
run_action: Callable[[Action, ExecuteRequest, str | None], ActionResult] | None = None,
|
|
236
293
|
run_fetcher: Callable[[FetcherDefinition, Selector, str | None], FetcherResult] | None = None,
|
|
237
294
|
setup_actions: Callable[[list[Action], str | None], list[Action]] | None = None,
|
|
238
|
-
supported_types: set[str] | None = None,
|
|
295
|
+
supported_types: set[str] | str | None = None,
|
|
239
296
|
validate_token: Callable[[], tuple[str | None, str | None]] | None = None,
|
|
240
297
|
) -> None:
|
|
241
298
|
"""Helper class for creating clue plugins with proper server responses and behaviour.
|
|
@@ -300,16 +357,31 @@ class CluePlugin:
|
|
|
300
357
|
A readyness probe for kubernetes implementations of Clue.
|
|
301
358
|
"""
|
|
302
359
|
self.alternate_bulk_lookup = alternate_bulk_lookup
|
|
360
|
+
# Create Flask app using the module name (before first dot) as app name
|
|
303
361
|
self.app = Flask(__name__.split(".")[0])
|
|
304
362
|
self.app_name = app_name
|
|
363
|
+
|
|
364
|
+
# Classification is required for security - must be specified via env var or parameter
|
|
365
|
+
if classification is None:
|
|
366
|
+
raise ClueValueError(
|
|
367
|
+
"Classification must be specified, either via the CLASSIFICATION environment variable, or when "
|
|
368
|
+
"intializing the plugin."
|
|
369
|
+
)
|
|
370
|
+
|
|
305
371
|
self.classification = classification
|
|
306
372
|
self.liveness = liveness
|
|
307
373
|
self.readyness = readyness
|
|
308
|
-
|
|
374
|
+
|
|
375
|
+
# Convert comma-separated string to set for easier membership testing
|
|
376
|
+
if isinstance(supported_types, str):
|
|
377
|
+
self.supported_types = set(supported_types.split(","))
|
|
378
|
+
else:
|
|
379
|
+
self.supported_types = supported_types
|
|
309
380
|
|
|
310
381
|
self.actions = actions
|
|
311
382
|
self.setup_actions = setup_actions
|
|
312
383
|
|
|
384
|
+
# Allow URLs with or without trailing slashes to match the same route
|
|
313
385
|
self.app.url_map.strict_slashes = False
|
|
314
386
|
|
|
315
387
|
self.logger = logger if logger else build_default_logger()
|
|
@@ -323,20 +395,25 @@ class CluePlugin:
|
|
|
323
395
|
|
|
324
396
|
self.__init_routes()
|
|
325
397
|
|
|
398
|
+
# Initialize Application Performance Monitoring if enabled
|
|
326
399
|
if enable_apm:
|
|
327
400
|
self.__init_apm()
|
|
328
401
|
|
|
402
|
+
# Set up caching based on configuration
|
|
329
403
|
if enable_cache:
|
|
330
|
-
#
|
|
404
|
+
# Support both boolean (use default cache type) and explicit cache type specification
|
|
331
405
|
if isinstance(enable_cache, bool):
|
|
406
|
+
# Use environment variable or default to redis
|
|
407
|
+
cache_type = cast(Union[Literal["redis"], Literal["local"]], os.environ.get("CACHE_TYPE", "redis"))
|
|
332
408
|
self.cache = Cache(
|
|
333
409
|
self.app_name,
|
|
334
410
|
self.app,
|
|
335
|
-
|
|
411
|
+
cache_type,
|
|
336
412
|
timeout=cache_timeout,
|
|
337
413
|
local_cache_options=local_cache_options,
|
|
338
414
|
)
|
|
339
415
|
else:
|
|
416
|
+
# Use explicitly specified cache type
|
|
340
417
|
self.cache = Cache(
|
|
341
418
|
self.app_name,
|
|
342
419
|
self.app,
|
|
@@ -347,69 +424,128 @@ class CluePlugin:
|
|
|
347
424
|
else:
|
|
348
425
|
self.cache = None
|
|
349
426
|
|
|
427
|
+
# Configure werkzeug (Flask's WSGI server) logging to reduce noise
|
|
428
|
+
# Set to WARNING level to suppress INFO messages about HTTP requests
|
|
350
429
|
wlog = logging.getLogger("werkzeug")
|
|
351
430
|
wlog.setLevel(logging.WARNING)
|
|
431
|
+
# If our logger has a parent, inherit its handlers for consistency
|
|
352
432
|
if self.logger.parent: # pragma: no cover
|
|
353
433
|
for h in self.logger.parent.handlers:
|
|
354
434
|
wlog.addHandler(h)
|
|
355
435
|
|
|
436
|
+
# Automatically inject the Flask "app" variable into the calling module's global namespace
|
|
437
|
+
# for compatibility with WSGI servers like gunicorn.
|
|
438
|
+
#
|
|
439
|
+
# This mechanism allows plugin developers to simply instantiate a CluePlugin without
|
|
440
|
+
# needing to explicitly expose the underlying Flask app. WSGI servers typically expect
|
|
441
|
+
# to find an 'app' variable in the module's global scope when using module:variable
|
|
442
|
+
# syntax (e.g., "mymodule:app").
|
|
443
|
+
#
|
|
444
|
+
# Example usage in a plugin module:
|
|
445
|
+
# plugin = CluePlugin("my-plugin", ...)
|
|
446
|
+
# # The 'app' variable is now automatically available for gunicorn
|
|
447
|
+
# # Command: gunicorn mymodule:app
|
|
448
|
+
current_frame = inspect.currentframe()
|
|
449
|
+
if current_frame:
|
|
450
|
+
caller_frame = current_frame.f_back
|
|
451
|
+
if caller_frame and "app" not in caller_frame.f_globals:
|
|
452
|
+
caller_frame.f_globals["app"] = self.app
|
|
453
|
+
|
|
356
454
|
self.logger.debug("Initialization complete!")
|
|
357
455
|
|
|
358
456
|
def __check_actions(self) -> list[Action] | None:
|
|
457
|
+
"""Validate token and retrieve dynamic actions if setup_actions is configured.
|
|
458
|
+
|
|
459
|
+
This method handles token validation when required and calls the setup_actions
|
|
460
|
+
function to get a potentially user-specific or dynamically generated list of actions.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
list[Action] | None: List of actions if setup_actions is configured, None otherwise
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
AuthenticationException: If token validation fails
|
|
467
|
+
"""
|
|
359
468
|
if self.setup_actions:
|
|
469
|
+
# Validate token if token validation is configured
|
|
360
470
|
if self.validate_token:
|
|
361
471
|
token, error = self.validate_token()
|
|
362
472
|
|
|
363
473
|
if error:
|
|
364
474
|
self.logger.error("Error on token validation: %s", error)
|
|
365
|
-
|
|
366
475
|
raise AuthenticationException(error)
|
|
367
476
|
else:
|
|
368
477
|
token = None
|
|
369
478
|
|
|
479
|
+
# Call user-defined setup_actions with base actions and validated token
|
|
370
480
|
return self.setup_actions(self.actions or [], token)
|
|
371
481
|
|
|
372
482
|
return None
|
|
373
483
|
|
|
374
484
|
def __init_apm(self):
|
|
375
|
-
"
|
|
376
|
-
|
|
485
|
+
"""Initialize Application Performance Monitoring (APM) using Elastic APM.
|
|
486
|
+
|
|
487
|
+
Sets up ElasticAPM integration with Flask if APM_SERVER_URL environment
|
|
488
|
+
variable is configured. This enables automatic collection of performance
|
|
489
|
+
metrics, error tracking, and distributed tracing.
|
|
377
490
|
|
|
491
|
+
Environment Variables:
|
|
492
|
+
APM_SERVER_URL: URL of the Elastic APM server to send metrics to
|
|
493
|
+
"""
|
|
494
|
+
# Check if APM server URL is configured via environment variable
|
|
378
495
|
apm_server_url = os.environ.get("APM_SERVER_URL")
|
|
379
496
|
if apm_server_url is None:
|
|
380
497
|
return
|
|
381
498
|
|
|
382
|
-
self.logger.debug("Initializing
|
|
499
|
+
self.logger.debug("Initializing APM")
|
|
383
500
|
|
|
501
|
+
# Import ElasticAPM components (lazy import to avoid dependency issues)
|
|
384
502
|
import elasticapm
|
|
385
503
|
from elasticapm.contrib.flask import ElasticAPM
|
|
386
504
|
|
|
387
505
|
self.logger.info(f"Exporting application metrics to: {apm_server_url}")
|
|
388
506
|
|
|
507
|
+
# Initialize ElasticAPM with Flask app and configure client
|
|
389
508
|
ElasticAPM(self.app, client=elasticapm.Client(server_url=apm_server_url, service_name=self.app_name))
|
|
390
509
|
|
|
391
510
|
def __build_ctx(self):
|
|
392
|
-
"
|
|
393
|
-
|
|
511
|
+
"""Create a context wrapper function for preserving Flask request context in greenlets.
|
|
512
|
+
|
|
513
|
+
Flask request context is thread-local and doesn't automatically propagate to
|
|
514
|
+
greenlets. This function captures the current request context and returns a
|
|
515
|
+
wrapper that pushes it into each greenlet before execution.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Callable: A wrapper function that preserves Flask context and handles exceptions
|
|
519
|
+
"""
|
|
520
|
+
# Capture the current Flask request context to propagate to greenlets
|
|
394
521
|
current_req_ctx = _cv_request.get(None)
|
|
395
522
|
reqctx = current_req_ctx.copy() if current_req_ctx else None
|
|
396
523
|
|
|
397
|
-
# Push the request context into the greenlet
|
|
398
524
|
def wrap_ctx(func: Callable, *args: Any, **kwargs) -> tuple[Any, Exception | None]:
|
|
525
|
+
"""Wrapper that pushes Flask context and handles enrichment function execution.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
func: The enrichment function to execute
|
|
529
|
+
*args: Arguments to pass to the function
|
|
530
|
+
**kwargs: Keyword arguments to pass to the function
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
tuple[Any, Exception | None]: (result, exception) tuple
|
|
534
|
+
"""
|
|
535
|
+
# Push the request context into this greenlet's scope
|
|
399
536
|
if reqctx:
|
|
400
537
|
reqctx.push()
|
|
401
538
|
|
|
402
539
|
try:
|
|
403
540
|
self.logger.debug("Executing enrichment function")
|
|
404
|
-
|
|
405
541
|
return func(*args, **kwargs), None
|
|
406
542
|
except NotFoundException:
|
|
543
|
+
# NotFoundException means no results found - return empty list, not an error
|
|
407
544
|
self.logger.warning("NotFoundException thrown in greenlet")
|
|
408
|
-
|
|
409
545
|
return [], None
|
|
410
546
|
except ClueException as e:
|
|
547
|
+
# Other Clue exceptions should be propagated as errors
|
|
411
548
|
self.logger.exception("ClueException thrown in greenlet")
|
|
412
|
-
|
|
413
549
|
return None, e
|
|
414
550
|
|
|
415
551
|
return wrap_ctx
|
|
@@ -421,58 +557,80 @@ class CluePlugin:
|
|
|
421
557
|
params: Params,
|
|
422
558
|
token: str | None,
|
|
423
559
|
):
|
|
424
|
-
"Default bulk lookup
|
|
560
|
+
"""Default bulk lookup implementation using greenlets for concurrent enrichment.
|
|
561
|
+
|
|
562
|
+
This method processes multiple enrichment requests concurrently by spawning
|
|
563
|
+
greenlets (lightweight threads) for each item. It uses the single-item enrich
|
|
564
|
+
function to process each request while maintaining Flask request context. Note
|
|
565
|
+
that this may lead to inefficient lookups (e.g. making ten requests to a database,
|
|
566
|
+
instead of a single bulk query)
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
bulk_result: Dictionary to populate with results, keyed by type then value
|
|
570
|
+
items: List of items to enrich, each containing 'type' and 'value' keys
|
|
571
|
+
params: Request parameters including timeouts and limits
|
|
572
|
+
token: Authentication token to pass to enrichment functions
|
|
573
|
+
"""
|
|
425
574
|
self.logger.debug("Using default bulk lookup")
|
|
426
575
|
|
|
427
|
-
#
|
|
576
|
+
# Create context wrapper to preserve Flask request context in greenlets
|
|
428
577
|
wrap_ctx = self.__build_ctx()
|
|
578
|
+
# Limit pool size to prevent resource exhaustion: min(items, cpu_count * 5 + 4)
|
|
429
579
|
thread_pool = gevent.pool.Pool(min(len(items), (os.cpu_count() or 0) * 5 + 4))
|
|
430
580
|
greenlets: list[tuple[str, str, Greenlet]] = []
|
|
431
581
|
|
|
582
|
+
# Spawn a greenlet for each enrichment request
|
|
432
583
|
for entry in items:
|
|
433
|
-
#
|
|
584
|
+
# Store type, value, and greenlet for later result processing
|
|
434
585
|
greenlets.append(
|
|
435
586
|
(
|
|
436
587
|
entry["type"],
|
|
437
588
|
entry["value"],
|
|
438
589
|
thread_pool.spawn(
|
|
439
|
-
wrap_ctx,
|
|
440
|
-
self.enrich,
|
|
441
|
-
entry["type"],
|
|
442
|
-
entry["value"],
|
|
443
|
-
params,
|
|
444
|
-
token,
|
|
590
|
+
wrap_ctx, # Context wrapper function
|
|
591
|
+
self.enrich, # User's enrichment function
|
|
592
|
+
entry["type"], # Selector type
|
|
593
|
+
entry["value"], # Selector value
|
|
594
|
+
params, # Request parameters
|
|
595
|
+
token, # Authentication token
|
|
445
596
|
),
|
|
446
597
|
)
|
|
447
598
|
)
|
|
448
599
|
|
|
600
|
+
# Calculate remaining time until deadline
|
|
449
601
|
timeout = params.deadline + params.max_timeout - time.time()
|
|
450
602
|
self.logger.debug("Joining threadpool (timeout=%s)", timeout)
|
|
451
603
|
|
|
604
|
+
# Wait for all greenlets to complete or timeout
|
|
452
605
|
thread_pool.join(timeout=timeout)
|
|
453
606
|
|
|
607
|
+
# Process results from all completed greenlets
|
|
454
608
|
for type_name, value, greenlet in greenlets:
|
|
455
609
|
greenlet_result = greenlet.value
|
|
456
610
|
|
|
611
|
+
# Check if greenlet completed successfully with results
|
|
457
612
|
if greenlet_result is not None and greenlet_result[0] is not None:
|
|
458
613
|
results: Union[list[QueryEntry], QueryEntry] = greenlet_result[0]
|
|
614
|
+
# Ensure results is always a list for consistent handling
|
|
459
615
|
if not isinstance(results, list):
|
|
460
616
|
results = [results]
|
|
461
617
|
|
|
462
618
|
bulk_result[type_name][value] = BulkEntry(items=results)
|
|
463
619
|
|
|
620
|
+
# Cache successful results if caching is enabled
|
|
464
621
|
if self.cache:
|
|
465
622
|
self.logger.info("Caching results for selector %s:%s", type_name, value)
|
|
466
|
-
|
|
467
623
|
try:
|
|
468
624
|
self.cache.set(type_name, value, params, results)
|
|
469
625
|
except KeyError:
|
|
470
626
|
self.logger.warning("Selector not present in bulk result, skipping cache step")
|
|
471
627
|
else:
|
|
628
|
+
# Handle errors: timeout, exceptions, or other failures
|
|
472
629
|
error = "Request Timed Out"
|
|
473
630
|
if greenlet_result is not None and greenlet_result[1] is not None:
|
|
474
631
|
error = str(greenlet_result[1])
|
|
475
632
|
|
|
633
|
+
# Use greenlet exception if available, otherwise use our error message
|
|
476
634
|
bulk_result[type_name][value] = BulkEntry(
|
|
477
635
|
error=(error if not greenlet.exception else str(greenlet.exception))
|
|
478
636
|
)
|
|
@@ -483,7 +641,19 @@ class CluePlugin:
|
|
|
483
641
|
)
|
|
484
642
|
|
|
485
643
|
def __init_routes(self):
|
|
486
|
-
"Set up
|
|
644
|
+
"""Set up all Flask routes for the plugin API endpoints.
|
|
645
|
+
|
|
646
|
+
Registers the following endpoints:
|
|
647
|
+
- GET /actions/: List available actions
|
|
648
|
+
- POST /actions/<action_id>/: Execute a specific action
|
|
649
|
+
- GET /fetchers/: List available fetchers
|
|
650
|
+
- POST /fetchers/<fetcher_id>: Execute a specific fetcher
|
|
651
|
+
- GET /types/: List supported types
|
|
652
|
+
- GET /lookup/<type_name>/<value>/: Single enrichment lookup
|
|
653
|
+
- POST /lookup/: Bulk enrichment lookup
|
|
654
|
+
- GET /healthz/live: Liveness probe
|
|
655
|
+
- GET /healthz/ready: Readiness probe
|
|
656
|
+
"""
|
|
487
657
|
self.logger.debug("Initializing routes")
|
|
488
658
|
|
|
489
659
|
self.app.add_url_rule("/actions/", self.get_actions.__name__, self.get_actions, methods=["GET"])
|
|
@@ -501,16 +671,38 @@ class CluePlugin:
|
|
|
501
671
|
self.app.add_url_rule("/healthz/ready", self.readyness.__name__, self.readyness)
|
|
502
672
|
|
|
503
673
|
def make_api_response(self: Self, data: Any, err: str = "", status_code: int = 200) -> Response:
|
|
504
|
-
"Create a
|
|
674
|
+
"""Create a standardized JSON response for all API endpoints.
|
|
675
|
+
|
|
676
|
+
This method ensures consistent response format across all plugin endpoints,
|
|
677
|
+
handles automatic error extraction from result objects, and logs all requests.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
data: The response data (will be JSON serialized)
|
|
681
|
+
err: Error message (if any)
|
|
682
|
+
status_code: HTTP status code (default: 200)
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
Response: Flask response with standardized JSON structure
|
|
686
|
+
|
|
687
|
+
Response Format:
|
|
688
|
+
{
|
|
689
|
+
"api_response": <data>,
|
|
690
|
+
"api_error_message": <error_string>,
|
|
691
|
+
"api_status_code": <status_code>
|
|
692
|
+
}
|
|
693
|
+
"""
|
|
694
|
+
# Extract error messages from specialized result objects
|
|
505
695
|
if isinstance(data, FetcherResult) and data.outcome == "failure" and not err:
|
|
506
696
|
err = data.error or err
|
|
507
697
|
|
|
508
698
|
if isinstance(data, ActionResult) and data.outcome == "failure" and not err:
|
|
509
699
|
err = data.summary or err
|
|
510
700
|
|
|
701
|
+
# Convert Pydantic models to dict for JSON serialization
|
|
511
702
|
if isinstance(data, BaseModel):
|
|
512
703
|
data = data.model_dump(mode="json", exclude_none=True)
|
|
513
704
|
|
|
705
|
+
# Log all API requests with method, path, status, and error (if any)
|
|
514
706
|
self.logger.info("%s %s - %s%s", request.method, request.path, status_code, f": {err}" if err else "")
|
|
515
707
|
|
|
516
708
|
return make_response(
|
|
@@ -525,7 +717,18 @@ class CluePlugin:
|
|
|
525
717
|
)
|
|
526
718
|
|
|
527
719
|
def get_type_names(self: Self) -> Response:
|
|
528
|
-
"Return supported
|
|
720
|
+
"""Return the list of supported selector types with their classifications.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Response: JSON response mapping each supported type to its classification level
|
|
724
|
+
|
|
725
|
+
Response Format:
|
|
726
|
+
{
|
|
727
|
+
"type1": "classification_level",
|
|
728
|
+
"type2": "classification_level",
|
|
729
|
+
...
|
|
730
|
+
}
|
|
731
|
+
"""
|
|
529
732
|
return self.make_api_response({tname: self.classification for tname in sorted(self.supported_types or [])})
|
|
530
733
|
|
|
531
734
|
def lookup(self: Self, type_name: str, value: str) -> Response: # noqa: C901
|
|
@@ -558,6 +761,7 @@ class CluePlugin:
|
|
|
558
761
|
if not self.enrich or not self.supported_types:
|
|
559
762
|
return self.make_api_response({}, err="Enrichment is not supported by this plugin.", status_code=400)
|
|
560
763
|
|
|
764
|
+
# Normalize generic "ip" type to specific "ipv4" or "ipv6" based on address format
|
|
561
765
|
if type_name == "ip":
|
|
562
766
|
is_ipv4 = isinstance(ipaddress.ip_address(value), ipaddress.IPv4Address)
|
|
563
767
|
type_name = "ipv4" if is_ipv4 else "ipv6"
|
|
@@ -569,8 +773,9 @@ class CluePlugin:
|
|
|
569
773
|
|
|
570
774
|
return self.make_api_response(None, str(e), 504)
|
|
571
775
|
|
|
776
|
+
# Double URL decode the value (required by API specification)
|
|
572
777
|
value = ul.unquote(ul.unquote(value))
|
|
573
|
-
#
|
|
778
|
+
# Validate that the requested type is supported by this plugin
|
|
574
779
|
if type_name not in self.supported_types:
|
|
575
780
|
return self.make_api_response(
|
|
576
781
|
None,
|
|
@@ -766,6 +971,7 @@ class CluePlugin:
|
|
|
766
971
|
else:
|
|
767
972
|
self.__default_bulk_lookup(bulk_result, remaining_items, params, token)
|
|
768
973
|
|
|
974
|
+
# Calculate how close we came to the deadline (positive = time remaining, negative = overrun)
|
|
769
975
|
variance = params.deadline - time.time()
|
|
770
976
|
|
|
771
977
|
if self.logger:
|
|
@@ -809,11 +1015,14 @@ class CluePlugin:
|
|
|
809
1015
|
|
|
810
1016
|
results: dict[str, dict[str, Any]] = {}
|
|
811
1017
|
for action in actions:
|
|
1018
|
+
# Extract base action fields (id, name, description, etc.)
|
|
812
1019
|
schema = action.model_dump(mode="json", include=set(ActionBase.model_fields.keys()), exclude_none=True)
|
|
1020
|
+
# Generate JSON schema for the action's parameter type
|
|
813
1021
|
schema["params"] = cast(
|
|
814
1022
|
BaseModel, cast(type[Any], action.model_fields["params"].annotation).__args__[0]
|
|
815
1023
|
).model_json_schema()
|
|
816
1024
|
|
|
1025
|
+
# Convert to ActionSpec format and add to results
|
|
817
1026
|
results[action.id] = ActionSpec.model_validate(schema).model_dump(mode="json", exclude_none=True)
|
|
818
1027
|
|
|
819
1028
|
return self.make_api_response(results)
|
|
@@ -857,6 +1066,7 @@ class CluePlugin:
|
|
|
857
1066
|
else:
|
|
858
1067
|
self.logger.warning("No token validation provided. The access token will not be provided to the action.")
|
|
859
1068
|
|
|
1069
|
+
# Extract the parameter type from the action definition for validation
|
|
860
1070
|
param_type: Any = action_to_run.model_fields["params"].annotation or Any
|
|
861
1071
|
|
|
862
1072
|
try:
|
|
@@ -866,6 +1076,7 @@ class CluePlugin:
|
|
|
866
1076
|
|
|
867
1077
|
return self.make_api_response(ActionResult(outcome="failure", summary="No request body specified."))
|
|
868
1078
|
|
|
1079
|
+
# Validate request body against the action's parameter schema
|
|
869
1080
|
action_request: ExecuteRequest = TypeAdapter(param_type.__args__[0]).validate_python(
|
|
870
1081
|
raw_request, context={"action": action_to_run}
|
|
871
1082
|
)
|
|
@@ -904,26 +1115,34 @@ class CluePlugin:
|
|
|
904
1115
|
return self.make_api_response(result)
|
|
905
1116
|
|
|
906
1117
|
def get_fetchers(self: Self) -> Response:
|
|
907
|
-
"""
|
|
1118
|
+
"""Get all available fetchers for this plugin.
|
|
908
1119
|
|
|
909
|
-
|
|
910
|
-
|
|
1120
|
+
Returns a dictionary of fetcher definitions, each containing the fetcher's
|
|
1121
|
+
schema including supported types, output format, and other metadata.
|
|
911
1122
|
|
|
912
1123
|
Returns:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1124
|
+
Response: JSON response containing fetcher definitions
|
|
1125
|
+
|
|
1126
|
+
Response Format:
|
|
1127
|
+
{
|
|
1128
|
+
"fetcher1": {
|
|
1129
|
+
"id": "fetcher1",
|
|
1130
|
+
"name": "Fetcher Name",
|
|
1131
|
+
"description": "Description",
|
|
1132
|
+
"supported_types": ["type1", "type2"],
|
|
1133
|
+
"output_format": "format",
|
|
1134
|
+
...
|
|
1135
|
+
},
|
|
1136
|
+
...
|
|
1137
|
+
}
|
|
919
1138
|
"""
|
|
920
1139
|
if not self.fetchers:
|
|
921
1140
|
self.logger.debug("No fetchers to show")
|
|
922
|
-
|
|
923
1141
|
return self.make_api_response({})
|
|
924
1142
|
|
|
925
1143
|
results: dict[str, dict[str, Any]] = {}
|
|
926
1144
|
for fetcher in self.fetchers:
|
|
1145
|
+
# Serialize fetcher definition to JSON-compatible dict
|
|
927
1146
|
schema = fetcher.model_dump(mode="json", exclude_none=True)
|
|
928
1147
|
results[fetcher.id] = schema
|
|
929
1148
|
|
|
@@ -941,6 +1160,7 @@ class CluePlugin:
|
|
|
941
1160
|
if not self.run_fetcher or not self.fetchers:
|
|
942
1161
|
return self.make_api_response({}, err=f"{self.app_name} does not support any fetchers.", status_code=400)
|
|
943
1162
|
|
|
1163
|
+
# Find the requested fetcher by ID
|
|
944
1164
|
fetcher_to_run = next((fetcher for fetcher in self.fetchers if fetcher.id == fetcher_id), None)
|
|
945
1165
|
if not fetcher_to_run:
|
|
946
1166
|
return self.make_api_response({}, err=f"Fetcher {fetcher_id} does not exist", status_code=404)
|
|
@@ -964,6 +1184,7 @@ class CluePlugin:
|
|
|
964
1184
|
status_code=400,
|
|
965
1185
|
)
|
|
966
1186
|
|
|
1187
|
+
# Validate request body as a Selector object
|
|
967
1188
|
raw_request = Selector.model_validate(request.json)
|
|
968
1189
|
|
|
969
1190
|
self.logger.info("Running fetcher '%s'", fetcher_id)
|
|
@@ -1006,3 +1227,58 @@ class CluePlugin:
|
|
|
1006
1227
|
self.logger.info("Error Message: %s", result.error)
|
|
1007
1228
|
|
|
1008
1229
|
return self.make_api_response(result, status_code=status_code)
|
|
1230
|
+
|
|
1231
|
+
def use(self, func: Callable):
|
|
1232
|
+
"""Register a function to be used by the CluePlugin for specific operations.
|
|
1233
|
+
|
|
1234
|
+
This decorator allows you to register functions that will be called during various
|
|
1235
|
+
plugin operations. The function name must match one of the supported overridable
|
|
1236
|
+
functions defined in OVERRIDABLE_FUNCTIONS.
|
|
1237
|
+
|
|
1238
|
+
Supported function names and their purposes:
|
|
1239
|
+
- enrich: Main enrichment function for processing selectors
|
|
1240
|
+
- alternate_bulk_lookup: Alternative bulk enrichment implementation
|
|
1241
|
+
- liveness: Kubernetes liveness probe endpoint
|
|
1242
|
+
- readyness: Kubernetes readiness probe endpoint
|
|
1243
|
+
- run_action: Function to execute plugin actions
|
|
1244
|
+
- run_fetcher: Function to execute plugin fetchers
|
|
1245
|
+
- setup_actions: Runtime action definition generation
|
|
1246
|
+
- validate_token: Custom authentication token validation
|
|
1247
|
+
|
|
1248
|
+
Args:
|
|
1249
|
+
func: The function to register. The function name determines which plugin
|
|
1250
|
+
operation it will be used for.
|
|
1251
|
+
|
|
1252
|
+
Returns:
|
|
1253
|
+
The original function (allows use as a decorator).
|
|
1254
|
+
|
|
1255
|
+
Example:
|
|
1256
|
+
```python
|
|
1257
|
+
plugin = CluePlugin("my_plugin")
|
|
1258
|
+
|
|
1259
|
+
@plugin.use
|
|
1260
|
+
def enrich(type_name: str, value: str, params: Params, token: str | None):
|
|
1261
|
+
# Your enrichment logic here
|
|
1262
|
+
return QueryEntry(...)
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
Note:
|
|
1266
|
+
If a function with the same name is already registered, a warning will be logged
|
|
1267
|
+
and the new function will replace the existing one.
|
|
1268
|
+
"""
|
|
1269
|
+
function_name = func.__name__
|
|
1270
|
+
if function_name not in OVERRIDABLE_FUNCTIONS:
|
|
1271
|
+
self.logger.error(
|
|
1272
|
+
"%s is not a valid function to use in a clue plugin. Supported list: %s",
|
|
1273
|
+
function_name,
|
|
1274
|
+
", ".join(OVERRIDABLE_FUNCTIONS),
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
# Warn if overwriting an existing function
|
|
1278
|
+
if getattr(self, function_name) is not None:
|
|
1279
|
+
self.logger.warning("plugin.uses decorator is overwriting existing function: %s", function_name)
|
|
1280
|
+
|
|
1281
|
+
# Dynamically set the function as an attribute of this plugin instance
|
|
1282
|
+
setattr(self, function_name, func)
|
|
1283
|
+
|
|
1284
|
+
return func
|
|
@@ -13,6 +13,9 @@ from termcolor import colored
|
|
|
13
13
|
|
|
14
14
|
from clue.plugin import CluePlugin
|
|
15
15
|
|
|
16
|
+
PLUGINS_PATH = Path(__file__).parent.parent.parent.parent / "plugins"
|
|
17
|
+
sys.path.insert(0, str(PLUGINS_PATH))
|
|
18
|
+
|
|
16
19
|
TESTABLE_FUNCTIONS = [
|
|
17
20
|
("get_actions", None),
|
|
18
21
|
("execute_action", "run_action"),
|
|
@@ -50,7 +53,7 @@ class CustomTestClient(FlaskClient):
|
|
|
50
53
|
"Custom test client to inject authorization headers"
|
|
51
54
|
|
|
52
55
|
def open(self, *args, buffered=False, follow_redirects=False, **kwargs):
|
|
53
|
-
"Overriden open
|
|
56
|
+
"Overriden open function to inject auth header"
|
|
54
57
|
headers = kwargs.setdefault("headers", {})
|
|
55
58
|
|
|
56
59
|
if "CLUE_ACCESS_TOKEN" in os.environ:
|
|
@@ -164,7 +167,7 @@ def main(): # noqa: C901
|
|
|
164
167
|
if len(sys.argv) > 1:
|
|
165
168
|
plugin_name = sys.argv[1]
|
|
166
169
|
|
|
167
|
-
if not (
|
|
170
|
+
if not (PLUGINS_PATH / plugin_name).exists():
|
|
168
171
|
error(f"Plugin {plugin_name} does not exist.")
|
|
169
172
|
plugin_name = None
|
|
170
173
|
|
|
@@ -175,7 +178,7 @@ def main(): # noqa: C901
|
|
|
175
178
|
plugin_name = None
|
|
176
179
|
|
|
177
180
|
try:
|
|
178
|
-
_module = importlib.import_module(f"
|
|
181
|
+
_module = importlib.import_module(f"{plugin_name}.app")
|
|
179
182
|
success(f"Initializing plugin {plugin_name} for interactivity")
|
|
180
183
|
except Exception:
|
|
181
184
|
error(f"Initializing plugin {plugin_name} for interactivity")
|
|
@@ -132,25 +132,7 @@ suppress-none-returning = true
|
|
|
132
132
|
"clue/plugin/interactive.py" = ["T201"]
|
|
133
133
|
"clue/remote/datatypes/*" = ["D", "ANN", "C901"]
|
|
134
134
|
"clue/security/__init__.py" = ["TRY301"]
|
|
135
|
-
"
|
|
136
|
-
"plugins/oracle/*" = [
|
|
137
|
-
"F403",
|
|
138
|
-
"D103",
|
|
139
|
-
"D205",
|
|
140
|
-
"F841",
|
|
141
|
-
"C901",
|
|
142
|
-
"ANN001",
|
|
143
|
-
"E501",
|
|
144
|
-
"T201",
|
|
145
|
-
"TRY301",
|
|
146
|
-
"TRY002",
|
|
147
|
-
"S113",
|
|
148
|
-
"F811",
|
|
149
|
-
"I001",
|
|
150
|
-
"N806",
|
|
151
|
-
"TRY004",
|
|
152
|
-
]
|
|
153
|
-
|
|
135
|
+
"test/conftest.py" = ["E402"]
|
|
154
136
|
|
|
155
137
|
###################
|
|
156
138
|
# pytest settings #
|
|
@@ -165,7 +147,7 @@ log_cli_level = "WARN"
|
|
|
165
147
|
[tool.poetry]
|
|
166
148
|
package-mode = true
|
|
167
149
|
name = "clue-api"
|
|
168
|
-
version = "1.0.1.
|
|
150
|
+
version = "1.0.1.dev58"
|
|
169
151
|
description = "Clue distributed enrichment service"
|
|
170
152
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
171
153
|
license = "MIT"
|
|
@@ -257,7 +239,7 @@ last_success = "build_scripts.last_success:main"
|
|
|
257
239
|
check_changes = "build_scripts.check_changes:main"
|
|
258
240
|
type_check = "build_scripts.type_check:main"
|
|
259
241
|
coverage_report = "build_scripts.coverage_reports:main"
|
|
260
|
-
|
|
242
|
+
plugin = "clue.plugin.interactive:main"
|
|
261
243
|
|
|
262
244
|
[tool.poetry.group.test.dependencies]
|
|
263
245
|
pytest = "^8.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|