howler-api 4.1.0.dev1389__tar.gz → 4.1.0.dev1405__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 (226) hide show
  1. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/PKG-INFO +1 -1
  2. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/action.py +2 -2
  3. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v2/ingest.py +2 -2
  4. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/collection.py +83 -9
  5. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/mixins.py +10 -4
  6. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/case_service.py +11 -10
  7. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/dossier_service.py +2 -2
  8. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/compat.py +3 -3
  9. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/pyproject.toml +1 -1
  10. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/README.md +0 -0
  11. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/__init__.py +0 -0
  12. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/__init__.py +0 -0
  13. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/add_label.py +0 -0
  14. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/add_to_bundle.py +0 -0
  15. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/add_to_case.py +0 -0
  16. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/change_field.py +0 -0
  17. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/demote.py +0 -0
  18. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/example_plugin.py +0 -0
  19. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/prioritization.py +0 -0
  20. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/promote.py +0 -0
  21. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/remove_from_bundle.py +0 -0
  22. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/remove_label.py +0 -0
  23. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/actions/transition.py +0 -0
  24. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/__init__.py +0 -0
  25. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/base.py +0 -0
  26. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/socket.py +0 -0
  27. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/__init__.py +0 -0
  28. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/analytic.py +0 -0
  29. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/auth.py +0 -0
  30. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/clue.py +0 -0
  31. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/configs.py +0 -0
  32. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/dossier.py +0 -0
  33. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/help.py +0 -0
  34. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/hit.py +0 -0
  35. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/notebook.py +0 -0
  36. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/overview.py +0 -0
  37. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/search.py +0 -0
  38. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/template.py +0 -0
  39. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/tool.py +0 -0
  40. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/user.py +0 -0
  41. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/utils/__init__.py +0 -0
  42. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/utils/etag.py +0 -0
  43. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/utils/params.py +0 -0
  44. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v1/view.py +0 -0
  45. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v2/__init__.py +0 -0
  46. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v2/case.py +0 -0
  47. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v2/fuzzy.py +0 -0
  48. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/api/v2/search.py +0 -0
  49. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/app.py +0 -0
  50. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/README.md +0 -0
  51. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/__init__.py +0 -0
  52. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/classification.py +0 -0
  53. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/classification.yml +0 -0
  54. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/exceptions.py +0 -0
  55. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/loader.py +0 -0
  56. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/logging/__init__.py +0 -0
  57. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/logging/audit.py +0 -0
  58. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/logging/format.py +0 -0
  59. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/net.py +0 -0
  60. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/net_static.py +0 -0
  61. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/random_user.py +0 -0
  62. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/common/swagger.py +0 -0
  63. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/config.py +0 -0
  64. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/cronjobs/__init__.py +0 -0
  65. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/cronjobs/action_queue_worker.py +0 -0
  66. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/cronjobs/correlation.py +0 -0
  67. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/cronjobs/retention.py +0 -0
  68. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/cronjobs/view_cleanup.py +0 -0
  69. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/README.md +0 -0
  70. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/__init__.py +0 -0
  71. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/bulk.py +0 -0
  72. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/constants.py +0 -0
  73. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/exceptions.py +0 -0
  74. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/howler_store.py +0 -0
  75. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/migrations/fix_process.py +0 -0
  76. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/operations.py +0 -0
  77. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/schemas.py +0 -0
  78. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/store.py +0 -0
  79. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/support/__init__.py +0 -0
  80. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/support/build.py +0 -0
  81. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/support/schemas.py +0 -0
  82. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/types.py +0 -0
  83. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/datastore/utils.py +0 -0
  84. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/error.py +0 -0
  85. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/README.md +0 -0
  86. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/__init__.py +0 -0
  87. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/generate_mitre.py +0 -0
  88. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/generate_sigma_rules.py +0 -0
  89. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/generate_tlds.py +0 -0
  90. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/reindex_data.py +0 -0
  91. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/external/wipe_databases.py +0 -0
  92. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/gunicorn_config.py +0 -0
  93. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/healthz.py +0 -0
  94. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/__init__.py +0 -0
  95. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/azure.py +0 -0
  96. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/discover.py +0 -0
  97. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/hit.py +0 -0
  98. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/oauth.py +0 -0
  99. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/search.py +0 -0
  100. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/workflow.py +0 -0
  101. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/helper/ws.py +0 -0
  102. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/README.md +0 -0
  103. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/__init__.py +0 -0
  104. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/base.py +0 -0
  105. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/charter.txt +0 -0
  106. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/constants.py +0 -0
  107. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/helper.py +0 -0
  108. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/howler_enum.py +0 -0
  109. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/__init__.py +0 -0
  110. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/action.py +0 -0
  111. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/analytic.py +0 -0
  112. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/assemblyline.py +0 -0
  113. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/aws.py +0 -0
  114. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/azure.py +0 -0
  115. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/case.py +0 -0
  116. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/cbs.py +0 -0
  117. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/clue.py +0 -0
  118. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/config.py +0 -0
  119. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/dossier.py +0 -0
  120. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/__init__.py +0 -0
  121. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/agent.py +0 -0
  122. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/autonomous_system.py +0 -0
  123. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/client.py +0 -0
  124. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/cloud.py +0 -0
  125. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/code_signature.py +0 -0
  126. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/container.py +0 -0
  127. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/dns.py +0 -0
  128. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/egress.py +0 -0
  129. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/elf.py +0 -0
  130. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/email.py +0 -0
  131. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/error.py +0 -0
  132. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/event.py +0 -0
  133. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/faas.py +0 -0
  134. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/file.py +0 -0
  135. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/geo.py +0 -0
  136. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/group.py +0 -0
  137. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/hash.py +0 -0
  138. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/host.py +0 -0
  139. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/http.py +0 -0
  140. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/ingress.py +0 -0
  141. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/interface.py +0 -0
  142. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/network.py +0 -0
  143. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/observer.py +0 -0
  144. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/organization.py +0 -0
  145. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/os.py +0 -0
  146. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/pe.py +0 -0
  147. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/process.py +0 -0
  148. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/registry.py +0 -0
  149. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/related.py +0 -0
  150. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/rule.py +0 -0
  151. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/server.py +0 -0
  152. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/threat.py +0 -0
  153. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/tls.py +0 -0
  154. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/url.py +0 -0
  155. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/user.py +0 -0
  156. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/user_agent.py +0 -0
  157. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/ecs/vulnerability.py +0 -0
  158. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/event.py +0 -0
  159. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/gcp.py +0 -0
  160. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/hit.py +0 -0
  161. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/howler_data.py +0 -0
  162. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/lead.py +0 -0
  163. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/localized_label.py +0 -0
  164. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/overview.py +0 -0
  165. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/pivot.py +0 -0
  166. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/record.py +0 -0
  167. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/template.py +0 -0
  168. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/user.py +0 -0
  169. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/models/view.py +0 -0
  170. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/random_data.py +0 -0
  171. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/odm/randomizer.py +0 -0
  172. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/patched.py +0 -0
  173. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/plugins/__init__.py +0 -0
  174. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/plugins/config.py +0 -0
  175. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/__init__.py +0 -0
  176. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/README.md +0 -0
  177. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/__init__.py +0 -0
  178. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/counters.py +0 -0
  179. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/events.py +0 -0
  180. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/hash.py +0 -0
  181. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/lock.py +0 -0
  182. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/queues/__init__.py +0 -0
  183. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/queues/comms.py +0 -0
  184. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/queues/multi.py +0 -0
  185. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/queues/named.py +0 -0
  186. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/queues/priority.py +0 -0
  187. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/set.py +0 -0
  188. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  189. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/security/__init__.py +0 -0
  190. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/security/socket.py +0 -0
  191. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/security/utils.py +0 -0
  192. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/__init__.py +0 -0
  193. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/action_service.py +0 -0
  194. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/analytic_service.py +0 -0
  195. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/auth_service.py +0 -0
  196. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/bundle_compat_service.py +0 -0
  197. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/comms_service.py +0 -0
  198. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/config_service.py +0 -0
  199. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/correlation_service.py +0 -0
  200. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/docs_service.py +0 -0
  201. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/event_service.py +0 -0
  202. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/fuzzy_service.py +0 -0
  203. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/hit_service.py +0 -0
  204. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/jwt_service.py +0 -0
  205. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/lucene_service.py +0 -0
  206. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/notebook_service.py +0 -0
  207. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/overview_service.py +0 -0
  208. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/search_service.py +0 -0
  209. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/template_service.py +0 -0
  210. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/user_service.py +0 -0
  211. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/services/viewer_service.py +0 -0
  212. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/telemetry.py +0 -0
  213. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/__init__.py +0 -0
  214. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/annotations.py +0 -0
  215. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/chunk.py +0 -0
  216. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/constants.py +0 -0
  217. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/dict_utils.py +0 -0
  218. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/indexes.py +0 -0
  219. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/isotime.py +0 -0
  220. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/list_utils.py +0 -0
  221. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/lucene.py +0 -0
  222. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/net_utils.py +0 -0
  223. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/path.py +0 -0
  224. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/socket_utils.py +0 -0
  225. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/str_utils.py +0 -0
  226. {howler_api-4.1.0.dev1389 → howler_api-4.1.0.dev1405}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 4.1.0.dev1389
