howler-api 4.0.0.dev799__tar.gz → 4.0.0.dev841__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 (220) hide show
  1. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/socket.py +36 -6
  3. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/case.py +103 -1
  4. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/ingest.py +35 -3
  5. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/app.py +5 -0
  6. howler_api-4.0.0.dev841/howler/cronjobs/correlation.py +36 -0
  7. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/collection.py +27 -0
  8. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/helper.py +0 -4
  9. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/case.py +25 -0
  10. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/config.py +14 -0
  11. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/howler_data.py +0 -4
  12. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/observable.py +0 -4
  13. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/random_data.py +29 -3
  14. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/case_service.py +273 -63
  15. howler_api-4.0.0.dev841/howler/services/correlation_service.py +168 -0
  16. howler_api-4.0.0.dev841/howler/services/event_service.py +134 -0
  17. howler_api-4.0.0.dev841/howler/services/viewer_service.py +43 -0
  18. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/socket_utils.py +4 -25
  19. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/pyproject.toml +1 -1
  20. howler_api-4.0.0.dev799/howler/services/event_service.py +0 -96
  21. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/README.md +0 -0
  22. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/__init__.py +0 -0
  23. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/__init__.py +0 -0
  24. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/add_label.py +0 -0
  25. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/add_to_bundle.py +0 -0
  26. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/add_to_case.py +0 -0
  27. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/change_field.py +0 -0
  28. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/demote.py +0 -0
  29. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/example_plugin.py +0 -0
  30. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/prioritization.py +0 -0
  31. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/promote.py +0 -0
  32. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/remove_from_bundle.py +0 -0
  33. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/remove_label.py +0 -0
  34. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/transition.py +0 -0
  35. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/__init__.py +0 -0
  36. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/base.py +0 -0
  37. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/__init__.py +0 -0
  38. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/action.py +0 -0
  39. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/analytic.py +0 -0
  40. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/auth.py +0 -0
  41. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/clue.py +0 -0
  42. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/configs.py +0 -0
  43. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/dossier.py +0 -0
  44. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/help.py +0 -0
  45. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/hit.py +0 -0
  46. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/notebook.py +0 -0
  47. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/overview.py +0 -0
  48. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/search.py +0 -0
  49. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/template.py +0 -0
  50. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/tool.py +0 -0
  51. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/user.py +0 -0
  52. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/utils/__init__.py +0 -0
  53. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/utils/etag.py +0 -0
  54. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/view.py +0 -0
  55. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/__init__.py +0 -0
  56. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/search.py +0 -0
  57. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/README.md +0 -0
  58. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/__init__.py +0 -0
  59. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/classification.py +0 -0
  60. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/classification.yml +0 -0
  61. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/exceptions.py +0 -0
  62. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/loader.py +0 -0
  63. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/logging/__init__.py +0 -0
  64. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/logging/audit.py +0 -0
  65. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/logging/format.py +0 -0
  66. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/net.py +0 -0
  67. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/net_static.py +0 -0
  68. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/random_user.py +0 -0
  69. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/swagger.py +0 -0
  70. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/config.py +0 -0
  71. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/cronjobs/__init__.py +0 -0
  72. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/cronjobs/retention.py +0 -0
  73. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/cronjobs/view_cleanup.py +0 -0
  74. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/README.md +0 -0
  75. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/__init__.py +0 -0
  76. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/bulk.py +0 -0
  77. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/constants.py +0 -0
  78. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/exceptions.py +0 -0
  79. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/howler_store.py +0 -0
  80. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/migrations/fix_process.py +0 -0
  81. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/operations.py +0 -0
  82. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/schemas.py +0 -0
  83. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/store.py +0 -0
  84. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/support/__init__.py +0 -0
  85. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/support/build.py +0 -0
  86. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/support/schemas.py +0 -0
  87. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/types.py +0 -0
  88. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/error.py +0 -0
  89. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/README.md +0 -0
  90. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/__init__.py +0 -0
  91. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/generate_mitre.py +0 -0
  92. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/generate_sigma_rules.py +0 -0
  93. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/generate_tlds.py +0 -0
  94. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/reindex_data.py +0 -0
  95. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/wipe_databases.py +0 -0
  96. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/gunicorn_config.py +0 -0
  97. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/healthz.py +0 -0
  98. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/__init__.py +0 -0
  99. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/azure.py +0 -0
  100. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/discover.py +0 -0
  101. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/hit.py +0 -0
  102. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/oauth.py +0 -0
  103. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/search.py +0 -0
  104. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/workflow.py +0 -0
  105. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/ws.py +0 -0
  106. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/README.md +0 -0
  107. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/__init__.py +0 -0
  108. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/base.py +0 -0
  109. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/charter.txt +0 -0
  110. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/constants.py +0 -0
  111. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/howler_enum.py +0 -0
  112. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/mixins.py +0 -0
  113. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/__init__.py +0 -0
  114. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/action.py +0 -0
  115. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/analytic.py +0 -0
  116. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/assemblyline.py +0 -0
  117. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/aws.py +0 -0
  118. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/azure.py +0 -0
  119. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/cbs.py +0 -0
  120. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/clue.py +0 -0
  121. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/dossier.py +0 -0
  122. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/__init__.py +0 -0
  123. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/agent.py +0 -0
  124. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/autonomous_system.py +0 -0
  125. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/client.py +0 -0
  126. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/cloud.py +0 -0
  127. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/code_signature.py +0 -0
  128. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/container.py +0 -0
  129. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/dns.py +0 -0
  130. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/egress.py +0 -0
  131. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/elf.py +0 -0
  132. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/email.py +0 -0
  133. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/error.py +0 -0
  134. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/event.py +0 -0
  135. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/faas.py +0 -0
  136. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/file.py +0 -0
  137. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/geo.py +0 -0
  138. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/group.py +0 -0
  139. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/hash.py +0 -0
  140. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/host.py +0 -0
  141. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/http.py +0 -0
  142. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/ingress.py +0 -0
  143. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/interface.py +0 -0
  144. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/network.py +0 -0
  145. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/observer.py +0 -0
  146. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/organization.py +0 -0
  147. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/os.py +0 -0
  148. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/pe.py +0 -0
  149. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/process.py +0 -0
  150. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/registry.py +0 -0
  151. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/related.py +0 -0
  152. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/rule.py +0 -0
  153. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/server.py +0 -0
  154. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/threat.py +0 -0
  155. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/tls.py +0 -0
  156. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/url.py +0 -0
  157. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user.py +0 -0
  158. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user_agent.py +0 -0
  159. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/vulnerability.py +0 -0
  160. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/gcp.py +0 -0
  161. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/hit.py +0 -0
  162. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/lead.py +0 -0
  163. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/localized_label.py +0 -0
  164. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/overview.py +0 -0
  165. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/pivot.py +0 -0
  166. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/record.py +0 -0
  167. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/template.py +0 -0
  168. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/user.py +0 -0
  169. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/view.py +0 -0
  170. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/randomizer.py +0 -0
  171. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/patched.py +0 -0
  172. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/plugins/__init__.py +0 -0
  173. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/plugins/config.py +0 -0
  174. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/__init__.py +0 -0
  175. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/README.md +0 -0
  176. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/__init__.py +0 -0
  177. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/counters.py +0 -0
  178. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/events.py +0 -0
  179. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/hash.py +0 -0
  180. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/lock.py +0 -0
  181. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/__init__.py +0 -0
  182. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/comms.py +0 -0
  183. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/multi.py +0 -0
  184. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/named.py +0 -0
  185. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/priority.py +0 -0
  186. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/set.py +0 -0
  187. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  188. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/security/__init__.py +0 -0
  189. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/security/socket.py +0 -0
  190. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/security/utils.py +0 -0
  191. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/__init__.py +0 -0
  192. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/action_service.py +0 -0
  193. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/analytic_service.py +0 -0
  194. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/auth_service.py +0 -0
  195. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/bundle_compat_service.py +0 -0
  196. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/config_service.py +0 -0
  197. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/docs_service.py +0 -0
  198. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/dossier_service.py +0 -0
  199. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/hit_service.py +0 -0
  200. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/jwt_service.py +0 -0
  201. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/lucene_service.py +0 -0
  202. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/notebook_service.py +0 -0
  203. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/observable_service.py +0 -0
  204. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/overview_service.py +0 -0
  205. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/search_service.py +0 -0
  206. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/template_service.py +0 -0
  207. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/user_service.py +0 -0
  208. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/telemetry.py +0 -0
  209. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/__init__.py +0 -0
  210. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/annotations.py +0 -0
  211. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/chunk.py +0 -0
  212. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/compat.py +0 -0
  213. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/constants.py +0 -0
  214. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/dict_utils.py +0 -0
  215. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/isotime.py +0 -0
  216. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/list_utils.py +0 -0
  217. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/lucene.py +0 -0
  218. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/path.py +0 -0
  219. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/str_utils.py +0 -0
  220. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 4.0.0.dev799
