clue-api 1.3.0.dev15__tar.gz → 1.3.0.dev28__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.
Files changed (91) hide show
  1. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/PKG-INFO +1 -1
  2. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/lookup.py +4 -1
  3. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/constants/supported_types.py +1 -1
  4. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/config.py +1 -1
  5. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/selector.py +4 -4
  6. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/auth_service.py +7 -16
  7. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/jwt_service.py +23 -2
  8. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/pyproject.toml +1 -1
  9. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/LICENSE +0 -0
  10. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/README.md +0 -0
  11. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/.gitignore +0 -0
  12. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/__init__.py +0 -0
  13. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/__init__.py +0 -0
  14. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/base.py +0 -0
  15. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/__init__.py +0 -0
  16. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/actions.py +0 -0
  17. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/auth.py +0 -0
  18. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/configs.py +0 -0
  19. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/fetchers.py +0 -0
  20. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/registration.py +0 -0
  21. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/static.py +0 -0
  22. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/app.py +0 -0
  23. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/cache/__init__.py +0 -0
  24. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/__init__.py +0 -0
  25. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/classification.py +0 -0
  26. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/classification.yml +0 -0
  27. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/dict_utils.py +0 -0
  28. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/exceptions.py +0 -0
  29. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/forge.py +0 -0
  30. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/json_utils.py +0 -0
  31. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/list_utils.py +0 -0
  32. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/logging/__init__.py +0 -0
  33. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/logging/audit.py +0 -0
  34. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/logging/format.py +0 -0
  35. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/regex.py +0 -0
  36. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/str_utils.py +0 -0
  37. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/swagger.py +0 -0
  38. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/uid.py +0 -0
  39. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/config.py +0 -0
  40. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/constants/__init__.py +0 -0
  41. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/cronjobs/__init__.py +0 -0
  42. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/cronjobs/plugins.py +0 -0
  43. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/error.py +0 -0
  44. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/extensions/__init__.py +0 -0
  45. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/extensions/config.py +0 -0
  46. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/gunicorn_config.py +0 -0
  47. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/healthz.py +0 -0
  48. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/helper/discover.py +0 -0
  49. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/helper/headers.py +0 -0
  50. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/helper/oauth.py +0 -0
  51. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/__init__.py +0 -0
  52. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/actions.py +0 -0
  53. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/fetchers.py +0 -0
  54. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/graph.py +0 -0
  55. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/model_list.py +0 -0
  56. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/network.py +0 -0
  57. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/__init__.py +0 -0
  58. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/base.py +0 -0
  59. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/graph.py +0 -0
  60. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/image.py +0 -0
  61. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/status.py +0 -0
  62. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/validation.py +0 -0
  63. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/validators.py +0 -0
  64. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/patched.py +0 -0
  65. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/__init__.py +0 -0
  66. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/__init__.py +0 -0
  67. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/central_server.py +0 -0
  68. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/email_render.py +0 -0
  69. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/token.py +0 -0
  70. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/trino.py +0 -0
  71. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/models.py +0 -0
  72. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/utils.py +0 -0
  73. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/py.typed +0 -0
  74. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/__init__.py +0 -0
  75. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/__init__.py +0 -0
  76. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/cache.py +0 -0
  77. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/events.py +0 -0
  78. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/hash.py +0 -0
  79. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/queues/__init__.py +0 -0
  80. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/queues/comms.py +0 -0
  81. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/set.py +0 -0
  82. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  83. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/security/__init__.py +0 -0
  84. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/security/obo.py +0 -0
  85. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/security/utils.py +0 -0
  86. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/action_service.py +0 -0
  87. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/config_service.py +0 -0
  88. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/fetcher_service.py +0 -0
  89. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/lookup_service.py +0 -0
  90. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/type_service.py +0 -0
  91. {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/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.3.0.dev15
3
+ Version: 1.3.0.dev28
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -205,13 +205,16 @@ def enrich(type_name: str, value: str, **kwargs) -> dict[str, QueryResult]:
205
205
  user = kwargs["user"]
206
206
 
207
207
  # For backwards compatability, if eml is used it is replaced with email
208
- type_name = type_name.replace("eml", "email")
208
+ type_name = type_name.lower().replace("eml", "email")
209
209
 
210
210
  if type_name == "telemetry":
211
211
  try:
212
212
  json.loads(urllib.parse.unquote(value))
213
213
  except json.JSONDecodeError:
214
214
  return bad_request(err="If type is telemetry, value must be a valid JSON object.")
215
+ else:
216
+ # Normalize to lowercase all non-telemetry inputs
217
+ value = value.lower()
215
218
 
216
219
  # re-encode the type after being decoded going through flask/wsgi route
217
220
  value = urllib.parse.quote(value, safe="")
@@ -37,4 +37,4 @@ SUPPORTED_TYPES = {
37
37
  "tenant-id": UUID4_REGEX,
38
38
  }
39
39
 
40
- CASE_INSENSITIVE_TYPES = ["ip", "domain", "port", "tenant-id"]
40
+ CASE_INSENSITIVE_TYPES = ["ip", "domain", "port", "tenant-id", "hbs_oid", "hbs_agent_id"]
@@ -69,7 +69,7 @@ class OAuthProvider(BaseModel):
69
69
  classification_map: dict[str, str] = Field(
70
70
  default={}, description="A mapping of OAuth groups to classification levels"
71
71
  )
72
- access_token_url: str | None = Field(description="URL to get access token")
72
+ access_token_url: str = Field(description="URL to get access token")
73
73
  authorize_url: str | None = Field(description="URL used to authorize access to a resource")
74
74
  api_base_url: str | None = Field(description="Base URL for downloading the user's and groups info")
75
75
  audience: str | None = Field(
@@ -1,9 +1,10 @@
1
1
  # ruff: noqa: D101
2
2
  import ipaddress
3
3
  import json
4
+ from typing import Annotated
4
5
 
5
6
  from flask import request
6
- from pydantic import BaseModel, Field, model_validator
7
+ from pydantic import BaseModel, Field, StringConstraints, model_validator
7
8
  from typing_extensions import Self
8
9
 
9
10
  from clue.common.logging import get_logger
@@ -14,7 +15,7 @@ logger = get_logger(__file__)
14
15
 
15
16
 
16
17
  class Selector(BaseModel):
17
- type: str
18
+ type: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)]
18
19
  value: str
