howler-api 4.0.0.dev596__tar.gz → 4.0.0.dev642__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.dev596 → howler_api-4.0.0.dev642}/PKG-INFO +2 -2
  2. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/dossier.py +1 -1
  3. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/search.py +2 -1
  4. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/case.py +1 -2
  5. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/ingest.py +1 -1
  6. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/search.py +1 -1
  7. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/case.py +8 -2
  8. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/observable.py +0 -1
  9. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/record.py +8 -1
  10. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/random_data.py +2 -7
  11. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/case_service.py +104 -46
  12. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/dossier_service.py +4 -2
  13. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/hit_service.py +4 -4
  14. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/lucene_service.py +5 -3
  15. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/pyproject.toml +3 -3
  16. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/README.md +0 -0
  17. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/__init__.py +0 -0
  18. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/__init__.py +0 -0
  19. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/add_label.py +0 -0
  20. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/change_field.py +0 -0
  21. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/demote.py +0 -0
  22. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/example_plugin.py +0 -0
  23. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/prioritization.py +0 -0
  24. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/promote.py +0 -0
  25. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/remove_label.py +0 -0
  26. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/transition.py +0 -0
  27. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/__init__.py +0 -0
  28. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/base.py +0 -0
  29. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/socket.py +0 -0
  30. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/__init__.py +0 -0
  31. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/action.py +0 -0
  32. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/analytic.py +0 -0
  33. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/auth.py +0 -0
  34. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/clue.py +0 -0
  35. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/configs.py +0 -0
  36. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/help.py +0 -0
  37. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/hit.py +0 -0
  38. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/notebook.py +0 -0
  39. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/overview.py +0 -0
  40. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/template.py +0 -0
  41. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/tool.py +0 -0
  42. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/user.py +0 -0
  43. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/utils/__init__.py +0 -0
  44. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/utils/etag.py +0 -0
  45. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/view.py +0 -0
  46. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/__init__.py +0 -0
  47. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/app.py +0 -0
  48. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/README.md +0 -0
  49. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/__init__.py +0 -0
  50. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/classification.py +0 -0
  51. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/classification.yml +0 -0
  52. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/exceptions.py +0 -0
  53. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/loader.py +0 -0
  54. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/logging/__init__.py +0 -0
  55. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/logging/audit.py +0 -0
  56. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/logging/format.py +0 -0
  57. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/net.py +0 -0
  58. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/net_static.py +0 -0
  59. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/random_user.py +0 -0
  60. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/swagger.py +0 -0
  61. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/config.py +0 -0
  62. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/cronjobs/__init__.py +0 -0
  63. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/cronjobs/retention.py +0 -0
  64. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/cronjobs/view_cleanup.py +0 -0
  65. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/README.md +0 -0
  66. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/__init__.py +0 -0
  67. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/bulk.py +0 -0
  68. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/collection.py +0 -0
  69. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/constants.py +0 -0
  70. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/exceptions.py +0 -0
  71. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/howler_store.py +0 -0
  72. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/migrations/fix_process.py +0 -0
  73. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/operations.py +0 -0
  74. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/schemas.py +0 -0
  75. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/store.py +0 -0
  76. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/support/__init__.py +0 -0
  77. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/support/build.py +0 -0
  78. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/support/schemas.py +0 -0
  79. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/types.py +0 -0
  80. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/error.py +0 -0
  81. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/__init__.py +0 -0
  82. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/generate_mitre.py +0 -0
  83. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/generate_sigma_rules.py +0 -0
  84. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/generate_tlds.py +0 -0
  85. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/reindex_data.py +0 -0
  86. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/wipe_databases.py +0 -0
  87. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/gunicorn_config.py +0 -0
  88. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/healthz.py +0 -0
  89. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/__init__.py +0 -0
  90. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/azure.py +0 -0
  91. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/discover.py +0 -0
  92. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/hit.py +0 -0
  93. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/oauth.py +0 -0
  94. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/search.py +0 -0
  95. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/workflow.py +0 -0
  96. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/ws.py +0 -0
  97. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/README.md +0 -0
  98. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/__init__.py +0 -0
  99. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/base.py +0 -0
  100. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/charter.txt +0 -0
  101. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/constants.py +0 -0
  102. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/helper.py +0 -0
  103. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/howler_enum.py +0 -0
  104. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/__init__.py +0 -0
  105. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/action.py +0 -0
  106. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/analytic.py +0 -0
  107. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/assemblyline.py +0 -0
  108. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/aws.py +0 -0
  109. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/azure.py +0 -0
  110. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/cbs.py +0 -0
  111. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/clue.py +0 -0
  112. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/config.py +0 -0
  113. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/dossier.py +0 -0
  114. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/__init__.py +0 -0
  115. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/agent.py +0 -0
  116. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/autonomous_system.py +0 -0
  117. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/client.py +0 -0
  118. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/cloud.py +0 -0
  119. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/code_signature.py +0 -0
  120. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/container.py +0 -0
  121. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/dns.py +0 -0
  122. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/egress.py +0 -0
  123. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/elf.py +0 -0
  124. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/email.py +0 -0
  125. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/error.py +0 -0
  126. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/event.py +0 -0
  127. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/faas.py +0 -0
  128. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/file.py +0 -0
  129. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/geo.py +0 -0
  130. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/group.py +0 -0
  131. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/hash.py +0 -0
  132. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/host.py +0 -0
  133. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/http.py +0 -0
  134. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/ingress.py +0 -0
  135. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/interface.py +0 -0
  136. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/network.py +0 -0
  137. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/observer.py +0 -0
  138. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/organization.py +0 -0
  139. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/os.py +0 -0
  140. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/pe.py +0 -0
  141. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/process.py +0 -0
  142. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/registry.py +0 -0
  143. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/related.py +0 -0
  144. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/rule.py +0 -0
  145. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/server.py +0 -0
  146. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/threat.py +0 -0
  147. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/tls.py +0 -0
  148. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/url.py +0 -0
  149. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/user.py +0 -0
  150. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/user_agent.py +0 -0
  151. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/vulnerability.py +0 -0
  152. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/gcp.py +0 -0
  153. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/hit.py +0 -0
  154. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/howler_data.py +0 -0
  155. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/lead.py +0 -0
  156. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/localized_label.py +0 -0
  157. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/overview.py +0 -0
  158. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/pivot.py +0 -0
  159. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/template.py +0 -0
  160. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/user.py +0 -0
  161. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/view.py +0 -0
  162. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/randomizer.py +0 -0
  163. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/patched.py +0 -0
  164. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/plugins/__init__.py +0 -0
  165. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/plugins/config.py +0 -0
  166. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/__init__.py +0 -0
  167. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/README.md +0 -0
  168. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/__init__.py +0 -0
  169. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/counters.py +0 -0
  170. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/events.py +0 -0
  171. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/hash.py +0 -0
  172. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/lock.py +0 -0
  173. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/__init__.py +0 -0
  174. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/comms.py +0 -0
  175. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/multi.py +0 -0
  176. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/named.py +0 -0
  177. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/priority.py +0 -0
  178. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/set.py +0 -0
  179. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  180. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/security/__init__.py +0 -0
  181. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/security/socket.py +0 -0
  182. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/security/utils.py +0 -0
  183. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/__init__.py +0 -0
  184. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/action_service.py +0 -0
  185. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/analytic_service.py +0 -0
  186. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/auth_service.py +0 -0
  187. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/config_service.py +0 -0
  188. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/docs_service.py +0 -0
  189. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/event_service.py +0 -0
  190. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/jwt_service.py +0 -0
  191. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/notebook_service.py +0 -0
  192. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/observable_service.py +0 -0
  193. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/overview_service.py +0 -0
  194. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/search_service.py +0 -0
  195. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/template_service.py +0 -0
  196. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/user_service.py +0 -0
  197. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/__init__.py +0 -0
  198. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/annotations.py +0 -0
  199. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/chunk.py +0 -0
  200. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/compat.py +0 -0
  201. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/dict_utils.py +0 -0
  202. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/isotime.py +0 -0
  203. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/list_utils.py +0 -0
  204. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/lucene.py +0 -0
  205. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/path.py +0 -0
  206. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/socket_utils.py +0 -0
  207. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/str_utils.py +0 -0
  208. {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/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.dev596
3
+ Version: 4.0.0.dev642
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -39,7 +39,7 @@ Requires-Dist: prometheus-client (==0.24.1)
39
39
  Requires-Dist: pydantic (>=2.11.4,<3.0.0)
40
40
  Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
41
41
  Requires-Dist: pydash (>=8.0.5,<9.0.0)
42
- Requires-Dist: pyjwt (==2.11.0)
42
+ Requires-Dist: pyjwt (==2.12.1)
43
43
  Requires-Dist: pysigma (==0.11.23)
44
44
  Requires-Dist: pysigma-backend-elasticsearch (>=1.1.2,<2.0.0)
45
45
  Requires-Dist: python-baseconv (==1.2.2)
@@ -139,7 +139,7 @@ def get_dossier_for_hit(id: str, user: User, **kwargs):
139
139
 
140
140
  hit = response["items"][0]
141
141
 
142
- return ok(dossier_service.get_matching_dossiers(hit))
142
+ return ok(dossier_service.get_matching_dossiers(hit, username=user.uname))
143
143
  except ValueError as e:
144
144
  return bad_request(err=str(e))
145
145
 
@@ -16,6 +16,7 @@ from howler.common.logging import get_logger
16
16
  from howler.common.swagger import generate_swagger_docs
17
17
  from howler.datastore.exceptions import SearchException
18
18
  from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
19
+ from howler.odm.models.user import User
19
20
  from howler.security import api_login
20
21
  from howler.services import hit_service, lucene_service
21
22
 
@@ -100,7 +101,7 @@ def search(index, **kwargs):
100
101
  "next_deep_paging_id": "asX3f...342", # ID to pass back for the next page during deep paging
101
102
  "items": []} # List of results
