assemblyline-core 4.6.0.dev11__tar.gz → 4.6.0.2__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 (90) hide show
  1. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/PKG-INFO +1 -1
  2. assemblyline_core-4.6.0.2/assemblyline_core/VERSION +1 -0
  3. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/dispatching/dispatcher.py +36 -8
  4. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/plumber/run_plumber.py +123 -50
  5. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/controllers/docker_ctl.py +9 -4
  6. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core.egg-info/PKG-INFO +1 -1
  7. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_plumber.py +52 -3
  8. assemblyline_core-4.6.0.dev11/assemblyline_core/VERSION +0 -1
  9. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/LICENCE.md +0 -0
  10. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/README.md +0 -0
  11. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/__init__.py +0 -0
  12. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/alerter/__init__.py +0 -0
  13. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/alerter/processing.py +0 -0
  14. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/alerter/run_alerter.py +0 -0
  15. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/archiver/__init__.py +0 -0
  16. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/archiver/run_archiver.py +0 -0
  17. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/badlist_client.py +0 -0
  18. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/dispatching/__init__.py +0 -0
  19. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/dispatching/__main__.py +0 -0
  20. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/dispatching/client.py +0 -0
  21. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/dispatching/schedules.py +0 -0
  22. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/dispatching/timeout.py +0 -0
  23. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/expiry/__init__.py +0 -0
  24. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/expiry/run_expiry.py +0 -0
  25. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/ingester/__init__.py +0 -0
  26. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/ingester/__main__.py +0 -0
  27. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/ingester/constants.py +0 -0
  28. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/ingester/ingester.py +0 -0
  29. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/__init__.py +0 -0
  30. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/es_metrics.py +0 -0
  31. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/heartbeat_formatter.py +0 -0
  32. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/helper.py +0 -0
  33. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/metrics_server.py +0 -0
  34. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/run_heartbeat_manager.py +0 -0
  35. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/run_metrics_aggregator.py +0 -0
  36. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/metrics/run_statistics_aggregator.py +0 -0
  37. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/plumber/__init__.py +0 -0
  38. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/__init__.py +0 -0
  39. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/client.py +0 -0
  40. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/creator/__init__.py +0 -0
  41. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/creator/run.py +0 -0
  42. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/creator/run_worker.py +0 -0
  43. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/loader/__init__.py +0 -0
  44. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/loader/run.py +0 -0
  45. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/loader/run_worker.py +0 -0
  46. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/replay/replay.py +0 -0
  47. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/safelist_client.py +0 -0
  48. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/__init__.py +0 -0
  49. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/collection.py +0 -0
  50. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/controllers/__init__.py +0 -0
  51. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/controllers/interface.py +0 -0
  52. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/controllers/kubernetes_ctl.py +0 -0
  53. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/run_scaler.py +0 -0
  54. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/scaler/scaler_server.py +0 -0
  55. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/server_base.py +0 -0
  56. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/signature_client.py +0 -0
  57. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/submission_client.py +0 -0
  58. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/tasking_client.py +0 -0
  59. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/updater/__init__.py +0 -0
  60. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/updater/helper.py +0 -0
  61. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/updater/run_updater.py +0 -0
  62. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/vacuum/__init__.py +0 -0
  63. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/vacuum/crawler.py +0 -0
  64. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/vacuum/department_map.py +0 -0
  65. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/vacuum/safelist.py +0 -0
  66. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/vacuum/stream_map.py +0 -0
  67. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/vacuum/worker.py +0 -0
  68. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/workflow/__init__.py +0 -0
  69. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core/workflow/run_workflow.py +0 -0
  70. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core.egg-info/SOURCES.txt +0 -0
  71. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core.egg-info/dependency_links.txt +0 -0
  72. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core.egg-info/requires.txt +0 -0
  73. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/assemblyline_core.egg-info/top_level.txt +0 -0
  74. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/setup.cfg +0 -0
  75. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/setup.py +0 -0
  76. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_alerter.py +0 -0
  77. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_badlist_client.py +0 -0
  78. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_dispatcher.py +0 -0
  79. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_expiry.py +0 -0
  80. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_replay.py +0 -0
  81. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_safelist_client.py +0 -0
  82. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_scaler.py +0 -0
  83. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_scheduler.py +0 -0
  84. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_signature_client.py +0 -0
  85. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_simulation.py +0 -0
  86. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_tasking_client.py +0 -0
  87. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_vacuum.py +0 -0
  88. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_worker_ingest.py +0 -0
  89. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_worker_submit.py +0 -0
  90. {assemblyline_core-4.6.0.dev11 → assemblyline_core-4.6.0.2}/test/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assemblyline-core
