clue-api 1.7.0.dev464__tar.gz → 1.7.0.dev465__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.dev464 → clue_api-1.7.0.dev465}/PKG-INFO +1 -1
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/auth.py +6 -5
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/config.py +73 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/pyproject.toml +1 -1
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/LICENSE +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/README.md +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/.gitignore +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/base.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/documentation.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/actions.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/configs.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/registration.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/static.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/sync.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v2/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v2/lookup.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/app.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/cache/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/classification.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/classification.yml +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/dict_utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/exceptions.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/forge.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/json_utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/list_utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/logging/audit.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/logging/format.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/regex.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/str_utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/swagger.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/uid.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/config.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/constants/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/constants/env.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/constants/supported_types.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/error.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/extensions/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/extensions/config.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/gunicorn_config.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/healthz.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/discover.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/headers.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/oauth.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/obo.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/plugin_requests.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/actions.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/auth_user.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/fetchers.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/graph.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/jwks.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/model_list.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/network.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/base.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/file.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/graph.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/image.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/status.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/validation.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/schema.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/selector.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/sync.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/validators.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/patched.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/models.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/py.typed +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/security/__init__.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/security/obo.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/security/utils.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/action_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/auth_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/config_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/jwt_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/lookup_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/mongo_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/type_service.py +0 -0
- {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/user_service.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
|
-
from urllib.parse import
|
|
2
|
+
from urllib.parse import urlencode
|
|
3
3
|
|
|
4
4
|
from authlib.integrations.base_client import OAuthError
|
|
5
5
|
from flask import current_app, request
|
|
@@ -121,10 +121,11 @@ def login(**_) -> dict[str, Any]: # noqa: C901
|
|
|
121
121
|
|
|
122
122
|
# This means that they want to start the oauth process, so we'll redirect them to their chosen provider
|
|
123
123
|
if "code" not in request.args and not refresh_token:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
# The redirect URI must only come from trusted configuration, never from request headers
|
|
125
|
+
if not config.api.frontend_url:
|
|
126
|
+
raise ClueValueError("api.frontend_url not set! Cannot start OAuth")
|
|
127
|
+
|
|
128
|
+
redirect_uri = f"{config.api.frontend_url.rstrip('/')}/login?{urlencode({'provider': oauth_provider})}"
|
|
128
129
|
return provider.authorize_redirect(redirect_uri=redirect_uri, nonce=request.args.get("nonce", None))
|
|
129
130
|
|
|
130
131
|
# At this point we know the code exists, so we're good to use that to exchange for an JSON Web Token with
|
|
@@ -543,6 +543,10 @@ class API(BaseModel):
|
|
|
543
543
|
audit: bool = Field(description="Should API calls be audited and saved to a separate log file?", default=True)
|
|
544
544
|
debug: bool = Field(description="Enable debugging?", default=False)
|
|
545
545
|
discover_url: str | None = Field(description="Discover URL", default=None)
|
|
546
|
+
frontend_url: str | None = Field(
|
|
547
|
+
description="Trusted Clue UI origin used to build the OAuth redirect URI. Required when OAuth is enabled.",
|
|
548
|
+
default=None,
|
|
549
|
+
)
|
|
546
550
|
external_sources: list[ExternalSource] = Field(description="List of external sources to query", default=[])
|
|
547
551
|
registration_allowed_origins: list[str] = Field(
|
|
548
552
|
description="Exact URL origins permitted for runtime external source registration", default=[]
|
|
@@ -562,6 +566,67 @@ class API(BaseModel):
|
|
|
562
566
|
description="Validate if the XSRF token matches the randomly generated token for the session", default=True
|
|
563
567
|
)
|
|
564
568
|
|
|
569
|
+
@field_validator("frontend_url")
|
|
570
|
+
@classmethod
|
|
571
|
+
def validate_frontend_url(cls, frontend_url: str | None) -> str | None: # noqa: ANN102
|
|
572
|
+
"""Validates that frontend_url is a plain http(s) origin, and normalizes it to ``scheme://host[:port]``.
|
|
573
|
+
|
|
574
|
+
The UI is served from the root of its origin (no router basename, absolute /api and /login paths), so a
|
|
575
|
+
path other than an optional trailing slash is rejected.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
frontend_url (str | None): The configured UI origin.
|
|
579
|
+
|
|
580
|
+
Raises:
|
|
581
|
+
ValueError: Raised whenever the URL is not a safe, unambiguous http(s) origin.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
str | None: The validated origin, without a trailing slash.
|
|
585
|
+
"""
|
|
586
|
+
if frontend_url is None:
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
error = (
|
|
590
|
+
"frontend_url must be an http(s) origin such as https://clue.example[:port] (an optional trailing slash "
|
|
591
|
+
"is allowed) with a valid ASCII host, and no credentials, path, query, fragment or percent-encoding"
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# urlsplit silently drops tabs/newlines, so check the raw string first
|
|
595
|
+
if any(not char.isprintable() or char.isspace() or ord(char) > 126 or char in "\\%" for char in frontend_url):
|
|
596
|
+
raise ValueError(error)
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
parsed = urlsplit(frontend_url)
|
|
600
|
+
port = parsed.port
|
|
601
|
+
host = parsed.hostname
|
|
602
|
+
except ValueError as err:
|
|
603
|
+
raise ValueError(error) from err
|
|
604
|
+
|
|
605
|
+
if (
|
|
606
|
+
parsed.scheme not in {"http", "https"}
|
|
607
|
+
or not host
|
|
608
|
+
or "@" in parsed.netloc
|
|
609
|
+
or parsed.netloc.endswith(":")
|
|
610
|
+
or (port is not None and port < 1)
|
|
611
|
+
or parsed.path not in {"", "/"}
|
|
612
|
+
or parsed.query
|
|
613
|
+
or parsed.fragment
|
|
614
|
+
or "?" in frontend_url
|
|
615
|
+
or "#" in frontend_url
|
|
616
|
+
):
|
|
617
|
+
raise ValueError(error)
|
|
618
|
+
|
|
619
|
+
TypeAdapter(HttpUrl).validate_python(frontend_url)
|
|
620
|
+
|
|
621
|
+
# Prevent Pydantic from accepting invalid domains such as clue..com
|
|
622
|
+
if ":" not in host:
|
|
623
|
+
if any(char not in "abcdefghijklmnopqrstuvwxyz0123456789_-." for char in host) or any(
|
|
624
|
+
not label or label.startswith("-") or label.endswith("-") for label in host.split(".")
|
|
625
|
+
):
|
|
626
|
+
raise ValueError(error)
|
|
627
|
+
|
|
628
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
629
|
+
|
|
565
630
|
|
|
566
631
|
class Retention(BaseModel):
|
|
567
632
|
enabled: Annotated[bool, Field(description="Should records be cached for users?")] = True
|
|
@@ -622,6 +687,14 @@ class Config(BaseSettings):
|
|
|
622
687
|
env_nested_delimiter="__",
|
|
623
688
|
)
|
|
624
689
|
|
|
690
|
+
@model_validator(mode="after")
|
|
691
|
+
def validate_oauth_frontend_url(self: Self) -> Self:
|
|
692
|
+
"""The OAuth redirect URI is built from api.frontend_url, so it must be set when OAuth is enabled."""
|
|
693
|
+
if self.auth.oauth.enabled and not self.api.frontend_url:
|
|
694
|
+
raise ValueError("api.frontend_url must be set when auth.oauth.enabled is true")
|
|
695
|
+
|
|
696
|
+
return self
|
|
697
|
+
|
|
625
698
|
@classmethod
|
|
626
699
|
def settings_customise_sources(
|
|
627
700
|
cls, # noqa: ANN102
|
|
@@ -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.dev465"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|