howler-api 4.0.0.dev1110__tar.gz → 4.0.0.dev1121__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 (223) hide show
  1. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v2/case.py +7 -3
  3. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/base.py +26 -2
  4. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/helper.py +4 -1
  5. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/case.py +20 -2
  6. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/random_data.py +3 -8
  7. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/randomizer.py +6 -2
  8. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/case_service.py +44 -3
  9. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/correlation_service.py +48 -10
  10. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/pyproject.toml +1 -1
  11. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/README.md +0 -0
  12. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/__init__.py +0 -0
  13. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/__init__.py +0 -0
  14. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/add_label.py +0 -0
  15. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/add_to_bundle.py +0 -0
  16. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/add_to_case.py +0 -0
  17. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/change_field.py +0 -0
  18. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/demote.py +0 -0
  19. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/example_plugin.py +0 -0
  20. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/prioritization.py +0 -0
  21. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/promote.py +0 -0
  22. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/remove_from_bundle.py +0 -0
  23. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/remove_label.py +0 -0
  24. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/actions/transition.py +0 -0
  25. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/__init__.py +0 -0
  26. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/base.py +0 -0
  27. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/socket.py +0 -0
  28. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/__init__.py +0 -0
  29. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/action.py +0 -0
  30. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/analytic.py +0 -0
  31. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/auth.py +0 -0
  32. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/clue.py +0 -0
  33. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/configs.py +0 -0
  34. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/dossier.py +0 -0
  35. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/help.py +0 -0
  36. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/hit.py +0 -0
  37. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/notebook.py +0 -0
  38. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/overview.py +0 -0
  39. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/search.py +0 -0
  40. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/template.py +0 -0
  41. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/tool.py +0 -0
  42. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/user.py +0 -0
  43. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/utils/__init__.py +0 -0
  44. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/utils/etag.py +0 -0
  45. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/utils/params.py +0 -0
  46. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v1/view.py +0 -0
  47. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v2/__init__.py +0 -0
  48. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v2/fuzzy.py +0 -0
  49. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v2/ingest.py +0 -0
  50. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/api/v2/search.py +0 -0
  51. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/app.py +0 -0
  52. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/README.md +0 -0
  53. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/__init__.py +0 -0
  54. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/classification.py +0 -0
  55. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/classification.yml +0 -0
  56. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/exceptions.py +0 -0
  57. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/loader.py +0 -0
  58. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/logging/__init__.py +0 -0
  59. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/logging/audit.py +0 -0
  60. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/logging/format.py +0 -0
  61. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/net.py +0 -0
  62. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/net_static.py +0 -0
  63. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/random_user.py +0 -0
  64. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/common/swagger.py +0 -0
  65. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/config.py +0 -0
  66. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/cronjobs/__init__.py +0 -0
  67. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/cronjobs/action_queue_worker.py +0 -0
  68. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/cronjobs/correlation.py +0 -0
  69. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/cronjobs/retention.py +0 -0
  70. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/cronjobs/view_cleanup.py +0 -0
  71. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/README.md +0 -0
  72. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/__init__.py +0 -0
  73. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/bulk.py +0 -0
  74. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/collection.py +0 -0
  75. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/constants.py +0 -0
  76. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/exceptions.py +0 -0
  77. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/howler_store.py +0 -0
  78. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/migrations/fix_process.py +0 -0
  79. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/operations.py +0 -0
  80. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/schemas.py +0 -0
  81. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/store.py +0 -0
  82. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/support/__init__.py +0 -0
  83. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/support/build.py +0 -0
  84. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/support/schemas.py +0 -0
  85. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/datastore/types.py +0 -0
  86. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/error.py +0 -0
  87. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/README.md +0 -0
  88. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/__init__.py +0 -0
  89. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/generate_mitre.py +0 -0
  90. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/generate_sigma_rules.py +0 -0
  91. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/generate_tlds.py +0 -0
  92. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/reindex_data.py +0 -0
  93. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/external/wipe_databases.py +0 -0
  94. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/gunicorn_config.py +0 -0
  95. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/healthz.py +0 -0
  96. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/__init__.py +0 -0
  97. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/azure.py +0 -0
  98. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/discover.py +0 -0
  99. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/hit.py +0 -0
  100. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/oauth.py +0 -0
  101. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/search.py +0 -0
  102. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/workflow.py +0 -0
  103. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/helper/ws.py +0 -0
  104. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/README.md +0 -0
  105. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/__init__.py +0 -0
  106. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/charter.txt +0 -0
  107. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/constants.py +0 -0
  108. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/howler_enum.py +0 -0
  109. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/mixins.py +0 -0
  110. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/__init__.py +0 -0
  111. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/action.py +0 -0
  112. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/analytic.py +0 -0
  113. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/assemblyline.py +0 -0
  114. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/aws.py +0 -0
  115. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/azure.py +0 -0
  116. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/cbs.py +0 -0
  117. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/clue.py +0 -0
  118. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/config.py +0 -0
  119. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/dossier.py +0 -0
  120. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/__init__.py +0 -0
  121. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/agent.py +0 -0
  122. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/autonomous_system.py +0 -0
  123. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/client.py +0 -0
  124. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/cloud.py +0 -0
  125. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/code_signature.py +0 -0
  126. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/container.py +0 -0
  127. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/dns.py +0 -0
  128. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/egress.py +0 -0
  129. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/elf.py +0 -0
  130. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/email.py +0 -0
  131. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/error.py +0 -0
  132. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/event.py +0 -0
  133. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/faas.py +0 -0
  134. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/file.py +0 -0
  135. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/geo.py +0 -0
  136. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/group.py +0 -0
  137. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/hash.py +0 -0
  138. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/host.py +0 -0
  139. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/http.py +0 -0
  140. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/ingress.py +0 -0
  141. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/interface.py +0 -0
  142. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/network.py +0 -0
  143. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/observer.py +0 -0
  144. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/organization.py +0 -0
  145. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/os.py +0 -0
  146. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/pe.py +0 -0
  147. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/process.py +0 -0
  148. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/registry.py +0 -0
  149. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/related.py +0 -0
  150. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/rule.py +0 -0
  151. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/server.py +0 -0
  152. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/threat.py +0 -0
  153. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/tls.py +0 -0
  154. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/url.py +0 -0
  155. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/user.py +0 -0
  156. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/user_agent.py +0 -0
  157. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/ecs/vulnerability.py +0 -0
  158. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/event.py +0 -0
  159. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/gcp.py +0 -0
  160. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/hit.py +0 -0
  161. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/howler_data.py +0 -0
  162. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/lead.py +0 -0
  163. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/localized_label.py +0 -0
  164. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/overview.py +0 -0
  165. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/pivot.py +0 -0
  166. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/record.py +0 -0
  167. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/template.py +0 -0
  168. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/user.py +0 -0
  169. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/odm/models/view.py +0 -0
  170. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/patched.py +0 -0
  171. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/plugins/__init__.py +0 -0
  172. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/plugins/config.py +0 -0
  173. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/__init__.py +0 -0
  174. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/README.md +0 -0
  175. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/__init__.py +0 -0
  176. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/counters.py +0 -0
  177. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/events.py +0 -0
  178. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/hash.py +0 -0
  179. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/lock.py +0 -0
  180. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/queues/__init__.py +0 -0
  181. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/queues/comms.py +0 -0
  182. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/queues/multi.py +0 -0
  183. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/queues/named.py +0 -0
  184. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/queues/priority.py +0 -0
  185. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/set.py +0 -0
  186. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  187. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/security/__init__.py +0 -0
  188. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/security/socket.py +0 -0
  189. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/security/utils.py +0 -0
  190. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/__init__.py +0 -0
  191. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/action_service.py +0 -0
  192. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/analytic_service.py +0 -0
  193. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/auth_service.py +0 -0
  194. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/bundle_compat_service.py +0 -0
  195. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/comms_service.py +0 -0
  196. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/config_service.py +0 -0
  197. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/docs_service.py +0 -0
  198. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/dossier_service.py +0 -0
  199. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/event_service.py +0 -0
  200. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/fuzzy_service.py +0 -0
  201. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/hit_service.py +0 -0
  202. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/jwt_service.py +0 -0
  203. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/lucene_service.py +0 -0
  204. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/notebook_service.py +0 -0
  205. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/overview_service.py +0 -0
  206. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/search_service.py +0 -0
  207. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/template_service.py +0 -0
  208. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/user_service.py +0 -0
  209. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/services/viewer_service.py +0 -0
  210. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/telemetry.py +0 -0
  211. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/__init__.py +0 -0
  212. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/annotations.py +0 -0
  213. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/chunk.py +0 -0
  214. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/compat.py +0 -0
  215. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/constants.py +0 -0
  216. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/dict_utils.py +0 -0
  217. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/isotime.py +0 -0
  218. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/list_utils.py +0 -0
  219. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/lucene.py +0 -0
  220. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/path.py +0 -0
  221. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/socket_utils.py +0 -0
  222. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/howler/utils/str_utils.py +0 -0
  223. {howler_api-4.0.0.dev1110 → howler_api-4.0.0.dev1121}/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.dev1110