102
103
  """
103
- user = kwargs["user"]
104
+ user: User = kwargs["user"]
104
105
  collection = get_collection(index, user)
105
106
  default_sort = get_default_sort(index, user)
106
107
 
@@ -48,8 +48,7 @@ def create_case(user: User, **kwargs):
48
48
  return bad_request(err="Request body must be a JSON object with case data.")
49
49
 
50
50
  try:
51
- new_case = case_service.create_case(case_data, user.uname)
52
- return created(new_case)
51
+ return created(case_service.create_case(case_data, user.uname))
53
52
  except InvalidDataException as e:
54
53
  return bad_request(err=str(e))
55
54
  except ResourceExists as e:
@@ -275,7 +275,7 @@ def update_by_query(indexes: str, **kwargs):
275
275
  """Update a set of hits using a query.
276
276
 
277
277
  Variables:
278
- None
278
+ indexes => Comma-separated list of indexes to update
279
279
 
280
280
  Arguments:
281
281
  None
@@ -279,7 +279,7 @@ def facet(indexes: str, **kwargs):
279
279
  values where the documents matches the specified queries.
280
280
 
281
281
  Variables:
282
- index => Index to search in (hit, user,...)
282
+ indexes => Comma-separated indexes to search in (hit, user,...)
283
283
 
