clue-api 1.7.0.dev465__tar.gz → 1.7.0.dev469__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.7.0.dev465 → clue_api-1.7.0.dev469}/PKG-INFO +2 -2
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/actions.py +9 -3
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/fetchers.py +4 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/helper/plugin_requests.py +10 -1
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/actions.py +6 -7
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/fetchers.py +6 -7
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/network.py +6 -0
- clue_api-1.7.0.dev469/clue/models/validators.py +50 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/action_service.py +20 -6
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/fetcher_service.py +18 -4
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/pyproject.toml +2 -2
- clue_api-1.7.0.dev465/clue/models/validators.py +0 -19
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/LICENSE +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/README.md +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/.gitignore +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/base.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/documentation.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/auth.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/configs.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/registration.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/static.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v1/sync.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v2/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/api/v2/lookup.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/app.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/cache/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/classification.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/classification.yml +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/dict_utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/exceptions.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/forge.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/json_utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/list_utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/logging/audit.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/logging/format.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/regex.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/str_utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/swagger.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/common/uid.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/config.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/constants/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/constants/env.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/constants/supported_types.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/error.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/extensions/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/extensions/config.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/gunicorn_config.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/healthz.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/helper/discover.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/helper/headers.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/helper/oauth.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/helper/obo.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/auth_user.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/config.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/graph.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/jwks.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/model_list.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/base.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/file.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/graph.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/image.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/status.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/results/validation.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/schema.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/selector.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/models/sync.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/patched.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/models.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/plugin/utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/py.typed +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/security/__init__.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/security/obo.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/security/utils.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/auth_service.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/config_service.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/jwt_service.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/lookup_service.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/mongo_service.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/type_service.py +0 -0
- {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev469}/clue/services/user_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clue-api
|
|
3
|
-
Version: 1.7.0.
|
|
3
|
+
Version: 1.7.0.dev469
|
|
4
4
|
Summary: Clue distributed enrichment service
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -47,7 +47,7 @@ Requires-Dist: python-baseconv (>=1.2.2,<2.0.0) ; extra == "server"
|
|
|
47
47
|
Requires-Dist: pytz (>=2024.1,<2025.0) ; extra == "server"
|
|
48
48
|
Requires-Dist: redis (>=5.0.3,<6.0.0)
|
|
49
49
|
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
50
|
-
Requires-Dist: setuptools (<
|
|
50
|
+
Requires-Dist: setuptools (<85.0.0)
|
|
51
51
|
Requires-Dist: trino (>=0.336.0,<0.337.0)
|
|
52
52
|
Project-URL: Documentation, https://github.com/CybercentreCanada/clue
|
|
53
53
|
Project-URL: Homepage, https://github.com/CybercentreCanada/clue
|
|
@@ -8,12 +8,13 @@ List and execute actions
|
|
|
8
8
|
|
|
9
9
|
from flask_cors import CORS
|
|
10
10
|
|
|
11
|
-
from clue.api import internal_error, make_subapi_blueprint, not_found, ok, service_unavailable
|
|
11
|
+
from clue.api import bad_request, internal_error, make_subapi_blueprint, not_found, ok, service_unavailable
|
|
12
12
|
from clue.common.exceptions import ClueException, NotFoundException
|
|
13
13
|
from clue.common.logging import get_logger
|
|
14
14
|
from clue.common.swagger import generate_swagger_docs
|
|
15
15
|
from clue.config import config
|
|
16
16
|
from clue.models.actions import Action, ActionResult
|
|
17
|
+
from clue.models.validators import validate_plugin_identifier, validate_task_id
|
|
17
18
|
from clue.security import api_login
|
|
18
19
|
from clue.services import action_service
|
|
19
20
|
|
|
@@ -85,10 +86,13 @@ def execute_action(plugin_id: str, action_id: str, **kwargs) -> ActionResult:
|
|
|
85
86
|
}
|
|
86
87
|
"""
|
|
87
88
|
try:
|
|
89
|
+
validate_plugin_identifier(action_id)
|
|
88
90
|
return ok(action_service.execute_action(plugin_id, action_id, kwargs["user"]))
|
|
89
91
|
except NotFoundException as err:
|
|
90
92
|
return not_found(err=err.message)
|
|
91
93
|
except ClueException as err:
|
|
94
|
+
if err.status_code == 400:
|
|
95
|
+
return bad_request(err=err.message)
|
|
92
96
|
return internal_error(err=err.message)
|
|
93
97
|
|
|
94
98
|
|
|
@@ -116,12 +120,14 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, **kwargs) ->
|
|
|
116
120
|
}
|
|
117
121
|
"""
|
|
118
122
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
validate_plugin_identifier(action_id)
|
|
124
|
+
validate_task_id(task_id)
|
|
121
125
|
return ok(action_service.get_action_status(plugin_id, action_id, task_id, kwargs["user"]))
|
|
122
126
|
except NotFoundException as err:
|
|
123
127
|
return not_found(err=err.message)
|
|
124
128
|
except ClueException as err:
|
|
129
|
+
if err.status_code == 400:
|
|
130
|
+
return bad_request(err=err.message)
|
|
125
131
|
if err.status_code == 503:
|
|
126
132
|
return service_unavailable(err=err.message)
|
|
127
133
|
return internal_error(err=err.message)
|
|
@@ -14,6 +14,7 @@ from clue.common.logging import get_logger
|
|
|
14
14
|
from clue.common.swagger import generate_swagger_docs
|
|
15
15
|
from clue.config import config
|
|
16
16
|
from clue.models.fetchers import FetcherDefinition
|
|
17
|
+
from clue.models.validators import validate_plugin_identifier, validate_task_id
|
|
17
18
|
from clue.security import api_login
|
|
18
19
|
from clue.services import fetcher_service
|
|
19
20
|
|
|
@@ -82,6 +83,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, **kwargs):
|
|
|
82
83
|
}
|
|
83
84
|
"""
|
|
84
85
|
try:
|
|
86
|
+
validate_plugin_identifier(fetcher_id)
|
|
85
87
|
return ok(fetcher_service.run_fetcher(plugin_id, fetcher_id, kwargs["user"]))
|
|
86
88
|
except NotFoundException as err:
|
|
87
89
|
return not_found(err=err.message)
|
|
@@ -116,6 +118,8 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, **kwargs):
|
|
|
116
118
|
}
|
|
117
119
|
"""
|
|
118
120
|
try:
|
|
121
|
+
validate_plugin_identifier(fetcher_id)
|
|
122
|
+
validate_task_id(task_id)
|
|
119
123
|
return ok(fetcher_service.get_fetcher_status(plugin_id, fetcher_id, task_id, kwargs["user"]))
|
|
120
124
|
except NotFoundException as err:
|
|
121
125
|
return not_found(err=err.message)
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from time import monotonic
|
|
3
3
|
from typing import Any, TypeAlias
|
|
4
|
-
from urllib.parse import urljoin, urlsplit
|
|
4
|
+
from urllib.parse import quote, urljoin, urlsplit
|
|
5
5
|
|
|
6
6
|
from requests import Response
|
|
7
7
|
from requests.exceptions import ConnectionError, Timeout
|
|
8
8
|
|
|
9
|
+
from clue.common.exceptions import NotFoundException
|
|
10
|
+
|
|
9
11
|
_Timeout: TypeAlias = int | float | tuple[int | float | None, int | float | None] | None
|
|
10
12
|
|
|
11
13
|
|
|
14
|
+
def quote_plugin_path_segment(value: str) -> str:
|
|
15
|
+
"""Encode an opaque plugin identifier, rejecting client-normalized dot segments."""
|
|
16
|
+
if value in {"", ".", ".."}:
|
|
17
|
+
raise NotFoundException("Invalid plugin identifier.", status_code=404)
|
|
18
|
+
return quote(value, safe="").replace(".", "%2E")
|
|
19
|
+
|
|
20
|
+
|
|
12
21
|
def _timeout_budget(timeout: _Timeout) -> float | None:
|
|
13
22
|
"""Calculate the total duration represented by a requests timeout.
|
|
14
23
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# ruff: noqa: D101
|
|
2
|
-
import re
|
|
3
2
|
from inspect import isclass
|
|
4
3
|
from typing import (
|
|
5
4
|
Any,
|
|
@@ -29,7 +28,7 @@ from clue.constants.supported_types import SUPPORTED_TYPES
|
|
|
29
28
|
from clue.models.results import DATA
|
|
30
29
|
from clue.models.results.validation import validate_result
|
|
31
30
|
from clue.models.selector import Selector
|
|
32
|
-
from clue.models.validators import validate_classification
|
|
31
|
+
from clue.models.validators import validate_classification, validate_plugin_identifier
|
|
33
32
|
|
|
34
33
|
logger = get_logger(__file__)
|
|
35
34
|
|
|
@@ -123,7 +122,10 @@ ER = TypeVar("ER", bound=ExecuteRequest)
|
|
|
123
122
|
|
|
124
123
|
|
|
125
124
|
class ActionBase(BaseModel):
|
|
126
|
-
id: str = Field(
|
|
125
|
+
id: str = Field(
|
|
126
|
+
description="Unique action identifier with optional dot-separated components using ASCII letters, digits, "
|
|
127
|
+
"underscores, or hyphens."
|
|
128
|
+
)
|
|
127
129
|
name: str = Field(description="Name of the action.")
|
|
128
130
|
classification: str = Field(
|
|
129
131
|
description="Classification of the action. Denotes the maximum classification of data sent to the action.",
|
|
@@ -162,10 +164,7 @@ class ActionBase(BaseModel):
|
|
|
162
164
|
Returns:
|
|
163
165
|
str: The validated ID.
|
|
164
166
|
"""
|
|
165
|
-
|
|
166
|
-
raise ClueValueError("Invalid action id - can only contain lowercase letters and underscores.")
|
|
167
|
-
|
|
168
|
-
return action_id
|
|
167
|
+
return validate_plugin_identifier(action_id)
|
|
169
168
|
|
|
170
169
|
@field_validator("classification")
|
|
171
170
|
@classmethod
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# ruff: noqa: D101
|
|
2
|
-
import re
|
|
3
2
|
from typing import Dict, Generic, Literal, Optional, Self
|
|
4
3
|
|
|
5
4
|
from pydantic import (
|
|
@@ -17,13 +16,16 @@ from clue.common.logging import get_logger
|
|
|
17
16
|
from clue.constants.supported_types import SUPPORTED_TYPES
|
|
18
17
|
from clue.models.results import DATA, FORMAT_MAPPINGS_REVERSE
|
|
19
18
|
from clue.models.results.validation import validate_result
|
|
20
|
-
from clue.models.validators import validate_classification
|
|
19
|
+
from clue.models.validators import validate_classification, validate_plugin_identifier
|
|
21
20
|
|
|
22
21
|
logger = get_logger(__file__)
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
class FetcherDefinition(BaseModel):
|
|
26
|
-
id: str = Field(
|
|
25
|
+
id: str = Field(
|
|
26
|
+
description="Fetcher identifier, optionally plugin_id.fetcher_id; components use ASCII letters, digits, "
|
|
27
|
+
"underscores, or hyphens."
|
|
28
|
+
)
|
|
27
29
|
classification: str = Field(
|
|
28
30
|
description="Classification of the fetcher. Denotes the maximum classification of data sent to the fetcher.",
|
|
29
31
|
)
|
|
@@ -49,10 +51,7 @@ class FetcherDefinition(BaseModel):
|
|
|
49
51
|
Returns:
|
|
50
52
|
str: The validated ID.
|
|
51
53
|
"""
|
|
52
|
-
|
|
53
|
-
raise ClueValueError("Invalid fetcher id - can only contain lowercase letters and underscores.")
|
|
54
|
-
|
|
55
|
-
return fetcher_id
|
|
54
|
+
return validate_plugin_identifier(fetcher_id)
|
|
56
55
|
|
|
57
56
|
@field_validator("classification")
|
|
58
57
|
@classmethod
|
|
@@ -529,6 +529,9 @@ class QueryResult(ResultMetadata):
|
|
|
529
529
|
Returns:
|
|
530
530
|
list[QueryEntry]: The validated items field.
|
|
531
531
|
"""
|
|
532
|
+
if not CLASSIFICATION.enforce:
|
|
533
|
+
return items
|
|
534
|
+
|
|
532
535
|
if not isinstance(info.context, dict) or "user" not in info.context:
|
|
533
536
|
logger.warning("No user context given, dropping all query items")
|
|
534
537
|
return []
|
|
@@ -538,6 +541,9 @@ class QueryResult(ResultMetadata):
|
|
|
538
541
|
@classmethod
|
|
539
542
|
def filter_items(cls, items: list[QueryEntry], user: Any) -> list[QueryEntry]: # noqa: ANN102
|
|
540
543
|
"""Apply classification authorization independently of response schema validation."""
|
|
544
|
+
if not CLASSIFICATION.enforce:
|
|
545
|
+
return items
|
|
546
|
+
|
|
541
547
|
user_classification = user.get("classification") if isinstance(user, dict) else None
|
|
542
548
|
if (
|
|
543
549
|
not isinstance(user_classification, str)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from clue.common.exceptions import ClueValueError
|
|
4
|
+
from clue.config import CLASSIFICATION
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_plugin_identifier(value: str) -> str:
|
|
8
|
+
"""Allow safe ASCII components separated by dots, never dot segments."""
|
|
9
|
+
if re.fullmatch(r"[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*", value) is None:
|
|
10
|
+
raise ClueValueError("Invalid plugin identifier.", status_code=400)
|
|
11
|
+
return value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MAX_TASK_ID_LENGTH = 256
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_task_id(value: str) -> str:
|
|
18
|
+
"""Accept any opaque plugin-issued task id that stays a single path segment once percent-encoded.
|
|
19
|
+
|
|
20
|
+
Task ids are minted by plugins (Celery UUIDs by default, but legacy/third-party backends may use other
|
|
21
|
+
formats), so only characters that cannot be safely encoded or routed are rejected. Callers must still encode
|
|
22
|
+
the value with ``quote_plugin_path_segment`` before building an upstream URL.
|
|
23
|
+
"""
|
|
24
|
+
if (
|
|
25
|
+
not value
|
|
26
|
+
or len(value) > MAX_TASK_ID_LENGTH
|
|
27
|
+
or value in {".", ".."}
|
|
28
|
+
or "/" in value
|
|
29
|
+
or re.search(r"[\x00-\x1f\x7f]", value)
|
|
30
|
+
):
|
|
31
|
+
raise ClueValueError("Invalid task id.", status_code=400)
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_classification(classification: str):
|
|
36
|
+
"""Validates the provided classification.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
classification (str): The classification to validate.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
AssertionError: Raised whenever the provided classification is not valid.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: The validated classification.
|
|
46
|
+
"""
|
|
47
|
+
if not CLASSIFICATION.is_valid(classification):
|
|
48
|
+
raise AssertionError(f"{classification} is not a valid classification.")
|
|
49
|
+
|
|
50
|
+
return classification
|
|
@@ -5,17 +5,18 @@ from urllib.parse import urljoin
|
|
|
5
5
|
import requests
|
|
6
6
|
from elasticapm.traces import capture_span
|
|
7
7
|
from flask import request
|
|
8
|
-
from pydantic import TypeAdapter
|
|
8
|
+
from pydantic import TypeAdapter, ValidationError
|
|
9
9
|
from requests import JSONDecodeError, exceptions
|
|
10
10
|
|
|
11
|
-
from clue.common.exceptions import AuthenticationException, ClueException, NotFoundException
|
|
11
|
+
from clue.common.exceptions import AuthenticationException, ClueException, ClueValueError, NotFoundException
|
|
12
12
|
from clue.common.logging import get_logger
|
|
13
13
|
from clue.config import CLASSIFICATION, config
|
|
14
14
|
from clue.helper.headers import generate_headers
|
|
15
15
|
from clue.helper.obo import get_obo_access_token
|
|
16
|
-
from clue.helper.plugin_requests import request_with_safe_redirects
|
|
16
|
+
from clue.helper.plugin_requests import quote_plugin_path_segment, request_with_safe_redirects
|
|
17
17
|
from clue.models.actions import ActionResult, ActionSpec
|
|
18
18
|
from clue.models.config import ExternalSource
|
|
19
|
+
from clue.models.validators import validate_plugin_identifier, validate_task_id
|
|
19
20
|
|
|
20
21
|
logger = get_logger(__file__)
|
|
21
22
|
|
|
@@ -60,7 +61,15 @@ def get_supported_actions(
|
|
|
60
61
|
_raise_action_metadata_unavailable()
|
|
61
62
|
return {}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
actions = TypeAdapter(dict[str, ActionSpec]).validate_python(result["api_response"])
|
|
65
|
+
for identifier in actions:
|
|
66
|
+
validate_plugin_identifier(identifier)
|
|
67
|
+
return actions
|
|
68
|
+
except (ClueValueError, ValidationError) as err:
|
|
69
|
+
logger.warning("Invalid action metadata from %s", source.name)
|
|
70
|
+
if raise_on_error:
|
|
71
|
+
_raise_action_metadata_unavailable(err)
|
|
72
|
+
return {}
|
|
64
73
|
except ClueException:
|
|
65
74
|
raise
|
|
66
75
|
except Exception as err:
|
|
@@ -141,6 +150,7 @@ def execute_action(plugin_id: str, action_id: str, user: dict[str, Any]) -> Acti
|
|
|
141
150
|
Returns:
|
|
142
151
|
ActionResult: The result of the action.
|
|
143
152
|
"""
|
|
153
|
+
validate_plugin_identifier(action_id)
|
|
144
154
|
plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
|
|
145
155
|
|
|
146
156
|
if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
|
|
@@ -164,7 +174,7 @@ def execute_action(plugin_id: str, action_id: str, user: dict[str, Any]) -> Acti
|
|
|
164
174
|
parameters = {}
|
|
165
175
|
|
|
166
176
|
try:
|
|
167
|
-
req_url = urljoin(plugin.url, f"actions/{action_id}")
|
|
177
|
+
req_url = urljoin(plugin.url, f"actions/{quote_plugin_path_segment(action_id)}")
|
|
168
178
|
logger.debug("Executing action %s for user %s", req_url, user["uname"])
|
|
169
179
|
|
|
170
180
|
response = request_with_safe_redirects(
|
|
@@ -205,6 +215,8 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
|
|
|
205
215
|
Returns:
|
|
206
216
|
ActionResult: The result of the action.
|
|
207
217
|
"""
|
|
218
|
+
validate_plugin_identifier(action_id)
|
|
219
|
+
validate_task_id(task_id)
|
|
208
220
|
plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
|
|
209
221
|
|
|
210
222
|
if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
|
|
@@ -232,7 +244,9 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
|
|
|
232
244
|
remaining_timeout = max(timeout - (monotonic() - metadata_started), 0.001)
|
|
233
245
|
|
|
234
246
|
try:
|
|
235
|
-
req_url = urljoin(
|
|
247
|
+
req_url = urljoin(
|
|
248
|
+
plugin.url, f"actions/{quote_plugin_path_segment(action_id)}/status/{quote_plugin_path_segment(task_id)}"
|
|
249
|
+
)
|
|
236
250
|
logger.debug("Getting status for action %s with task_id %s for user %s", req_url, task_id, user["uname"])
|
|
237
251
|
|
|
238
252
|
response = request_with_safe_redirects(
|
|
@@ -19,10 +19,11 @@ from clue.common.logging import get_logger
|
|
|
19
19
|
from clue.config import CLASSIFICATION, config
|
|
20
20
|
from clue.helper.headers import generate_headers
|
|
21
21
|
from clue.helper.obo import get_obo_access_token
|
|
22
|
-
from clue.helper.plugin_requests import request_with_safe_redirects
|
|
22
|
+
from clue.helper.plugin_requests import quote_plugin_path_segment, request_with_safe_redirects
|
|
23
23
|
from clue.models.config import ExternalSource
|
|
24
24
|
from clue.models.fetchers import FetcherDefinition, FetcherResult
|
|
25
25
|
from clue.models.selector import Selector
|
|
26
|
+
from clue.models.validators import validate_plugin_identifier, validate_task_id
|
|
26
27
|
|
|
27
28
|
logger = get_logger(__file__)
|
|
28
29
|
|
|
@@ -67,7 +68,15 @@ def get_supported_fetchers(
|
|
|
67
68
|
_raise_fetcher_metadata_unavailable()
|
|
68
69
|
return {}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
fetchers = TypeAdapter(dict[str, FetcherDefinition]).validate_python(result["api_response"])
|
|
72
|
+
for identifier in fetchers:
|
|
73
|
+
validate_plugin_identifier(identifier)
|
|
74
|
+
return fetchers
|
|
75
|
+
except (ClueValueError, ValidationError) as err:
|
|
76
|
+
logger.warning("Invalid fetcher metadata from %s", source.name)
|
|
77
|
+
if raise_on_error:
|
|
78
|
+
_raise_fetcher_metadata_unavailable(err)
|
|
79
|
+
return {}
|
|
71
80
|
except ClueException:
|
|
72
81
|
raise
|
|
73
82
|
except Exception as err:
|
|
@@ -157,6 +166,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
|
|
|
157
166
|
Returns:
|
|
158
167
|
ActionResult: The result of the action.
|
|
159
168
|
"""
|
|
169
|
+
validate_plugin_identifier(fetcher_id)
|
|
160
170
|
plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
|
|
161
171
|
|
|
162
172
|
if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
|
|
@@ -188,7 +198,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
|
|
|
188
198
|
|
|
189
199
|
response = request_with_safe_redirects(
|
|
190
200
|
requests.post,
|
|
191
|
-
urljoin(plugin.url, f"fetchers/{fetcher_id}"),
|
|
201
|
+
urljoin(plugin.url, f"fetchers/{quote_plugin_path_segment(fetcher_id)}"),
|
|
192
202
|
get_method=requests.get,
|
|
193
203
|
json=parameters,
|
|
194
204
|
headers=headers,
|
|
@@ -232,6 +242,8 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
|
|
|
232
242
|
Returns:
|
|
233
243
|
ActionResult: The result of the action.
|
|
234
244
|
"""
|
|
245
|
+
validate_plugin_identifier(fetcher_id)
|
|
246
|
+
validate_task_id(task_id)
|
|
235
247
|
plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
|
|
236
248
|
|
|
237
249
|
if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
|
|
@@ -256,7 +268,9 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
|
|
|
256
268
|
remaining_timeout = max(timeout - (monotonic() - metadata_started), 0.001)
|
|
257
269
|
|
|
258
270
|
try:
|
|
259
|
-
req_url = urljoin(
|
|
271
|
+
req_url = urljoin(
|
|
272
|
+
plugin.url, f"fetchers/{quote_plugin_path_segment(fetcher_id)}/status/{quote_plugin_path_segment(task_id)}"
|
|
273
|
+
)
|
|
260
274
|
logger.debug("Getting status for action %s with task_id %s for user %s", req_url, task_id, user["uname"])
|
|
261
275
|
|
|
262
276
|
response = request_with_safe_redirects(
|
|
@@ -141,7 +141,7 @@ log_cli_level = "WARN"
|
|
|
141
141
|
[tool.poetry]
|
|
142
142
|
package-mode = true
|
|
143
143
|
name = "clue-api"
|
|
144
|
-
version = "1.7.0.
|
|
144
|
+
version = "1.7.0.dev469"
|
|
145
145
|
description = "Clue distributed enrichment service"
|
|
146
146
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
147
147
|
license = "MIT"
|
|
@@ -186,7 +186,7 @@ beautifulsoup4 = "^4.13.3"
|
|
|
186
186
|
imgkit = "^1.2.3"
|
|
187
187
|
pillow = ">=11.1,<14.0"
|
|
188
188
|
cart = "^1.2.3"
|
|
189
|
-
setuptools = "<
|
|
189
|
+
setuptools = "<85.0.0"
|
|
190
190
|
requests = "^2.32.5"
|
|
191
191
|
|
|
192
192
|
# Dependencies required for the main server
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from clue.config import CLASSIFICATION
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def validate_classification(classification: str):
|
|
5
|
-
"""Validates the provided classification.
|
|
6
|
-
|
|
7
|
-
Args:
|
|
8
|
-
classification (str): The classification to validate.
|
|
9
|
-
|
|
10
|
-
Raises:
|
|
11
|
-
AssertionError: Raised whenever the provided classification is not valid.
|
|
12
|
-
|
|
13
|
-
Returns:
|
|
14
|
-
str: The validated classification.
|
|
15
|
-
"""
|
|
16
|
-
if not CLASSIFICATION.is_valid(classification):
|
|
17
|
-
raise AssertionError(f"{classification} is not a valid classification.")
|
|
18
|
-
|
|
19
|
-
return classification
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|