3
+ Version: 4.0.0.dev841
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -7,11 +7,11 @@ from flask import Blueprint, request
7
7
  from opentelemetry import trace
8
8
 
9
9
  import howler.services.event_service as event_service
10
+ import howler.services.viewer_service as viewer_service
10
11
  from howler.api import ok, unauthorized
11
12
  from howler.common.logging import get_logger
12
- from howler.datastore.operations import OdmHelper
13
13
  from howler.helper.ws import ConnectionClosed, Server
14
- from howler.odm.models.hit import Hit
14
+ from howler.security import api_login
15
15
  from howler.security.socket import websocket_auth, ws_response
16
16
  from howler.utils.socket_utils import check_action
17
17
 
@@ -24,13 +24,16 @@ socket_api._doc = "Endpoints concerning websocket connectivity between the clien
24
24
  logger = get_logger(__file__)
25
25
  tracer = trace.get_tracer(__name__)
26
26
 
27
- hit_helper = OdmHelper(Hit)
28
-
29
27
 
30
28
  @tracer.start_as_current_span(f"{__name__}.emit")
31
29
  @socket_api.route("/emit/<event>", methods=["POST"])
32
30
  def emit(event: str):
33
- """Emit an event to all listening websockets"""
31
+ """Emit an event to all listening websockets.
32
+
33
+ .. deprecated::
34
+ This endpoint is deprecated. Events are now propagated via Redis pubsub
35
+ and no longer require a dedicated websocket pod.
36
+ """
34
37
  if "Authorization" not in request.headers:
35
38
  return unauthorized(err="Missing authorization header")
36
39
 
@@ -49,10 +52,25 @@ def emit(event: str):
49
52
  return ok()
50
53
 
51
54
 
55
+ @tracer.start_as_current_span(f"{__name__}.get_viewers")
56
+ @socket_api.route("/viewers/<entity_id>", methods=["GET"])
57
+ @api_login(audit=False, required_priv=["R"])
58
+ def get_viewers(entity_id: str, **kwargs):
59
+ """Get the list of users currently viewing the specified entity
60
+
61
+ Variables:
62
+ entity_id => The ID of the entity to get viewers for
63
+
64
+ Result Example:
65
+ ["user1", "user2"]
66
+ """
67
+ return ok(viewer_service.get_viewers(entity_id))
68
+
69
+
52
70
  @tracer.start_as_current_span(f"{__name__}.connect")
53
71
  @socket_api.route("/connect", websocket=True) # type: ignore
54
72
  @websocket_auth(required_priv=["R"])
55
- def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
73
+ def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
56
74
  """Connect to the server to monitor for updates via websocket
57
75
 
58
76
  Variables:
@@ -78,10 +96,20 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
78
96
  logger.debug("Sending action: %s", data)
79
97
  ws.send(ws_response("action", data))
80
98
 
99
+ def send_case(data: dict[str, Any]):
100
+ logger.debug("Sending case update: %s", data.get("case", {}).get("case_id", "unknown"))
101
+ ws.send(ws_response("cases", data))
102
+
103
+ def send_viewers_update(data: dict[str, Any]):
104
+ logger.debug("Sending viewers update: %s", data.get("id", "unknown"))
105
+ ws.send(ws_response("viewers_update", data))
106
+
81
107
  try:
82
108
  event_service.on("hits", send_hit)
83
109
  event_service.on("broadcast", send_broadcast)
84
110
  event_service.on("action", send_action)
111
+ event_service.on("cases", send_case)
112
+ event_service.on("viewers_update", send_viewers_update)
85
113
  while ws.connected:
86
114
  data = ws.receive(10)
87
115
  if data:
@@ -113,6 +141,8 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
113
141
  event_service.off("hits", send_hit)
114
142
  event_service.off("broadcast", send_broadcast)
115
143
  event_service.off("action", send_action)
144
+ event_service.off("cases", send_case)
145
+ event_service.off("viewers_update", send_viewers_update)
116
146
 
117
147
  for id, action, broadcast in outstanding_actions:
118
148
  outstanding_actions = check_action(id, action, broadcast, outstanding_actions=outstanding_actions, **kwargs)
@@ -290,7 +290,7 @@ def delete_item(case_id: str, **kwargs):
290
290
 
291
291
 
292
292
  @generate_swagger_docs()
293
- @case_api.route("/<case_id>/items", methods=["PATCH"])
293
+ @case_api.route("/<case_id>/items", methods=["PUT"])
294
294
  @api_login(required_priv=["R", "W"])
295
295
  def rename_item(case_id: str, **kwargs):
296
296
  """Rename (re-path) an item within a case
