clue-api 1.6.0.dev320__tar.gz → 1.6.0.dev335__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 (99) hide show
  1. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/PKG-INFO +1 -1
  2. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/network.py +5 -6
  3. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/fetcher_service.py +56 -35
  4. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/pyproject.toml +1 -1
  5. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/LICENSE +0 -0
  6. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/README.md +0 -0
  7. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/.gitignore +0 -0
  8. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/__init__.py +0 -0
  9. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/__init__.py +0 -0
  10. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/base.py +0 -0
  11. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/__init__.py +0 -0
  12. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/actions.py +0 -0
  13. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/auth.py +0 -0
  14. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/configs.py +0 -0
  15. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/fetchers.py +0 -0
  16. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/lookup.py +0 -0
  17. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/registration.py +0 -0
  18. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/static.py +0 -0
  19. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/sync.py +0 -0
  20. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/app.py +0 -0
  21. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/cache/__init__.py +0 -0
  22. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/__init__.py +0 -0
  23. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/bytes_utils.py +0 -0
  24. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/classification.py +0 -0
  25. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/classification.yml +0 -0
  26. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/dict_utils.py +0 -0
  27. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/exceptions.py +0 -0
  28. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/forge.py +0 -0
  29. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/json_utils.py +0 -0
  30. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/list_utils.py +0 -0
  31. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/logging/__init__.py +0 -0
  32. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/logging/audit.py +0 -0
  33. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/logging/format.py +0 -0
  34. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/regex.py +0 -0
  35. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/str_utils.py +0 -0
  36. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/swagger.py +0 -0
  37. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/uid.py +0 -0
  38. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/config.py +0 -0
  39. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/constants/__init__.py +0 -0
  40. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/constants/env.py +0 -0
  41. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/constants/supported_types.py +0 -0
  42. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/cronjobs/__init__.py +0 -0
  43. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/cronjobs/plugins.py +0 -0
  44. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/error.py +0 -0
  45. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/extensions/__init__.py +0 -0
  46. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/extensions/config.py +0 -0
  47. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/gunicorn_config.py +0 -0
  48. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/healthz.py +0 -0
  49. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/helper/discover.py +0 -0
  50. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/helper/headers.py +0 -0
  51. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/helper/oauth.py +0 -0
  52. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/__init__.py +0 -0
  53. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/actions.py +0 -0
  54. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/config.py +0 -0
  55. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/fetchers.py +0 -0
  56. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/graph.py +0 -0
  57. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/model_list.py +0 -0
  58. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/__init__.py +0 -0
  59. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/base.py +0 -0
  60. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/file.py +0 -0
  61. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/graph.py +0 -0
  62. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/image.py +0 -0
  63. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/status.py +0 -0
  64. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/validation.py +0 -0
  65. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/schema.py +0 -0
  66. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/selector.py +0 -0
  67. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/sync.py +0 -0
  68. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/validators.py +0 -0
  69. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/patched.py +0 -0
  70. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/__init__.py +0 -0
  71. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/celery_app.py +0 -0
  72. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/__init__.py +0 -0
  73. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/central_server.py +0 -0
  74. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/email_render.py +0 -0
  75. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/token.py +0 -0
  76. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/trino.py +0 -0
  77. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/models.py +0 -0
  78. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/utils.py +0 -0
  79. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/py.typed +0 -0
  80. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/__init__.py +0 -0
  81. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/__init__.py +0 -0
  82. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/cache.py +0 -0
  83. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/events.py +0 -0
  84. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/hash.py +0 -0
  85. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/queues/__init__.py +0 -0
  86. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/queues/comms.py +0 -0
  87. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/set.py +0 -0
  88. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  89. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/security/__init__.py +0 -0
  90. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/security/obo.py +0 -0
  91. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/security/utils.py +0 -0
  92. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/action_service.py +0 -0
  93. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/auth_service.py +0 -0
  94. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/config_service.py +0 -0
  95. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/jwt_service.py +0 -0
  96. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/lookup_service.py +0 -0
  97. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/mongo_service.py +0 -0
  98. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/type_service.py +0 -0
  99. {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/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.6.0.dev320
3
+ Version: 1.6.0.dev335
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -76,11 +76,6 @@ class ClueResponse(BaseModel):
76
76
  Timestamp = Annotated[
77
77
  datetime,
78
78
  BeforeValidator(parse_datetime),
79
- Field(
80
- description="A timestamp describing when the knowledge was generated.",
81
- default_factory=lambda: datetime.now(timezone.utc),
82
- examples=[datetime.now(timezone.utc), datetime.now(timezone.utc) - timedelta(weeks=2)],
83
- ),
84
79
  ]
85
80
 
86
81
 
@@ -121,7 +116,11 @@ class Annotation(BaseModel):
121
116
  examples=["v0.0.1", "1.0.0", None],
122
117
  ),