3
+ Version: 4.1.0.dev1405
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -143,7 +143,7 @@ def update_action(id: str, user: User, **kwargs) -> Response:
143
143
 
144
144
  ds = datastore()
145
145
 
146
- existing_action = ds.action.get(id, as_obj=False)
146
+ existing_action, server_version = ds.action.get(id, as_obj=False, version=True)
147
147
 
148
148
  if not existing_action:
149
149
  return not_found(err="The specified automation does not exist")
@@ -166,7 +166,7 @@ def update_action(id: str, user: User, **kwargs) -> Response:
166
166
  action_obj = Action(updated_action)
167
167
  action_obj.action_id = id
168
168
 
169
- ds.action.save(action_obj.action_id, action_obj, refresh=refresh)
169
+ ds.action.save(action_obj.action_id, action_obj, version=server_version, refresh=refresh)
170
170
  except HowlerException as e:
171
171
  return bad_request(err=str(e))
172
172
 
@@ -265,7 +265,7 @@ def overwrite(index: str, id: str, **kwargs):
265
265
 
266
266
  ds = datastore()
267
267
 
268
- record = ds[index].get(id, as_obj=False, version=False)
268
+ record, server_version = ds[index].get(id, as_obj=False, version=True)
269
269
  if not record:
270
270
  return not_found(err="Record %s does not exist" % id)