@@ -331,3 +331,105 @@ def rename_item(case_id: str, **kwargs):
331
331
  return internal_error(err=str(e))
332
332
  except (InvalidDataException, NotFoundException) as e:
333
333
  return bad_request(err=str(e))
334
+
335
+
336
+ @generate_swagger_docs()
337
+ @case_api.route("/<id>/rules", methods=["POST"])
338
+ @api_login(required_priv=["R", "W"])
339
+ def add_rule(id: str, user: User, **kwargs):
340
+ """Add a correlation rule to a case
341
+
342
+ Creates a new correlation rule that will match incoming alerts into the case.
343
+ The rule's id and author are generated server-side.
344
+
345
+ Variables:
346
+ id => The id of the case to add a rule to
347
+
348
+ Arguments:
349
+ None
350
+
351
+ Data Block:
352
+ {
353
+ "query": "howler.analytic:Suspicious*",
354
+ "destination": "alerts/{{howler.analytic}}",
355
+ "timeframe": "2026-05-06T00:00:00Z" // optional, null means no expiry
356
+ }
357
+
358
+ Result Example:
359
+ {
360
+ ...case # The updated case data
361
+ }
362
+ """
363
+ body = request.json
364
+
365
+ if not body or not isinstance(body, dict):
366
+ return bad_request(err="Request body must be a JSON object with rule data.")
367
+
368
+ try:
369
+ return ok(case_service.add_case_rule(id, body, user))
370
+ except NotFoundException as e:
371
+ return not_found(err=str(e))
372
+ except InvalidDataException as e:
373
+ return bad_request(err=str(e))
374
+
375
+
376
+ @generate_swagger_docs()
377
+ @case_api.route("/<id>/rules/<rule_id>", methods=["DELETE"])
378
+ @api_login(required_priv=["R", "W"])
379
+ def delete_rule(id: str, rule_id: str, user: User, **kwargs):
380
+ """Delete a correlation rule from a case
381
+
382
+ Variables:
383
+ id => The id of the case
384
+ rule_id => The id of the rule to delete
385
+
386
+ Arguments:
387
+ None
388
+
389
+ Result Example:
390
+ {
391
+ ...case # The updated case data
392
+ }
393
+ """
394
+ try:
395
+ return ok(case_service.remove_case_rule(id, rule_id, user))
396
+ except NotFoundException as e:
397
+ return not_found(err=str(e))
398
+
399
+
400
+ @generate_swagger_docs()
401
+ @case_api.route("/<id>/rules/<rule_id>", methods=["PUT"])
402
+ @api_login(required_priv=["R", "W"])
403
+ def update_rule(id: str, rule_id: str, user: User, **kwargs):
404
+ """Update a correlation rule on a case
405
+
406
+ Allows updating individual fields on a rule: enabled, query, destination, timeframe.
407
+
408
+ Variables:
409
+ id => The id of the case
410
+ rule_id => The id of the rule to update
411
+
412
+ Arguments:
413
+ None
414
+
415
+ Data Block:
416
+ {
417
+ "enabled": false
418
+ }
419
+
420
+ Result Example:
421
+ {
422
+ ...case # The updated case data
423
+ }
424
+ """
425
+ body = request.json
426
+
427
+ if not body or not isinstance(body, dict):
428
+ return bad_request(err="Request body must be a JSON object with fields to update.")
429
+
430
+ try:
431
+ return ok(case_service.update_case_rule(id, rule_id, body, user))
432
+ except NotFoundException as e:
433
+ return not_found(err=str(e))
434
+ except InvalidDataException as e:
435
+ return bad_request(err=str(e))
@@ -9,6 +9,7 @@ from howler.common.exceptions import HowlerException, HowlerValueError
9
9
  from howler.common.loader import datastore
10
10
  from howler.common.logging import get_logger
11
11
  from howler.common.swagger import generate_swagger_docs