284
284
  Optional Arguments:
285
285
  query => Query to search for
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Any, Optional
2
2
 
3
3
  from howler import odm
4
4
  from howler.common.exceptions import HowlerValueError
@@ -62,7 +62,6 @@ class CaseLog(odm.Model):
62
62
  class CaseItem(odm.Model):
63
63
  path: str = odm.Keyword(description="Path of the item in the case hierarchy.")
64
64
  type: str = odm.Enum(values=CaseItemTypes, description="Type of case item.")
65
- id: str | None = odm.Keyword(description="Identifier for the backing object when available.", optional=True)
66
65
  value: str = odm.Keyword(description="String reference value for the item (ID, URL, or token).")
67
66
  visible: bool = odm.Boolean(default=True, description="Whether the item is visible/accessible in the frontend.")
68
67
 
@@ -177,3 +176,10 @@ class Case(odm.Model):
177
176
  description="A list of changes to the case with timestamps and attribution.",
178
177
  )
179
178
  )
179
+
180
+ def as_primitives(self, hidden_fields=False, strip_null=True) -> dict[str, Any]:
181
+ result = super().as_primitives(hidden_fields, strip_null)
182
+
183
+ result["__index"] = self.__class__.__name__.lower()
184
+
185
+ return result
@@ -53,7 +53,6 @@ class Log(odm.Model):
53
53
 
54
54
 
55
55
  DEFAULT_LABELS = {"assignments": [], "generic": []}
56
- DEFAULT_ASSIGNMENT = "unassigned"
57
56
 
58
57
 
