howler-api 4.0.0.dev652__tar.gz → 4.0.0.dev664__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 (208) hide show
  1. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v2/case.py +27 -23
  3. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/view.py +8 -0
  4. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/case_service.py +103 -67
  5. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/pyproject.toml +1 -1
  6. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/README.md +0 -0
  7. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/__init__.py +0 -0
  8. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/__init__.py +0 -0
  9. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/add_label.py +0 -0
  10. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/change_field.py +0 -0
  11. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/demote.py +0 -0
  12. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/example_plugin.py +0 -0
  13. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/prioritization.py +0 -0
  14. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/promote.py +0 -0
  15. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/remove_label.py +0 -0
  16. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/actions/transition.py +0 -0
  17. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/__init__.py +0 -0
  18. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/base.py +0 -0
  19. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/socket.py +0 -0
  20. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/__init__.py +0 -0
  21. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/action.py +0 -0
  22. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/analytic.py +0 -0
  23. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/auth.py +0 -0
  24. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/clue.py +0 -0
  25. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/configs.py +0 -0
  26. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/dossier.py +0 -0
  27. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/help.py +0 -0
  28. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/hit.py +0 -0
  29. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/notebook.py +0 -0
  30. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/overview.py +0 -0
  31. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/search.py +0 -0
  32. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/template.py +0 -0
  33. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/tool.py +0 -0
  34. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/user.py +0 -0
  35. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/utils/__init__.py +0 -0
  36. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/utils/etag.py +0 -0
  37. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v1/view.py +0 -0
  38. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v2/__init__.py +0 -0
  39. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v2/ingest.py +0 -0
  40. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/api/v2/search.py +0 -0
  41. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/app.py +0 -0
  42. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/README.md +0 -0
  43. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/__init__.py +0 -0
  44. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/classification.py +0 -0
  45. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/classification.yml +0 -0
  46. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/exceptions.py +0 -0
  47. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/loader.py +0 -0
  48. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/logging/__init__.py +0 -0
  49. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/logging/audit.py +0 -0
  50. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/logging/format.py +0 -0
  51. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/net.py +0 -0
  52. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/net_static.py +0 -0
  53. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/random_user.py +0 -0
  54. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/common/swagger.py +0 -0
  55. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/config.py +0 -0
  56. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/cronjobs/__init__.py +0 -0
  57. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/cronjobs/retention.py +0 -0
  58. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/cronjobs/view_cleanup.py +0 -0
  59. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/README.md +0 -0
  60. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/__init__.py +0 -0
  61. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/bulk.py +0 -0
  62. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/collection.py +0 -0
  63. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/constants.py +0 -0
  64. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/exceptions.py +0 -0
  65. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/howler_store.py +0 -0
  66. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/migrations/fix_process.py +0 -0
  67. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/operations.py +0 -0
  68. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/schemas.py +0 -0
  69. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/store.py +0 -0
  70. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/support/__init__.py +0 -0
  71. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/support/build.py +0 -0
  72. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/support/schemas.py +0 -0
  73. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/datastore/types.py +0 -0
  74. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/error.py +0 -0
  75. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/external/__init__.py +0 -0
  76. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/external/generate_mitre.py +0 -0
  77. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/external/generate_sigma_rules.py +0 -0
  78. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/external/generate_tlds.py +0 -0
  79. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/external/reindex_data.py +0 -0
  80. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/external/wipe_databases.py +0 -0
  81. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/gunicorn_config.py +0 -0
  82. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/healthz.py +0 -0
  83. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/__init__.py +0 -0
  84. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/azure.py +0 -0
  85. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/discover.py +0 -0
  86. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/hit.py +0 -0
  87. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/oauth.py +0 -0
  88. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/search.py +0 -0
  89. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/workflow.py +0 -0
  90. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/helper/ws.py +0 -0
  91. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/README.md +0 -0
  92. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/__init__.py +0 -0
  93. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/base.py +0 -0
  94. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/charter.txt +0 -0
  95. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/constants.py +0 -0
  96. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/helper.py +0 -0
  97. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/howler_enum.py +0 -0
  98. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/__init__.py +0 -0
  99. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/action.py +0 -0
  100. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/analytic.py +0 -0
  101. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/assemblyline.py +0 -0
  102. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/aws.py +0 -0
  103. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/azure.py +0 -0
  104. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/case.py +0 -0
  105. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/cbs.py +0 -0
  106. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/clue.py +0 -0
  107. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/config.py +0 -0
  108. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/dossier.py +0 -0
  109. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/__init__.py +0 -0
  110. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/agent.py +0 -0
  111. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/autonomous_system.py +0 -0
  112. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/client.py +0 -0
  113. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/cloud.py +0 -0
  114. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/code_signature.py +0 -0
  115. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/container.py +0 -0
  116. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/dns.py +0 -0
  117. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/egress.py +0 -0
  118. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/elf.py +0 -0
  119. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/email.py +0 -0
  120. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/error.py +0 -0
  121. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/event.py +0 -0
  122. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/faas.py +0 -0
  123. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/file.py +0 -0
  124. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/geo.py +0 -0
  125. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/group.py +0 -0
  126. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/hash.py +0 -0
  127. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/host.py +0 -0
  128. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/http.py +0 -0
  129. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/ingress.py +0 -0
  130. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/interface.py +0 -0
  131. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/network.py +0 -0
  132. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/observer.py +0 -0
  133. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/organization.py +0 -0
  134. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/os.py +0 -0
  135. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/pe.py +0 -0
  136. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/process.py +0 -0
  137. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/registry.py +0 -0
  138. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/related.py +0 -0
  139. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/rule.py +0 -0
  140. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/server.py +0 -0
  141. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/threat.py +0 -0
  142. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/tls.py +0 -0
  143. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/url.py +0 -0
  144. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/user.py +0 -0
  145. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/user_agent.py +0 -0
  146. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/ecs/vulnerability.py +0 -0
  147. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/gcp.py +0 -0
  148. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/hit.py +0 -0
  149. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/howler_data.py +0 -0
  150. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/lead.py +0 -0
  151. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/localized_label.py +0 -0
  152. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/observable.py +0 -0
  153. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/overview.py +0 -0
  154. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/pivot.py +0 -0
  155. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/record.py +0 -0
  156. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/template.py +0 -0
  157. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/models/user.py +0 -0
  158. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/random_data.py +0 -0
  159. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/odm/randomizer.py +0 -0
  160. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/patched.py +0 -0
  161. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/plugins/__init__.py +0 -0
  162. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/plugins/config.py +0 -0
  163. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/__init__.py +0 -0
  164. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/README.md +0 -0
  165. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/__init__.py +0 -0
  166. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/counters.py +0 -0
  167. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/events.py +0 -0
  168. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/hash.py +0 -0
  169. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/lock.py +0 -0
  170. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/queues/__init__.py +0 -0
  171. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/queues/comms.py +0 -0
  172. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/queues/multi.py +0 -0
  173. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/queues/named.py +0 -0
  174. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/queues/priority.py +0 -0
  175. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/set.py +0 -0
  176. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  177. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/security/__init__.py +0 -0
  178. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/security/socket.py +0 -0
  179. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/security/utils.py +0 -0
  180. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/__init__.py +0 -0
  181. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/action_service.py +0 -0
  182. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/analytic_service.py +0 -0
  183. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/auth_service.py +0 -0
  184. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/config_service.py +0 -0
  185. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/docs_service.py +0 -0
  186. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/dossier_service.py +0 -0
  187. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/event_service.py +0 -0
  188. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/hit_service.py +0 -0
  189. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/jwt_service.py +0 -0
  190. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/lucene_service.py +0 -0
  191. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/notebook_service.py +0 -0
  192. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/observable_service.py +0 -0
  193. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/overview_service.py +0 -0
  194. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/search_service.py +0 -0
  195. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/template_service.py +0 -0
  196. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/services/user_service.py +0 -0
  197. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/__init__.py +0 -0
  198. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/annotations.py +0 -0
  199. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/chunk.py +0 -0
  200. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/compat.py +0 -0
  201. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/dict_utils.py +0 -0
  202. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/isotime.py +0 -0
  203. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/list_utils.py +0 -0
  204. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/lucene.py +0 -0
  205. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/path.py +0 -0
  206. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/socket_utils.py +0 -0
  207. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/howler/utils/str_utils.py +0 -0
  208. {howler_api-4.0.0.dev652 → howler_api-4.0.0.dev664}/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.dev652
