howler-api 4.0.0.dev803__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.dev803 → howler_api-4.0.0.dev841}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/socket.py +6 -1
  3. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/case.py +103 -1
  4. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/ingest.py +27 -0
  5. {howler_api-4.0.0.dev803 → 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.dev803 → howler_api-4.0.0.dev841}/howler/datastore/collection.py +27 -0
  8. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/case.py +25 -0
  9. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/config.py +14 -0
  10. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/random_data.py +29 -3
  11. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/case_service.py +242 -60
  12. howler_api-4.0.0.dev841/howler/services/correlation_service.py +168 -0
  13. howler_api-4.0.0.dev841/howler/services/event_service.py +134 -0
  14. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/pyproject.toml +1 -1
  15. howler_api-4.0.0.dev803/howler/services/event_service.py +0 -96
  16. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/README.md +0 -0
  17. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/__init__.py +0 -0
  18. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/__init__.py +0 -0
  19. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/add_label.py +0 -0
  20. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/add_to_bundle.py +0 -0
  21. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/add_to_case.py +0 -0
  22. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/change_field.py +0 -0
  23. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/demote.py +0 -0
  24. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/example_plugin.py +0 -0
  25. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/prioritization.py +0 -0
  26. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/promote.py +0 -0
  27. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/remove_from_bundle.py +0 -0
  28. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/remove_label.py +0 -0
  29. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/transition.py +0 -0
  30. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/__init__.py +0 -0
  31. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/base.py +0 -0
  32. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/__init__.py +0 -0
  33. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/action.py +0 -0
  34. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/analytic.py +0 -0
  35. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/auth.py +0 -0
  36. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/clue.py +0 -0
  37. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/configs.py +0 -0
  38. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/dossier.py +0 -0
  39. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/help.py +0 -0
  40. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/hit.py +0 -0
  41. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/notebook.py +0 -0
  42. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/overview.py +0 -0
  43. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/search.py +0 -0
  44. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/template.py +0 -0
  45. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/tool.py +0 -0
  46. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/user.py +0 -0
  47. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/utils/__init__.py +0 -0
  48. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/utils/etag.py +0 -0
  49. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/view.py +0 -0
  50. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/__init__.py +0 -0
  51. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/search.py +0 -0
  52. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/README.md +0 -0
  53. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/__init__.py +0 -0
  54. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/classification.py +0 -0
  55. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/classification.yml +0 -0
  56. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/exceptions.py +0 -0
  57. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/loader.py +0 -0
  58. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/logging/__init__.py +0 -0
  59. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/logging/audit.py +0 -0
  60. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/logging/format.py +0 -0
  61. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/net.py +0 -0
  62. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/net_static.py +0 -0
  63. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/random_user.py +0 -0
  64. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/swagger.py +0 -0
  65. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/config.py +0 -0
  66. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/cronjobs/__init__.py +0 -0
  67. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/cronjobs/retention.py +0 -0
  68. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/cronjobs/view_cleanup.py +0 -0
  69. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/README.md +0 -0
  70. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/__init__.py +0 -0
  71. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/bulk.py +0 -0
  72. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/constants.py +0 -0
  73. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/exceptions.py +0 -0
  74. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/howler_store.py +0 -0
  75. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/migrations/fix_process.py +0 -0
  76. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/operations.py +0 -0
  77. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/schemas.py +0 -0
  78. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/store.py +0 -0
  79. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/support/__init__.py +0 -0
  80. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/support/build.py +0 -0
  81. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/support/schemas.py +0 -0
  82. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/types.py +0 -0
  83. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/error.py +0 -0
  84. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/README.md +0 -0
  85. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/__init__.py +0 -0
  86. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/generate_mitre.py +0 -0
  87. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/generate_sigma_rules.py +0 -0
  88. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/generate_tlds.py +0 -0
  89. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/reindex_data.py +0 -0
  90. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/wipe_databases.py +0 -0
  91. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/gunicorn_config.py +0 -0
  92. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/healthz.py +0 -0
  93. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/__init__.py +0 -0
  94. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/azure.py +0 -0
  95. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/discover.py +0 -0
  96. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/hit.py +0 -0
  97. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/oauth.py +0 -0
  98. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/search.py +0 -0
  99. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/workflow.py +0 -0
  100. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/ws.py +0 -0
  101. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/README.md +0 -0
  102. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/__init__.py +0 -0
  103. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/base.py +0 -0
  104. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/charter.txt +0 -0
  105. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/constants.py +0 -0
  106. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/helper.py +0 -0
  107. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/howler_enum.py +0 -0
  108. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/mixins.py +0 -0
  109. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/__init__.py +0 -0
  110. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/action.py +0 -0
  111. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/analytic.py +0 -0
  112. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/assemblyline.py +0 -0
  113. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/aws.py +0 -0
  114. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/azure.py +0 -0
  115. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/cbs.py +0 -0
  116. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/clue.py +0 -0
  117. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/dossier.py +0 -0
  118. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/__init__.py +0 -0
  119. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/agent.py +0 -0
  120. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/autonomous_system.py +0 -0
  121. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/client.py +0 -0
  122. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/cloud.py +0 -0
  123. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/code_signature.py +0 -0
  124. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/container.py +0 -0
  125. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/dns.py +0 -0
  126. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/egress.py +0 -0
  127. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/elf.py +0 -0
  128. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/email.py +0 -0
  129. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/error.py +0 -0
  130. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/event.py +0 -0
  131. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/faas.py +0 -0
  132. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/file.py +0 -0
  133. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/geo.py +0 -0
  134. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/group.py +0 -0
  135. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/hash.py +0 -0
  136. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/host.py +0 -0
  137. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/http.py +0 -0
  138. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/ingress.py +0 -0
  139. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/interface.py +0 -0
  140. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/network.py +0 -0
  141. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/observer.py +0 -0
  142. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/organization.py +0 -0
  143. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/os.py +0 -0
  144. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/pe.py +0 -0
  145. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/process.py +0 -0
  146. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/registry.py +0 -0
  147. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/related.py +0 -0
  148. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/rule.py +0 -0
  149. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/server.py +0 -0
  150. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/threat.py +0 -0
  151. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/tls.py +0 -0
  152. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/url.py +0 -0
  153. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user.py +0 -0
  154. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user_agent.py +0 -0
  155. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/vulnerability.py +0 -0
  156. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/gcp.py +0 -0
  157. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/hit.py +0 -0
  158. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/howler_data.py +0 -0
  159. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/lead.py +0 -0
  160. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/localized_label.py +0 -0
  161. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/observable.py +0 -0
  162. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/overview.py +0 -0
  163. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/pivot.py +0 -0
  164. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/record.py +0 -0
  165. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/template.py +0 -0
  166. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/user.py +0 -0
  167. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/view.py +0 -0
  168. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/randomizer.py +0 -0
  169. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/patched.py +0 -0
  170. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/plugins/__init__.py +0 -0
  171. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/plugins/config.py +0 -0
  172. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/__init__.py +0 -0
  173. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/README.md +0 -0
  174. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/__init__.py +0 -0
  175. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/counters.py +0 -0
  176. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/events.py +0 -0
  177. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/hash.py +0 -0
  178. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/lock.py +0 -0
  179. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/__init__.py +0 -0
  180. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/comms.py +0 -0
  181. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/multi.py +0 -0
  182. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/named.py +0 -0
  183. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/priority.py +0 -0
  184. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/set.py +0 -0
  185. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  186. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/security/__init__.py +0 -0
  187. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/security/socket.py +0 -0
  188. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/security/utils.py +0 -0
  189. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/__init__.py +0 -0
  190. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/action_service.py +0 -0
  191. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/analytic_service.py +0 -0
  192. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/auth_service.py +0 -0
  193. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/bundle_compat_service.py +0 -0
  194. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/config_service.py +0 -0
  195. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/docs_service.py +0 -0
  196. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/dossier_service.py +0 -0
  197. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/hit_service.py +0 -0
  198. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/jwt_service.py +0 -0
  199. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/lucene_service.py +0 -0
  200. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/notebook_service.py +0 -0
  201. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/observable_service.py +0 -0
  202. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/overview_service.py +0 -0
  203. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/search_service.py +0 -0
  204. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/template_service.py +0 -0
  205. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/user_service.py +0 -0
  206. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/viewer_service.py +0 -0
  207. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/telemetry.py +0 -0
  208. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/__init__.py +0 -0
  209. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/annotations.py +0 -0
  210. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/chunk.py +0 -0
  211. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/compat.py +0 -0
  212. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/constants.py +0 -0
  213. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/dict_utils.py +0 -0
  214. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/isotime.py +0 -0
  215. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/list_utils.py +0 -0
  216. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/lucene.py +0 -0
  217. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/path.py +0 -0
  218. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/socket_utils.py +0 -0
  219. {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/str_utils.py +0 -0
  220. {howler_api-4.0.0.dev803 → 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.dev803
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
@@ -28,7 +28,12 @@ tracer = trace.get_tracer(__name__)
28
28
  @tracer.start_as_current_span(f"{__name__}.emit")
29
29
  @socket_api.route("/emit/<event>", methods=["POST"])
30
30
  def emit(event: str):
31
- """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
+ """
32
37
  if "Authorization" not in request.headers:
33
38
  return unauthorized(err="Missing authorization header")
34
39
 
@@ -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
 
@@ -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,
@@ -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):
@@ -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
  }