3
- Version: 4.6.0.dev11
3
+ Version: 4.6.0.2
4
4
  Summary: Assemblyline 4 - Core components
5
5
  Home-page: https://github.com/CybercentreCanada/assemblyline-core/
6
6
  Author: CCCS Assemblyline development team
@@ -0,0 +1 @@
1
+ 4.6.0.2
@@ -54,7 +54,7 @@ from assemblyline.odm.messages.task import Task as ServiceTask
54
54
  from assemblyline.odm.models.error import Error
55
55
  from assemblyline.odm.models.result import Result
56
56
  from assemblyline.odm.models.service import Service
57
- from assemblyline.odm.models.submission import Submission
57
+ from assemblyline.odm.models.submission import Submission, TraceEvent
58
58
  from assemblyline.odm.models.user import User
59
59
  from assemblyline.remote.datatypes.events import EventWatcher
60
60
  from assemblyline.remote.datatypes.exporting_counter import export_metrics_once
@@ -311,6 +311,16 @@ class SubmissionTask:
311
311
  """Shortcut to read submission SID"""
312
312
  return self.submission.sid
313
313
 
314
+ def trace(self, event_type: str, sha256: Optional[str] = None,
315
+ service: Optional[str] = None, message: Optional[str] = None) -> None:
316
+ if self.submission.params.trace:
317
+ self.submission.tracing_events.append(TraceEvent({
318
+ 'event_type': event_type,
319
+ 'service': service,
320
+ 'file': sha256,
321
+ 'message': message,
322
+ }))
323
+
314
324
  def forbid_for_children(self, sha256: str, service_name: str):
315
325
  """Mark that children of a given file should not be routed to a service."""
316
326
  try:
@@ -729,6 +739,7 @@ class Dispatcher(ThreadedCoreBase):
729
739
 
730
740
  if not self.active_submissions.exists(sid):
731
741
  self.log.info("[%s] New submission received", sid)
742
+ task.trace('submission_start')
732
743
  self.active_submissions.add(sid, {
733
744
  'completed_queue': task.completed_queue,
734
745
  'submission': submission.as_primitives()
@@ -742,6 +753,7 @@ class Dispatcher(ThreadedCoreBase):
742
753
  }).as_primitives())
743
754
 
744
755
  else:
756
+ task.trace('submission_start', message='Received a pre-existing submission')
745
757
  self.log.warning(f"[{sid}] Received a pre-existing submission, check if it is complete")
746
758
 
747
759
  # Refresh the quota hold
@@ -840,6 +852,7 @@ class Dispatcher(ThreadedCoreBase):
840
852
  sid = submission.sid
841
853
  if self.apm_client:
842
854
  elasticapm.label(sid=sid, sha256=sha256)
855
+ task.trace('dispatch_file', sha256=sha256)
843
856
 
844
857
  file_depth: int = task.file_depth[sha256]
845
858
  # If its the first time we've seen this file, we won't have a schedule for it
@@ -859,6 +872,9 @@ class Dispatcher(ThreadedCoreBase):
859
872
  task.file_schedules[sha256] = self.scheduler.build_schedule(submission, file_info.type,
860
873
  file_depth, forbidden_services,
861
874
  task.service_access_control)
875
+ schedule_summary = [list(stage.keys()) for stage in task.file_schedules[sha256]]
876
+ task.trace('schedule_built', sha256=sha256, message=str(schedule_summary))
877
+
862
878
 
863
879
  file_info = task.file_info[sha256]
864
880
  schedule: list = list(task.file_schedules[sha256])
@@ -990,9 +1006,9 @@ class Dispatcher(ThreadedCoreBase):
990
1006
 
991
1007
  if sent or enqueued or running:
992
1008
  # If we have confirmed that we are waiting, or have taken an action, log that.