59
58
  @odm.model(
@@ -1,5 +1,5 @@
1
1
  # mypy: ignore-errors
2
- from typing import Optional
2
+ from typing import Any, Optional
3
3
 
4
4
  from howler import odm
5
5
  from howler.common.logging import get_logger
@@ -334,3 +334,10 @@ class Record(odm.Model):
334
334
  reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-vulnerability.html",
335
335
  )
336
336
  )
337
+
338
+ def as_primitives(self, hidden_fields=False, strip_null=True) -> dict[str, Any]:
339
+ result = super().as_primitives(hidden_fields, strip_null)
340
+
341
+ result["__index"] = self.__class__.__name__.lower()
342
+
343
+ return result
@@ -717,7 +717,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
717
717
  {
718
718
  "path": f"alerts/{hit['howler']['analytic']} ({hit['howler']['id']})",
719
719
  "type": "hit",
720
- "id": hit_id,
721
720
  "value": hit_id,
722
721
  }
723
722
  )
@@ -731,7 +730,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
731
730
  {
732
731
  "path": f"observable/{observable['howler']['id']}",
733
732
  "type": "observable",
734
- "id": observable_id,
735
733
  "value": observable_id,
736
734
  }
737
735
  )
@@ -747,7 +745,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
747
745
  {
748
746
  "path": f"alerts/{get_random_word()}/{get_random_word()}",
749
747
  "type": "hit",
750
- "id": nested_hit_id,
751
748
  "value": nested_hit_id,
752
749
  }
753
750
  )
@@ -769,7 +766,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
769
766
  {
770
767
  "path": f"alerts/{get_random_word()}/{get_random_word()}/{get_random_word()}",
771
768
  "type": "observable",
772
- "id": nested_observable_id,
773
769
  "value": nested_observable_id,
774
770
  }
775
771
  )
@@ -788,7 +784,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
788
784
  {
789
785
  "path": f"cases/Related Case {idx}",
790
786
  "type": "case",
791
- "id": related_case_id,
792
787
  "value": related_case_id,
793
788
  }
794
789
  )
@@ -883,8 +878,8 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
883
878
  ds.case.save(case_id, case_data)
884
879
  generated_case_ids.append(case_id)
885
880
 
886
- case_hit_ids = list({item["id"] for item in items if item.get("type") == "hit"})
887
- case_observable_ids = list({item["id"] for item in items if item.get("type") == "observable"})
881
+ case_hit_ids = list({item["value"] for item in items if item.get("type") == "hit"})
882
+ case_observable_ids = list({item["value"] for item in items if item.get("type") == "observable"})
888
883
 
889
884
  for hit_id in case_hit_ids:
890
885
  ds.hit.update(hit_id, [hit_helper.list_add("howler.related", case_id)])
@@ -4,7 +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 typing import Any, overload
7
+ from typing import Any, cast, overload
8
8
 
9
9
  from prometheus_client import Counter
10
10
 
@@ -13,6 +13,7 @@ from howler.common.loader import APP_NAME, datastore
13
13
  from howler.common.logging import get_logger
14
14
  from howler.datastore.exceptions import DataStoreException
15
15
  from howler.odm.models.case import Case, CaseItem, CaseItemTypes, CaseLog
16
+ from howler.odm.models.ecs.related import Related
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
@@ -22,7 +23,7 @@ logger = get_logger(__file__)
22
23
  CREATED_CASES = Counter(f"{APP_NAME.replace('-', '_')}_created_cases_total", "The number of created cases")
23
24
 
24
25
 
25
- def create_case(_case: dict, user: str = None) -> dict[str, Any]: # type: ignore
26
+ def create_case(_case: dict, user: str = None) -> Case: # type: ignore
26
27
  """Create a new case in the datastore.
27
28
 
28
29
  Args:
@@ -50,8 +51,9 @@ def create_case(_case: dict, user: str = None) -> dict[str, Any]: # type: ignor
50
51
  append_case_item(case.case_id, item=CaseItem(item))
51
52
 
52
53
  if items:
53
- return datastore().case.get_if_exists(case.case_id, as_obj=False)
54
- return case.as_primitives()
54
+ return cast(Case, datastore().case.get(case.case_id))
55
+
56
+ return case
55
57
 
56
58
 
57
59
  def hide_cases(case_ids: set[str], user: str) -> None:
@@ -66,19 +68,20 @@ def hide_cases(case_ids: set[str], user: str) -> None:
66
68
  """
67
69
  ds = datastore()
68
70
 
