howler-api 4.0.0.dev799__tar.gz → 4.0.0.dev803__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 (217) hide show
  1. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/socket.py +30 -5
  3. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/ingest.py +8 -3
  4. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/helper.py +0 -4
  5. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/howler_data.py +0 -4
  6. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/observable.py +0 -4
  7. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/case_service.py +31 -3
  8. howler_api-4.0.0.dev803/howler/services/viewer_service.py +43 -0
  9. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/socket_utils.py +4 -25
  10. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/pyproject.toml +1 -1
  11. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/README.md +0 -0
  12. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/__init__.py +0 -0
  13. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/__init__.py +0 -0
  14. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/add_label.py +0 -0
  15. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/add_to_bundle.py +0 -0
  16. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/add_to_case.py +0 -0
  17. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/change_field.py +0 -0
  18. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/demote.py +0 -0
  19. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/example_plugin.py +0 -0
  20. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/prioritization.py +0 -0
  21. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/promote.py +0 -0
  22. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/remove_from_bundle.py +0 -0
  23. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/remove_label.py +0 -0
  24. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/transition.py +0 -0
  25. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/__init__.py +0 -0
  26. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/base.py +0 -0
  27. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/__init__.py +0 -0
  28. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/action.py +0 -0
  29. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/analytic.py +0 -0
  30. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/auth.py +0 -0
  31. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/clue.py +0 -0
  32. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/configs.py +0 -0
  33. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/dossier.py +0 -0
  34. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/help.py +0 -0
  35. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/hit.py +0 -0
  36. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/notebook.py +0 -0
  37. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/overview.py +0 -0
  38. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/search.py +0 -0
  39. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/template.py +0 -0
  40. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/tool.py +0 -0
  41. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/user.py +0 -0
  42. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/utils/__init__.py +0 -0
  43. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/utils/etag.py +0 -0
  44. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/view.py +0 -0
  45. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/__init__.py +0 -0
  46. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/case.py +0 -0
  47. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/search.py +0 -0
  48. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/app.py +0 -0
  49. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/README.md +0 -0
  50. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/__init__.py +0 -0
  51. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/classification.py +0 -0
  52. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/classification.yml +0 -0
  53. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/exceptions.py +0 -0
  54. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/loader.py +0 -0
  55. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/logging/__init__.py +0 -0
  56. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/logging/audit.py +0 -0
  57. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/logging/format.py +0 -0
  58. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/net.py +0 -0
  59. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/net_static.py +0 -0
  60. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/random_user.py +0 -0
  61. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/swagger.py +0 -0
  62. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/config.py +0 -0
  63. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/cronjobs/__init__.py +0 -0
  64. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/cronjobs/retention.py +0 -0
  65. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/cronjobs/view_cleanup.py +0 -0
  66. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/README.md +0 -0
  67. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/__init__.py +0 -0
  68. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/bulk.py +0 -0
  69. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/collection.py +0 -0
  70. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/constants.py +0 -0
  71. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/exceptions.py +0 -0
  72. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/howler_store.py +0 -0
  73. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/migrations/fix_process.py +0 -0
  74. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/operations.py +0 -0
  75. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/schemas.py +0 -0
  76. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/store.py +0 -0
  77. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/support/__init__.py +0 -0
  78. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/support/build.py +0 -0
  79. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/support/schemas.py +0 -0
  80. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/types.py +0 -0
  81. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/error.py +0 -0
  82. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/README.md +0 -0
  83. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/__init__.py +0 -0
  84. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/generate_mitre.py +0 -0
  85. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/generate_sigma_rules.py +0 -0
  86. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/generate_tlds.py +0 -0
  87. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/reindex_data.py +0 -0
  88. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/wipe_databases.py +0 -0
  89. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/gunicorn_config.py +0 -0
  90. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/healthz.py +0 -0
  91. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/__init__.py +0 -0
  92. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/azure.py +0 -0
  93. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/discover.py +0 -0
  94. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/hit.py +0 -0
  95. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/oauth.py +0 -0
  96. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/search.py +0 -0
  97. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/workflow.py +0 -0
  98. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/ws.py +0 -0
  99. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/README.md +0 -0
  100. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/__init__.py +0 -0
  101. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/base.py +0 -0
  102. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/charter.txt +0 -0
  103. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/constants.py +0 -0
  104. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/howler_enum.py +0 -0
  105. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/mixins.py +0 -0
  106. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/__init__.py +0 -0
  107. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/action.py +0 -0
  108. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/analytic.py +0 -0
  109. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/assemblyline.py +0 -0
  110. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/aws.py +0 -0
  111. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/azure.py +0 -0
  112. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/case.py +0 -0
  113. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/cbs.py +0 -0
  114. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/clue.py +0 -0
  115. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/config.py +0 -0
  116. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/dossier.py +0 -0
  117. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/__init__.py +0 -0
  118. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/agent.py +0 -0
  119. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/autonomous_system.py +0 -0
  120. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/client.py +0 -0
  121. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/cloud.py +0 -0
  122. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/code_signature.py +0 -0
  123. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/container.py +0 -0
  124. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/dns.py +0 -0
  125. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/egress.py +0 -0
  126. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/elf.py +0 -0
  127. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/email.py +0 -0
  128. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/error.py +0 -0
  129. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/event.py +0 -0
  130. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/faas.py +0 -0
  131. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/file.py +0 -0
  132. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/geo.py +0 -0
  133. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/group.py +0 -0
  134. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/hash.py +0 -0
  135. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/host.py +0 -0
  136. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/http.py +0 -0
  137. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/ingress.py +0 -0
  138. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/interface.py +0 -0
  139. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/network.py +0 -0
  140. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/observer.py +0 -0
  141. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/organization.py +0 -0
  142. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/os.py +0 -0
  143. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/pe.py +0 -0
  144. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/process.py +0 -0
  145. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/registry.py +0 -0
  146. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/related.py +0 -0
  147. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/rule.py +0 -0
  148. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/server.py +0 -0
  149. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/threat.py +0 -0
  150. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/tls.py +0 -0
  151. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/url.py +0 -0
  152. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/user.py +0 -0
  153. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/user_agent.py +0 -0
  154. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/vulnerability.py +0 -0
  155. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/gcp.py +0 -0
  156. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/hit.py +0 -0
  157. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/lead.py +0 -0
  158. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/localized_label.py +0 -0
  159. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/overview.py +0 -0
  160. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/pivot.py +0 -0
  161. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/record.py +0 -0
  162. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/template.py +0 -0
  163. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/user.py +0 -0
  164. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/view.py +0 -0
  165. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/random_data.py +0 -0
  166. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/randomizer.py +0 -0
  167. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/patched.py +0 -0
  168. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/plugins/__init__.py +0 -0
  169. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/plugins/config.py +0 -0
  170. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/__init__.py +0 -0
  171. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/README.md +0 -0
  172. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/__init__.py +0 -0
  173. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/counters.py +0 -0
  174. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/events.py +0 -0
  175. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/hash.py +0 -0
  176. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/lock.py +0 -0
  177. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/__init__.py +0 -0
  178. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/comms.py +0 -0
  179. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/multi.py +0 -0
  180. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/named.py +0 -0
  181. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/priority.py +0 -0
  182. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/set.py +0 -0
  183. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  184. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/security/__init__.py +0 -0
  185. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/security/socket.py +0 -0
  186. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/security/utils.py +0 -0
  187. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/__init__.py +0 -0
  188. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/action_service.py +0 -0
  189. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/analytic_service.py +0 -0
  190. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/auth_service.py +0 -0
  191. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/bundle_compat_service.py +0 -0
  192. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/config_service.py +0 -0
  193. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/docs_service.py +0 -0
  194. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/dossier_service.py +0 -0
  195. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/event_service.py +0 -0
  196. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/hit_service.py +0 -0
  197. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/jwt_service.py +0 -0
  198. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/lucene_service.py +0 -0
  199. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/notebook_service.py +0 -0
  200. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/observable_service.py +0 -0
  201. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/overview_service.py +0 -0
  202. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/search_service.py +0 -0
  203. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/template_service.py +0 -0
  204. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/user_service.py +0 -0
  205. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/telemetry.py +0 -0
  206. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/__init__.py +0 -0
  207. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/annotations.py +0 -0
  208. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/chunk.py +0 -0
  209. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/compat.py +0 -0
  210. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/constants.py +0 -0
  211. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/dict_utils.py +0 -0
  212. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/isotime.py +0 -0
  213. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/list_utils.py +0 -0
  214. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/lucene.py +0 -0
  215. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/path.py +0 -0
  216. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/str_utils.py +0 -0
  217. {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 4.0.0.dev799
3
+ Version: 4.0.0.dev803
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -7,11 +7,11 @@ from flask import Blueprint, request
7
7
  from opentelemetry import trace
8
8
 
9
9
  import howler.services.event_service as event_service
10
+ import howler.services.viewer_service as viewer_service
10
11
  from howler.api import ok, unauthorized
11
12
  from howler.common.logging import get_logger
12
- from howler.datastore.operations import OdmHelper
13
13
  from howler.helper.ws import ConnectionClosed, Server
14
- from howler.odm.models.hit import Hit
14
+ from howler.security import api_login
15
15
  from howler.security.socket import websocket_auth, ws_response
16
16
  from howler.utils.socket_utils import check_action
17
17
 
@@ -24,8 +24,6 @@ socket_api._doc = "Endpoints concerning websocket connectivity between the clien
24
24
  logger = get_logger(__file__)
25
25
  tracer = trace.get_tracer(__name__)
26
26
 
27
- hit_helper = OdmHelper(Hit)
28
-
29
27
 
30
28
  @tracer.start_as_current_span(f"{__name__}.emit")
31
29
  @socket_api.route("/emit/<event>", methods=["POST"])
@@ -49,10 +47,25 @@ def emit(event: str):
49
47
  return ok()
50
48
 
51
49
 
50
+ @tracer.start_as_current_span(f"{__name__}.get_viewers")
51
+ @socket_api.route("/viewers/<entity_id>", methods=["GET"])
52
+ @api_login(audit=False, required_priv=["R"])
53
+ def get_viewers(entity_id: str, **kwargs):
54
+ """Get the list of users currently viewing the specified entity
55
+
56
+ Variables:
57
+ entity_id => The ID of the entity to get viewers for
58
+
59
+ Result Example:
60
+ ["user1", "user2"]
61
+ """
62
+ return ok(viewer_service.get_viewers(entity_id))
63
+
64
+
52
65
  @tracer.start_as_current_span(f"{__name__}.connect")
53
66
  @socket_api.route("/connect", websocket=True) # type: ignore
54
67
  @websocket_auth(required_priv=["R"])
55
- def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
68
+ def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
56
69
  """Connect to the server to monitor for updates via websocket
57
70
 
58
71
  Variables:
@@ -78,10 +91,20 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
78
91
  logger.debug("Sending action: %s", data)
79
92
  ws.send(ws_response("action", data))
80
93
 
94
+ def send_case(data: dict[str, Any]):
95
+ logger.debug("Sending case update: %s", data.get("case", {}).get("case_id", "unknown"))
96
+ ws.send(ws_response("cases", data))
97
+
98
+ def send_viewers_update(data: dict[str, Any]):
99
+ logger.debug("Sending viewers update: %s", data.get("id", "unknown"))
100
+ ws.send(ws_response("viewers_update", data))
101
+
81
102
  try:
82
103
  event_service.on("hits", send_hit)
83
104
  event_service.on("broadcast", send_broadcast)
84
105
  event_service.on("action", send_action)
106
+ event_service.on("cases", send_case)
107
+ event_service.on("viewers_update", send_viewers_update)
85
108
  while ws.connected:
86
109
  data = ws.receive(10)
87
110
  if data:
@@ -113,6 +136,8 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
113
136
  event_service.off("hits", send_hit)
114
137
  event_service.off("broadcast", send_broadcast)
115
138
  event_service.off("action", send_action)
139
+ event_service.off("cases", send_case)
140
+ event_service.off("viewers_update", send_viewers_update)
116
141
 
117
142
  for id, action, broadcast in outstanding_actions:
118
143
  outstanding_actions = check_action(id, action, broadcast, outstanding_actions=outstanding_actions, **kwargs)
@@ -120,15 +120,20 @@ def delete(indexes: str, user: User, **kwargs):
120
120
  "success": True # Deleting the hits succeded
121
121
  }
122
122
  """
123
- hit_ids = request.json
123
+ ids = request.json
124
124
 
125
- if hit_ids is None:
125
+ if ids is None:
126
126
  return bad_request(err="No hit ids were sent.")
127
127
 
128
128
  if "admin" not in user["type"]:
129
129
  return forbidden(err="Cannot delete hit, only administrators are permitted to delete.")
130
130
 
131
- index_list = indexes.split(",") # noqa: F841
131
+ index_list = indexes.split(",")
132
+
133
+ ds = datastore()
134
+
135
+ if non_existing_hit_ids := [id for id in ids if all(not ds[index].exists(id) for index in index_list)]:
136
+ return not_found(err=f"Record ids [{','.join(non_existing_hit_ids)}] do not exist.")
132
137
 
133
138
  # TODO: Reimplement in a generic function
134
139
  # hit_service.delete_hits(hit_ids, indexes=index_list)
@@ -249,8 +249,6 @@ def generate_useful_hit( # noqa: C901
249
249
  except IndexError:
250
250
  pass
251
251
 
252
- hit.howler.viewers = []
253
-
254
252
  hit.howler.dossier = [
255
253
  Lead(
256
254
  {
@@ -446,8 +444,6 @@ def generate_useful_observable( # noqa: C901
446
444
  ),
447
445
  ]
448
446
 
449
- observable.howler.viewers = []
450
-
451
447
  return observable
452
448
 
453
449
 
@@ -293,7 +293,3 @@ class HowlerData(odm.Model):
293
293
  dossier: list[Lead] = odm.List(
294
294
  odm.Compound(Lead), default=[], description="A list of leads forming the dossier associated with this hit"
295
295
  )
296
- viewers: list[str] = odm.List(
297
- odm.Keyword(description="A list of users currently viewing the hit"),
298
- default=[],
299
- )
@@ -101,10 +101,6 @@ class ObservableData(odm.Model):
101
101
  default=[],
102
102
  description="A list of changes to the observable with timestamps and attribution.",
103
103
  )
104
- viewers: list[str] = odm.List(
105
- odm.Keyword(description="A list of users currently viewing the observable"),
106
- default=[],
107
- )
108
104
 
109
105
 
110
106
  @odm.model(
@@ -4,11 +4,11 @@ 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, cast, overload
7
+ from typing import Any, overload
8
8
 
9
9
  from prometheus_client import Counter
10
10
 
11
- from howler.common.exceptions import InvalidDataException, NotFoundException
11
+ from howler.common.exceptions import HowlerValueError, InvalidDataException, NotFoundException
12
12
  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
@@ -17,6 +17,7 @@ from howler.odm.models.ecs.related import Related
17
17
  from howler.odm.models.hit import Hit
18
18
  from howler.odm.models.observable import Observable
19
19
  from howler.odm.models.user import User
20
+ from howler.services import event_service
20
21
 
21
22
  logger = get_logger(__file__)
22
23
 
@@ -51,7 +52,14 @@ def create_case(_case: dict, user: str = None) -> Case: # type: ignore
51
52
  append_case_item(case.case_id, item=CaseItem(item))
52
53
 
53
54
  if items:
54
- return cast(Case, datastore().case.get(case.case_id))
55
+ updated_case = datastore().case.get(case.case_id)
56
+
57
+ if not updated_case:
58
+ raise HowlerValueError("Error occurred when creating case")
59
+
60
+ case = updated_case
61
+
62
+ event_service.emit("cases", {"case": case.as_primitives()})
55
63
 
56
64
  return case
57
65
 
@@ -218,6 +226,8 @@ def update_case(case_id: str, case_data: dict[str, Any], user: User) -> Case:
218
226
  case.updated = "NOW"
219
227
  ds.case.save(case_id, case)
220
228
 
229
+ event_service.emit("cases", {"case": case.as_primitives()})
230
+
221
231
  return case
222
232
 
223
233
 
@@ -329,6 +339,10 @@ def append_hit(case_id: str, item: CaseItem) -> Case:
329
339
 
330
340
  _sync_case_metadata(_case.case_id)
331
341
 
342
+ updated_case = ds.case.get(_case.case_id)
343
+ if updated_case:
344
+ event_service.emit("cases", {"case": updated_case.as_primitives()})
345
+
332
346
  return _case
333
347
 
334
348
 
@@ -371,6 +385,10 @@ def append_observable(case_id: str, item: CaseItem) -> Case:
371
385
  _add_backreference(observable, _case.case_id)
372
386
  _sync_case_metadata(case_id)
373
387
 
388
+ updated_case = ds.case.get(_case.case_id)
389
+ if updated_case:
390
+ event_service.emit("cases", {"case": updated_case.as_primitives()})
391
+
374
392
  return _case
375
393
 
376
394
 
@@ -410,6 +428,8 @@ def append_case(case_id: str, item: CaseItem) -> Case:
410
428
  if not datastore().case.save(_case.case_id, _case):
411
429
  raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
412
430
 
431
+ event_service.emit("cases", {"case": _case.as_primitives()})
432
+
413
433
  return _case
414
434
 
415
435
 
@@ -473,6 +493,8 @@ def append_reference(case_id: str, item: CaseItem) -> Case:
473
493
  if not datastore().case.save(_case.case_id, _case):
474
494
  raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
475
495
 
496
+ event_service.emit("cases", {"case": _case.as_primitives()})
497
+
476
498
  return _case
477
499
 
478
500
 
@@ -646,6 +668,10 @@ def remove_case_items(case_id: str, values: list[str]):
646
668
 
647
669
  _sync_case_metadata(case_id)
648
670
 
671
+ updated_case = ds.case.get(_case.case_id)
672
+ if updated_case:
673
+ event_service.emit("cases", {"case": updated_case.as_primitives()})
674
+
649
675
  return _case
650
676
 
651
677
 
@@ -692,4 +718,6 @@ def rename_case_item(case_id: str, item_value: str, new_path: str) -> Case:
692
718
  if not ds.case.save(_case.case_id, _case):
693
719
  raise DataStoreException("Failed to save case after item rename")
694
720
 
721
+ event_service.emit("cases", {"case": _case.as_primitives()})
722
+
695
723
  return _case
@@ -0,0 +1,43 @@
1
+ """Viewer service for tracking active viewers of entities (hits, cases, observables) in Redis.
2
+
3
+ Stores viewer presence as ephemeral Redis sets with TTL, replacing the
4
+ previous approach of persisting viewers directly in the ODM/ElasticSearch.
5
+ """
6
+
7
+ from howler.config import redis
8
+ from howler.remote.datatypes import retry_call
9
+ from howler.services import event_service
10
+
11
+ VIEWER_KEY_PREFIX = "viewers"
12
+ VIEWER_TTL = 3600 # 1 hour
13
+
14
+
15
+ def _key(entity_id: str) -> str:
16
+ return f"{VIEWER_KEY_PREFIX}:{entity_id}"
17
+
18
+
19
+ def add_viewer(entity_id: str, username: str) -> None:
20
+ """Record that a user is viewing the given entity."""
21
+ key = _key(entity_id)
22
+ retry_call(redis.sadd, key, username)
23
+ retry_call(redis.expire, key, VIEWER_TTL)
24
+ _emit_update(entity_id)
25
+
26
+
27
+ def remove_viewer(entity_id: str, username: str) -> None:
28
+ """Record that a user has stopped viewing the given entity."""
29
+ retry_call(redis.srem, _key(entity_id), username)
30
+ _emit_update(entity_id)
31
+
32
+
33
+ def get_viewers(entity_id: str) -> list[str]:
34
+ """Return the list of usernames currently viewing the given entity."""
35
+ members = retry_call(redis.smembers, _key(entity_id))
36
+ return sorted(m.decode() if isinstance(m, bytes) else m for m in members)
37
+
38
+
39
+ def _emit_update(entity_id: str) -> None:
40
+ event_service.emit(
41
+ "viewers_update",
42
+ {"id": entity_id, "viewers": get_viewers(entity_id)},
43
+ )
@@ -1,12 +1,8 @@
1
1
  from howler.common.logging import get_logger
2
- from howler.datastore.operations import OdmHelper
3
- from howler.odm.models.hit import Hit
4
- from howler.services import event_service, hit_service
2
+ from howler.services import event_service, viewer_service
5
3
 
6
4
  logger = get_logger(__file__)
7
5
 
8
- hit_helper = OdmHelper(Hit)
9
-
10
6
 
11
7
  def check_action(
12
8
  id: str, action: str, broadcast: bool, outstanding_actions: list[tuple[str, str, bool]] = [], **kwargs
@@ -35,27 +31,10 @@ def check_action(
35
31
  outstanding_actions = [a for a in outstanding_actions if a[1] != "stop_typing"]
36
32
 
37
33
  elif action == "viewing":
38
- if hit_service.exists(id):
39
- outstanding_actions.append((id, "stop_viewing", False))
40
- hit_service.update_hit(
41
- id,
42
- [
43
- hit_helper.list_add(
44
- "howler.viewers",
45
- kwargs["username"],
46
- silent=True,
47
- if_missing=True,
48
- )
49
- ],
50
- user=kwargs["username"],
51
- )
34
+ outstanding_actions.append((id, "stop_viewing", False))
35
+ viewer_service.add_viewer(id, kwargs["username"])
52
36
  elif action == "stop_viewing":
53
- if hit_service.exists(id):
54
- hit_service.update_hit(
55
- id,
56
- [hit_helper.list_remove("howler.viewers", kwargs["username"], silent=True)],
57
- user=kwargs["username"],
58
- )
37
+ viewer_service.remove_viewer(id, kwargs["username"])
59
38
  outstanding_actions = [a for a in outstanding_actions if a[1] != "stop_viewing"]
60
39
 
61
40
  return outstanding_actions
@@ -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.dev799"
155
+ version = "4.0.0.dev803"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",