993
- self.log.info(f"[{sid}] File {sha256} sent to: {sent} "
994
- f"already in queue for: {enqueued} "
995
- f"running on: {running}")
1009
+ logs = f"sent to: {sent} already in queue for: {enqueued} running on: {running}"
1010
+ self.log.info(f"[{sid}] File {sha256} {logs}")
1011
+ task.trace('dispatch_file_result', sha256=sha256, message=logs)
996
1012
  return False
997
1013
  elif skipped:
998
1014
  # Not waiting for anything, and have started skipping what is left over
@@ -1007,10 +1023,12 @@ class Dispatcher(ThreadedCoreBase):
1007
1023
 
1008
1024
  self.counter.increment('files_completed')
1009
1025
  if len(task.queue_keys) > 0 or len(task.running_services) > 0:
1010
- self.log.info(f"[{sid}] Finished processing file '{sha256}', submission incomplete "
1011
- f"(queued: {len(task.queue_keys)} running: {len(task.running_services)})")
1026
+ logs = f"queued: {len(task.queue_keys)} running: {len(task.running_services)}"
1027
+ self.log.info(f"[{sid}] Finished processing file '{sha256}', submission incomplete ({logs})")
1028
+ task.trace('file_finished', sha256=sha256, message=logs)
1012
1029
  else:
1013
1030
  self.log.info(f"[{sid}] Finished processing file '{sha256}', checking if submission complete")
1031
+ task.trace('file_finished', sha256=sha256)
1014
1032
  return self.check_submission(task)
1015
1033
  return False
1016
1034
 
@@ -1104,14 +1122,17 @@ class Dispatcher(ThreadedCoreBase):
1104
1122
  # file isn't done yet, and hasn't been filtered by any of the previous few steps
1105
1123
  # poke those files.
1106
1124
  if pending_files:
1125
+ task.trace('submission_check', message=f"Dispatching {list(pending_files)}")
1107
1126
  self.log.debug(f"[{task.submission.sid}] Dispatching {len(pending_files)} files: {list(pending_files)}")
1108
1127
  for file_hash in pending_files:
1109
1128
  if self.dispatch_file(task, file_hash):
1110
1129
  return True
1111
1130
  elif processing_files:
1131
+ task.trace('submission_check', message=f"Waiting for {list(processing_files)}")
1112
1132
  self.log.debug("[%s] Not finished waiting on %d files: %s",
1113
1133
  task.submission.sid, len(processing_files), list(processing_files))
1114
1134
  else:
1135
+ task.trace('submission_check', message="Finished")
1115
1136
  self.log.debug("[%s] Finalizing submission.", task.submission.sid)
1116
1137
  max_score = max(file_scores.values()) if file_scores else 0 # Submissions with no results have no score
1117
1138
  if self.tasks.pop(task.sid, None):
@@ -1341,6 +1362,8 @@ class Dispatcher(ThreadedCoreBase):
1341
1362
  self.log.warning(f'[{message.sid}] Service started missing data.')
1342
1363
  continue
1343
1364
 
1365
+ task.trace('service_start', sha256=message.sha, service=message.service_name,
1366
+ message=message.worker_id)
1344
1367
  key = (message.sha, message.service_name)
1345
1368
  if task.queue_keys.pop(key, None) is not None:
1346
1369
  # If this task is already finished (result message processed before start
@@ -1384,8 +1407,9 @@ class Dispatcher(ThreadedCoreBase):
1384
1407
  continue
1385
1408
 
1386
1409
  if task:
1387
- task.service_logs[(message.sha, message.service_name)].append(
1388
- f'Service timeout at {now_as_iso()} on worker {message.worker_id}')
1410
+ log = f'Service timeout at {now_as_iso()} on worker {message.worker_id}'
1411
+ task.service_logs[(message.sha, message.service_name)].append(log)
1412
+ task.trace('service_timeout', sha256=message.sha, service=message.service_name, message=log)
1389
1413
  self.timeout_service(task, message.sha, message.service_name, message.worker_id)
1390
1414
 
1391
1415
  elif kind == Action.dispatch_file:
@@ -1429,6 +1453,8 @@ class Dispatcher(ThreadedCoreBase):
1429
1453
  self.log.exception(f"Malformed result message, missing key: {missing}")
