clue-api 1.6.0.dev359__tar.gz → 1.6.0.dev370__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.6.0.dev359 → clue_api-1.6.0.dev370}/PKG-INFO +1 -1
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/lookup_service.py +19 -11
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/mongo_service.py +42 -6
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/pyproject.toml +1 -1
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/LICENSE +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/README.md +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/.gitignore +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/base.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/actions.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/auth.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/configs.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/registration.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/static.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/api/v1/sync.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/app.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/cache/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/classification.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/classification.yml +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/dict_utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/exceptions.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/forge.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/json_utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/list_utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/logging/audit.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/logging/format.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/regex.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/str_utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/swagger.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/common/uid.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/config.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/constants/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/constants/env.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/constants/supported_types.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/error.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/extensions/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/extensions/config.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/gunicorn_config.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/healthz.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/helper/discover.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/helper/headers.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/helper/oauth.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/actions.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/config.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/fetchers.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/graph.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/model_list.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/network.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/base.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/file.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/graph.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/image.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/status.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/results/validation.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/schema.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/selector.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/sync.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/models/validators.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/patched.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/models.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/plugin/utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/py.typed +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/security/__init__.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/security/obo.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/security/utils.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/action_service.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/auth_service.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/config_service.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/jwt_service.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/type_service.py +0 -0
- {clue_api-1.6.0.dev359 → clue_api-1.6.0.dev370}/clue/services/user_service.py +0 -0
|
@@ -678,6 +678,13 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
|
|
|
678
678
|
query_sources = query_params.query_sources
|
|
679
679
|
excluded_sources = query_params.excluded_sources
|
|
680
680
|
available_sources = get_sources(user)
|
|
681
|
+
selected_sources = [
|
|
682
|
+
source
|
|
683
|
+
for source in available_sources
|
|
684
|
+
if (not query_sources or source.name in query_sources)
|
|
685
|
+
and source.name not in excluded_sources
|
|
686
|
+
and (query_sources or source.include_default)
|
|
687
|
+
]
|
|
681
688
|
|
|
682
689
|
logger.debug(
|
|
683
690
|
f"Beginning enrichment for {len(data)} selectors on sources "
|
|
@@ -698,12 +705,20 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
|
|
|
698
705
|
bulk_result.setdefault(entry.type, {})
|
|
699
706
|
bulk_result[entry.type].setdefault(entry.value, {})
|
|
700
707
|
|
|
701
|
-
|
|
702
|
-
|
|
708
|
+
if not selected_sources:
|
|
709
|
+
raise InvalidDataException("You must provide at least one source.")
|
|
703
710
|
|
|
711
|
+
pool_size = min(len(data) * len(selected_sources), int(os.environ.get("EXECUTOR_THREADS", 32)))
|
|
712
|
+
thread_pool = Pool(pool_size)
|
|
704
713
|
existing_results: dict[str, list[dict[str, str]]] = {}
|
|
705
714
|
if config.ui.replication:
|
|
706
|
-
|
|
715
|
+
if query_params.no_cache:
|
|
716
|
+
# no_cache bypasses the mongo cache check entirely (forcing every source to be re-queried) and
|
|
717
|
+
# invalidates whatever was previously cached so the fresh results replace it, rather than piling
|
|
718
|
+
# up as duplicates alongside it.
|
|
719
|
+
mongo_service.invalidate_existing(user["uname"], "selectors", data, selected_sources)
|
|
720
|
+
else:
|
|
721
|
+
existing_results = mongo_service.existing_results(user["uname"], "selectors", data, selected_sources)
|
|
707
722
|
|
|
708
723
|
sources_per_entry: dict[tuple[str, str], tuple[list[str], list[str]]] = {}
|
|
709
724
|
for entry in data:
|
|
@@ -723,14 +738,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
|
|
|
723
738
|
)
|
|
724
739
|
|
|
725
740
|
greenlets: list[tuple[list[Selector], ExternalSource, Greenlet[Any, dict[str, dict[str, QueryResult]]]]] = []
|
|
726
|
-
for source in
|
|
727
|
-
if query_sources and source.name not in query_sources:
|
|
728
|
-
continue
|
|
729
|
-
if excluded_sources and source.name in excluded_sources:
|
|
730
|
-
continue
|
|
731
|
-
if not query_sources and not source.include_default:
|
|
732
|
-
continue
|
|
733
|
-
|
|
741
|
+
for source in selected_sources:
|
|
734
742
|
obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
|
|
735
743
|
|
|
736
744
|
if error:
|
|
@@ -4,7 +4,7 @@ load_dotenv()
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import time
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
from flask import Response
|
|
@@ -19,7 +19,7 @@ from clue.config import config, get_redis
|
|
|
19
19
|
from clue.models.config import ExternalSource
|
|
20
20
|
from clue.models.schema import get_bson_schema
|
|
21
21
|
from clue.models.selector import Selector
|
|
22
|
-
from clue.models.sync import ChangeRow, Checkpoint, PublishEvent, SelectorDocument
|
|
22
|
+
from clue.models.sync import ChangeRow, Checkpoint, PublishEvent, SelectorDocument, generate_updated_at
|
|
23
23
|
|
|
24
24
|
logger = get_logger(__file__)
|
|
25
25
|
|
|
@@ -329,9 +329,8 @@ def existing_results(
|
|
|
329
329
|
return {}
|
|
330
330
|
|
|
331
331
|
try:
|
|
332
|
-
types = [selector.type for selector in selectors]
|
|
333
|
-
values = [selector.value for selector in selectors]
|
|
334
332
|
sources = [source.name for source in external_sources]
|
|
333
|
+
selector_pairs = [{"type": selector.type, "value": selector.value} for selector in selectors]
|
|
335
334
|
|
|
336
335
|
raw_result = (
|
|
337
336
|
_get_collection(user, collection)
|
|
@@ -339,11 +338,14 @@ def existing_results(
|
|
|
339
338
|
[
|
|
340
339
|
{
|
|
341
340
|
"$match": {
|
|
342
|
-
"type": {"$in": types},
|
|
343
|
-
"value": {"$in": values},
|
|
344
341
|
"source": {"$in": sources},
|
|
345
342
|
"_deleted": False,
|
|
346
343
|
"error": None,
|
|
344
|
+
"$and": [
|
|
345
|
+
{"$or": selector_pairs},
|
|
346
|
+
# Only records with no expiry, or one that hasn't passed yet, count as "existing".
|
|
347
|
+
{"$or": [{"expiry": None}, {"expiry": {"$gt": datetime.now(timezone.utc)}}]},
|
|
348
|
+
],
|
|
347
349
|
}
|
|
348
350
|
},
|
|
349
351
|
{"$group": {"_id": "$source", "records": {"$push": {"type": "$type", "value": "$value"}}}},
|
|
@@ -356,3 +358,37 @@ def existing_results(
|
|
|
356
358
|
except ClueRuntimeError:
|
|
357
359
|
# ClueRuntimeError if mongodb isn't functional - we'll silently fail so enrichment can continue
|
|
358
360
|
return {}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def invalidate_existing(
|
|
364
|
+
user: str, collection: str, selectors: list[Selector], external_sources: list[ExternalSource]
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Mark cached documents matching the given selectors/sources as deleted.
|
|
367
|
+
|
|
368
|
+
Used when a caller requests `no_cache`, so the stale cached results don't stick around
|
|
369
|
+
(as duplicates) once the freshly queried results are replicated back into the collection.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
user (str): The username to use as the collection name.
|
|
373
|
+
collection (str): The collection name within the user's database.
|
|
374
|
+
selectors (list[Selector]): List of selectors (type/value pairs) to invalidate.
|
|
375
|
+
external_sources (list[ExternalSource]): List of external sources to filter by.
|
|
376
|
+
"""
|
|
377
|
+
if not config.ui.replication:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
sources = [source.name for source in external_sources]
|
|
382
|
+
selector_pairs = [{"type": selector.type, "value": selector.value} for selector in selectors]
|
|
383
|
+
|
|
384
|
+
_get_collection(user, collection).update_many(
|
|
385
|
+
{
|
|
386
|
+
"source": {"$in": sources},
|
|
387
|
+
"_deleted": False,
|
|
388
|
+
"$or": selector_pairs,
|
|
389
|
+
},
|
|
390
|
+
{"$set": {"_deleted": True, "updated_at": generate_updated_at()}},
|
|
391
|
+
)
|
|
392
|
+
except ClueRuntimeError:
|
|
393
|
+
# ClueRuntimeError if mongodb isn't functional - we'll silently fail so enrichment can continue
|
|
394
|
+
pass
|
|
@@ -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.6.0.
|
|
144
|
+
version = "1.6.0.dev370"
|
|
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
|