3
+ Version: 4.0.0.dev664
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -7,6 +7,7 @@ from howler.common.loader import datastore
7
7
  from howler.common.logging import get_logger
8
8
  from howler.common.swagger import generate_swagger_docs
9
9
  from howler.datastore.exceptions import DataStoreException
10
+ from howler.odm.models.case import CaseItem
10
11
  from howler.odm.models.user import User
11
12
  from howler.security import api_login
12
13
  from howler.services import case_service
@@ -217,7 +218,8 @@ def append_item(id: str, user: User, **kwargs): # noqa: C901
217
218
  Data Block:
218
219
  {
219
220
  "type": "hit", # Type of item to append: "hit", "observable", "case", "table", "lead", or "reference"
220
- "value": "item-id-123" # The ID or reference value for the item
221
+ "value": "item-id-123" # The ID or reference value for the item,
222
+ "path": "example/path/Title"
221
223
  }
222
224
 
223
225
  Result Example:
@@ -230,56 +232,58 @@ def append_item(id: str, user: User, **kwargs): # noqa: C901
230
232
  except UnsupportedMediaType:
231
233
  return bad_request(err="Invalid JSON body")
232
234
 
233
- if "value" not in body:
234
- return bad_request(err="Case 'value' is required")
235
-
236
- if "type" not in body:
237
- return bad_request(err="Case 'type' missing")
235
+ for field in ["value", "type", "path"]:
236
+ if field not in body:
237
+ return bad_request(err=f"CaseItem '{field}' is required")
238
238
 