1430
1454
  return
1431
1455
 
1456
+ task.trace('process_result', sha256=sha256, service=service_name, message="Processing result " + summary.key)
1457
+
1432
1458
  # Add SHA256s of files that allowed to run regardless of Dynamic Recursion Prevention
1433
1459
  task.dynamic_recursion_bypass = task.dynamic_recursion_bypass.union(set(dynamic_recursion_bypass))
1434
1460
 
@@ -1599,6 +1625,8 @@ class Dispatcher(ThreadedCoreBase):
1599
1625
  @elasticapm.capture_span(span_type='dispatcher')
1600
1626
  def process_service_error(self, task: SubmissionTask, error_key, error: Error):
1601
1627
  self.log.info(f'[{task.submission.sid}] Error from service {error.response.service_name} on {error.sha256}')
1628
+ task.trace('process_error', sha256=error.sha256, service=error.response.service_name,
1629
+ message="Service error " + error_key)
1602
1630
  self.clear_timeout(task, error.sha256, error.response.service_name)
1603
1631
  key = (error.sha256, error.response.service_name)
1604
1632
  if error.response.status == "FAIL_NONRECOVERABLE":
@@ -7,22 +7,22 @@ disabled or deleted for which a service queue exists, the dispatcher will be inf
7
7
  had an error.