123
118
  ] = None
124
- timestamp: Timestamp
119
+ timestamp: Timestamp = Field(
120
+ description="A timestamp describing when the knowledge was generated.",
121
+ default_factory=lambda: datetime.now(timezone.utc),
122
+ examples=[datetime.now(timezone.utc), datetime.now(timezone.utc) - timedelta(weeks=2)],
123
+ )
125
124
  type: Annotated[
126
125
  Literal["opinion", "frequency", "assessment", "mitigation", "context"],
127
126
  Field(
@@ -3,7 +3,7 @@ from urllib.parse import urljoin
3
3
 
4
4
  import elasticapm
5
5
  import requests
6
- from flask import request
6
+ from flask import has_request_context, request
7
7
  from pydantic import TypeAdapter, ValidationError
8
8
  from requests import JSONDecodeError, exceptions
9
9
 
@@ -11,6 +11,7 @@ from clue.common.exceptions import (
11
11
  AuthenticationException,
12
12
  ClueException,
13
13
  ClueValueError,
14
+ InvalidDataException,
14
15
  NotFoundException,
15
16
  )
16
17
  from clue.common.logging import get_logger
@@ -26,6 +27,26 @@ logger = get_logger(__file__)
26
27
  CACHE_TIMEOUT: int = 1 if DEBUG else 5 * 60
27
28
 
28
29
 
30
+ def get_obo_access_token(
31
+ source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
32
+ ) -> tuple[Optional[str], Optional[str]]:
33
+ """Get the caller access token and an OBO token for an external source when needed."""
34
+ if access_token is None and has_request_context():
35
+ auth_header = request.headers.get("Authorization", type=str)
36
+ if auth_header:
37
+ parts = auth_header.split(" ", 1)
38
+ access_token = parts[1] if len(parts) == 2 and parts[0].lower() == "bearer" else auth_header
39
+ if not access_token:
40
+ return None, None
41
+
42
+ obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
43
+ if error:
44
+ logger.error("%s: %s", source.name, error)
45
+ raise AuthenticationException("Invalid token provided for this enrichment.")
46
+
47
+ return access_token, obo_access_token
48
+
49
+
29
50
  @cache.memoize(timeout=1 if DEBUG else 5 * 60, args_to_ignore=["access_token"]) # Cached for 5 minutes
30
51
  def get_supported_fetchers(
31
52
  source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
@@ -43,13 +64,10 @@ def get_supported_fetchers(
43
64
 
44
65
  url = urljoin(source.url, "fetchers/")
45
66
 
46
- obo_access_token = None
47
- if access_token:
48
- obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
49
-
50
- if error:
51
- logger.error("%s: %s", source.name, error)
52
- return {}
67
+ try:
68
+ access_token, obo_access_token = get_obo_access_token(source, user, access_token)
69
+ except AuthenticationException:
70
+ return {}
53
71
 
54
72
  headers = {"Accept": "application/json"}
55
73
  if obo_access_token or access_token:
@@ -130,6 +148,15 @@ def get_plugins_supported_fetchers(user: dict[str, Any]) -> dict[str, FetcherDef
130
148
  return available_fetchers
131
149
 
132
150
 
151
+ def _validate_fetcher_classification(fetcher: FetcherDefinition | None, selector: Selector, fetcher_id: str) -> None:
152
+ if fetcher and not CLASSIFICATION.is_accessible(fetcher.classification, selector.classification):
153
+ raise InvalidDataException(
154
+ f"Cannot send data classified as {selector.classification} to fetcher {fetcher_id} "
155
+ f"at classification {fetcher.classification}.",
156
+ status_code=400,
157
+ )
158
+
159
+
133
160
  def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> FetcherResult:
134
161
  """Executes a specified fetcher.
135
162
 
@@ -150,30 +177,34 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
150
177
  if not plugin:
151
178
  raise NotFoundException(f"Plugin {plugin_id} does not exist.")
152
179
 
153
- access_token = request.headers.get("Authorization", type=str)
154
- if access_token:
155
- access_token = access_token.split(" ")[1]
156
-
157
- obo_access_token = None
158
- if access_token:
159
- obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
160
-
161
- if error:
162
- logger.error("%s: %s", plugin.name, error)
163
- raise AuthenticationException("Invalid token provided for this enrichment.")
180
+ access_token, obo_access_token = get_obo_access_token(plugin, user)
164
181
 
165
182
  headers = {"Accept": "application/json"}
166
183
  if obo_access_token or access_token:
167
184
  headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
168
185
 
169
- if request.content_type == "application/json":
186
+ if request.is_json:
170
187
  parameters = request.json
171
188
  else:
172
- # TODO: Pass parameters via urlencode?
173
- parameters = {}
189
+ logger.error(
190
+ "Invalid content-type detected: %s",
191
+ )
192
+ raise ClueValueError(
193
+ "The request body must be of type application/json.",
194
+ status_code=400,
195
+ )
174
196
 
175
197
  try:
176
- Selector.model_validate(parameters)
198
+ selector = Selector.model_validate(parameters)
199
+ supported_fetchers = get_supported_fetchers(plugin, user, access_token=access_token)
200
+
201
+ if len(supported_fetchers) < 1:
202
+ raise NotFoundException(f"{plugin_id} does not support any fetchers.")
203
+
204
+ fetcher = supported_fetchers.get(fetcher_id)
205
+ if fetcher is None:
206
+ raise NotFoundException(f"Fetcher {fetcher_id} does not exist", status_code=404)
207
+ _validate_fetcher_classification(fetcher, selector, fetcher_id)
177
208
 
178
209
  response = requests.post(
179
210
  urljoin(plugin.url, f"fetchers/{fetcher_id}"),
@@ -192,7 +223,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
192
223
  return FetcherResult.model_validate(result["api_response"], context={"is_response": True})
193
224
  except ValidationError as err:
194
225
  logger.exception("Invalid Request Body:")
195
- raise ClueValueError(
226
+ raise InvalidDataException(
196
227
  "Validation error encountered on request body. Ensure your request body is properly formatted.",
197
228
  status_code=400,
198
229
  ) from err
@@ -224,17 +255,7 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
224
255
  if not plugin:
225
256
  raise NotFoundException(f"Plugin {plugin_id} does not exist.")
226
257
 
227
- access_token = request.headers.get("Authorization", type=str)
228
- if access_token:
229
- access_token = access_token.split(" ")[1]
230
-
231
- obo_access_token = None
232
- if access_token:
233
- obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
234
-
235
- if error:
236
- logger.error("%s: %s", plugin.name, error)
237
- raise AuthenticationException("Invalid token provided for this enrichment.")
258
+ access_token, obo_access_token = get_obo_access_token(plugin, user)
238
259
 
239
260
  headers = {"Accept": "application/json"}
240
261
  if obo_access_token or access_token:
@@ -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.dev320"
144
+ version = "1.6.0.dev335"
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