clue-api 1.5.0.dev244__tar.gz → 1.5.0.dev251__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.5.0.dev244 → clue_api-1.5.0.dev251}/PKG-INFO +1 -1
  2. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/helpers/email_render.py +137 -6
  3. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/pyproject.toml +1 -1
  4. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/LICENSE +0 -0
  5. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/README.md +0 -0
  6. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/.gitignore +0 -0
  7. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/__init__.py +0 -0
  8. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/__init__.py +0 -0
  9. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/base.py +0 -0
  10. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/__init__.py +0 -0
  11. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/actions.py +0 -0
  12. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/auth.py +0 -0
  13. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/configs.py +0 -0
  14. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/fetchers.py +0 -0
  15. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/lookup.py +0 -0
  16. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/registration.py +0 -0
  17. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/static.py +0 -0
  18. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/api/v1/sync.py +0 -0
  19. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/app.py +0 -0
  20. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/cache/__init__.py +0 -0
  21. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/__init__.py +0 -0
  22. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/bytes_utils.py +0 -0
  23. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/classification.py +0 -0
  24. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/classification.yml +0 -0
  25. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/dict_utils.py +0 -0
  26. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/exceptions.py +0 -0
  27. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/forge.py +0 -0
  28. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/json_utils.py +0 -0
  29. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/list_utils.py +0 -0
  30. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/logging/__init__.py +0 -0
  31. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/logging/audit.py +0 -0
  32. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/logging/format.py +0 -0
  33. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/regex.py +0 -0
  34. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/str_utils.py +0 -0
  35. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/swagger.py +0 -0
  36. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/common/uid.py +0 -0
  37. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/config.py +0 -0
  38. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/constants/__init__.py +0 -0
  39. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/constants/env.py +0 -0
  40. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/constants/supported_types.py +0 -0
  41. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/cronjobs/__init__.py +0 -0
  42. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/cronjobs/plugins.py +0 -0
  43. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/error.py +0 -0
  44. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/extensions/__init__.py +0 -0
  45. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/extensions/config.py +0 -0
  46. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/gunicorn_config.py +0 -0
  47. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/healthz.py +0 -0
  48. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/helper/discover.py +0 -0
  49. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/helper/headers.py +0 -0
  50. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/helper/oauth.py +0 -0
  51. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/__init__.py +0 -0
  52. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/actions.py +0 -0
  53. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/config.py +0 -0
  54. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/fetchers.py +0 -0
  55. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/graph.py +0 -0
  56. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/model_list.py +0 -0
  57. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/network.py +0 -0
  58. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/__init__.py +0 -0
  59. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/base.py +0 -0
  60. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/file.py +0 -0
  61. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/graph.py +0 -0
  62. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/image.py +0 -0
  63. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/status.py +0 -0
  64. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/results/validation.py +0 -0
  65. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/schema.py +0 -0
  66. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/selector.py +0 -0
  67. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/sync.py +0 -0
  68. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/models/validators.py +0 -0
  69. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/patched.py +0 -0
  70. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/__init__.py +0 -0
  71. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/celery_app.py +0 -0
  72. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/helpers/__init__.py +0 -0
  73. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/helpers/central_server.py +0 -0
  74. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/helpers/token.py +0 -0
  75. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/helpers/trino.py +0 -0
  76. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/models.py +0 -0
  77. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/plugin/utils.py +0 -0
  78. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/py.typed +0 -0
  79. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/__init__.py +0 -0
  80. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/__init__.py +0 -0
  81. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/cache.py +0 -0
  82. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/events.py +0 -0
  83. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/hash.py +0 -0
  84. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/queues/__init__.py +0 -0
  85. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/queues/comms.py +0 -0
  86. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/set.py +0 -0
  87. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  88. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/security/__init__.py +0 -0
  89. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/security/obo.py +0 -0
  90. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/security/utils.py +0 -0
  91. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/action_service.py +0 -0
  92. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/auth_service.py +0 -0
  93. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/config_service.py +0 -0
  94. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/fetcher_service.py +0 -0
  95. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/jwt_service.py +0 -0
  96. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/lookup_service.py +0 -0
  97. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/mongo_service.py +0 -0
  98. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/clue/services/type_service.py +0 -0
  99. {clue_api-1.5.0.dev244 → clue_api-1.5.0.dev251}/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.5.0.dev244
