clue-api 1.4.0.dev180__tar.gz → 1.4.0.dev184__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.4.0.dev180 → clue_api-1.4.0.dev184}/PKG-INFO +1 -1
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/actions.py +1 -1
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/fetchers.py +38 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/fetchers.py +15 -3
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/__init__.py +109 -10
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/action_service.py +1 -1
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/fetcher_service.py +68 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/pyproject.toml +1 -1
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/LICENSE +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/README.md +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/.gitignore +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/base.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/auth.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/configs.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/registration.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/static.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/app.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/cache/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/classification.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/classification.yml +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/dict_utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/exceptions.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/forge.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/json_utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/list_utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/logging/audit.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/logging/format.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/regex.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/str_utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/swagger.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/uid.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/config.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/constants/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/constants/env.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/constants/supported_types.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/error.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/extensions/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/extensions/config.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/gunicorn_config.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/healthz.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/helper/discover.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/helper/headers.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/helper/oauth.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/actions.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/config.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/graph.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/model_list.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/network.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/base.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/file.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/graph.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/image.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/status.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/validation.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/selector.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/validators.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/patched.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/models.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/py.typed +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/security/__init__.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/security/obo.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/security/utils.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/auth_service.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/config_service.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/jwt_service.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/lookup_service.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/type_service.py +0 -0
- {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/user_service.py +0 -0
|
@@ -104,7 +104,7 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, **kwargs) ->
|
|
|
104
104
|
task_id (str): the ID of the specific task to get the status of
|
|
105
105
|
|
|
106
106
|
Arguments:
|
|
107
|
-
|
|
107
|
+
None
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
Result Example:
|
|
@@ -92,3 +92,41 @@ def run_fetcher(plugin_id: str, fetcher_id: str, **kwargs):
|
|
|
92
92
|
|
|
93
93
|
logger.warning("Unknown error from fetcher %s.%s: %s", plugin_id, fetcher_id, err.message)
|
|
94
94
|
return bad_gateway(err=err.message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@generate_swagger_docs(responses={200: "Successfully fetched status of fetcher"})
|
|
98
|
+
@fetchers_api.route("/<plugin_id>/<fetcher_id>/status/<task_id>", methods=["GET"])
|
|
99
|
+
@api_login()
|
|
100
|
+
def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, **kwargs):
|
|
101
|
+
"""Get the status or result of a fetcher
|
|
102
|
+
|
|
103
|
+
Variables:
|
|
104
|
+
plugin_id (str): the ID of the plugin who owns the action to execute
|
|
105
|
+
fetcher_id (str): the ID of the action to execute
|
|
106
|
+
task_id (str): the ID of the specific task to get the status of
|
|
107
|
+
|
|
108
|
+
Arguments:
|
|
109
|
+
None
|
|
110
|
+
|
|
111
|
+
Result Example:
|
|
112
|
+
{
|
|
113
|
+
"outcome": "success | failure", # was this execution a success or failure?
|
|
114
|
+
"format": "link", # What format is the output in?
|
|
115
|
+
"output": "http://example.com" # The output of the action. Can be any data structure.
|
|
116
|
+
}
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
return ok(fetcher_service.get_fetcher_status(plugin_id, fetcher_id, task_id, kwargs["user"]))
|
|
120
|
+
except NotFoundException as err:
|
|
121
|
+
return not_found(err=err.message)
|
|
122
|
+
except ClueException as err:
|
|
123
|
+
if err.status_code == 400:
|
|
124
|
+
logger.warning(
|
|
125
|
+
"Bad request from fetcher %s.%s with task_id: %s: %s", plugin_id, fetcher_id, task_id, err.message
|
|
126
|
+
)
|
|
127
|
+
return bad_request(err=err.message)
|
|
128
|
+
|
|
129
|
+
logger.warning(
|
|
130
|
+
"Unknown error from fetcher %s.%s with task_id: %s: %s", plugin_id, fetcher_id, task_id, err.message
|
|
131
|
+
)
|
|
132
|
+
return bad_gateway(err=err.message)
|
|
@@ -30,6 +30,7 @@ class FetcherDefinition(BaseModel):
|
|
|
30
30
|
description: str = Field(description="A basic description of the fetcher's usage.")
|
|
31
31
|
format: str = Field(description="The output format of the fetcher's result.")
|
|
32
32
|
supported_types: set[str] = Field(description="A list of types this fetcher supports.")
|
|
33
|
+
async_result: bool = Field(description="Does this fetcher run asynchronously?", default=False)
|
|
33
34
|
extra_data: Optional[Dict[str, JsonValue]] = Field(
|
|
34
35
|
default=None, description="Extra data you want to define for a fetcher"
|
|
35
36
|
)
|
|
@@ -101,14 +102,18 @@ class FetcherDefinition(BaseModel):
|
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
class FetcherResult(BaseModel, Generic[DATA]):
|
|
104
|
-
outcome: Literal["success", "failure"] = Field(
|
|
105
|
+
outcome: Literal["success", "failure", "pending"] = Field(
|
|
106
|
+
description="Did the fetcher succeed or fail, or is it pending?"
|
|
107
|
+
)
|
|
105
108
|
data: DATA | None = Field(description="The output of the fetcher.", default=None)
|
|
106
109
|
error: str | None = Field(description="If the fetcher failed, contains the relevant error message.", default=None)
|
|
107
|
-
format: str = Field(
|
|
110
|
+
format: str | None = Field(
|
|
108
111
|
description="What is the format of the output? Used to indicate what component to use when rendering "
|
|
109
112
|
"the output.",
|
|
113
|
+
default=None,
|
|
110
114
|
)
|
|
111
115
|
link: Optional[Url] = Field(description="Link to more information on the fetcher", default=None)
|
|
116
|
+
task_id: str | None = Field(description="The task id if the fetcher result is pending.", default=None)
|
|
112
117
|
|
|
113
118
|
@model_validator(mode="after")
|
|
114
119
|
def validate_model(self: Self, info: ValidationInfo) -> Self: # noqa: C901
|
|
@@ -123,6 +128,9 @@ class FetcherResult(BaseModel, Generic[DATA]):
|
|
|
123
128
|
if self.outcome == "success" and self.data is None:
|
|
124
129
|
raise ClueValueError("Successful fetcher results must return data.")
|
|
125
130
|
|
|
131
|
+
if not self.task_id and self.outcome == "pending":
|
|
132
|
+
raise ClueValueError("task_id must be set if outcome is pending.")
|
|
133
|
+
|
|
126
134
|
if self.outcome == "failure":
|
|
127
135
|
if self.data is not None:
|
|
128
136
|
raise ClueValueError("Failed fetcher results cannot return data.")
|
|
@@ -133,7 +141,7 @@ class FetcherResult(BaseModel, Generic[DATA]):
|
|
|
133
141
|
elif self.error:
|
|
134
142
|
raise ClueValueError("Errors can only be specified if the outcome is failure.")
|
|
135
143
|
|
|
136
|
-
self.data = validate_result(self.format, self.data, info)
|
|
144
|
+
self.data = validate_result(self.format, self.data, info) if self.format else None
|
|
137
145
|
|
|
138
146
|
return self
|
|
139
147
|
|
|
@@ -141,3 +149,7 @@ class FetcherResult(BaseModel, Generic[DATA]):
|
|
|
141
149
|
def error_result(err: str) -> "FetcherResult":
|
|
142
150
|
"Helper function to generate a failed fetcher result"
|
|
143
151
|
return FetcherResult(outcome="failure", format="error", error=err)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class FetcherStatusRequest(BaseModel):
|
|
155
|
+
task_id: str = Field(description="The task id to get the status for.")
|
|
@@ -36,7 +36,7 @@ from clue.models.actions import (
|
|
|
36
36
|
ExecuteRequest,
|
|
37
37
|
)
|
|
38
38
|
from clue.models.config import Config
|
|
39
|
-
from clue.models.fetchers import FetcherDefinition, FetcherResult
|
|
39
|
+
from clue.models.fetchers import FetcherDefinition, FetcherResult, FetcherStatusRequest
|
|
40
40
|
from clue.models.network import QueryEntry
|
|
41
41
|
from clue.models.selector import Selector
|
|
42
42
|
from clue.plugin.celery_app import celery_init_app
|
|
@@ -55,7 +55,8 @@ OVERRIDABLE_FUNCTIONS = [
|
|
|
55
55
|
"liveness", # Kubernetes liveness probe endpoint
|
|
56
56
|
"readiness", # Kubernetes readiness probe endpoint
|
|
57
57
|
"run_action", # Function to execute plugin actions
|
|
58
|
-
"
|
|
58
|
+
"get_action_status", # Function to check the status or result of a pending action
|
|
59
|
+
"get_fetcher_status", # Function to check the status or result of a pending fetcher
|
|
59
60
|
"run_fetcher", # Function to execute plugin fetchers
|
|
60
61
|
"setup_actions", # Runtime action definition generation
|
|
61
62
|
"validate_token", # Custom authentication token validation
|
|
@@ -294,7 +295,13 @@ class CluePlugin:
|
|
|
294
295
|
user parameters) that instance will be passed instead, and casting the argument will be necessary.
|
|
295
296
|
"""
|
|
296
297
|
|
|
297
|
-
|
|
298
|
+
get_action_status: Callable[[Action, ActionStatusRequest, str | None], ActionResult] | None
|
|
299
|
+
"""The function to get the status and result of running actions.
|
|
300
|
+
|
|
301
|
+
Accepts the selected action definition as well as an ActionStatusRequest instance which contains the
|
|
302
|
+
specific task id to check the status of.
|
|
303
|
+
"""
|
|
304
|
+
get_fetcher_status: Callable[[FetcherDefinition, FetcherStatusRequest, str | None], FetcherResult] | None
|
|
298
305
|
"""The function to get the status and result of running actions.
|
|
299
306
|
|
|
300
307
|
Accepts the selected action definition as well as an ActionStatusRequest instance which contains the
|
|
@@ -338,7 +345,9 @@ class CluePlugin:
|
|
|
338
345
|
logger: logging.Logger | None = None,
|
|
339
346
|
readiness: Callable[[], Response] = default_readiness,
|
|
340
347
|
run_action: Callable[[Action, ExecuteRequest, str | None], ActionResult] | None = None,
|
|
341
|
-
|
|
348
|
+
get_action_status: Callable[[Action, ActionStatusRequest, str | None], ActionResult] | None = None,
|
|
349
|
+
get_fetcher_status: Callable[[FetcherDefinition, FetcherStatusRequest, str | None], FetcherResult]
|
|
350
|
+
| None = None,
|
|
342
351
|
run_fetcher: Callable[[FetcherDefinition, Selector, str | None], FetcherResult] | None = None,
|
|
343
352
|
setup_actions: Callable[[list[Action], str | None], list[Action]] | None = None,
|
|
344
353
|
supported_types: set[str] | str | None = None,
|
|
@@ -439,7 +448,8 @@ class CluePlugin:
|
|
|
439
448
|
self.enrich = enrich
|
|
440
449
|
self.enable_celery = enable_celery
|
|
441
450
|
self.run_action = run_action
|
|
442
|
-
self.
|
|
451
|
+
self.get_action_status = get_action_status
|
|
452
|
+
self.get_fetcher_status = get_fetcher_status
|
|
443
453
|
self.validate_token = validate_token
|
|
444
454
|
|
|
445
455
|
self.fetchers = fetchers
|
|
@@ -714,14 +724,20 @@ class CluePlugin:
|
|
|
714
724
|
)
|
|
715
725
|
self.app.add_url_rule(
|
|
716
726
|
"/actions/<action_id>/status/<task_id>",
|
|
717
|
-
self.
|
|
718
|
-
self.
|
|
727
|
+
self.action_status.__name__,
|
|
728
|
+
self.action_status,
|
|
719
729
|
methods=["GET"],
|
|
720
730
|
)
|
|
721
731
|
self.app.add_url_rule("/fetchers/", self.get_fetchers.__name__, self.get_fetchers, methods=["GET"])
|
|
722
732
|
self.app.add_url_rule(
|
|
723
733
|
"/fetchers/<fetcher_id>", self.execute_fetcher.__name__, self.execute_fetcher, methods=["POST"]
|
|
724
734
|
)
|
|
735
|
+
self.app.add_url_rule(
|
|
736
|
+
"/fetchers/<fetcher_id>/status/<task_id>",
|
|
737
|
+
self.fetcher_status.__name__,
|
|
738
|
+
self.fetcher_status,
|
|
739
|
+
methods=["GET"],
|
|
740
|
+
)
|
|
725
741
|
self.app.add_url_rule("/types/", self.get_type_names.__name__, self.get_type_names, methods=["GET"])
|
|
726
742
|
self.app.add_url_rule("/lookup/<type_name>/<value>/", self.lookup.__name__, self.lookup, methods=["GET"])
|
|
727
743
|
self.app.add_url_rule("/lookup/", self.bulk_lookup.__name__, self.bulk_lookup, methods=["POST"])
|
|
@@ -1177,7 +1193,7 @@ class CluePlugin:
|
|
|
1177
1193
|
|
|
1178
1194
|
return self.make_api_response(result)
|
|
1179
1195
|
|
|
1180
|
-
def
|
|
1196
|
+
def action_status(self: Self, action_id: str, task_id: str): # noqa: C901
|
|
1181
1197
|
"""Retrieves the status of the specified action.
|
|
1182
1198
|
|
|
1183
1199
|
Args:
|
|
@@ -1192,7 +1208,7 @@ class CluePlugin:
|
|
|
1192
1208
|
{}, err="task id not provided. task id is required for this request.", status_code=400
|
|
1193
1209
|
)
|
|
1194
1210
|
|
|
1195
|
-
if not self.
|
|
1211
|
+
if not self.get_action_status:
|
|
1196
1212
|
return self.make_api_response(
|
|
1197
1213
|
{}, err=f"{self.app_name} does not support the get action status functions.", status_code=400
|
|
1198
1214
|
)
|
|
@@ -1234,7 +1250,7 @@ class CluePlugin:
|
|
|
1234
1250
|
task_id,
|
|
1235
1251
|
)
|
|
1236
1252
|
|
|
1237
|
-
result = self.
|
|
1253
|
+
result = self.get_action_status(action_to_check, status_request, token)
|
|
1238
1254
|
except json.JSONDecodeError as e:
|
|
1239
1255
|
self.logger.warning("JSON decoding error while getting status: %s", str(e))
|
|
1240
1256
|
|
|
@@ -1375,6 +1391,89 @@ class CluePlugin:
|
|
|
1375
1391
|
|
|
1376
1392
|
return self.make_api_response(result, status_code=status_code)
|
|
1377
1393
|
|
|
1394
|
+
def fetcher_status(self: Self, fetcher_id: str, task_id: str): # noqa: C901
|
|
1395
|
+
"""Gets the status of a pending fetcher with the specified task_id
|
|
1396
|
+
|
|
1397
|
+
Args:
|
|
1398
|
+
fetcher_id (str): The ID of the fetcher to get the status of
|
|
1399
|
+
task_id (str): The ID of the pending task
|
|
1400
|
+
|
|
1401
|
+
Returns:
|
|
1402
|
+
Response: A Response object with a FetcherResult as the body.
|
|
1403
|
+
"""
|
|
1404
|
+
if not task_id:
|
|
1405
|
+
return self.make_api_response(
|
|
1406
|
+
{}, err="task id not provided. task id is required for this request.", status_code=400
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
if not self.get_fetcher_status:
|
|
1410
|
+
return self.make_api_response(
|
|
1411
|
+
{}, err=f"{self.app_name} does not support the get fetcher status functions.", status_code=400
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
if not self.fetchers:
|
|
1415
|
+
return self.make_api_response({}, err=f"{self.app_name} does not support any fetchers.", status_code=400)
|
|
1416
|
+
|
|
1417
|
+
# Find the requested fetcher by ID
|
|
1418
|
+
fetcher = next((fetcher for fetcher in self.fetchers if fetcher.id == fetcher_id), None)
|
|
1419
|
+
if not fetcher:
|
|
1420
|
+
return self.make_api_response({}, err=f"Fetcher {fetcher_id} does not exist", status_code=404)
|
|
1421
|
+
|
|
1422
|
+
token: str | None = None
|
|
1423
|
+
if self.validate_token:
|
|
1424
|
+
self.logger.debug("Executing plugin-provided token validator")
|
|
1425
|
+
|
|
1426
|
+
token, error = self.validate_token()
|
|
1427
|
+
|
|
1428
|
+
if error:
|
|
1429
|
+
return self.make_api_response(None, f"Error on token validation: {error}", status_code=401)
|
|
1430
|
+
|
|
1431
|
+
self.logger.debug("Token is valid")
|
|
1432
|
+
else:
|
|
1433
|
+
self.logger.warning("No token validation provided. The access token will not be provided to the fetcher.")
|
|
1434
|
+
|
|
1435
|
+
status_code = 200
|
|
1436
|
+
try:
|
|
1437
|
+
status_request = FetcherStatusRequest(task_id=task_id)
|
|
1438
|
+
|
|
1439
|
+
self.logger.info("Getting status for fetcher '%s'", fetcher_id)
|
|
1440
|
+
|
|
1441
|
+
result = self.get_fetcher_status(fetcher, status_request, token)
|
|
1442
|
+
except json.JSONDecodeError as e:
|
|
1443
|
+
self.logger.warning("JSON decoding error during execution: %s", str(e))
|
|
1444
|
+
|
|
1445
|
+
status_code = 400
|
|
1446
|
+
result = FetcherResult(
|
|
1447
|
+
outcome="failure",
|
|
1448
|
+
format="error",
|
|
1449
|
+
error=f"Invalid request format. Response body must be valid JSON. Error: {str(e)}",
|
|
1450
|
+
)
|
|
1451
|
+
except ValidationError as err:
|
|
1452
|
+
self.logger.warning("Validation error during execution: %s", str(err))
|
|
1453
|
+
status_code = 400
|
|
1454
|
+
result = FetcherResult(outcome="failure", format="error", error=str(err))
|
|
1455
|
+
except ClueException as e:
|
|
1456
|
+
self.logger.exception("ClueException during execution:")
|
|
1457
|
+
status_code = 500
|
|
1458
|
+
result = FetcherResult(
|
|
1459
|
+
outcome="failure", format="error", error=f"Error encountered during execution: {e.message}"
|
|
1460
|
+
)
|
|
1461
|
+
except Exception as e:
|
|
1462
|
+
self.logger.exception("%s during execution:", e.__class__.__name__)
|
|
1463
|
+
status_code = 500
|
|
1464
|
+
result = FetcherResult(
|
|
1465
|
+
outcome="failure", format="error", error=f"An unknown error occurred during execution: {str(e)}"
|
|
1466
|
+
)
|
|
1467
|
+
finally:
|
|
1468
|
+
self.logger.info("Fetcher completed.")
|
|
1469
|
+
|
|
1470
|
+
self.logger.info("Fetcher outcome: %s", result.outcome)
|
|
1471
|
+
|
|
1472
|
+
if result.error:
|
|
1473
|
+
self.logger.info("Error Message: %s", result.error)
|
|
1474
|
+
|
|
1475
|
+
return self.make_api_response(result, status_code=status_code)
|
|
1476
|
+
|
|
1378
1477
|
def use(self, func: Callable):
|
|
1379
1478
|
"""Register a function to be used by the CluePlugin for specific operations.
|
|
1380
1479
|
|
|
@@ -196,7 +196,7 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
|
|
|
196
196
|
Args:
|
|
197
197
|
plugin_id (str): The ID of the plugin.
|
|
198
198
|
action_id (str): The ID of the action to run.
|
|
199
|
-
task_id (str): The
|
|
199
|
+
task_id (str): The task id to fetch the status for
|
|
200
200
|
user (dict[str, Any]): The user dict of the user running the action.
|
|
201
201
|
|
|
202
202
|
Raises:
|
|
@@ -201,3 +201,71 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
|
|
|
201
201
|
raise ClueException(
|
|
202
202
|
f"Something went wrong when running fetcher from plugin '{plugin_id}': {err.__class__.__name__}."
|
|
203
203
|
) from err
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict[str, Any]) -> FetcherResult:
|
|
207
|
+
"""Executes a specified fetcher.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
plugin_id (str): The ID of the plugin.
|
|
211
|
+
fetcher_id (str): The ID of the action to run.
|
|
212
|
+
task_id (str): The task id to fetch the status for
|
|
213
|
+
user (dict[str, Any]): The user dict of the user running the action.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
NotFoundException: Raised whenever the plugin or the action doesn't exist.
|
|
217
|
+
ClueException: Raised whenever an error is returned by the plugin endpoint.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
ActionResult: The result of the action.
|
|
221
|
+
"""
|
|
222
|
+
plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
|
|
223
|
+
|
|
224
|
+
if not plugin:
|
|
225
|
+
raise NotFoundException(f"Plugin {plugin_id} does not exist.")
|
|
226
|
+
|
|
227
|
+
access_token = request.headers.get("Authorization", type=str)
|
|
228
|
+
if access_token:
|
|
229
|
+
access_token = access_token.split(" ")[1]
|
|
230
|
+
|
|
231
|
+
obo_access_token = None
|
|
232
|
+
if access_token:
|
|
233
|
+
obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
|
|
234
|
+
|
|
235
|
+
if error:
|
|
236
|
+
logger.error("%s: %s", plugin.name, error)
|
|
237
|
+
raise AuthenticationException("Invalid token provided for this enrichment.")
|
|
238
|
+
|
|
239
|
+
headers = {"Accept": "application/json"}
|
|
240
|
+
if obo_access_token or access_token:
|
|
241
|
+
headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
req_url = urljoin(plugin.url, f"fetchers/{fetcher_id}/status/{task_id}")
|
|
245
|
+
logger.debug("Getting status for action %s with task_id %s for user %s", req_url, task_id, user["uname"])
|
|
246
|
+
|
|
247
|
+
response = requests.get(
|
|
248
|
+
req_url,
|
|
249
|
+
headers=headers,
|
|
250
|
+
timeout=request.args.get("max_timeout", 60.0, type=float),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
result = response.json()
|
|
254
|
+
|
|
255
|
+
if not response.ok:
|
|
256
|
+
raise ClueException(
|
|
257
|
+
result["api_error_message"] or result["api_response"].get("error", ""), status_code=response.status_code
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return FetcherResult.model_validate(result["api_response"], context={"is_response": True})
|
|
261
|
+
except ValidationError as err:
|
|
262
|
+
logger.exception("Invalid Request Body:")
|
|
263
|
+
raise ClueValueError(
|
|
264
|
+
"Validation error encountered on response body.",
|
|
265
|
+
status_code=400,
|
|
266
|
+
) from err
|
|
267
|
+
except (JSONDecodeError, exceptions.ConnectionError) as err:
|
|
268
|
+
logger.exception(f"Something went wrong when getting the status of the fetcher from plugin '{plugin_id}'")
|
|
269
|
+
raise ClueException(
|
|
270
|
+
f"Something went wrong getting the status of fetcher from plugin '{plugin_id}': {err.__class__.__name__}."
|
|
271
|
+
) from err
|
|
@@ -139,7 +139,7 @@ log_cli_level = "WARN"
|
|
|
139
139
|
[tool.poetry]
|
|
140
140
|
package-mode = true
|
|
141
141
|
name = "clue-api"
|
|
142
|
-
version = "1.4.0.
|
|
142
|
+
version = "1.4.0.dev184"
|
|
143
143
|
description = "Clue distributed enrichment service"
|
|
144
144
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
145
145
|
license = "MIT"
|
|
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
|