271
271
 
@@ -292,7 +292,7 @@ def overwrite(index: str, id: str, **kwargs):
292
292
  ds[index].save(
293
293
  id,
294
294
  odm(new_record) if odm else new_record,
295
- version=kwargs.get("server_version"),
295
+ version=server_version,
296
296
  refresh=refresh,
297
297
  )
298
298
 
@@ -16,6 +16,7 @@ from typing import Any, Callable, Dict, Generic, Literal, Optional, TypeVar, Uni
16
16
  import elasticsearch
17
17
  from datemath import dm
18
18
  from datemath.helpers import DateMathException
19
+ from elastic_transport import ApiResponseMeta
19
20
  from opentelemetry import trace
20
21
 
21
22
  from howler import odm
@@ -1136,13 +1137,46 @@ class ESCollection(Generic[ModelType]):
1136
1137
 
1137
1138
  return data
1138
1139
 
1140
+ def _search_exists(self, key) -> bool:
1141
+ """Check document existence with an alias-safe search query."""
1142
+ result = self.with_retries(
1143
+ self.datastore.client.search,
1144
+ index=self.name,
1145
+ query={"ids": {"values": [key]}},
1146
+ size=0,
1147
+ track_total_hits=True,
1148
+ )
1149
+ total = result["hits"]["total"]
1150
+ return total["value"] > 0 if isinstance(total, dict) else total > 0
1151
+
1139
1152
  def exists(self, key) -> bool:
1140
1153
  """Check if a document exists in the datastore.
1141
1154
 
1142
1155
  :param key: key of the document to get from the datastore
1143
1156
  :return: true/false depending if the document exists or not
1144
1157
  """
1145
- return cast(bool, self.with_retries(self.datastore.client.exists, index=self.name, id=key, _source=False))
1158
+ if self.ilm_config:
1159
+ return self._search_exists(key)
1160
+
1161
+ try:
1162
+ return cast(bool, self.with_retries(self.datastore.client.exists, index=self.name, id=key, _source=False))
1163
+ except elasticsearch.BadRequestError:
1164
+ logger.exception(
1165
+ "Single-document existence check failed on %s; falling back to an alias-safe search.",
1166
+ self.name,
1167
+ )
1168
+ return self._search_exists(key)
1169
+
1170
+ def _raise_document_not_found(self, key: str, result: Any) -> typing.NoReturn:
1171
+ """Raise the same error shape as an Elasticsearch document lookup for an empty search."""
1172
+ meta = ApiResponseMeta(
1173
+ status=404,
1174
+ http_version="1.1",
1175
+ headers=cast(Any, {}),
1176
+ duration=0.0,
1177
+ node=cast(Any, None),
1178
+ )
1179
+ raise elasticsearch.exceptions.NotFoundError(f"Document with id {key} not found", meta, result)
1146
1180
 
1147
1181
  @overload
1148
1182
  def _get(self, key, retries, version: Literal[False]) -> dict[str, Any]: ...
@@ -1154,7 +1188,7 @@ class ESCollection(Generic[ModelType]):
1154
1188
  """Versioned get-save for atomic update has two paths:
1155
1189
  1. Document doesn't exist at all. Create token will be returned for version.
1156
1190
  This way only the first query to try and create the document will succeed.
1157
- 2. Document exists in hot. A version string with the info needed to do a versioned save is returned.
1191
+ 2. Document exists. A version string with the info needed to do a versioned save is returned.
1158
1192
 
1159
1193
  The create token is needed to differentiate between "I'm saving a new
1160
1194
  document non-atomic (version=None)" and "I'm saving a new document
@@ -1173,11 +1207,29 @@ class ESCollection(Generic[ModelType]):
1173
1207
  done = False
1174
1208
  while not done:
1175
1209
  try:
1176
- doc = self.with_retries(self.datastore.client.get, index=self.name, id=key)
1210
+ if self.ilm_config:
1211
+ result = self.with_retries(
1212
+ self.datastore.client.search,
1213
+ index=self.name,
1214
+ query={"ids": {"values": [key]}},
1215
+ size=1,
1216
+ seq_no_primary_term=True,
1217
+ )
1218
+ hits = result["hits"]["hits"]
1219
+ if not hits:
1220
+ self._raise_document_not_found(key, result)
1221
+ doc = hits[0]
1222
+ else:
1223
+ doc = self.with_retries(self.datastore.client.get, index=self.name, id=key)
1224
+
1177
1225
  if version:
1226
+ if self.ilm_config:
1227
+ version_token = f"{doc['_index']}---{doc['_seq_no']}---{doc['_primary_term']}"
1228
+ else:
1229
+ version_token = f"{doc['_seq_no']}---{doc['_primary_term']}"
1178
1230
  return (
1179
1231
  normalize_output(doc["_source"]),
1180
- f"{doc['_seq_no']}---{doc['_primary_term']}",
1232
+ version_token,
1181
1233
  )
1182
1234
  return normalize_output(doc["_source"])
1183
1235
  except elasticsearch.exceptions.NotFoundError:
@@ -1196,6 +1248,19 @@ class ESCollection(Generic[ModelType]):
1196
1248
 
1197
1249
  return None
1198
1250
 
1251
+ def _get_version_write_target(self, version: str) -> tuple[str, str, str]:
1252
+ """Return the concrete write target and optimistic-concurrency values from a version token."""
1253
+ version_parts = version.split("---")
1254
+ if len(version_parts) == 3:
1255
+ index, seq_no, primary_term = version_parts
1256
+ return index, seq_no, primary_term
1257
+
1258
+ if len(version_parts) == 2:
1259
+ seq_no, primary_term = version_parts
1260
+ return self.name, seq_no, primary_term
1261
+
1262
+ raise DataStoreException(f"Invalid version token for {self.name}: {version!r}")
1263
+
1199
1264
  @overload
1200
1265
  def get(self, key, as_obj: Literal[True], version: Literal[True]) -> tuple[ModelType | None, str]: ...
1201
1266
 
@@ -1325,13 +1390,17 @@ class ESCollection(Generic[ModelType]):
1325
1390
 
1326
1391
  saved_data["id"] = key
1327
1392
  operation = "index"
1393
+ index = self.name
1328
1394
  seq_no = None
1329
1395
  primary_term = None
1330
1396
 
1397
+ if version is None and self.ilm_config:
1398
+ _, version = self.get_if_exists(key, as_obj=False, version=True)
1399
+
1331
1400
  if version == CREATE_TOKEN:
1332
1401
  operation = "create"
1333
1402
  elif version:
1334
- seq_no, primary_term = version.split("---")
1403
+ index, seq_no, primary_term = self._get_version_write_target(version)
1335
1404
 
1336
1405
  if refresh == "true":
1337
1406
  logger.warning(
@@ -1341,7 +1410,7 @@ class ESCollection(Generic[ModelType]):
1341
1410
  try:
1342
1411
  self.with_retries(
1343
1412
  self.datastore.client.index,
1344
- index=self.name,
1413
+ index=index,
1345
1414
  id=key,
1346
1415
  document=json.dumps(saved_data),
1347
1416
  op_type=operation,
@@ -1541,15 +1610,16 @@ class ESCollection(Generic[ModelType]):
1541
1610
  """