19
20
  classification: str | None = Field(default=None)
20
21
  sources: list[str] | None = Field(default=None)
@@ -41,8 +42,7 @@ class Selector(BaseModel):
41
42
  json.loads(self.value)
42
43
  except json.JSONDecodeError as e:
43
44
  raise AssertionError("If type is telemetry, value must be a valid JSON object.") from e
44
-
45
- if self.type in CASE_INSENSITIVE_TYPES:
45
+ elif self.type in CASE_INSENSITIVE_TYPES:
46
46
  self.value = self.value.lower()
47
47
 
48
48
  if not self.classification:
@@ -2,7 +2,7 @@ import base64
2
2
  import hashlib
3
3
  from typing import Any, Optional, Union
4
4
 
5
- import elasticapm
5
+ from elasticapm.traces import capture_span
6
6
  from flask import request
7
7
 
8
8
  from clue.common.exceptions import (
@@ -17,7 +17,7 @@ from clue.config import config, get_redis
17
17
  from clue.models.config import ExternalSource
18
18
  from clue.remote.datatypes.set import ExpiringSet
19
19
  from clue.security.obo import get_obo_token
20
- from clue.security.utils import decode_jwt_payload, generate_random_secret
20
+ from clue.security.utils import generate_random_secret
21
21
  from clue.services import jwt_service, user_service
22
22
 
23
23
  logger = get_logger(__file__)
@@ -123,7 +123,7 @@ def validate_token(username: str, token: str) -> Optional[list[str]]:
123
123
  return None
124
124
 
125
125
 
126
- @elasticapm.capture_span(span_type="authentication")
126
+ @capture_span(span_type="authentication")
127
127
  def bearer_auth(
128
128
  data: str, skip_jwt: bool = False, skip_internal: bool = False
129
129
  ) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
@@ -164,7 +164,7 @@ def bearer_auth(
164
164
  raise InvalidDataException("Not a valid authentication type for this endpoint.")
165
165
 
166
166
 
167
- @elasticapm.capture_span(span_type="authentication")
167
+ @capture_span(span_type="authentication")
168
168
  def validate_apikey(name: str, apikey: str) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
169
169
  """This function identifies the user via the internal API key functionality.
170
170
 
@@ -243,7 +243,7 @@ def decode_b64(b64_str: str) -> str:
243
243
  raise InvalidDataException("Basic authentication data must be base64 encoded") from e
244
244
 
245
245
 
246
- @elasticapm.capture_span(span_type="authentication")
246
+ @capture_span(span_type="authentication")
247
247
  def basic_auth(
248
248
  data: str, is_base64: bool = True, skip_apikey: bool = False, skip_password: bool = False
249
249
  ) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
@@ -267,6 +267,7 @@ def basic_auth(
267
267
  [username, data] = key_pair.split(":", maxsplit=1)
268
268
 
269
269
  validated_user = None
270
+ priv = None
270
271
  if not skip_apikey:
271
272
  validated_user, priv = validate_apikey(username, data)
272
273
 
@@ -299,16 +300,6 @@ def basic_auth(
299
300
  return validated_user, priv
300
301
 
301
302
 
302
- def extract_audience(access_token: str) -> list[str]:
303
- "Extract the audience from an encoded JWT."
304
- audience: list[str] | str | None = decode_jwt_payload(access_token).get("aud", None)
305
-
306
- if not audience:
307
- return []
308
-
309
- return [audience] if not isinstance(audience, list) else audience
310
-
311
-
312
303
  # TODO: sa-clue support
313
304
  def check_obo(source: ExternalSource, access_token: str, username: str) -> tuple[Optional[str], Optional[str]]:
314
305
  """Checks whether a token's audience matches the source, and if it doesn't, tries to get an OBO token for the source
@@ -333,7 +324,7 @@ def check_obo(source: ExternalSource, access_token: str, username: str) -> tuple
333
324
 
334
325
  access_token = sa_token
335
326
 
336
- audience = extract_audience(access_token)
327
+ audience = jwt_service.extract_audience(access_token)
337
328
 
338
329
  # Check if this is a standard clue token
339
330
  if jwt_service.get_audience(jwt_service.get_provider(access_token)) in audience:
@@ -11,6 +11,7 @@ from jwt.api_jwk import PyJWK
11
11
  from clue.common.exceptions import ClueKeyError, ClueValueError
12
12
  from clue.common.logging import get_logger
13
13
  from clue.config import cache, config
14
+ from clue.security.utils import decode_jwt_payload
14
15
 
15
16
  logger = get_logger(__file__)
16
17
 
@@ -19,6 +20,9 @@ def get_jwk(access_token: str) -> PyJWK:
19
20
  """Get the JSON Web Key associated with the given JWT"""
20
21
  # "kid" is the JSON Web Key's identifier. It tells us which key was used to validate the token.
21
22
  kid = jwt.get_unverified_header(access_token).get("kid")
23
+ if not kid or not isinstance(kid, str):
24
+ raise ClueValueError("Unexpected kid value in access token: %s", kid)
25
+
22
26
  jwks, _ = get_jwks()
23
27
 
24
28
  try:
@@ -50,6 +54,9 @@ def get_provider(access_token: str) -> str:
50
54
  """
51
55
  # "kid" is the JSON Web Key's identifier. It tells us which key was used to validate the token.
52
56
  kid = jwt.get_unverified_header(access_token).get("kid")
57
+ if not kid or not isinstance(kid, str):
58
+ raise ClueValueError("Unexpected kid value in access token: %s", kid)
59
+
53
60
  _, providers = get_jwks()
54
61
 
55
62
  try:
@@ -93,6 +100,16 @@ def get_jwks() -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
93
100
  return (jwks, providers)
94
101
 
95
102
 
103
+ def extract_audience(access_token: str) -> list[str]:
104
+ "Extract the audience from an encoded JWT."
105
+ audience: list[str] | str | None = decode_jwt_payload(access_token).get("aud", None)
106
+
107
+ if not audience:
108
+ return []
109
+
110
+ return [audience] if not isinstance(audience, list) else audience
111
+
112
+
96
113
  def get_audience(oauth_provider: str) -> str:
97
114
  """Get the audience for the specified OAuth provider
98
115
 
@@ -156,7 +173,7 @@ def decode(
156
173
  options={"verify_aud": validate_audience},
157
174
  **kwargs,
158
175
  ) # type: ignore
159
- except jwt.InvalidAudienceError:
176
+ except jwt.exceptions.InvalidAudienceError:
160
177
  logger.debug("Default audience did not match - checking additional audiences")
161
178
  if config.auth.oauth.other_audiences is not None:
162
179
  # The main audience isn't valid, let's try the others
@@ -174,7 +191,11 @@ def decode(
174
191
  except jwt.InvalidAudienceError:
175
192
  continue
176
193
 
177
- logger.debug("Default and additional audiences failed to validate")
194
+ logger.warning(
195
+ "Default and additional audiences failed to validate. Expected: %s, Actual: %s",
196
+ audience,
197
+ ",".join(extract_audience(access_token)),
198
+ )
178
199
  raise
179
200
 
180
201
 
@@ -146,7 +146,7 @@ log_cli_level = "WARN"
146
146
  [tool.poetry]
147
147
  package-mode = true
148
148
  name = "clue-api"
149
- version = "1.3.0.dev15"
149
+ version = "1.3.0.dev28"
150
150
  description = "Clue distributed enrichment service"
151
151
  authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
152
152
  license = "MIT"
File without changes
File without changes