clue-api 1.7.0.dev450__tar.gz → 1.7.0.dev459__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.dev450 → clue_api-1.7.0.dev459}/PKG-INFO +1 -1
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/config.py +5 -1
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/network.py +42 -32
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/lookup_service.py +61 -42
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/pyproject.toml +1 -1
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/LICENSE +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/README.md +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/.gitignore +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/base.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/actions.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/auth.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/configs.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/registration.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/static.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/api/v1/sync.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/app.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/cache/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/classification.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/classification.yml +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/dict_utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/exceptions.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/forge.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/json_utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/list_utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/logging/audit.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/logging/format.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/regex.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/str_utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/swagger.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/common/uid.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/config.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/constants/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/constants/env.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/constants/supported_types.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/error.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/extensions/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/extensions/config.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/gunicorn_config.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/healthz.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/helper/discover.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/helper/headers.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/helper/oauth.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/helper/obo.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/helper/plugin_requests.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/actions.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/auth_user.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/fetchers.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/graph.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/jwks.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/model_list.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/base.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/file.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/graph.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/image.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/status.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/results/validation.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/schema.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/selector.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/sync.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/models/validators.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/patched.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/models.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/plugin/utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/py.typed +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/security/__init__.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/security/obo.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/security/utils.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/action_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/auth_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/config_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/jwt_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/mongo_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/type_service.py +0 -0
- {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev459}/clue/services/user_service.py +0 -0
|
@@ -406,7 +406,11 @@ class ExternalSource(BaseModel):
|
|
|
406
406
|
datahub_link: Url | None = Field(description="Link to datahub entry on this enrichment", default=None)
|
|
407
407
|
documentation_link: Url | None = Field(description="Link to documentation on this enrichment", default=None)
|
|
408
408
|
production: bool = Field(
|
|
409
|
-
description=
|
|
409
|
+
description=(
|
|
410
|
+
"Is this source ready for production? Skips entry validation for increased speed. "
|
|
411
|
+
"Bulk responses also bypass result schema validation. "
|
|
412
|
+
"Classification validation and user-clearance filtering always apply."
|
|
413
|
+
),
|
|
410
414
|
default=False,
|
|
411
415
|
)
|
|
412
416
|
include_default: bool = Field(
|
|
@@ -520,7 +520,7 @@ class QueryResult(ResultMetadata):
|
|
|
520
520
|
@field_validator("items")
|
|
521
521
|
@classmethod
|
|
522
522
|
def validate_items(cls, items: list[QueryEntry], info: ValidationInfo) -> list[QueryEntry]: # noqa: ANN102
|
|
523
|
-
"""
|
|
523
|
+
"""Retain only items accessible to the user, failing closed without user classification.
|
|
524
524
|
|
|
525
525
|
Args:
|
|
526
526
|
items (list[QueryEntry]): The items to validate.
|
|
@@ -529,39 +529,49 @@ class QueryResult(ResultMetadata):
|
|
|
529
529
|
Returns:
|
|
530
530
|
list[QueryEntry]: The validated items field.
|
|
531
531
|
"""
|
|
532
|
-
if info.context:
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
532
|
+
if not isinstance(info.context, dict) or "user" not in info.context:
|
|
533
|
+
logger.warning("No user context given, dropping all query items")
|
|
534
|
+
return []
|
|
535
|
+
|
|
536
|
+
return cls.filter_items(items, info.context["user"])
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def filter_items(cls, items: list[QueryEntry], user: Any) -> list[QueryEntry]: # noqa: ANN102
|
|
540
|
+
"""Apply classification authorization independently of response schema validation."""
|
|
541
|
+
user_classification = user.get("classification") if isinstance(user, dict) else None
|
|
542
|
+
if (
|
|
543
|
+
not isinstance(user_classification, str)
|
|
544
|
+
or not user_classification
|
|
545
|
+
or user_classification.upper().partition("//")[0] in {"INV", CLASSIFICATION.INVALID_CLASSIFICATION}
|
|
546
|
+
or not CLASSIFICATION.is_valid(user_classification)
|
|
547
|
+
):
|
|
548
|
+
logger.warning("Missing or invalid user classification, dropping all query items")
|
|
549
|
+
return []
|
|
550
|
+
|
|
551
|
+
filtered_results: list[QueryEntry] = []
|
|
552
|
+
for item in items:
|
|
553
|
+
if (
|
|
554
|
+
"classification" in item.model_fields_set
|
|
555
|
+
and isinstance(item.classification, str)
|
|
556
|
+
and item.classification
|
|
557
|
+
and item.classification.upper().partition("//")[0] not in {"INV", CLASSIFICATION.INVALID_CLASSIFICATION}
|
|
558
|
+
and CLASSIFICATION.is_valid(item.classification)
|
|
559
|
+
and CLASSIFICATION.is_accessible(user_classification, item.classification, ignore_invalid=True)
|
|
560
|
+
):
|
|
561
|
+
filtered_results.append(item)
|
|
559
562
|
else:
|
|
560
|
-
logger.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
+
logger.debug("Removing item with invalid or inaccessible classification")
|
|
564
|
+
|
|
565
|
+
if len(items) > len(filtered_results):
|
|
566
|
+
logger.info(
|
|
567
|
+
"Dropped %s items due to inaccessible classification (user classification: %s)",
|
|
568
|
+
len(items) - len(filtered_results),
|
|
569
|
+
user_classification,
|
|
570
|
+
)
|
|
571
|
+
elif DEBUG:
|
|
572
|
+
logger.debug("All %s values are accessible by user classification %s", len(items), user_classification)
|
|
563
573
|
|
|
564
|
-
return
|
|
574
|
+
return filtered_results
|
|
565
575
|
|
|
566
576
|
|
|
567
577
|
class PluginResponse(BaseModel):
|
|
@@ -72,7 +72,14 @@ def get_client(base_url: str, timeout: float) -> Session:
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def build_result(
|
|
75
|
-
type_name: str,
|
|
75
|
+
type_name: str,
|
|
76
|
+
value: str,
|
|
77
|
+
source: ExternalSource,
|
|
78
|
+
error: Optional[str] = None,
|
|
79
|
+
latency: Optional[float] = None,
|
|
80
|
+
*,
|
|
81
|
+
user: dict[str, Any] | None = None,
|
|
82
|
+
items: list[QueryEntry] | None = None,
|
|
76
83
|
):
|
|
77
84
|
"""Builds the QueryResult object using the provided values.
|
|
78
85
|
|
|
@@ -83,6 +90,8 @@ def build_result(
|
|
|
83
90
|
error (Optional[str], optional): The error that occured during the request. Defaults to None.
|
|
84
91
|
latency (Optional[float], optional): The amount of time between the request and the response (in milliseconds).
|
|
85
92
|
Defaults to None.
|
|
93
|
+
user (dict[str, Any] | None): The user whose classification controls access to items.
|
|
94
|
+
items (list[QueryEntry] | None): The items returned by the source.
|
|
86
95
|
|
|
87
96
|
Returns:
|
|
88
97
|
QueryResult: The QueryResult object built.
|
|
@@ -93,15 +102,19 @@ def build_result(
|
|
|
93
102
|
if DEBUG:
|
|
94
103
|
logger.debug("Building query result for source %s", source.name)
|
|
95
104
|
|
|
96
|
-
return QueryResult(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
return QueryResult.model_validate(
|
|
106
|
+
{
|
|
107
|
+
"type": type_name,
|
|
108
|
+
"value": value,
|
|
109
|
+
"source": source.name,
|
|
110
|
+
"maintainer": source.maintainer,
|
|
111
|
+
"datahub_link": source.datahub_link,
|
|
112
|
+
"documentation_link": source.documentation_link,
|
|
113
|
+
"error": error,
|
|
114
|
+
"latency": latency or 0,
|
|
115
|
+
"items": items or [],
|
|
116
|
+
},
|
|
117
|
+
context={"user": user},
|
|
105
118
|
)
|
|
106
119
|
|
|
107
120
|
|
|
@@ -275,12 +288,14 @@ def parse_response(source: ExternalSource, user: dict[str, Any], api_response: A
|
|
|
275
288
|
)
|
|
276
289
|
|
|
277
290
|
if source.production:
|
|
278
|
-
logger.debug(
|
|
279
|
-
items
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
logger.debug("Skipping validation for production source %s", source.name)
|
|
292
|
+
items = [
|
|
293
|
+
QueryEntry.model_construct(**{**data, "classification": data.get("classification")})
|
|
294
|
+
for data in api_response
|
|
295
|
+
]
|
|
296
|
+
return QueryResult.filter_items(items, user)
|
|
282
297
|
|
|
283
|
-
return
|
|
298
|
+
return [QueryEntry.model_validate(data, context={"user": user}) for data in api_response]
|
|
284
299
|
|
|
285
300
|
|
|
286
301
|
def parse_bulk_response(
|
|
@@ -302,9 +317,6 @@ def parse_bulk_response(
|
|
|
302
317
|
"""
|
|
303
318
|
bulk_result: dict[str, dict[str, QueryResult]] = {}
|
|
304
319
|
|
|
305
|
-
if source.production:
|
|
306
|
-
logger.debug(f"Skipping validation for production source {source.name}")
|
|
307
|
-
|
|
308
320
|
with capture_span(f"{source.name}-bulk", "parsing"):
|
|
309
321
|
for type in api_response:
|
|
310
322
|
bulk_result.setdefault(type, {})
|
|
@@ -322,6 +334,8 @@ def parse_bulk_response(
|
|
|
322
334
|
data = {**data, **api_response[type][value], "latency": latency or 0.0}
|
|
323
335
|
|
|
324
336
|
if source.production:
|
|
337
|
+
logger.debug("Skipping validation for production source %s", source.name)
|
|
338
|
+
data["items"] = parse_response(source, user, data.get("items", []))
|
|
325
339
|
bulk_result[type][value] = QueryResult.model_construct(**data)
|
|
326
340
|
else:
|
|
327
341
|
bulk_result[type][value] = QueryResult.model_validate(
|
|
@@ -342,16 +356,12 @@ def handle_validation_error(source: ExternalSource, err: ValidationError) -> str
|
|
|
342
356
|
Returns:
|
|
343
357
|
str: A formatted error message.
|
|
344
358
|
"""
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
pydantic_errs.append(f'"{loc}": {validation_err["msg"]}')
|
|
352
|
-
|
|
353
|
-
err_msg = f"{source.name} returned an improperly formatted response: {', '.join(pydantic_errs)}"
|
|
354
|
-
err_id = log_error(logger, err_msg, err)
|
|
359
|
+
err_msg = f"{source.name} returned an improperly formatted response"
|
|
360
|
+
details = [
|
|
361
|
+
{"type": detail["type"], "loc": detail["loc"]}
|
|
362
|
+
for detail in err.errors(include_input=False, include_context=False)
|
|
363
|
+
]
|
|
364
|
+
err_id = log_error(logger, err_msg, details)
|
|
355
365
|
return f"{err_msg}. Error ID: {err_id}"
|
|
356
366
|
|
|
357
367
|
|
|
@@ -373,7 +383,7 @@ def query_external(
|
|
|
373
383
|
if apm_transaction:
|
|
374
384
|
execution_context.set_transaction(apm_transaction)
|
|
375
385
|
|
|
376
|
-
finish_result = functools.partial(build_result, type_name, value, source)
|
|
386
|
+
finish_result = functools.partial(build_result, type_name, value, source, user=user)
|
|
377
387
|
|
|
378
388
|
with capture_span(query_external.__name__, span_type="greenlet"):
|
|
379
389
|
supported_types = type_service.get_supported_types(
|
|
@@ -433,17 +443,16 @@ def query_external(
|
|
|
433
443
|
)
|
|
434
444
|
|
|
435
445
|
try:
|
|
436
|
-
result = finish_result(latency=(time.perf_counter() - start) * 1000)
|
|
437
|
-
|
|
438
446
|
api_response = response["api_response"]
|
|
439
|
-
|
|
440
|
-
|
|
447
|
+
result = finish_result(
|
|
448
|
+
latency=(time.perf_counter() - start) * 1000,
|
|
449
|
+
items=parse_response(source, user, api_response) if api_response else [],
|
|
450
|
+
)
|
|
441
451
|
|
|
442
452
|
logger.debug("Returning valid result from source %s", source)
|
|
443
453
|
|
|
444
454
|
return result
|
|
445
455
|
except ValidationError as err:
|
|
446
|
-
logger.exception("Validation error on response from %s", source)
|
|
447
456
|
return finish_result(
|
|
448
457
|
error=handle_validation_error(source, err),
|
|
449
458
|
latency=(time.perf_counter() - start) * 1000,
|
|
@@ -495,7 +504,7 @@ def enrich(type_name: str, value: str, user: dict[str, Any]): # noqa: C901
|
|
|
495
504
|
if not query_sources and not source.include_default:
|
|
496
505
|
continue
|
|
497
506
|
|
|
498
|
-
finish_result = functools.partial(build_result, type_name, value, source)
|
|
507
|
+
finish_result = functools.partial(build_result, type_name, value, source, user=user)
|
|
499
508
|
|
|
500
509
|
try:
|
|
501
510
|
access_token, obo_access_token = get_obo_access_token(source, user)
|
|
@@ -547,7 +556,11 @@ def enrich(type_name: str, value: str, user: dict[str, Any]): # noqa: C901
|
|
|
547
556
|
results[source.name] = result
|
|
548
557
|
else:
|
|
549
558
|
results[source.name] = build_result(
|
|
550
|
-
type_name,
|
|
559
|
+
type_name,
|
|
560
|
+
value,
|
|
561
|
+
source,
|
|
562
|
+
"Request Timed Out" if not greenlet.exception else str(greenlet.exception),
|
|
563
|
+
user=user,
|
|
551
564
|
)
|
|
552
565
|
|
|
553
566
|
thread_pool.kill(block=False)
|
|
@@ -583,7 +596,9 @@ def bulk_query_external( # noqa: C901
|
|
|
583
596
|
bulk_result.setdefault(entry.type, {})
|
|
584
597
|
|
|
585
598
|
if not supported_types or entry.type not in supported_types:
|
|
586
|
-
bulk_result[entry.type][entry.value] = build_result(
|
|
599
|
+
bulk_result[entry.type][entry.value] = build_result(
|
|
600
|
+
entry.type, entry.value, source, "invalid_type", user=user
|
|
601
|
+
)
|
|
587
602
|
continue
|
|
588
603
|
|
|
589
604
|
filtered_data.append(entry)
|
|
@@ -609,7 +624,9 @@ def bulk_query_external( # noqa: C901
|
|
|
609
624
|
if quota_error := user_service.check_quota(source, user):
|
|
610
625
|
for entry in data:
|
|
611
626
|
bulk_result.setdefault(entry.type, {})
|
|
612
|
-
bulk_result[entry.type][entry.value] = build_result(
|
|
627
|
+
bulk_result[entry.type][entry.value] = build_result(
|
|
628
|
+
entry.type, entry.value, source, error=quota_error, user=user
|
|
629
|
+
)
|
|
613
630
|
|
|
614
631
|
return bulk_result
|
|
615
632
|
|
|
@@ -652,7 +669,7 @@ def bulk_query_external( # noqa: C901
|
|
|
652
669
|
if error:
|
|
653
670
|
for entry in data:
|
|
654
671
|
bulk_result[entry.type][entry.value] = build_result(
|
|
655
|
-
entry.type, entry.value, source, error=error, latency=latency
|
|
672
|
+
entry.type, entry.value, source, error=error, latency=latency, user=user
|
|
656
673
|
)
|
|
657
674
|
|
|
658
675
|
return bulk_result
|
|
@@ -663,7 +680,7 @@ def bulk_query_external( # noqa: C901
|
|
|
663
680
|
if not api_response:
|
|
664
681
|
for entry in data:
|
|
665
682
|
bulk_result[entry.type][entry.value] = build_result(
|
|
666
|
-
entry.type, entry.value, source, latency=latency
|
|
683
|
+
entry.type, entry.value, source, latency=latency, user=user
|
|
667
684
|
)
|
|
668
685
|
else:
|
|
669
686
|
bulk_result = parse_bulk_response(source, user, api_response, latency)
|
|
@@ -672,7 +689,7 @@ def bulk_query_external( # noqa: C901
|
|
|
672
689
|
|
|
673
690
|
for entry in data:
|
|
674
691
|
bulk_result[entry.type][entry.value] = build_result(
|
|
675
|
-
entry.type, entry.value, source, error=error_message, latency=latency
|
|
692
|
+
entry.type, entry.value, source, error=error_message, latency=latency, user=user
|
|
676
693
|
)
|
|
677
694
|
|
|
678
695
|
return bulk_result
|
|
@@ -745,7 +762,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
|
|
|
745
762
|
except AuthenticationException as err:
|
|
746
763
|
for entry in data:
|
|
747
764
|
bulk_result[entry.type][entry.value][source.name] = build_result(
|
|
748
|
-
entry.type, entry.value, source, err.message
|
|
765
|
+
entry.type, entry.value, source, err.message, user=user
|
|
749
766
|
)
|
|
750
767
|
continue
|
|
751
768
|
|
|
@@ -782,6 +799,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
|
|
|
782
799
|
f"Selector classification ({entry.classification}) exceeds max classification "
|
|
783
800
|
f"of source: {source.name} ({source.max_classification})."
|
|
784
801
|
),
|
|
802
|
+
user=user,
|
|
785
803
|
)
|
|
786
804
|
|
|
787
805
|
continue
|
|
@@ -828,6 +846,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
|
|
|
828
846
|
source,
|
|
829
847
|
"Request Timed Out" if not greenlet.exception else str(greenlet.exception),
|
|
830
848
|
(time.perf_counter() - start) * 1000,
|
|
849
|
+
user=user,
|
|
831
850
|
)
|
|
832
851
|
|
|
833
852
|
continue
|
|
@@ -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.dev459"
|
|
145
145
|
description = "Clue distributed enrichment service"
|
|
146
146
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
147
147
|
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
|
|
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
|