12
+ from howler.config import config
12
13
  from howler.datastore.collection import ESCollection
13
14
  from howler.datastore.exceptions import DataStoreException
14
15
  from howler.datastore.howler_store import INDEXES
@@ -16,6 +17,7 @@ from howler.datastore.operations import OdmHelper, OdmUpdateOperation
16
17
  from howler.odm.models.hit import Hit
17
18
  from howler.odm.models.observable import Observable
18
19
  from howler.odm.models.user import User
20
+ from howler.remote.datatypes.queues.named import NamedQueue
19
21
  from howler.security import api_login
20
22
  from howler.services import hit_service, observable_service
21
23
  from howler.utils.dict_utils import flatten
@@ -32,6 +34,24 @@ logger = get_logger(__file__)
32
34
 
33
35
  hit_helper = OdmHelper(Hit)
34
36
 
37
+ # Persistent queue for the correlation worker to consume newly ingested hit IDs.
38
+ _ingestion_queue: NamedQueue[str] | None = None
39
+
40
+
41
+ def _get_ingestion_queue() -> NamedQueue[str]:
42
+ """Return the shared ingestion queue, creating it on first use."""
43
+ global _ingestion_queue
44
+
45
+ if _ingestion_queue is None:
46
+ _ingestion_queue = NamedQueue(
47
+ "howler.ingestion_queue",
48
+ host=config.core.redis.persistent.host,
49
+ port=config.core.redis.persistent.port,
50
+ private=False,
51
+ )
52
+
53
+ return _ingestion_queue
54
+
35
55
 
36
56
  @generate_swagger_docs()
37
57
  @ingest_api.route("/<index>", methods=["POST"])
@@ -93,6 +113,13 @@ def create(index: str, user: User, **kwargs):
93
113
  logger.exception("Ingestion failed.")
94
114
  return bad_request(err=f"Ingestion failure on record at index {i}: {e}")
95
115
 
116
+ # Enqueue newly created hit IDs for the correlation worker.
117
+ if ids:
118
+ try:
119
+ _get_ingestion_queue().push(*ids)
120
+ except Exception:
121
+ logger.exception("Failed to enqueue hit IDs for correlation")
122
+
96
123
  return created(ids, warnings=warnings)
97
124
 
98
125
 
@@ -120,15 +147,20 @@ def delete(indexes: str, user: User, **kwargs):
120
147
  "success": True # Deleting the hits succeded
121
148
  }