69
- items_query = f"items.id:({' OR '.join(case_ids)})"
71
+ items_query = f"items.value:({' OR '.join(case_ids)})"
70
72
  for case in ds.case.stream_search(items_query, as_obj=False):
71
73
  related_case_id = case["case_id"]
72
74
  if related_case_id in case_ids:
73
75
  continue
74
76
 
75
- related_case = ds.case.get_if_exists(related_case_id, as_obj=True)
77
+ related_case = ds.case.get(related_case_id)
76
78
  if related_case:
77
79
  hidden_ids: list[str] = []
78
80
  for item in related_case.items:
79
- if item.id in case_ids:
81
+ if item.value in case_ids:
80
82
  item.visible = False
81
- hidden_ids.append(item.id)
83
+ hidden_ids.append(item.value)
84
+
82
85
  if hidden_ids:
83
86
  related_case.log.append(
84
87
  CaseLog(
@@ -92,7 +95,7 @@ def hide_cases(case_ids: set[str], user: str) -> None:
92
95
  ds.case.save(related_case_id, related_case)
93
96
 
94
97
  for case_id in case_ids:
95
- case = ds.case.get_if_exists(case_id, as_obj=True)
98
+ case = ds.case.get(case_id)
96
99
  if case:
97
100
  case.visible = False
98
101
  case.log.append(
@@ -122,15 +125,15 @@ def delete_cases(case_ids: set[str]) -> bool:
122
125
  """
123
126
  ds = datastore()
124
127
 
125
- items_query = f"items.id:({' OR '.join(case_ids)})"
128
+ items_query = f"items.value:({' OR '.join(case_ids)})"
126
129
  for case in ds.case.stream_search(items_query, as_obj=False):
127
130
  related_case_id = case["case_id"]
128
131
  if related_case_id in case_ids:
129
132
  continue
130
133
 
131
- related_case = ds.case.get_if_exists(related_case_id, as_obj=True)
134
+ related_case = ds.case.get(related_case_id)
132
135
  if related_case:
133
- related_case.items = [item for item in related_case.items if item.id not in case_ids]
136
+ related_case.items = [item for item in related_case.items if item.value not in case_ids]
134
137
  ds.case.save(related_case_id, related_case)
135
138
 
136
139
  return ds.case.delete_by_query(f"case_id:({' OR '.join(case_ids)})")
@@ -156,7 +159,7 @@ def update_case(case_id: str, case_data: dict[str, Any], user: User) -> Case:
156
159
  """
157
160
  ds = datastore()
158
161
 
159
- case = ds.case.get_if_exists(case_id, as_obj=True)
162
+ case = ds.case.get(case_id)
160
163
  if case is None:
161
164
  raise NotFoundException(f"Case {case_id} does not exist")
162
165
 
@@ -303,7 +306,7 @@ def append_hit(case_id: str, item: CaseItem):
303
306
  """
304
307
  ds = datastore()
305
308
 
306
- case: Case = ds.case.get_if_exists(key=case_id, as_obj=True)
309
+ case = ds.case.get(key=case_id)
307
310
 
308
311
  if case is None:
309
312
  raise NotFoundException(f"Case {case_id} does not exist")
@@ -311,13 +314,11 @@ def append_hit(case_id: str, item: CaseItem):
311
314
  if any(item.value == case_item["value"] for case_item in case.items):
312
315
  raise InvalidDataException(f"Hit {item.value} already exists in case {case_id}")
313
316
 
314
- hit: Hit = ds.hit.get_if_exists(key=item.value, as_obj=True)
317
+ hit = ds.hit.get(key=item.value)
315
318
 
316
319
  if hit is None:
317
320
  raise NotFoundException(f"Hit {item.value} not found, cannot be added to case")
318
321
 
319
- item.id = item.value
320
-
321
322
  if item.path == "related/":
322
323
  item.path = f"alerts/{hit.howler.analytic} ({hit.howler.id})"
323
324
 
@@ -326,7 +327,8 @@ def append_hit(case_id: str, item: CaseItem):
326
327
  if not datastore().case.save(case.case_id, case):
327
328
  raise DataStoreException(f"Failed to save {case.case_id} with new item {item.value}")
328
329
 
329
- add_backreference(hit, case.case_id)
330
+ _add_backreference(hit, case.case_id)
331
+ _sync_case_metadata(case_id)
330
332
 
331
333
 
332
334
  def append_observable(case_id: str, item: CaseItem):
@@ -348,30 +350,29 @@ def append_observable(case_id: str, item: CaseItem):
348
350
  """
349
351
  ds = datastore()
350
352
 
351
- case: Case = ds.case.get_if_exists(key=case_id, as_obj=True)
353
+ _case = ds.case.get(key=case_id)
352
354
 
353
- if case is None:
355
+ if _case is None:
354
356
  raise NotFoundException(f"Case {case_id} does not exist")
355
357
 
356
- if any(item.value == case_item["value"] for case_item in case.items):
358
+ if any(item.value == case_item["value"] for case_item in _case.items):
357
359
  raise InvalidDataException(f"Observable {item.value} already exists in case {case_id}")
358
360
 
359
- observable: Observable = ds.observable.get_if_exists(key=item.value, as_obj=True)
361
+ observable = ds.observable.get(key=item.value)
360
362
 
361
363
  if observable is None:
362
364
  raise NotFoundException(f"Observable {item.value} not found, cannot be added to case")
363
365
 
364
- item.id = item.value
365
-
366
366
  if item.path == "related/":
367
367
  item.path = f"observables/{observable.howler.id}"
368
368
 
369
- case.items.append(item)
369
+ _case.items.append(item)
370
370
 
371
- if not datastore().case.save(case.case_id, case):
372
- raise DataStoreException(f"Failed to save {case.case_id} with new item {item.value}")
371
+ if not datastore().case.save(_case.case_id, _case):
372
+ raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
373
373
 
374
- add_backreference(observable, case.case_id)
374
+ _add_backreference(observable, _case.case_id)
375
+ _sync_case_metadata(case_id)
375
376
 
376
377
 
377
378
  def append_case(case_id: str, item: CaseItem):
@@ -393,30 +394,28 @@ def append_case(case_id: str, item: CaseItem):
393
394
  """
394
395
  ds = datastore()
395
396
 
396
- case: Case = ds.case.get_if_exists(key=case_id, as_obj=True)
397
+ _case = ds.case.get(key=case_id)
397
398
 
398
- if case is None:
399
+ if _case is None:
399
400
  raise NotFoundException(f"Case {case_id} does not exist")
400
401
 
401
- if any(item.value == case_item["value"] for case_item in case.items):
402
+ if any(item.value == case_item["value"] for case_item in _case.items):
402
403
  raise InvalidDataException(f"Observable {item.value} already exists in case {case_id}")
403
404
 
404
- referenced_case: Case = ds.case.get_if_exists(key=item.value, as_obj=True)
405
+ referenced_case = ds.case.get(item.value)
405
406
 
406
407
  if referenced_case is None:
407
408
  raise NotFoundException(f"Referenced case {item.value} not found, cannot be added to case")
408
409
 
409
- item.id = item.value
410
-
411
410
  if item.path == "related/":
412
411
  item.path = "cases/"
413
412
 
414
413
  item.path += f"{referenced_case.case_id}"
415
414
 
416
- case.items.append(item)
415
+ _case.items.append(item)
417
416
 
418
- if not datastore().case.save(case.case_id, case):
419
- raise DataStoreException(f"Failed to save {case.case_id} with new item {item.value}")
417
+ if not datastore().case.save(_case.case_id, _case):
418
+ raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
420
419
 
421
420
 
422
421
  def append_table(case_id: str, item: CaseItem):
@@ -464,7 +463,67 @@ def append_reference(case_id: str, item: CaseItem):
464
463
  raise NotImplementedError
465
464
 
466
465
 
467
- def add_backreference(backing_obj: Hit | Observable, case_id: str):
466
+ def _collect_indicators_from_related(related: Related | None) -> set[str]:
467
+ """Extract all indicator values from a Related ECS compound object."""
468
+ if related is None:
469
+ return set()
470
+
471
+ indicators: set[str] = set()
472
+ for key in related.fields().keys():
473
+ value = related[key]
474
+ if value:
475
+ indicators.update(str(v) for v in value if v)
476
+
477
+ return indicators
478
+
479
+
480
+ def _sync_case_metadata(case_id: str) -> None: # noqa: C901
481
+ """Re-compute and persist threat/target/indicator lists from all case items.
482
+
483
+ Iterates over hit and observable items in the case and re-derives the
484
+ ``targets``, ``threats``, and ``indicators`` lists from the backing
485
+ objects' ECS ``related.*`` fields and, for hits, the outline fields.
486
+ """
487
+ ds = datastore()
488
+ _case = ds.case.get(case_id)
489
+ if _case is None:
490
+ return
491
+
492
+ targets: set[str] = set()
493
+ threats: set[str] = set()
494
+ indicators: set[str] = set()
495
+
496
+ for item in _case.items:
497
+ if item.type == CaseItemTypes.HIT and item.value:
498
+ hit = ds.hit.get(item.value)
499
+ if hit is None:
500
+ continue
501
+
502
+ indicators.update(_collect_indicators_from_related(hit.related))
503
+
504
+ if hit.howler.outline:
505
+ outline = hit.howler.outline
506
+ if outline.threat:
507
+ threats.add(outline.threat)
508
+ if outline.target:
509
+ targets.add(outline.target)
510
+ if outline.indicators:
511
+ indicators.update(str(v) for v in outline.indicators if v)
512
+
513
+ elif item.type == CaseItemTypes.OBSERVABLE and item.value:
514
+ observable = ds.observable.get(item.value)
515
+ if observable is None:
516
+ continue
517
+
518
+ indicators.update(_collect_indicators_from_related(observable.related))
519
+
520
+ _case.targets = sorted(targets)
521
+ _case.threats = sorted(threats)
522
+ _case.indicators = sorted(indicators)
523
+ ds.case.save(case_id, _case)
524
+
525
+
526
+ def _add_backreference(backing_obj: Hit | Observable | None, case_id: str):
468
527
  """Add a back-reference from a hit or observable to a case.
469
528
 
470
529
  Records the case ID in the backing object's ``howler.related_ids`` set so
@@ -491,7 +550,7 @@ def add_backreference(backing_obj: Hit | Observable, case_id: str):
491
550
  datastore()[backing_obj.__class__.__name__.lower()].save(backing_obj.howler.id, backing_obj)
492
551
 
493
552
 
494
- def remove_backreference(backing_obj: Hit | Observable, case_id: str):
553
+ def remove_backreference(backing_obj: Hit | Observable | None, case_id: str):
495
554
  """Remove a back-reference from a hit or observable to a case.
496
555
 
497
556
  Removes the case ID from the backing object's ``howler.related`` list
@@ -533,7 +592,7 @@ def remove_case_item(case_id: str, item_value: str):
533
592
  """
534
593
  ds = datastore()
535
594
 
536
- _case = ds.case.get(key=case_id, as_obj=True)
595
+ _case = ds.case.get(key=case_id)
537
596
 
538
597
  if not _case:
539
598
  raise NotFoundException(f"Case {case_id} does not exist")
@@ -543,16 +602,15 @@ def remove_case_item(case_id: str, item_value: str):
543
602
  raise NotFoundException(f"Case item {item_value} does not exist")
544
603
 
545
604
  backing_obj: Hit | Observable | None = None
546
- match case_item.type:
547
- case CaseItemTypes.HIT:
548
- backing_obj = datastore().hit.get(case_item.id)
549
- case CaseItemTypes.OBSERVABLE:
550
- backing_obj = datastore().observable.get(case_item.id)
605
+ if case_item.type in [CaseItemTypes.HIT, CaseItemTypes.OBSERVABLE]:
606
+ backing_obj = ds[case_item.type].get(case_item.value)
551
607
 
552
608
  _case.items.remove(case_item)
553
609
 
554
- if not datastore().case.save(_case.case_id, _case):
610
+ if not ds.case.save(_case.case_id, _case):
555
611
  raise DataStoreException("Failed to save case after item removal")
556
612
 
557
613
  if backing_obj:
558
614
  remove_backreference(backing_obj, _case.case_id)
615
+
616
+ _sync_case_metadata(case_id)
@@ -235,7 +235,9 @@ def update_dossier(dossier_id: str, dossier_data: dict[str, Any], user: User) ->
235
235
  raise InvalidDataException("We were unable to update the dossier.", cause=e) from e
236
236
 
237
237
 
238
- def get_matching_dossiers(hit: dict[str, Any], dossiers: list[dict[str, Any]] | None = None):
238
+ def get_matching_dossiers(
239
+ hit: dict[str, Any], dossiers: list[dict[str, Any]] | None = None, username: str | None = None
240
+ ):
239
241
  """Get a list of dossiers that match a specific security alert/hit.
240
242
 
241
243
  This function evaluates each dossier's query against the provided hit data
@@ -256,7 +258,7 @@ def get_matching_dossiers(hit: dict[str, Any], dossiers: list[dict[str, Any]] |
256
258
  # Retrieve all dossiers if none provided
257
259
  if dossiers is None:
258
260
  dossiers = datastore().dossier.search(
259
- "dossier_id:*",
261
+ f"type:global OR owner:{username}" if username else "type:global",
260
262
  as_obj=False,
261
263
  # TODO: Eventually implement caching here
262
264
  rows=1000,
@@ -800,7 +800,7 @@ def __match_metadata(candidates: list[dict[str, Any]], hit: dict[str, Any]) -> O
800
800
  return sorted(matching_candidates, key=functools.cmp_to_key(__compare_metadata))[0]
801
801
 
802
802
 
803
- def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadata: list[str], user: dict[str, Any]): # noqa: C901
803
+ def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadata: list[str], user: User): # noqa: C901
804
804
  """Augment hit search results with additional metadata.
805
805
 
806
806
  This function enriches hit data by adding related information such as templates,
@@ -831,7 +831,7 @@ def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadat
831
831
  logger.debug("Augmenting %s hits with %s", len(hits), ",".join(metadata))
832
832
 
833
833
  if "template" in metadata:
834
- template_candidates = template_service.get_matching_templates(hits, as_odm=False, uname=user["uname"])
834
+ template_candidates = template_service.get_matching_templates(hits, as_odm=False, uname=user.uname)
835
835
 
836
836
  logger.debug("\tRetrieved %s matching templates", len(template_candidates))
837
837
 
@@ -864,11 +864,11 @@ def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadat
864
864
 
865
865
  if "dossiers" in metadata:
866
866
  dossiers: list[dict[str, Any]] = datastore().dossier.search(
867
- "dossier_id:*",
867
+ f"type:global OR owner:{user.uname}",
868
868
  as_obj=False,
869
869
  # TODO: Eventually implement caching here
870
870
  rows=1000,
871
871
  )["items"]
872
872
 
873
873
  for hit in hits:
874
- hit["__dossiers"] = dossier_service.get_matching_dossiers(hit, dossiers)
874
+ hit["__dossiers"] = dossier_service.get_matching_dossiers(hit, dossiers, username=user.uname)
@@ -60,12 +60,14 @@ class LuceneProcessor(TreeVisitor):
60
60
  for child in node.children:
61
61
  child_context = self.child_context(node, child, context)
62
62
  for result in self.visit_iter(child, context=child_context):
63
- # If we run across a MUST or MUST NOT (plus, probhit) object and the value doesn't match, we immediately
64
- # shortcircuit and return false.
63
+ # If we run across a MUST or MUST NOT (plus, prohibit) object and the value doesn't match, we
64
+ # immediately shortcircuit and return false.
65
+ # NOTE: visit_prohibit already negates the inner result, so a violated MUST NOT arrives here as
66
+ # False (not True). We therefore short-circuit on `not result` rather than `result`.
65
67
  if isinstance(child, Plus) and not result:
66
68
  yield False
67
69
  return
68
- elif isinstance(child, Prohibit) and result:
70
+ elif isinstance(child, Prohibit) and not result:
69
71
  yield False
70
72
  return
71
73
 
@@ -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.dev596"
155
+ version = "4.0.0.dev642"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
@@ -208,7 +208,7 @@ gunicorn = "23.0.0"
208
208
  packaging = "<25.0"
209
209
  passlib = "1.7.4"
210
210
  prometheus-client = "0.24.1"
211
- pyjwt = "2.11.0"
211
+ pyjwt = "2.12.1"
212
212
  python-baseconv = "1.2.2"
213
213
  python-datemath = "3.0.3"
214
214
  pyyaml = "6.0.3"
@@ -233,7 +233,7 @@ bcrypt = "4.3.0"
233
233
  [tool.poetry.group.dev.dependencies]
234
234
  pre-commit = "^3.7.0"
235
235
  ruff = ">=0.8,<0.16"
236
- pyright = {extras = ["nodejs"], version = "^1.1.408"}
236
+ pyright = { extras = ["nodejs"], version = "^1.1.408" }
237
237
  mypy = "^1.6.1"
238
238
 
239
239
  [tool.poetry.group.test.dependencies]