8
8
  """
9
9
  import threading
10
- import warnings
10
+ from copy import deepcopy
11
11
  from typing import Optional
12
12
 
13
13
  from assemblyline.common.constants import service_queue_name
14
- from assemblyline.common.forge import get_service_queue, get_config
14
+ from assemblyline.common.forge import get_config, get_service_queue
15
15
  from assemblyline.common.isotime import DAY_IN_SECONDS, now_as_iso
16
16
  from assemblyline.odm.models.apikey import get_apikey_id
17
17
  from assemblyline.odm.models.error import Error
18
18
  from assemblyline.odm.models.service import Service
19
19
  from assemblyline.odm.models.user import load_roles, load_roles_form_acls
20
+ from assemblyline.odm.models.user_settings import SubmissionProfileParams
20
21
  from assemblyline.remote.datatypes import retry_call
21
22
  from assemblyline.remote.datatypes.queues.named import NamedQueue
22
23
  from assemblyline_core.dispatching.client import DispatchClient
23
24
  from assemblyline_core.server_base import CoreBase, ServiceStage
24
25
 
25
-
26
26
  DAY = 60 * 60 * 24
27
27
  TASK_DELETE_CHUNK = 10000
28
28
 
@@ -56,8 +56,14 @@ class Plumber(CoreBase):
56
56
  name="redis_notification_queue_cleanup")
57
57
  nq_thread.start()
58
58
 
59
+ # Start a user apikey cleanup thread
59
60
  ua_thread = threading.Thread(target=self.user_apikey_cleanup, daemon=True, name="user_apikey_cleanup")
60
61
  ua_thread.start()
62
+
63
+ # Start a user setting migration thread
64
+ usm_thread = threading.Thread(target=self.migrate_user_settings, daemon=True, name="migrate_user_settings")
65
+ usm_thread.start()
66
+
61
67
  self.service_queue_plumbing()
62
68
 
63
69
  def service_queue_plumbing(self):
@@ -193,60 +199,127 @@ class Plumber(CoreBase):
193
199
  self.sleep(2)
194
200
  self.log.info(f"Done watching {service_name} service queue")
195
201
 
196
-
197
-
198
202
  def user_apikey_cleanup(self):
199
- query = "id:*"
200
- offset = 0
201
- rows = 100
202
- total = 1
203
- cur_total = 0
204
-
205
- config = get_config()
206
- apikey_max_dtl = config.auth.apikey_max_dtl
207
-
208
- expiry_ts = now_as_iso(apikey_max_dtl * DAY_IN_SECONDS) if apikey_max_dtl is not None else None
209
-
210
- while cur_total < total:
211
- result = self.datastore.user.search(query, offset=offset, rows=rows)
212
- total = result.get('total', 0)
213
- cur_total = cur_total + (result.get("count", total))
214
-
215
- # check for API keys in total
216
- users = result.get('items', [])
203
+ expiry_ts = None
204
+ if self.config.auth.apikey_max_dtl is not None:
205
+ expiry_ts = now_as_iso(self.config.auth.apikey_max_dtl * DAY_IN_SECONDS)
217
206
 
218
- for u in users:
219
- uname = u['uname']
220
- user = self.datastore.user.get(uname)
221
- apikeys = user.apikeys
207
+ for user in self.datastore.user.stream_search(query="*", fl="*", as_obj=False):
208
+ uname = user['uname']
209
+ apikeys = user['apikeys']
222
210
 
223
- for key in apikeys:
224
- old_apikey = apikeys[key]
225
- key_id = get_apikey_id(key, uname)
211
+ for key in apikeys:
212
+ old_apikey = apikeys[key]
213
+ key_id = get_apikey_id(key, uname)
226
214
 
227
- roles = None
228
- if old_apikey['acl'] == ["C"]:
215
+ roles = None
216
+ if old_apikey['acl'] == ["C"]:
229
217
 
230
- roles = [r for r in old_apikey['roles']
231
- if r in load_roles(user['type'], user['roles'])]
232
-
233
- else:
234
- roles = [r for r in load_roles_form_acls(old_apikey['acl'], roles)
218
+ roles = [r for r in old_apikey['roles']
235
219
  if r in load_roles(user['type'], user['roles'])]
236
- new_apikey = {
237
- "password": old_apikey['password'],
238
- "acl": old_apikey['acl'],
239
- "uname": uname,
240
- "key_name": key,
241
- "roles": roles,
242
- "expiry_ts": expiry_ts
243
- }
244
- self.datastore.apikey.save(key_id, new_apikey)
245
-
246
- user['apikeys'] = {}
247
- self.datastore.user.save(uname, user)
248
-
249
220
 
221
+ else:
222
+ roles = [r for r in load_roles_form_acls(old_apikey['acl'], roles)
223
+ if r in load_roles(user['type'], user['roles'])]
224
+ new_apikey = {
225
+ "password": old_apikey['password'],
226
+ "acl": old_apikey['acl'],
227
+ "uname": uname,
228
+ "key_name": key,
229
+ "roles": roles,
230
+ "expiry_ts": expiry_ts
231
+ }
232
+ self.datastore.apikey.save(key_id, new_apikey)
233
+
234
+ user['apikeys'] = {}
235
+ self.datastore.user.save(uname, user)
236
+
237
+ # Commit changes made to indices
238
+ self.datastore.user.commit()
239
+ self.datastore.apikey.commit()
240
+
241
+ def migrate_user_settings(self):
242
+ service_list = self.datastore.list_all_services(as_obj=False)
243
+
244
+ # Migrate user settings to the new format
245
+ for doc in self.datastore.user_settings.scan_with_search_after(query={
246
+ "bool": {
247
+ "must": {
248
+ "query_string": {
249
+ "query": "*",
250
+ }
251
+ }
252
+ }
253
+ }):
254
+ user_settings = doc["_source"]
255
+ if user_settings.get('submission_profiles'):
256
+ # User settings already migrated, skip
257
+ continue
258
+
259
+ # Create the list of submission profiles
260
+ submission_profiles = {
261
+ # Grant everyone a default of which all settings from before 4.6 should be transferred
262
+ "default" : SubmissionProfileParams(user_settings).as_primitives(strip_null=True)
263
+ }
264
+
265
+ profile_error_checks = {}
266
+ # Try to apply the original settings to the system-defined profiles
267
+ for profile in self.config.submission.profiles:
268
+ updates = deepcopy(user_settings)
269
+ profile_error_checks.setdefault(profile.name, 0)
270
+ validated_profile = profile.params.as_primitives(strip_null=True)
271
+
272
+ updates.setdefault("services", {})
273
+ updates["services"].setdefault("selected", [])
274
+ updates["services"].setdefault("excluded", [])
275
+
276
+ # Append the exclusion list set by the profile
277
+ updates['services']['excluded'] = updates['services']['excluded'] + \
278
+ list(validated_profile.get("services", {}).get("excluded", []))
279
+
280
+ # Ensure the selected services are not in the excluded list
281
+ for service in service_list:
282
+ if service['name'] in updates['services']['selected'] and service['name'] in updates['services']['excluded']:
283
+ updates['services']['selected'].remove(service['name'])
284
+ profile_error_checks[profile.name] += 1
285
+ elif service['category'] in updates['services']['selected'] and service['category'] in updates['services']['excluded']:
286
+ updates['services']['selected'].remove(service['category'])
287
+ profile_error_checks[profile.name] += 1
288
+
289
+
290
+ # Check the services parameters
291
+ for param_type, list_of_params in profile.restricted_params.items():
292
+ # Check if there are restricted submission parameters
293
+ if param_type == "submission":
294
+ requested_params = (set(list_of_params) & set(updates.keys())) - set({'services', 'service_spec'})
295
+ if requested_params:
296
+ # Track the number of errors for each profile
297
+ profile_error_checks[profile.name] += len(requested_params)
298
+ for param in requested_params:
299
+ # Remove the parameter from the updates
300
+ updates.pop(param, None)
301
+
302
+ # Check if there are restricted service parameters
303
+ else:
304
+ service_spec = updates.get('service_spec', {}).get(param_type, {})
305
+ requested_params = set(list_of_params) & set(service_spec)
306
+ if requested_params:
307
+ # Track the number of errors for each profile
308
+ profile_error_checks[profile.name] += len(requested_params)
309
+ for param in requested_params:
310
+ # Remove the parameter from the updates
311
+ service_spec.pop(param, None)
312
+
313
+ if not service_spec:
314
+ # Remove the service spec if empty
315
+ updates['service_spec'].pop(param_type, None)
316
+ submission_profiles[profile.name] = SubmissionProfileParams(updates).as_primitives(strip_null=True)
317
+
318
+ # Assign the profile with the least number of errors
319
+ user_settings['submission_profiles'] = submission_profiles
320
+ user_settings['preferred_submission_profile'] = sorted(profile_error_checks.items(), key=lambda x: x[1])[0][0]
321
+
322
+ self.datastore.user_settings.save(doc["_id"], user_settings)
250
323
 
251
324
 
252
325
 
@@ -1,13 +1,16 @@
1
1
  from __future__ import annotations
2
- import docker
2
+
3
3
  import os
4
4
  import threading
5
5
  import time
6
- from collections import defaultdict
7
- from typing import List, Optional, Tuple, Dict
8
6
  import uuid
7
+ from collections import defaultdict
8
+ from typing import Dict, List, Optional, Tuple
9
+
10
+ import docker
9
11
 
10
12
  from assemblyline.odm.models.service import DependencyConfig, DockerConfig
13
+
11
14
  from .interface import ControllerInterface, ServiceControlError
12
15
 
13
16
  # Where to find the update directory inside this container.
@@ -382,7 +385,7 @@ class DockerController(ControllerInterface):
382
385
  container.labels]
383
386
  running = running[0:-delta]
384
387
  for container in running:
385
- container.kill()
388
+ container.kill(signal='SIGTERM')
386
389
 
387
390
  if delta > 0:
388
391
  # Start delta instances of the service
@@ -531,6 +534,7 @@ class DockerController(ControllerInterface):
531
534
  dynamically rather than in prepare_network.
532
535
  """