122
149
  """
123
- hit_ids = request.json
150
+ ids = request.json
124
151
 
125
- if hit_ids is None:
152
+ if ids is None:
126
153
  return bad_request(err="No hit ids were sent.")
127
154
 
128
155
  if "admin" not in user["type"]:
129
156
  return forbidden(err="Cannot delete hit, only administrators are permitted to delete.")
130
157
 
131
- index_list = indexes.split(",") # noqa: F841
158
+ index_list = indexes.split(",")
159
+
160
+ ds = datastore()
161
+
162
+ if non_existing_hit_ids := [id for id in ids if all(not ds[index].exists(id) for index in index_list)]:
163
+ return not_found(err=f"Record ids [{','.join(non_existing_hit_ids)}] do not exist.")
132
164
 
133
165
  # TODO: Reimplement in a generic function
134
166
  # hit_service.delete_hits(hit_ids, indexes=index_list)
@@ -174,6 +174,11 @@ else:
174
174
  if HWL_USE_WEBSOCKET_API or DEBUG:
175
175
  logger.debug("Enabled Websocket API")
176
176
  app.register_blueprint(socket_api)
177
+
178
+ # Start the Redis pubsub watcher so this pod receives events from all pods
179
+ import howler.services.event_service as event_service
180
+
181
+ event_service.start_watcher()
177
182
  else:
178
183
  logger.info("Disabled Websocket API")
179
184
 
@@ -0,0 +1,36 @@
1
+ """Correlation cronjob — starts the correlation worker thread.
2
+
3
+ Auto-discovered by ``howler.cronjobs.setup_jobs`` when ``HWL_USE_JOB_SYSTEM``
4
+ is enabled. Instead of scheduling a periodic APScheduler job, this module
5
+ starts a long-running daemon thread that drains the ingestion queue.
6
+ """
7
+
8
+ import threading
9
+
10
+ from apscheduler.schedulers.base import BaseScheduler
11
+
12
+ from howler.common.logging import get_logger
13
+ from howler.odm.models.config import config
14
+
15
+ logger = get_logger(__file__)
16
+
17
+ _thread: threading.Thread | None = None
18
+
19
+
20
+ def setup_job(sched: BaseScheduler):
21
+ """Start the correlation worker thread if correlation is enabled."""
22
+ global _thread
23
+
24
+ if not config.system.correlation.enabled:
25
+ logger.info("Correlation worker disabled by configuration")
26
+ return
27
+
28
+ if _thread is not None and _thread.is_alive():
29
+ logger.debug("Correlation worker thread already running")
30
+ return
31
+
32
+ from howler.services.correlation_service import run_worker
33
+
34
+ _thread = threading.Thread(target=run_worker, name="correlation-worker", daemon=True)
35
+ _thread.start()
36
+ logger.info("Correlation worker thread started")
@@ -1365,6 +1365,7 @@ class ESCollection(Generic[ModelType]):
1365
1365
  extra_fields["_index"] = result["_index"]
1366
1366
  if "*" in fields:
1367
1367
  fields = None
1368
+
1368
1369
  return self.model_class(source_data, mask=fields, docid=item_id, extra_fields=extra_fields)
1369
1370
  else:
1370
1371
  source_data = recursive_update(source_data, extra_fields, allow_recursion=False)
@@ -1692,6 +1693,32 @@ class ESCollection(Generic[ModelType]):
1692
1693
 
1693
1694
  return ret_data
1694
1695
 
1696
+ @overload
1697
+ def stream_search(
1698
+ self,
1699
+ query: str,
1700
+ fl: str | None = None,
1701
+ filters: list[str] | str | None = None,
1702
+ access_control: typing.Any = None,
1703
+ item_buffer_size: int = 200,
1704
+ *,
1705
+ as_obj: Literal[True] = True,
1706
+ use_archive: bool = False,
1707
+ ) -> typing.Generator[ModelType, None, None]: ...
1708
+
1709
+ @overload
1710
+ def stream_search(
1711
+ self,
1712
+ query: str,
1713
+ fl: str | None = None,
1714
+ filters: list[str] | str | None = None,
1715
+ access_control: typing.Any = None,
1716
+ item_buffer_size: int = 200,
1717
+ *,
1718
+ as_obj: Literal[False],
1719
+ use_archive: bool = False,
1720
+ ) -> typing.Generator[dict[str, typing.Any], None, None]: ...
1721
+
1695
1722
  def stream_search(
1696
1723
  self,
1697
1724
  query,
@@ -249,8 +249,6 @@ def generate_useful_hit( # noqa: C901
249
249
  except IndexError:
250
250
  pass
251
251
 
252
- hit.howler.viewers = []
253
-
254
252
  hit.howler.dossier = [
255
253
  Lead(
256
254
  {
@@ -446,8 +444,6 @@ def generate_useful_observable( # noqa: C901
446
444
  ),
447
445
  ]
448
446
 
449
- observable.howler.viewers = []
450
-
451
447
  return observable
452
448
 
453
449
 
@@ -8,6 +8,19 @@ from howler.utils.compat import StrEnum
8
8
 
9
9
  CASE_ITEM_TYPES = {"observable", "hit", "case", "lead", "reference"}
10
10
 
11
+ RULE_INDEX_TYPES = {"hit", "observable"}
12
+
13
+
14
+ class RuleIndexTypes(StrEnum):
15
+ """Enumeration of valid index types for case rules.
16
+
17
+ Determines which Elasticsearch indexes a case rule query runs against
18
+ during correlation.
19
+ """
20
+
21
+ HIT = "hit"
22
+ OBSERVABLE = "observable"
23
+
11
24
 
12
25
  class CaseItemTypes(StrEnum):
13
26
  """Enumeration of valid case item types.
@@ -69,8 +82,20 @@ class CaseItem(odm.Model):
69
82
 
70
83
  @odm.model(index=True, store=True, description="Rule used to place/query data into case paths.")
71
84
  class CaseRule(odm.Model):
85
+ rule_id: str = odm.UUID(description="Unique rule identifier.")
72
86
  destination: str = odm.Keyword(description="Destination case path template.")
73
87
  query: str = odm.Keyword(description="Lucene query used by this rule.")