3
+ Version: 4.0.0.dev1121
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -354,7 +354,8 @@ def add_rule(id: str, user: User, **kwargs):
354
354
  {
355
355
  "query": "howler.analytic:Suspicious*",
356
356
  "destination": "alerts/{{howler.analytic}}",
357
- "timeframe": "2026-05-06T00:00:00Z" // optional, null means no expiry
357
+ "timeframe": 14, // optional, days until expiry (null = no expiry)
358
+ "expire_after_resolved": true // optional, start countdown after case resolution
358
359
  }
359
360
 
360
361
  Result Example:
@@ -405,7 +406,8 @@ def delete_rule(id: str, rule_id: str, user: User, **kwargs):
405
406
  def update_rule(id: str, rule_id: str, user: User, **kwargs):
406
407
  """Update a correlation rule on a case
407
408
 
408
- Allows updating individual fields on a rule: enabled, query, destination, timeframe.
409
+ Allows updating individual fields on a rule: enabled, query, destination,
410
+ timeframe, expire_after_resolved.
409
411
 
410
412
  Variables:
411
413
  id => The id of the case
@@ -416,7 +418,9 @@ def update_rule(id: str, rule_id: str, user: User, **kwargs):
416
418
 
417
419
  Data Block:
418
420
  {
419
- "enabled": false
421
+ "enabled": false,
422
+ "timeframe": 7, // days until expiry
423
+ "expire_after_resolved": true // start countdown after resolution
420
424
  }
421
425
 
422
426
  Result Example:
@@ -637,6 +637,11 @@ class IndexText(_Field):
637
637
  class Integer(_Field):
638
638
  """A field storing an integer value."""
639
639
 
640
+ def __init__(self, *args, min=None, max=None, **kwargs):
641
+ super().__init__(*args, **kwargs)
642
+ self.min = min
643
+ self.max = max
644
+
640
645
  def check(self, value, context=[], **kwargs):
641
646
  if self.optional and value is None:
642
647
  return None
@@ -646,10 +651,17 @@ class Integer(_Field):
646
651
  return self.default
647
652
 
648
653
  try:
649
- return int(value)
654
+ result = int(value)
650
655
  except ValueError as e:
651
656
  raise HowlerValueError(f"[{'.'.join(context)}]: {str(e)}")
652
657
 
658
+ if self.min is not None and result < self.min:
659
+ raise HowlerValueError(f"[{'.'.join(context)}]: {result} is less than minimum value {self.min}")
660
+ if self.max is not None and result > self.max:
661
+ raise HowlerValueError(f"[{'.'.join(context)}]: {result} is greater than maximum value {self.max}")
662
+
663
+ return result
664
+
653
665
 
654
666
  class Long(_Field):
655
667
  """