533
536
  from docker.errors import NotFound
537
+
534
538
  # Create network for service
535
539
  network_name = f'{COMPOSE_PROJECT}_service-net-{service_name}'
536
540
  try:
@@ -553,6 +557,7 @@ class DockerController(ControllerInterface):
553
557
  This lets us override the auth_config on a per image basis.
554
558
  """
555
559
  from docker.errors import ImageNotFound
560
+
556
561
  # Split the image string into "[registry/]image_name" and "tag"
557
562
  repository, _, tag = service.container_config.image.rpartition(':')
558
563
  if '/' in tag:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assemblyline-core
3
- Version: 4.6.0.dev11
3
+ Version: 4.6.0.2
4
4
  Summary: Assemblyline 4 - Core components
5
5
  Home-page: https://github.com/CybercentreCanada/assemblyline-core/
6
6
  Author: CCCS Assemblyline development team
@@ -1,14 +1,16 @@
1
1
  from time import sleep
2
2
  from unittest import mock
3
3
 
4
- from assemblyline.odm.messages.task import Task
5
- from assemblyline.odm.models.service import Service
6
- from assemblyline.odm.random_data import random_model_obj
7
4
  from assemblyline_core.plumber.run_plumber import Plumber
8
5
  from assemblyline_core.server_base import ServiceStage
9
6
  from mocking import TrueCountTimes
10
7
  from redis import Redis
11
8
 
9
+ from assemblyline.odm.messages.task import Task
10
+ from assemblyline.odm.models.service import Service
11
+ from assemblyline.odm.models.user import ApiKey, User
12
+ from assemblyline.odm.random_data import random_model_obj
13
+
12
14
 
13
15
  def test_expire_missing_service():
14
16
  redis = mock.MagicMock(spec=Redis)
@@ -107,3 +109,50 @@ def test_cleanup_old_tasks(datastore_connection):
107
109
  q="task.start_time_in_millis:0",
108
110
  track_total_hits=True,
109
111
  size=0)['hits']['total']['value'] == 0
112
+
113
+ def test_user_setting_migrations(datastore_connection):
114
+ from assemblyline.odm.models.config import SubmissionProfileParams
115
+
116
+ SubmissionProfileParams.fields().keys()
117
+ # Create a bunch of random "old" tasks and clean them up
118
+ redis = mock.MagicMock(spec=Redis)
119
+ redis_persist = mock.MagicMock(spec=Redis)
120
+ plumber = Plumber(redis=redis, redis_persist=redis_persist, datastore=datastore_connection, delay=1)
121
+
122
+ # Create a user with old settings (format prior to 4.6)
123
+ settings = {'classification': 'TLP:CLEAR', 'deep_scan': False, 'description': '', 'download_encoding': 'cart', 'default_external_sources': ['Malware Bazaar', 'VirusTotal'], 'default_zip_password': 'zippy', 'executive_summary': False, 'expand_min_score': 500, 'generate_alert': False, 'ignore_cache': False, 'ignore_dynamic_recursion_prevention': False, 'ignore_recursion_prevention': False, 'ignore_filtering': False, 'malicious': False, 'priority': 369, 'profile': False, 'service_spec': {'AVClass': {'include_malpedia_dataset': False}}, 'services': {'selected': ['Extraction', 'ConfigExtractor', 'YARA'], 'excluded': [], 'rescan': [], 'resubmit': [], 'runtime_excluded': []}, 'submission_view': 'report', 'ttl': 0}
124
+
125
+ user_account = random_model_obj(User, as_json=True)
126
+ user_account['uname'] = "admin"
127
+ user_account['apikeys'] = {'test': random_model_obj(ApiKey, as_json=True)}
128
+ datastore_connection.ds.client.index(index="user_settings", id="admin", document=settings)
129
+ datastore_connection.ds.client.index(index="user", id="admin", document=user_account)
130
+
131
+ datastore_connection.user_settings.commit()
132
+ datastore_connection.user.commit()
133
+
134
+ # Initiate the migration
135
+ plumber.user_apikey_cleanup()
136
+ plumber.migrate_user_settings()
137
+
138
+ # Check that the settings have been migrated
139
+ migrated_settings = datastore_connection.user_settings.get("admin", as_obj=False)
140
+
141
+ # Check to see if API keys for the user were transferred to the new index
142
+ assert datastore_connection.apikey.search('uname:admin', rows=0)['total'] > 0
143
+
144
+ # Deprecated settings should be removed
145
+ assert "ignore_dynamic_recursion_prevention" not in migrated_settings
146
+
147
+ # All former submission settings at the root-level should be moved to submission profiles
148
+ assert all([key not in migrated_settings for key in SubmissionProfileParams.fields().keys()] )
149
+
150
+ for settings in migrated_settings['submission_profiles'].values():
151
+ assert settings['classification'] == 'TLP:C'
152
+ assert settings['deep_scan'] is False
153
+ assert settings['generate_alert'] is False
154
+ assert settings['ignore_cache'] is False
155
+ assert settings['priority'] == 369
156
+ # Full service spec should be preserved in default profile (along with others by default if there's no restricted parameters)
157
+ assert settings['service_spec'] == {'AVClass': {'include_malpedia_dataset': False}}
158
+ assert settings['ttl'] == 0
@@ -1 +0,0 @@
1
- 4.6.0.dev11