239
239
  try:
240
- case_service.append_case_item(
241
- id, item_type=body["type"], item_value=body["value"], item_path=body.get("path", None)
242
- )
240
+ return ok(case_service.append_case_item(id, item=CaseItem(body)))
243
241
  except DataStoreException as e:
244
242
  logger.exception("Save Error")
245
243
  return internal_error(err=str(e))
246
244
  except InvalidDataException as e:
247
245
  return bad_request(err=str(e))
248
246
 
249
- return ok()
250
-
251
247
 
252
248
  @generate_swagger_docs()
253
- @case_api.route("/<id>/items/<value>", methods=["DELETE"])
249
+ @case_api.route("/<case_id>/items", methods=["DELETE"])
254
250
  @api_login(required_priv=["R", "W"])
255
- def delete_item(id: str, value: str, **kwargs):
256
- """Delete an item from a case
251
+ def delete_item(case_id: str, **kwargs):
252
+ """Delete one or more items from a case
257
253
 
258
- This endpoint removes an item from a case's items list. If the item is a hit or
254
+ This endpoint removes items from a case's items list. If an item is a hit or
259
255
  observable, the bidirectional relationship is cleaned up - the case reference will
260
256
  be removed from the backing object's related.cases list.
261
257
 
262
258
  Variables:
263
- id => The id of the case to modify
264
- value => The value of the item to delete (must match the item's value field)
259
+ case_id => The id of the case to modify
265
260
 
266
261
  Arguments:
267
262
  None
268
263
 
269
264
  Data Block:
270
- None
265
+ {
266
+ "values": ["item-id-123", "item-id-456"] # The values of the items to delete
267
+ }
271
268
 
272
269
  Result Example:
273
270
  {
274
- "success": true # Did the deletion succeed?
271
+ ...case # The updated case data
275
272
  }
276
273
  """
274
+ body = request.json
275
+
276
+ if not body or not isinstance(body, dict) or "values" not in body:
277
+ return bad_request(err="Request body must be a JSON object with a 'values' field.")
278
+
279
+ values = body["values"]
280
+ if not isinstance(values, list) or not values:
281
+ return bad_request(err="'values' must be a non-empty list.")
282
+
277
283
  try:
278
- case_service.remove_case_item(id, item_value=value)
284
+ return ok(case_service.remove_case_items(case_id, values))
279
285
  except DataStoreException as e:
280
286
  logger.exception("Save Error")
281
287
  return internal_error(err=str(e))
282
- except InvalidDataException as e:
288
+ except (InvalidDataException, NotFoundException) as e:
283
289
  return bad_request(err=str(e))
284
-
285
- return ok()
@@ -11,9 +11,17 @@ class Settings(odm.Model):
11
11
  )
12
12
 
13
13
 
14
+ DEFAULT_INDEXES = ["hit"]
15
+
16
+
14
17
  @odm.model(index=True, store=True, description="Model of views")
15
18
  class View(odm.Model):
16
19
  view_id: str = odm.UUID(description="A UUID for this view")
20
+ indexes: list[str] = odm.List(
21
+ odm.Keyword(),
22
+ default=DEFAULT_INDEXES,
23
+ description="What indexes this view applies to.",
24
+ )
17
25
  title: str = odm.CaseInsensitiveKeyword(description="The name of this view.")
18
26
  query: str = odm.Keyword(description="The query to run in this view.")
19
27
  sort: str = odm.Keyword(description="The sorting to use with this view.", optional=True)
@@ -222,13 +222,13 @@ def update_case(case_id: str, case_data: dict[str, Any], user: User) -> Case:
222
222
 
223
223
 
224
224
  @overload
225
- def append_case_item(case_id: str, item: CaseItem): ...
225
+ def append_case_item(case_id: str, item: CaseItem) -> Case: ...
226
226
 
227
227
 
228
228
  @overload
229
229
  def append_case_item(
230
230
  case_id: str, item: None = None, item_type: str = ..., item_value: str = ..., item_path: str = ...
231
- ): ...
231
+ ) -> Case: ...
232
232
 
233
233
 
234
234
  def append_case_item( # noqa: C901
@@ -236,8 +236,8 @@ def append_case_item( # noqa: C901
236
236
  item: CaseItem | None = None,
237
237
  item_type: str | None = None,
238
238
  item_value: str | None = None,
239
- item_path: str = "related/",
240
- ):
239
+ item_path: str = "related",
240
+ ) -> Case:
241
241
  """Append an item to a case, dispatching to the appropriate handler based on item type.
242
242
 
243
243
  Can be called either with a pre-built CaseItem object or with individual
@@ -251,12 +251,13 @@ def append_case_item( # noqa: C901
251
251
  "table", "lead", "reference"). Required if item is not provided.
252
252
  item_value: The value/identifier of the item to append. Required if item
253
253
  is not provided.
254
- item_path: Optional path prefix for organizing the item within the case.
255
- A trailing "/" is appended automatically if not present.
254
+ item_path: Path for organizing the item within the case. Must not end
255
+ with a trailing "/".
256
256
 
257
257
  Raises:
258
258
  InvalidDataException: If item is not provided and item_type or item_value
259
- are missing, or if item_type is not a valid CaseItemTypes value.
259
+ are missing, or if item_type is not a valid CaseItemTypes value, or
260
+ if the resolved item path ends with a trailing "/".
260
261
  """
261
262
  if item is None:
262
263
  if not all([item_type, item_value]):
@@ -266,28 +267,31 @@ def append_case_item( # noqa: C901
266
267
  raise InvalidDataException(f"Invalid item type: {item_type}, valid types are: {', '.join(CaseItemTypes)}")
267
268
 
268
269
  if not item_path:
269
- item_path = "related/"
270
+ item_path = "related"
270
271
 
271
272
  item = CaseItem({"type": item_type, "value": item_value, "path": item_path})
272
273
 
274
+ if item.path.endswith("/"):
275
+ raise InvalidDataException("item path must not end with a trailing '/'")
276
+
273
277
  match item.type:
274
278
  case CaseItemTypes.HIT:
275
- append_hit(case_id, item)
279
+ return append_hit(case_id, item)
276
280
  case CaseItemTypes.OBSERVABLE:
277
- append_observable(case_id, item)
281
+ return append_observable(case_id, item)
278
282
  case CaseItemTypes.CASE:
279
- append_case(case_id, item)
283
+ return append_case(case_id, item)
280
284
  case CaseItemTypes.TABLE:
281
- append_table(case_id, item)
285
+ return append_table(case_id, item)
282
286
  case CaseItemTypes.LEAD:
283
- append_lead(case_id, item)
287
+ return append_lead(case_id, item)
284
288
  case CaseItemTypes.REFERENCE:
285
- append_reference(case_id, item)
289
+ return append_reference(case_id, item)
286
290
  case _:
287
291
  raise InvalidDataException(f"Unsupported item type: {item_type}")
288
292
 
289
293
 
290
- def append_hit(case_id: str, item: CaseItem):
294
+ def append_hit(case_id: str, item: CaseItem) -> Case:
291
295
  """Append a hit item to a case and create a back-reference on the hit.
292
296
 
293
297
  Validates that the case and hit both exist and that the hit is not already
@@ -306,37 +310,36 @@ def append_hit(case_id: str, item: CaseItem):
306
310
  """
307
311
  ds = datastore()
308
312
 
309
- case = ds.case.get(key=case_id)
313
+ _case = ds.case.get(case_id)
310
314
 
311
- if case is None:
315
+ if _case is None:
312
316
  raise NotFoundException(f"Case {case_id} does not exist")
313
317
 
314
- if any(item.value == case_item["value"] for case_item in case.items):
318
+ if any(item.value == case_item["value"] for case_item in _case.items):
315
319
  raise InvalidDataException(f"Hit {item.value} already exists in case {case_id}")
316
320
 
317
- hit = ds.hit.get(key=item.value)
321
+ hit = ds.hit.get(item.value)
318
322
 
319
323
  if hit is None:
320
324
  raise NotFoundException(f"Hit {item.value} not found, cannot be added to case")
321
325
 
322
- if item.path == "related/":
323
- item.path = f"alerts/{hit.howler.analytic} ({hit.howler.id})"
326
+ _case.items.append(item)
324
327
 
325
- case.items.append(item)
328
+ if not ds.case.save(_case.case_id, _case):
329
+ raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
326
330
 
327
- if not datastore().case.save(case.case_id, case):
328
- raise DataStoreException(f"Failed to save {case.case_id} with new item {item.value}")
331
+ _add_backreference(hit, _case.case_id)
329
332
 
330
- _add_backreference(hit, case.case_id)
331
- _sync_case_metadata(case_id)
333
+ _sync_case_metadata(_case.case_id)
332
334
 
335
+ return _case
333
336
 
334
- def append_observable(case_id: str, item: CaseItem):
337
+
338
+ def append_observable(case_id: str, item: CaseItem) -> Case:
335
339
  """Append an observable item to a case and create a back-reference on the observable.
336
340
 
337
341
  Validates that the case and observable both exist and that the observable is
338
- not already present in the case. Sets the item's path to include the
339
- observable's ID, then persists the updated case and adds a back-reference
342
+ not already present in the case. It then persists the updated case and adds a back-reference
340
343
  from the observable to the case.
341
344
 
342
345
  Args:
@@ -363,24 +366,22 @@ def append_observable(case_id: str, item: CaseItem):
363
366
  if observable is None:
364
367
  raise NotFoundException(f"Observable {item.value} not found, cannot be added to case")
365
368
 
366
- if item.path == "related/":
367
- item.path = f"observables/{observable.howler.id}"
368
-
369
369
  _case.items.append(item)
370
370
 
371
- if not datastore().case.save(_case.case_id, _case):
371
+ if not ds.case.save(_case.case_id, _case):
372
372
  raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
373
373
 
374
374
  _add_backreference(observable, _case.case_id)
375
375
  _sync_case_metadata(case_id)
376
376
 
377
+ return _case
378
+
377
379
 
378
- def append_case(case_id: str, item: CaseItem):
380
+ def append_case(case_id: str, item: CaseItem) -> Case:
379
381
  """Append a case reference item to a case.
380
382
 
381
383
  Validates that both the parent case and the referenced case exist, and that
382
- the referenced case is not already present in the parent case. Sets the
383
- item's path to include the referenced case's ID, then persists the updated
384
+ the referenced case is not already present in the parent case. It then persists the updated
384
385
  parent case.
385
386
 
386
387
  Args:
@@ -394,7 +395,7 @@ def append_case(case_id: str, item: CaseItem):
394
395
  """
395
396
  ds = datastore()
396
397
 
397
- _case = ds.case.get(key=case_id)
398
+ _case = ds.case.get(case_id)
398
399
 
399
400
  if _case is None:
400
401
  raise NotFoundException(f"Case {case_id} does not exist")
@@ -407,18 +408,15 @@ def append_case(case_id: str, item: CaseItem):
407
408
  if referenced_case is None:
408
409
  raise NotFoundException(f"Referenced case {item.value} not found, cannot be added to case")
409
410
 
410
- if item.path == "related/":
411
- item.path = "cases/"
412
-
413
- item.path += f"{referenced_case.case_id}"
414
-
415
411
  _case.items.append(item)
416
412
 
417
413
  if not datastore().case.save(_case.case_id, _case):
418
414
  raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
419
415
 
416
+ return _case
417
+
420
418
 
421
- def append_table(case_id: str, item: CaseItem):
419
+ def append_table(case_id: str, item: CaseItem) -> Case:
422
420
  """Append a table item to a case.
423
421
 
424
422
  Not yet implemented.
@@ -433,7 +431,7 @@ def append_table(case_id: str, item: CaseItem):
433
431
  raise NotImplementedError
434
432
 
435
433
 
436
- def append_lead(case_id: str, item: CaseItem):
434
+ def append_lead(case_id: str, item: CaseItem) -> Case:
437
435
  """Append a lead item to a case.
438
436
 
439
437
  Not yet implemented.
@@ -448,19 +446,37 @@ def append_lead(case_id: str, item: CaseItem):
448
446
  raise NotImplementedError
449
447
 
450
448
 
451
- def append_reference(case_id: str, item: CaseItem):
452
- """Append a reference item to a case.
449
+ def append_reference(case_id: str, item: CaseItem) -> Case:
450
+ """Append an external reference item to a case.
453
451
 
454
- Not yet implemented.
452
+ Validates that the case exists and that the reference URL is not already
453
+ present in the case. It then persists the updated case.
455
454
 
456
455
  Args:
457
456
  case_id: Unique identifier of the case to append the reference to.
458
- item: A CaseItem representing the external reference to append.
457
+ item: A CaseItem whose ``value`` is the external URL to reference.
459
458
 
460
459
  Raises:
461
- NotImplementedError: Always raised; this feature is not yet implemented.
460
+ NotFoundException: If the case does not exist.
461
+ InvalidDataException: If the reference URL is already present in the case.
462
+ DataStoreException: If saving the updated case fails.
462
463
  """
463
- raise NotImplementedError
464
+ ds = datastore()
465
+
466
+ _case = ds.case.get_if_exists(key=case_id, as_obj=True)
467
+
468
+ if _case is None:
469
+ raise NotFoundException(f"Case {case_id} does not exist")
470
+
471
+ if any(item.value == case_item["value"] for case_item in _case.items):
472
+ raise InvalidDataException(f"Reference {item.value} already exists in case {case_id}")
473
+
474
+ _case.items.append(item)
475
+
476
+ if not datastore().case.save(_case.case_id, _case):
477
+ raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
478
+
479
+ return _case
464
480
 
465
481
 
466
482
  def _collect_indicators_from_related(related: Related | None) -> set[str]:
@@ -575,20 +591,26 @@ def remove_backreference(backing_obj: Hit | Observable | None, case_id: str):
575
591
  datastore()[backing_obj.__class__.__name__.lower()].save(backing_obj.howler.id, backing_obj)
576
592
 
577
593
 
578
- def remove_case_item(case_id: str, item_value: str):
579
- """Remove an item from a case and clean up any associated back-references.
594
+ def remove_case_items(case_id: str, values: list[str]):
595
+ """Remove one or more items from a case in a single atomic operation.
580
596
 
581
- Locates the item within the case by its value, removes it from the case's
582
- items list, persists the updated case, and removes the back-reference from
583
- the backing hit or observable if applicable.
597
+ Validates that every requested value exists within the case before making
598
+ any modifications. If any value is missing the call raises NotFoundException
599
+ without altering the case. When all values are confirmed, all matching
600
+ items are removed in memory, the case is persisted once, and back-references
601
+ are cleaned up from any associated hits or observables.
584
602
 
585
603
  Args:
586
- case_id: Unique identifier of the case to remove the item from.
587
- item_value: The value/identifier of the item to remove.
604
+ case_id: Unique identifier of the case to remove items from.
605
+ item_values: List of item values (IDs / URLs) to remove.
606
+
607
+ Returns:
608
+ The updated Case object.
588
609
 
589
610
  Raises:
590
- NotFoundException: If the case does not exist.
591
- DataStoreException: If saving the updated case fails.
611
+ NotFoundException: If the case does not exist, or if any requested value
612
+ is not present in the case's items list.
613
+ DataStoreException: If persisting the updated case fails.
592
614
  """
593
615
  ds = datastore()
594
616
 
@@ -597,20 +619,34 @@ def remove_case_item(case_id: str, item_value: str):
597
619
  if not _case:
598
620
  raise NotFoundException(f"Case {case_id} does not exist")
599
621
 
600
- case_item = next((item for item in _case.items if item["value"] == item_value), None)
601
- if not case_item:
602
- raise NotFoundException(f"Case item {item_value} does not exist")
622
+ # Build a lookup of value → item for all items currently in the case.
623
+ items_by_value = {_item["value"]: _item for _item in _case.items}
603
624
 
604
- backing_obj: Hit | Observable | None = None
605
- if case_item.type in [CaseItemTypes.HIT, CaseItemTypes.OBSERVABLE]:
606
- backing_obj = ds[case_item.type].get(case_item.value)
625
+ # Pre-validate all requested values before touching anything.
626
+ missing = [v for v in values if v not in items_by_value]
627
+ if missing:
628
+ raise NotFoundException(f"Case item(s) not found in case: {', '.join(missing)}")
607
629
 
608
- _case.items.remove(case_item)
630
+ # Resolve items and collect backing objects that need back-reference cleanup.
631
+ items_to_remove = [items_by_value[v] for v in values]
632
+ backing_objs: list[Hit | Observable] = []
633
+ for item in items_to_remove:
634
+ if item.type in [CaseItemTypes.HIT, CaseItemTypes.OBSERVABLE]:
635
+ obj = ds[item.type].get(item.value)
636
+ if obj:
637
+ backing_objs.append(obj)
638
+
639
+ # Remove all items in memory, then persist the case once.
640
+ for item in items_to_remove:
641
+ _case.items.remove(item)
609
642
 
610
643
  if not ds.case.save(_case.case_id, _case):
611
644
  raise DataStoreException("Failed to save case after item removal")
612
645
 
613
- if backing_obj:
646
+ # Clean up back-references after the case is safely persisted.
647
+ for backing_obj in backing_objs:
614
648
  remove_backreference(backing_obj, _case.case_id)
615
649
 
616
650
  _sync_case_metadata(case_id)
651
+
652
+ return _case
@@ -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.dev652"
155
+ version = "4.0.0.dev664"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",