@@ -658,6 +670,11 @@ class Long(_Field):
658
670
  In Elasticsearch, Integer supports values from -2^31 to 2^31-1, while Long supports values from -2^63 to 2^63-1.
659
671
  """
660
672
 
673
+ def __init__(self, *args, min=None, max=None, **kwargs):
674
+ super().__init__(*args, **kwargs)
675
+ self.min = min
676
+ self.max = max
677
+
661
678
  def check(self, value, context=[], **kwargs):
662
679
  if self.optional and value is None:
663
680
  return None
@@ -667,10 +684,17 @@ class Long(_Field):
667
684
  return self.default
668
685
 
669
686
  try:
670
- return int(value)
687
+ result = int(value)
671
688
  except ValueError as e:
672
689
  raise HowlerValueError(f"[{'.'.join(context)}]: {str(e)}")
673
690
 
691
+ if self.min is not None and result < self.min:
692
+ raise HowlerValueError(f"[{'.'.join(context)}]: {result} is less than minimum value {self.min}")
693
+ if self.max is not None and result > self.max:
694
+ raise HowlerValueError(f"[{'.'.join(context)}]: {result} is greater than maximum value {self.max}")
695
+
696
+ return result
697
+
674
698
 
675
699
  class Float(_Field):
676
700
  """A field storing a floating point value."""
@@ -7,7 +7,7 @@ from random import choice, randint, sample
7
7
  from typing import Any, cast
8
8
 
9
9
  from howler.common.logging import get_logger
10
- from howler.config import config
10
+ from howler.config import CLASSIFICATION, config
11
11
  from howler.datastore.howler_store import HowlerDatastore
12
12
  from howler.helper.discover import get_apps_list
13
13
  from howler.odm.base import Model
@@ -49,6 +49,9 @@ def generate_useful_hit( # noqa: C901
49
49
  "Create a random, useful/cogent hit for synthetic data"
50
50
  hit: Hit = random_model_obj(cast(Model, Hit))
51
51
 
52
+ if CLASSIFICATION.enforce:
53
+ hit.classification = CLASSIFICATION.UNRESTRICTED
54
+
52
55
  rand_seed = random.random()
53
56
 
54
57
  timestamp = datetime.now() - timedelta(
@@ -88,16 +88,34 @@ class CaseRule(odm.Model):
88
88
  query: str = odm.Keyword(description="Lucene query used by this rule.")
89
89
  author: str = odm.Keyword(description="Username who created the rule.")
90
90
  enabled: bool = odm.Boolean(default=True, description="Whether the rule is currently active.")
91
- timeframe: Optional[str] = odm.Optional(
92
- odm.Date(description="ISO datetime when rule expires. Null means no expiry."),
91
+ created_at: str = odm.Date(
92
+ default="NOW",
93
+ description="Timestamp when the rule was created.",
94
+ )
95
+ timeframe: Optional[int] = odm.Optional(
96
+ odm.Integer(min=1, description="Number of days the rule stays active. Null means no expiry."),
93
97
  default=None,
94
98
  )
99
+ expire_after_resolved: bool = odm.Boolean(
100
+ default=False,
101
+ description="When true, the timeframe countdown starts from the case's last "
102
+ "resolution time instead of from rule creation.",
103
+ )
95
104
  indexes: list[str] = odm.List(
96
105
  odm.Enum(values=RuleIndexTypes),
97
106
  default=[RuleIndexTypes.HIT],
98
107
  description="Indexes to run this rule against (hit, event, or both).",
99
108
  )
100
109
 
110
+ def __init__(self, data: dict = None, *args, **kwargs):
111
+ timeframe = data.get("timeframe") if data else None
112
+ if timeframe is not None and (isinstance(timeframe, bool) or not isinstance(timeframe, int) or timeframe <= 0):
113
+ raise HowlerValueError("Rule timeframe must be a positive integer or None")
114
+ elif timeframe is None and data.get("expire_after_resolved", False):
115
+ raise HowlerValueError("Rule cannot expire after resolved when no timeframe is set")
116
+
117
+ super().__init__(data, *args, **kwargs)
118
+
101
119
 
102
120
  @odm.model(index=True, store=True, description="Task associated with a case item path.")
103
121
  class CaseTask(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, timedelta
22
+ from datetime import datetime
23
23
  from random import choice, randint, sample
24
24
  from typing import Any, Callable, cast
25
25
  from uuid import uuid4
@@ -908,13 +908,8 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
908
908
  ),
909
909
  "author": choice(selected_participants or ["admin"]),
910
910
  "enabled": choice([True, True, True, False]),
911
- "timeframe": choice(
912
- [
913
- (datetime.now() + timedelta(days=randint(7, 28))).isoformat(),
914
- (datetime.now() + timedelta(days=randint(7, 28))).isoformat(),
915
- None,
916
- ]
917
- ),
911
+ "timeframe": choice([7, 14, 28, None]),
912
+ "expire_after_resolved": choice([True, False]),
918
913
  }
919
914
  for _ in range(randint(1, 3))
920
915
  ],
@@ -490,10 +490,14 @@ def random_data_for_field(field: _Field, name: str, minimal: bool = False) -> _A
490
490
  elif isinstance(field, Date):
491
491
  return get_random_iso_date()
492
492
  elif isinstance(field, Integer):
493
- return random.randint(-2_147_483_648, 2_147_483_647)
493
+ lo = field.min if field.min is not None else -2_147_483_648
494
+ hi = field.max if field.max is not None else 2_147_483_647
495
+ return random.randint(lo, hi)
494
496
  elif isinstance(field, Long):
495
497
  # Generate a random 64-bit signed integer
496
- return random.randint(-9_223_372_036_854_775_808, 9_223_372_036_854_775_807)
498
+ lo = field.min if field.min is not None else -9_223_372_036_854_775_808
499
+ hi = field.max if field.max is not None else 9_223_372_036_854_775_807
500
+ return random.randint(lo, hi)
497
501
  elif isinstance(field, Float):
498
502
  # Generate a random float in a reasonable range
499
503
  return random.uniform(-1e6, 1e6)
@@ -4,6 +4,7 @@ This module provides functionality for creating, updating, retrieving, and manag
4
4
  cases - collections of security alerts and investigation data organized by analysts.
5
5
  """