88
+ author: str = odm.Keyword(description="Username who created the rule.")
89
+ enabled: bool = odm.Boolean(default=True, description="Whether the rule is currently active.")
90
+ timeframe: Optional[str] = odm.Optional(
91
+ odm.Date(description="ISO datetime when rule expires. Null means no expiry."),
92
+ default=None,
93
+ )
94
+ indexes: list[str] = odm.List(
95
+ odm.Enum(values=RuleIndexTypes),
96
+ default=[RuleIndexTypes.HIT],
97
+ description="Indexes to run this rule against (hit, observable, or both).",
98
+ )
74
99
 
75
100
 
76
101
  @odm.model(index=True, store=True, description="Task associated with a case item path.")
@@ -353,6 +353,18 @@ class ViewCleanup(BaseModel):
353
353
  )
354
354
 
355
355
 
356
+ class Correlation(BaseModel):
357
+ """Correlation worker configuration.
358
+
359
+ Controls the background worker that matches newly ingested alerts
360
+ against active case rules.
361
+ """
362
+
363
+ enabled: bool = Field(default=True, description="Enable the correlation worker?")
364
+ batch_size: int = Field(default=100, description="Max alerts per batch.")
365
+ batch_timeout: int = Field(default=10, description="Seconds to wait before flushing a partial batch.")
366
+
367
+
356
368
  class System(BaseModel):
357
369
  """System-level configuration for Howler.
358
370
 
@@ -366,6 +378,8 @@ class System(BaseModel):
366
378
  "Retention Configuration"
367
379
  view_cleanup: ViewCleanup = ViewCleanup()
368
380
  "View Cleanup Configuration"
381
+ correlation: Correlation = Correlation()
382
+ "Correlation Worker Configuration"
369
383
 
370
384
 
371
385
  class UI(BaseModel):
@@ -293,7 +293,3 @@ class HowlerData(odm.Model):
293
293
  dossier: list[Lead] = odm.List(
294
294
  odm.Compound(Lead), default=[], description="A list of leads forming the dossier associated with this hit"
295
295
  )
296
- viewers: list[str] = odm.List(
297
- odm.Keyword(description="A list of users currently viewing the hit"),
298
- default=[],
299
- )
@@ -101,10 +101,6 @@ class ObservableData(odm.Model):
101
101
  default=[],
102
102
  description="A list of changes to the observable with timestamps and attribution.",
103
103
  )
104
- viewers: list[str] = odm.List(
105
- odm.Keyword(description="A list of users currently viewing the observable"),
106
- default=[],
107
- )
108
104
 
109
105
 
110
106
  @odm.model(
@@ -19,7 +19,7 @@ import importlib
19
19
  import json
20
20
  import random
21
21
  import textwrap
22
- from datetime import datetime
22
+ from datetime import datetime, timedelta
23
23
  from random import choice, randint, sample
24
24
  from typing import Any, Callable, cast
25
25
  from uuid import uuid4
@@ -866,9 +866,35 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
866
866
  "enrichments": [],
867
867
  "rules": [
868
868
  {
869
- "destination": "alerts/{{howler.id}}",
870
- "query": f"destination.domain:{choice(threat_pool)}",
869
+ "destination": choice(
870
+ [
871
+ "alerts/{{howler.analytic}}",
872
+ "incoming/{{event.kind}}",
873
+ "alerts/{{howler.analytic}}/{{event.category}}",
874
+ "correlated/{{source.ip}}",
875
+ "triage/{{howler.escalation}}",
876
+ ]
877
+ ),
878
+ "query": choice(
879
+ [
880
+ f"destination.domain:{choice(threat_pool)}",
881
+ "source.ip:10.0.0.0/8 AND howler.analytic:Suspicious*",
882
+ "event.category:authentication AND event.outcome:failure",
883
+ "howler.escalation:focus OR howler.escalation:crisis",
884
+ f"destination.domain:{choice(threat_pool)} AND event.kind:alert",
885
+ ]
886
+ ),
887
+ "author": choice(selected_participants or ["admin"]),
888
+ "enabled": choice([True, True, True, False]),
889
+ "timeframe": choice(
890
+ [
891
+ (datetime.now() + timedelta(days=randint(7, 28))).isoformat(),
892
+ (datetime.now() + timedelta(days=randint(7, 28))).isoformat(),
893
+ None,
894
+ ]
895
+ ),
871
896
  }
897
+ for _ in range(randint(1, 3))
872
898
  ],
873
899
  "tasks": tasks,
874
900
  }