3
+ Version: 1.5.0.dev251
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -93,7 +93,98 @@ def filter_elements(payload: str) -> str:
93
93
  return cast(str, soup.prettify())
94
94
 
95
95
 
96
- def process_eml(data, output_dir, load_images=False): # noqa: C901
96
+ def _render_simplified_part(payload: str, output_path: str, imgkit_options: dict, viewport_width: int) -> None:
97
+ "Render a text MIME part in simplified mode: no word-wrap, overflow clipped, with a truncation badge if needed."
98
+ probe_html = f"""
99
+ <html>
100
+ <head>
101
+ <style>
102
+ body * {{ white-space: nowrap !important; word-wrap: normal !important;
103
+ word-break: normal !important; }}
104
+ </style>
105
+ </head>
106
+ <body>
107
+ {payload}
108
+ </body>
109
+ </html>
110
+ """
111
+ probe_fd, probe_path = tempfile.mkstemp(suffix=".jpeg")
112
+ os.close(probe_fd)
113
+ probe_width: int | None = None
114
+ overflows = False
115
+ try:
116
+ imgkit.from_string(probe_html, probe_path, options=imgkit_options)
117
+ try:
118
+ with Image.open(probe_path) as probe_img:
119
+ probe_width = probe_img.size[0]
120
+ except Image.DecompressionBombError:
121
+ logger.warning(
122
+ "Probe image exceeded PIL decompression-bomb limits; treating payload as overflowed",
123
+ )
124
+ overflows = True
125
+ finally:
126
+ if os.path.exists(probe_path):
127
+ os.remove(probe_path)
128
+
129
+ if probe_width is not None:
130
+ overflows = probe_width > viewport_width
131
+ if overflows:
132
+ if probe_width is not None:
133
+ logger.warning(
134
+ "Payload overflowed viewport (%dpx > %dpx), adding truncation indicator",
135
+ probe_width,
136
+ viewport_width,
137
+ )
138
+ else:
139
+ logger.warning(
140
+ "Payload may overflow viewport; adding truncation indicator",
141
+ )
142
+ truncation_open = (
143
+ "<div style='border: 2px dashed red; padding: 4px;'>"
144
+ "<div style='color: red; font-size: 24px;'><b>[Content truncated / Contenu tronqué]</b></div>"
145
+ "<div style='color: red; font-size: 12px;'>Use 'Full' mode to see full content / "
146
+ "Utilisez le mode &laquo; Full &raquo; pour afficher l'intégralité du contenu</div>"
147
+ if overflows
148
+ else ""
149
+ )
150
+ truncation_close = "</div>" if overflows else ""
151
+ final_html = f"""
152
+ <html>
153
+ <head>
154
+ <style>
155
+ body * {{ max-width: {viewport_width}px !important; overflow: hidden !important;
156
+ white-space: nowrap !important; word-wrap: normal !important;
157
+ word-break: normal !important; }}
158
+ </style>
159
+ </head>
160
+ <body style="max-width: {viewport_width}px; overflow: hidden;">
161
+ {truncation_open}
162
+ {payload}
163
+ {truncation_close}
164
+ </body>
165
+ </html>
166
+ """
167
+ imgkit.from_string(final_html, output_path, options=imgkit_options)
168
+
169
+
170
+ def _render_full_part(payload: str, output_path: str, imgkit_options: dict, viewport_width: int) -> None:
171
+ "Render a text MIME part in full mode: word-wrap enabled, no clipping."
172
+ html = f"""
173
+ <html>
174
+ <head>
175
+ <style>
176
+ body * {{ max-width: {viewport_width}px; word-wrap: break-word; }}
177
+ </style>
178
+ </head>
179
+ <body style="max-width: {viewport_width}px;">
180
+ {payload}
181
+ </body>
182
+ </html>
183
+ """
184
+ imgkit.from_string(html, output_path, options=imgkit_options)
185
+
186
+
187
+ def process_eml(data, output_dir, load_images=False, mode="simplified"): # noqa: C901
97
188
  "Process the email (bytes), extract MIME parts and useful headers. Generate a JPEG picture of the mail"
98
189
  logger.debug("Beginning eml processing")
99
190
 
@@ -105,7 +196,8 @@ def process_eml(data, output_dir, load_images=False): # noqa: C901
105
196
  subject_field = get_header_data(msg, "Subject")