1542
1611
  operations = self._validate_operations(operations)
1543
1612
  script = self._create_scripts_from_operations(operations)
1613
+ index = self.name
1544
1614
  seq_no = None
1545
1615
  primary_term = None
1546
1616
  if version:
1547
- seq_no, primary_term = version.split("---")
1617
+ index, seq_no, primary_term = self._get_version_write_target(version)
1548
1618
 
1549
1619
  try:
1550
1620
  res = self.with_retries(
1551
1621
  self.datastore.client.update,
1552
- index=self.name,
1622
+ index=index,
1553
1623
  id=key,
1554
1624
  script=script,
1555
1625
  if_seq_no=seq_no,
@@ -1559,7 +1629,11 @@ class ESCollection(Generic[ModelType]):
1559
1629
  )
1560
1630
  return (
1561
1631
  res["result"] == "updated",
1562
- f"{res['_seq_no']}---{res['_primary_term']}",
1632
+ (
1633
+ f"{res['_index']}---{res['_seq_no']}---{res['_primary_term']}"
1634
+ if self.ilm_config
1635
+ else f"{res['_seq_no']}---{res['_primary_term']}"
1636
+ ),
1563
1637
  )
1564
1638
  except elasticsearch.NotFoundError as e:
1565
1639
  logger.warning("Update - elasticsearch.NotFoundError: %s %s", e.message, e.info)
@@ -15,6 +15,7 @@ from howler.common.exceptions import HowlerRuntimeError
15
15
  from howler.common.loader import datastore
16
16
  from howler.datastore.collection import ESCollection
17
17
  from howler.odm.base import Model
18
+ from howler.utils.compat import Self
18
19
 
19
20
  ModelType = TypeVar("ModelType", bound=Model)
20
21
 