6
6
 
7
+ from datetime import datetime, timezone
7
8
  from typing import Any, overload
8
9
 
9
10
  from prometheus_client import Counter
@@ -149,6 +150,34 @@ def delete_cases(case_ids: set[str]) -> bool:
149
150
  return ds.case.delete_by_query(f"case_id:({' OR '.join(case_ids)})")
150
151
 
151
152
 
153
+ def get_last_resolved_time(case: Case) -> datetime | None:
154
+ """Return the timestamp of the most recent resolution of a case.
155
+
156
+ Scans the case log newest-to-oldest for an entry where ``key='status'``
157
+ and ``new_value='resolved'``. In the case of multiple resolve/unresolve
158
+ cycles, returns the **final** resolved timestamp.
159
+
160
+ Args:
161
+ case: The Case object whose log to scan.
162
+
163
+ Returns:
164
+ A timezone-aware datetime if the case was ever resolved, else None.
165
+ """
166
+ for entry in reversed(case.log):
167
+ if entry.key == "status" and entry.new_value == "resolved":
168
+ ts = entry.timestamp
169
+ if isinstance(ts, datetime):
170
+ if ts.tzinfo is None:
171
+ return ts.replace(tzinfo=timezone.utc)
172
+ return ts
173
+ try:
174
+ dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
175
+ return dt
176
+ except (ValueError, TypeError):
177
+ continue
178
+ return None
179
+
180
+
152
181
  def _describe_field_change(
153
182
  key: str, previous: Any, new: Any, compound_fields: set[str]
154
183
  ) -> tuple[str, str | None, str | None]:
@@ -823,10 +852,16 @@ def add_case_rule(case_id: str, rule_data: dict, user: User) -> Case:
823
852
  raise InvalidDataException("Rule 'destination' is required")
824
853
 
825
854
  rule_data.pop("rule_id", None)
855
+ rule_data.pop("created_at", None)
826
856
  rule_data["author"] = user.uname
827
857
  rule_data.setdefault("enabled", True)
858
+ rule_data.setdefault("expire_after_resolved", False)
859
+
860
+ try:
861
+ rule = CaseRule(rule_data)
862
+ except HowlerValueError as ex:
863
+ raise InvalidDataException(str(ex)) from ex
828
864
 
829
- rule = CaseRule(rule_data)
830
865
  _case.rules.append(rule)
831
866
 
832
867
  _case.log.append(
@@ -890,7 +925,8 @@ def remove_case_rule(case_id: str, rule_id: str, user: User) -> Case:
890
925
  def update_case_rule(case_id: str, rule_id: str, update_data: dict, user: User) -> Case:
891
926
  """Update fields on an existing correlation rule.
892
927
 
893
- Allowed fields: ``enabled``, ``query``, ``destination``, ``timeframe``.
928
+ Allowed fields: ``enabled``, ``query``, ``destination``, ``timeframe``,
929
+ ``expire_after_resolved``.
894
930
 
895
931
  Args:
896
932
  case_id: Unique identifier of the case.
@@ -911,7 +947,7 @@ def update_case_rule(case_id: str, rule_id: str, update_data: dict, user: User)
911
947
  if _case is None:
912
948
  raise NotFoundException(f"Case {case_id} does not exist")
913
949
 
914
- allowed_fields = {"enabled", "query", "destination", "timeframe"}
950
+ allowed_fields = {"enabled", "query", "destination", "timeframe", "expire_after_resolved"}
915
951
  patch = {k: v for k, v in update_data.items() if k in allowed_fields}
916
952
  if not patch:
917
953
  raise InvalidDataException(
@@ -928,6 +964,11 @@ def update_case_rule(case_id: str, rule_id: str, update_data: dict, user: User)
928
964
  setattr(rule, key, value)
929
965
  changes.append(f"{key}: '{old_value}' → '{value}'")
930
966
 
967
+ if rule.timeframe is not None and (not isinstance(rule.timeframe, int) or rule.timeframe <= 0):
968
+ raise HowlerValueError("Rule timeframe must be a positive integer or None")
969
+ elif rule.timeframe is None and rule.expire_after_resolved:
970
+ raise InvalidDataException("Rule cannot expire after resolved when no timeframe is set")
971
+
931
972
  _case.log.append(
932
973
  CaseLog(
933
974
  {
@@ -8,7 +8,7 @@ The public API consists of three functions:
8
8
  calls ``process_batch`` in debounced batches.
9
9
  """
10
10
 
11
- from datetime import datetime, timezone
11
+ from datetime import datetime, timedelta, timezone
12
12
 
13
13
  import chevron
14
14
  from opentelemetry import trace
@@ -29,9 +29,24 @@ BATCH_SIZE: int = config.system.correlation.batch_size
29
29
  BATCH_TIMEOUT: int = config.system.correlation.batch_timeout
30
30
 
31
31
 
32
- def get_active_rules() -> list[tuple[str, CaseRule]]:
32
+ def _normalize_utc(ts: datetime) -> datetime:
33
+ """Normalize a datetime to UTC, assuming naive timestamps are UTC."""
34
+ if ts.tzinfo is None:
35
+ return ts.replace(tzinfo=timezone.utc)
36
+ return ts
37
+
38
+
39
+ def get_active_rules() -> list[tuple[str, CaseRule]]: # noqa: C901
33
40
  """Return all active (enabled, non-expired) rules across every case.
34
41
 
42
+ A rule's ``timeframe`` is an optional integer representing how many days
43
+ the rule stays active. When ``expire_after_resolved`` is False (default),
44
+ the countdown starts from ``rule.created_at``. When True, it starts from
45
+ the case's most recent resolution time (if the case has never been resolved
46
+ the timer has not started and the rule remains active).
47
+
48
+ If ``timeframe`` is None the rule never expires.
49
+
35
50
  Returns:
36
51
  A list of ``(case_id, rule)`` tuples for rules that should be evaluated.
37
52
  """
@@ -41,20 +56,43 @@ def get_active_rules() -> list[tuple[str, CaseRule]]:
41
56
 
42
57
  # Only fetch cases that actually have rules.
43
58
  for _case in ds.case.stream_search("_exists_:rules.rule_id"):
59
+ # Lazily compute last resolved time only if needed by at least one rule.
60
+ _last_resolved: datetime | None = None
61
+ _last_resolved_computed = False
62
+
44
63
  for rule in _case.rules:
45
64
  if not rule.enabled:
46
65
  continue
47
66
 
48
- if rule.timeframe is not None:
49
- try:
50
- expiry = datetime.fromisoformat(str(rule.timeframe))
51
- if expiry <= now:
52
- continue
53
- except (ValueError, TypeError):
54
- logger.warning("Invalid timeframe on rule %s in case %s", rule.rule_id, _case.case_id)
67
+ if rule.timeframe is None:
68
+ # No expiry configured — rule is always active.
69
+ active.append((_case.case_id, rule))
70
+ continue
71
+
72
+ # Skip rules whose timeframe is not a valid positive integer.
73
+ if isinstance(rule.timeframe, bool) or not isinstance(rule.timeframe, int) or rule.timeframe <= 0:
74
+ logger.warning("Skipping rule %s with invalid timeframe: %r", rule.rule_id, rule.timeframe)
75
+ continue
76
+
77
+ start: datetime
78
+ if not rule.expire_after_resolved:
79
+ start = datetime.fromisoformat(str(rule.created_at).replace("Z", "+00:00"))
80
+ else:
81
+ # Timer starts from last resolution.
82
+ if not _last_resolved_computed:
83
+ _last_resolved = case_service.get_last_resolved_time(_case)
84
+ _last_resolved_computed = True
85
+
86
+ if _last_resolved is None:
87
+ # Case not yet resolved — timer hasn't started.
88
+ active.append((_case.case_id, rule))
55
89
  continue
56
90
 
57
- active.append((_case.case_id, rule))
91
+ start = _normalize_utc(_last_resolved)
92
+
93
+ expiry = start + timedelta(days=rule.timeframe)
94
+ if expiry > now:
95
+ active.append((_case.case_id, rule))
58
96
 
59
97
  return active
60
98
 
@@ -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.0.0.dev1110"
155
+ version = "4.0.0.dev1121"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",