106
197
  id_field = get_header_data(msg, "Message-Id")
107
198
 
108
- imgkit_options = {"load-error-handling": "skip", "no-images": None}
199
+ viewport_width = 2048
200
+ imgkit_options = {"load-error-handling": "skip", "no-images": None, "width": viewport_width}
109
201
 
110
202
  images_list = []
111
203
 
@@ -155,7 +247,10 @@ def process_eml(data, output_dir, load_images=False): # noqa: C901
155
247
 
156
248
  try:
157
249
  payload_path = NamedTemporaryFile(suffix=".jpeg").name
158
- imgkit.from_string(payload, payload_path, options=imgkit_options)
250
+ if mode == "simplified":
251
+ _render_simplified_part(payload, payload_path, imgkit_options, viewport_width)
252
+ else:
253
+ _render_full_part(payload, payload_path, imgkit_options, viewport_width)
159
254
  logger.info("Decoded %s" % payload_path)
160
255
  images_list.append(payload_path)
161
256
  except Exception as e:
@@ -175,8 +270,43 @@ def process_eml(data, output_dir, load_images=False): # noqa: C901
175
270
 
176
271
  result_image = os.path.join(output_dir, "output.jpeg")
177
272
  if len(images_list) > 0:
178
- images = [img.convert("RGB") if img.mode != "RGB" else img for img in map(Image.open, images_list)]
179
- combo = append_images(images)
273
+ # Open images with decompression bomb protection
274
+ opened_images = []
275
+ for image_path in images_list:
276
+ try:
277
+ with Image.open(image_path) as img:
278
+ # Force load and convert to RGB to catch decompression bombs
279
+ rgb_img = img.convert("RGB")
280
+ opened_images.append(rgb_img)
281
+ except Image.DecompressionBombError:
282
+ logger.warning(f"Image too large (decompression bomb): {image_path}. Creating placeholder.")
283
+ # Create a placeholder HTML message
284
+ placeholder_html = """
285
+ <html>
286
+ <body style="background-color: #ffebee; padding: 20px; text-align: center;">
287
+ <h2 style="color: #c62828;">Component Too Large / Composant trop volumineux</h2>
288
+ <p>This image exceeds the maximum allowed size and cannot be displayed. /
289
+ Ce fichier joint dépasse la taille maximale autorisée et ne peut pas être affiché. </p>
290
+ </body>
291
+ </html>
292
+ """
293
+ try:
294
+ placeholder_fd, placeholder_path = tempfile.mkstemp(suffix=".jpeg")
295
+ os.close(placeholder_fd)
296
+ try:
297
+ imgkit.from_string(placeholder_html, placeholder_path, options=imgkit_options)
298
+ with Image.open(placeholder_path) as placeholder_img:
299
+ # Force load and convert to RGB
300
+ rgb_placeholder = placeholder_img.convert("RGB")
301
+ opened_images.append(rgb_placeholder)
302
+ finally:
303
+ if os.path.exists(placeholder_path):
304
+ os.remove(placeholder_path)
305
+ except Exception:
306
+ logger.exception("Failed to create placeholder image")
307
+ # Continue without this image
308
+
309
+ combo = append_images(opened_images)
180
310
  combo.save(result_image)
181
311
  # Clean up temporary images
182
312
  for i in images_list:
@@ -189,7 +319,7 @@ def process_eml(data, output_dir, load_images=False): # noqa: C901
189
319
  raise ClueException("Error when processing email") from e
190
320
 
191
321
 
192
- def render(email_path: str, cart_buffer: io.BytesIO) -> ImageResult | None:
322
+ def render(email_path: str, cart_buffer: io.BytesIO, mode: str = "simplified") -> ImageResult | None:
193
323
  "Helper function that, given a buffer containing a carted email, returns an image rendering of it."
194
324
  cart_buffer.seek(0)
195
325
  buf = io.BytesIO()
@@ -203,6 +333,7 @@ def render(email_path: str, cart_buffer: io.BytesIO) -> ImageResult | None:
203
333
  process_eml(
204
334
  buf.read(),
205
335
  tmp_dir,
336
+ mode=mode,
206
337
  )
207
338
 
208
339
  error = None
@@ -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.5.0.dev244"
144
+ version = "1.5.0.dev251"
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