@@ -28,10 +29,10 @@ class _ObjectsDescriptor(Generic[ModelType]):
28
29
  """
29
30
 
30
31
  @overload
31
- def __get__(self, obj: None, objtype: type[ModelType]) -> ESCollection[ModelType]: ...
32
+ def __get__(self: Self, obj: None, objtype: type[ModelType]) -> ESCollection[ModelType]: ...
32
33
 
33
34
  @overload
34
- def __get__(self, obj: ModelType, objtype: type[ModelType]) -> ESCollection[ModelType]: ...
35
+ def __get__(self: Self, obj: ModelType, objtype: type[ModelType]) -> ESCollection[ModelType]: ...
35
36
 
36
37
  def __get__(self, obj: ModelType | None, objtype: type[ModelType] | None = None) -> ESCollection[ModelType]:
37
38
  """Return the ESCollection for the owner class.
@@ -82,7 +83,11 @@ class DatastoreMixin(Generic[ModelType]):
82
83
  """
83
84
  return datastore()
84
85
 
85
- def save(self, refresh: Literal["true", "false", "wait_for"] | None = None) -> bool:
86
+ def save(
87
+ self: Self,
88
+ refresh: Literal["true", "false", "wait_for"] | None = None,
89
+ version: str | None = None,
90
+ ) -> bool:
86
91
  """Persist the current model instance to the datastore.
87
92
 
88
93
  Determines the target index from the lowercase class name, extracts the
@@ -96,5 +101,6 @@ class DatastoreMixin(Generic[ModelType]):
96
101
  index_name = self.__class__.__name__.lower()
97
102
  id_field = self.__class__._Model__id_field # type: ignore[attr-defined]
98
103
  current_id = attrgetter(id_field)(self)
104
+ collection = self.ds[index_name]
99
105
 
100
- return self.ds[index_name].save(current_id, self, refresh=refresh)
106
+ return collection.save(current_id, self, version=version, refresh=refresh)
@@ -19,6 +19,7 @@ from howler.common.exceptions import (
19
19
  from howler.common.loader import APP_NAME, datastore
20
20
  from howler.common.logging import get_logger
21
21
  from howler.config import CLASSIFICATION
22
+ from howler.datastore.collection import CREATE_TOKEN
22
23
  from howler.datastore.exceptions import DataStoreException
23
24
  from howler.odm.models.case import Case, CaseItem, CaseItemTypes, CaseLog, CaseRule
24
25
  from howler.odm.models.ecs.related import Related
@@ -56,7 +57,7 @@ def create_case(
56
57
 
57
58
  case = Case(case_data)
58
59
  case.log = [CaseLog({"timestamp": "NOW", "explanation": "Case created", "user": user.uname if user else "system"})]
59
- case.save(refresh="wait_for")
60
+ case.save(refresh="wait_for", version=CREATE_TOKEN)
60
61
  CREATED_CASES.inc()
61
62
 
62
63
  for item in items:
@@ -693,18 +694,18 @@ def remove_case_items( # noqa: C901
693
694
 
694
695
  # Resolve backing objects for back-reference cleanup
695
696
  items_to_remove = [items_by_id[iid] for iid in ids_to_remove if iid in items_by_id]
696
- backing_objs: list[Hit | Event] = []
697
+ backing_objs: list[tuple[Hit | Event, str]] = []
697
698
  for item in items_to_remove:
698
699
  if item.type in [CaseItemTypes.HIT, CaseItemTypes.EVENT]:
699
- obj = ds[item.type].get(item.value)
700
+ obj, version = ds[item.type].get(item.value, as_obj=True, version=True)
700
701
  if obj:
701
- backing_objs.append(obj)
702
+ backing_objs.append((obj, version))
702
703
 
703
704
  case.items = [item for item in case.items if item.id not in ids_to_remove]
704
705
 
705
- for backing_obj in backing_objs:
706
+ for backing_obj, version in backing_objs:
706
707
  remove_backreference(backing_obj, case.case_id)
707
- backing_obj.save()
708
+ backing_obj.save(version=version)
708
709
 
709
710
  recompute_case_metadata(case)
710
711
 
@@ -745,7 +746,7 @@ def append_hit(case: Case, item: CaseItem, refresh: Literal["true", "false", "wa
745
746
  """
746
747
  ds = datastore()
747
748
 
748
- hit = ds.hit.get(item.value)
749
+ hit, version = ds.hit.get(item.value, as_obj=True, version=True)
749
750
  if hit is None:
750
751
  raise NotFoundException(f"Hit {item.value} not found, cannot be added to case")
751
752
 
@@ -754,7 +755,7 @@ def append_hit(case: Case, item: CaseItem, refresh: Literal["true", "false", "wa
754
755
  case.items.append(item)
755
756
 
756
757
  add_backreference(hit, case.case_id)
757
- hit.save()
758
+ hit.save(version=version)
758
759
 
759
760
  recompute_case_metadata(case)
760
761
  if not case.save(refresh=refresh): # pragma: no cover
@@ -783,7 +784,7 @@ def append_event(case: Case, item: CaseItem, refresh: Literal["true", "false", "
783
784
  """
784
785
  ds = datastore()
785
786
 
786
- event = ds.event.get(key=item.value)
787
+ event, version = ds.event.get(key=item.value, as_obj=True, version=True)
787
788
 
788
789
  if event is None:
789
790
  raise NotFoundException(f"Event {item.value} not found, cannot be added to case")
@@ -793,7 +794,7 @@ def append_event(case: Case, item: CaseItem, refresh: Literal["true", "false", "
793
794
  case.items.append(item)
794
795
 
795
796
  add_backreference(event, case.case_id)
796
- event.save()
797
+ event.save(version=version)
797
798
 
798
799
  recompute_case_metadata(case)
799
800
  if not case.save(refresh=refresh): # pragma: no cover
@@ -199,7 +199,7 @@ def update_dossier( # noqa: C901
199
199
  storage = datastore()
200
200
 
201
201
  # Retrieve the existing dossier for access control checks
202
- existing_dossier: Dossier = get_dossier(dossier_id, as_odm=True)
202
+ existing_dossier, server_version = get_dossier(dossier_id, as_odm=True, version=True)
203
203
 
204
204
  # Enforce access control for personal dossiers
205
205
  # Only the owner or admin users can modify personal dossiers
@@ -228,7 +228,7 @@ def update_dossier( # noqa: C901
228
228
  # Merge the new data with existing dossier data
229
229
  new_data = Dossier(cast(dict, merge({}, existing_dossier.as_primitives(), dossier_data)))
230
230
 
231
- storage.dossier.save(dossier_id, new_data, refresh=refresh)
231
+ storage.dossier.save(dossier_id, new_data, version=server_version, refresh=refresh)
232
232
 
233
233
  return new_data
234
234
  except SearchException:
@@ -7,7 +7,7 @@ import sys
7
7
 
8
8
  if sys.version_info >= (3, 11):
9
9
  from enum import StrEnum
10
- from typing import NotRequired, TypedDict
10
+ from typing import NotRequired, Self, TypedDict
11
11
  else:
12
12
  from enum import Enum as _Enum
13
13
 
@@ -22,6 +22,6 @@ else:
22
22
 
23
23
  # typing_extensions.TypedDict supports Generic[T] mixing on Python < 3.11;
24
24
  # the stdlib version does not gain that until 3.11.
25
- from typing_extensions import NotRequired, TypedDict # noqa: F401
25
+ from typing_extensions import NotRequired, Self, TypedDict # noqa: F401
26
26
 
27
- __all__ = ["NotRequired", "StrEnum", "TypedDict"]
27
+ __all__ = ["NotRequired", "StrEnum", "TypedDict", "Self"]
@@ -152,7 +152,7 @@ suppress-none-returning = true
152
152
  [tool.poetry]
153
153
  package-mode = true
154
154
  name = "howler-api"
155
- version = "4.1.0.dev1389"
155
+ version = "4.1.0.dev1405"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",