howler-api 4.0.0.dev847__tar.gz → 4.0.0.dev871__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/PKG-INFO +1 -1
  2. howler_api-4.0.0.dev871/howler/api/v1/utils/etag.py +106 -0
  3. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v2/ingest.py +21 -4
  4. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v2/search.py +8 -11
  5. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/collection.py +1 -1
  6. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/base.py +21 -10
  7. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/search_service.py +4 -0
  8. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/compat.py +6 -0
  9. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/pyproject.toml +1 -1
  10. howler_api-4.0.0.dev847/howler/api/v1/utils/etag.py +0 -84
  11. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/README.md +0 -0
  12. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/__init__.py +0 -0
  13. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/__init__.py +0 -0
  14. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/add_label.py +0 -0
  15. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/add_to_bundle.py +0 -0
  16. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/add_to_case.py +0 -0
  17. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/change_field.py +0 -0
  18. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/demote.py +0 -0
  19. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/example_plugin.py +0 -0
  20. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/prioritization.py +0 -0
  21. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/promote.py +0 -0
  22. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/remove_from_bundle.py +0 -0
  23. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/remove_label.py +0 -0
  24. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/actions/transition.py +0 -0
  25. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/__init__.py +0 -0
  26. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/base.py +0 -0
  27. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/socket.py +0 -0
  28. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/__init__.py +0 -0
  29. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/action.py +0 -0
  30. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/analytic.py +0 -0
  31. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/auth.py +0 -0
  32. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/clue.py +0 -0
  33. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/configs.py +0 -0
  34. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/dossier.py +0 -0
  35. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/help.py +0 -0
  36. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/hit.py +0 -0
  37. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/notebook.py +0 -0
  38. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/overview.py +0 -0
  39. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/search.py +0 -0
  40. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/template.py +0 -0
  41. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/tool.py +0 -0
  42. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/user.py +0 -0
  43. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/utils/__init__.py +0 -0
  44. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v1/view.py +0 -0
  45. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v2/__init__.py +0 -0
  46. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/api/v2/case.py +0 -0
  47. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/app.py +0 -0
  48. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/README.md +0 -0
  49. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/__init__.py +0 -0
  50. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/classification.py +0 -0
  51. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/classification.yml +0 -0
  52. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/exceptions.py +0 -0
  53. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/loader.py +0 -0
  54. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/logging/__init__.py +0 -0
  55. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/logging/audit.py +0 -0
  56. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/logging/format.py +0 -0
  57. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/net.py +0 -0
  58. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/net_static.py +0 -0
  59. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/random_user.py +0 -0
  60. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/common/swagger.py +0 -0
  61. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/config.py +0 -0
  62. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/cronjobs/__init__.py +0 -0
  63. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/cronjobs/correlation.py +0 -0
  64. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/cronjobs/retention.py +0 -0
  65. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/cronjobs/view_cleanup.py +0 -0
  66. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/README.md +0 -0
  67. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/__init__.py +0 -0
  68. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/bulk.py +0 -0
  69. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/constants.py +0 -0
  70. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/exceptions.py +0 -0
  71. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/howler_store.py +0 -0
  72. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/migrations/fix_process.py +0 -0
  73. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/operations.py +0 -0
  74. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/schemas.py +0 -0
  75. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/store.py +0 -0
  76. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/support/__init__.py +0 -0
  77. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/support/build.py +0 -0
  78. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/support/schemas.py +0 -0
  79. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/datastore/types.py +0 -0
  80. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/error.py +0 -0
  81. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/README.md +0 -0
  82. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/__init__.py +0 -0
  83. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/generate_mitre.py +0 -0
  84. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/generate_sigma_rules.py +0 -0
  85. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/generate_tlds.py +0 -0
  86. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/reindex_data.py +0 -0
  87. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/external/wipe_databases.py +0 -0
  88. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/gunicorn_config.py +0 -0
  89. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/healthz.py +0 -0
  90. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/__init__.py +0 -0
  91. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/azure.py +0 -0
  92. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/discover.py +0 -0
  93. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/hit.py +0 -0
  94. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/oauth.py +0 -0
  95. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/search.py +0 -0
  96. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/workflow.py +0 -0
  97. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/helper/ws.py +0 -0
  98. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/README.md +0 -0
  99. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/__init__.py +0 -0
  100. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/charter.txt +0 -0
  101. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/constants.py +0 -0
  102. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/helper.py +0 -0
  103. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/howler_enum.py +0 -0
  104. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/mixins.py +0 -0
  105. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/__init__.py +0 -0
  106. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/action.py +0 -0
  107. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/analytic.py +0 -0
  108. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/assemblyline.py +0 -0
  109. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/aws.py +0 -0
  110. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/azure.py +0 -0
  111. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/case.py +0 -0
  112. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/cbs.py +0 -0
  113. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/clue.py +0 -0
  114. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/config.py +0 -0
  115. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/dossier.py +0 -0
  116. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/__init__.py +0 -0
  117. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/agent.py +0 -0
  118. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/autonomous_system.py +0 -0
  119. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/client.py +0 -0
  120. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/cloud.py +0 -0
  121. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/code_signature.py +0 -0
  122. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/container.py +0 -0
  123. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/dns.py +0 -0
  124. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/egress.py +0 -0
  125. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/elf.py +0 -0
  126. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/email.py +0 -0
  127. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/error.py +0 -0
  128. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/event.py +0 -0
  129. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/faas.py +0 -0
  130. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/file.py +0 -0
  131. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/geo.py +0 -0
  132. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/group.py +0 -0
  133. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/hash.py +0 -0
  134. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/host.py +0 -0
  135. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/http.py +0 -0
  136. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/ingress.py +0 -0
  137. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/interface.py +0 -0
  138. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/network.py +0 -0
  139. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/observer.py +0 -0
  140. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/organization.py +0 -0
  141. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/os.py +0 -0
  142. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/pe.py +0 -0
  143. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/process.py +0 -0
  144. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/registry.py +0 -0
  145. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/related.py +0 -0
  146. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/rule.py +0 -0
  147. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/server.py +0 -0
  148. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/threat.py +0 -0
  149. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/tls.py +0 -0
  150. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/url.py +0 -0
  151. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/user.py +0 -0
  152. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/user_agent.py +0 -0
  153. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/ecs/vulnerability.py +0 -0
  154. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/gcp.py +0 -0
  155. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/hit.py +0 -0
  156. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/howler_data.py +0 -0
  157. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/lead.py +0 -0
  158. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/localized_label.py +0 -0
  159. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/observable.py +0 -0
  160. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/overview.py +0 -0
  161. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/pivot.py +0 -0
  162. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/record.py +0 -0
  163. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/template.py +0 -0
  164. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/user.py +0 -0
  165. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/models/view.py +0 -0
  166. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/random_data.py +0 -0
  167. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/odm/randomizer.py +0 -0
  168. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/patched.py +0 -0
  169. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/plugins/__init__.py +0 -0
  170. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/plugins/config.py +0 -0
  171. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/__init__.py +0 -0
  172. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/README.md +0 -0
  173. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/__init__.py +0 -0
  174. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/counters.py +0 -0
  175. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/events.py +0 -0
  176. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/hash.py +0 -0
  177. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/lock.py +0 -0
  178. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/queues/__init__.py +0 -0
  179. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/queues/comms.py +0 -0
  180. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/queues/multi.py +0 -0
  181. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/queues/named.py +0 -0
  182. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/queues/priority.py +0 -0
  183. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/set.py +0 -0
  184. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  185. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/security/__init__.py +0 -0
  186. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/security/socket.py +0 -0
  187. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/security/utils.py +0 -0
  188. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/__init__.py +0 -0
  189. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/action_service.py +0 -0
  190. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/analytic_service.py +0 -0
  191. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/auth_service.py +0 -0
  192. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/bundle_compat_service.py +0 -0
  193. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/case_service.py +0 -0
  194. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/config_service.py +0 -0
  195. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/correlation_service.py +0 -0
  196. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/docs_service.py +0 -0
  197. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/dossier_service.py +0 -0
  198. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/event_service.py +0 -0
  199. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/hit_service.py +0 -0
  200. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/jwt_service.py +0 -0
  201. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/lucene_service.py +0 -0
  202. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/notebook_service.py +0 -0
  203. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/observable_service.py +0 -0
  204. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/overview_service.py +0 -0
  205. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/template_service.py +0 -0
  206. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/user_service.py +0 -0
  207. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/services/viewer_service.py +0 -0
  208. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/telemetry.py +0 -0
  209. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/__init__.py +0 -0
  210. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/annotations.py +0 -0
  211. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/chunk.py +0 -0
  212. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/constants.py +0 -0
  213. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/dict_utils.py +0 -0
  214. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/isotime.py +0 -0
  215. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/list_utils.py +0 -0
  216. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/lucene.py +0 -0
  217. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/path.py +0 -0
  218. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/socket_utils.py +0 -0
  219. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/howler/utils/str_utils.py +0 -0
  220. {howler_api-4.0.0.dev847 → howler_api-4.0.0.dev871}/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.dev847
3
+ Version: 4.0.0.dev871
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -0,0 +1,106 @@
1
+ """ETag utility module for handling HTTP ETags in Flask responses.
2
+
3
+ ETags (Entity Tags) are HTTP headers used for web cache validation and conditional requests.
4
+ They help optimize performance by allowing clients to cache responses and only fetch
5
+ new data when the resource has actually changed.
6
+ """
7
+
8
+ import functools
9
+ import re
10
+
11
+ from flask import Response, request
12
+
13
+ from howler.api import not_modified
14
+
15
+
16
+ def add_etag(getter=None, check_if_match=True):
17
+ """Decorator to add ETag handling to a Flask response.
18
+
19
+ This decorator implements HTTP ETag functionality for API endpoints, enabling:
20
+ - Conditional requests using If-Match headers
21
+ - Cache validation to prevent unnecessary data transfers
22
+ - Version tracking for resources
23
+
24
+ When ``getter`` is provided, the decorator pre-fetches the object and its
25
+ version, injects ``server_version`` into kwargs, caches the object, and
26
+ supports ``If-Match`` conditional requests.
27
+
28
+ When ``getter`` is ``None``, the decorator only handles converting
29
+ ``(Response, version)`` return tuples into a single Response with the
30
+ ``ETag`` header set. This is useful for endpoints that manage their own
31
+ version retrieval (e.g. v2 endpoints).
32
+
33
+ Args:
34
+ getter: Optional function that retrieves the object and its version.
35
+ When None, the decorator only handles ETag header setting.
36
+ check_if_match (bool): Whether to check If-Match headers for conditional requests.
37
+ Only used when getter is provided.
38
+
39
+ Returns:
40
+ Decorated function with ETag support
41
+ """
42
+
43
+ def wrapper(f):
44
+ """Inner wrapper function that applies ETag functionality to the decorated function."""
45
+
46
+ @functools.wraps(f)
47
+ def generate_etag(*args, **kwargs):
48
+ """Generate and handle ETags for the HTTP response."""
49
+ if getter is not None:
50
+ # Retrieve the object and its version using the provided getter function
51
+ # The getter should return (object, version) tuple
52
+ obj, version = getter(
53
+ kwargs.get("id", kwargs.get("username", None)),
54
+ as_odm=True,
55
+ version=True,
56
+ )
57
+
58
+ # Handle conditional requests with If-Match header
59
+ # If the client's version matches the current version and it's a GET request
60
+ # without metadata parameter, return 304 Not Modified to save bandwidth
61
+ if (
62
+ check_if_match
63
+ and "If-Match" in request.headers
64
+ and request.headers["If-Match"] == version
65
+ and request.method == "GET"
66
+ and "metadata" not in request.args
67
+ ):
68
+ return not_modified()
69
+
70
+ # Extract the resource type from the API path and create a cache key
71
+ # e.g., "/api/v1/users/123" becomes "cached_users"
72
+ key = re.sub(r"^\/api\/v\d+\/(\w+)\/.+$", r"cached_\1", request.path)
73
+ kwargs[key] = obj
74
+
75
+ # Call the original function with the cached object and version
76
+ values = f(*args, server_version=version, **kwargs)
77
+
78
+ # Handle different return value formats from the decorated function
79
+ # If there is only one return, it's just the response
80
+ if isinstance(values, Response):
81
+ # Only add ETag header for successful responses (not 409 Conflict or 400 Bad Request)
82
+ if values.status_code != 409 and values.status_code != 400:
83
+ values.headers["ETag"] = version
84
+ return values
85
+
86
+ # If there are two returns, it's the response and the new version
87
+ # This happens when the function modifies the resource and returns an updated version
88
+ else:
89
+ if values[0].status_code != 409 and values[0].status_code != 400:
90
+ # Add the new ETag version to successful responses
91
+ values[0].headers["ETag"] = values[1]
92
+ return values[0]
93
+
94
+ # No getter: just call the function and handle (Response, version) tuples
95
+ values = f(*args, **kwargs)
96
+
97
+ if isinstance(values, Response):
98
+ return values
99
+
100
+ if values[0].status_code != 409 and values[0].status_code != 400:
101
+ values[0].headers["ETag"] = values[1]
102
+ return values[0]
103
+
104
+ return generate_etag
105
+
106
+ return wrapper
@@ -5,6 +5,7 @@ from flask import request
5
5
  from mergedeep import Strategy, merge
6
6
 
7
7
  from howler.api import bad_request, created, forbidden, internal_error, make_subapi_blueprint, no_content, not_found, ok
8
+ from howler.api.v1.utils.etag import add_etag
8
9
  from howler.common.exceptions import HowlerException, HowlerValueError
9
10
  from howler.common.loader import datastore
10
11
  from howler.common.logging import get_logger
@@ -162,8 +163,23 @@ def delete(indexes: str, user: User, **kwargs):
162
163
  if non_existing_hit_ids := [id for id in ids if all(not ds[index].exists(id) for index in index_list)]:
163
164
  return not_found(err=f"Record ids [{','.join(non_existing_hit_ids)}] do not exist.")
164
165
 
165
- # TODO: Reimplement in a generic function
166
- # hit_service.delete_hits(hit_ids, indexes=index_list)
166
+ try:
167
+ remaining = set(ids)
168
+ for index in index_list:
169
+ if not remaining:
170
+ break
171
+
172
+ existing = [record_id for record_id in remaining if ds[index].exists(record_id)]
173
+ if not existing:
174
+ continue
175
+
176
+ for record_id in existing:
177
+ ds[index].delete(record_id)
178
+
179
+ remaining -= set(existing)
180
+ ds[index].commit()
181
+ except DataStoreException as e:
182
+ return internal_error(err=str(e))
167
183
 
168
184
  return no_content()
169
185
 
@@ -239,7 +255,8 @@ def validate(index: str, **kwargs):
239
255
  @generate_swagger_docs()
240
256
  @ingest_api.route("/<index>/<id>/overwrite", methods=["PATCH"])
241
257
  @api_login(audit=False, required_priv=["W"])
242
- def overwrite(index: str, id: str, server_version: str, **kwargs):
258
+ @add_etag()
259
+ def overwrite(index: str, id: str, **kwargs):
243
260
  """Overwrite a record.
244
261
 
245
262
  Variables:
@@ -286,7 +303,7 @@ def overwrite(index: str, id: str, server_version: str, **kwargs):
286
303
  ),
287
304
  )
288
305
 
289
- ds[index].save(id, odm(new_record) if odm else new_record, version=server_version)
306
+ ds[index].save(id, odm(new_record) if odm else new_record, version=kwargs.get("server_version"))
290
307
 
291
308
  new_record, new_version = ds[index].get(id, as_obj=False, version=True)
292
309
 
@@ -5,7 +5,6 @@ from typing import Any
5
5
  from elasticsearch import BadRequestError
6
6
  from elasticsearch._sync.client.indices import IndicesClient
7
7
  from flask import Request, request
8
- from werkzeug.exceptions import BadRequest
9
8
 
10
9
  from howler.api import bad_request, make_subapi_blueprint, ok
11
10
  from howler.common.loader import datastore
@@ -30,10 +29,7 @@ def generate_params(request: Request, fields: list[str], multi_fields: list[str]
30
29
  params = {}
31
30
 
32
31
  if request.method == "POST":
33
- try:
34
- req_data = request.json
35
- except BadRequest:
36
- req_data = {"query": "*:*"}
32
+ req_data = request.get_json(silent=True) or {"query": "*:*"}
37
33
 
38
34
  params = {
39
35
  **params,
@@ -131,7 +127,8 @@ def search(indexes: str, **kwargs):
131
127
  return bad_request(err="There was no search query.")
132
128
 
133
129
  metadata = params.pop("metadata", [])
134
- result = search_service.search(indexes, query, **params)
130
+ access_control = params.pop("access_control", None)
131
+ result = search_service.search(indexes, query, access_control=access_control, **params)
135
132
 
136
133
  if metadata and any(idx in index_list for idx in ["hit"]):
137
134
  hit_service.augment_metadata(result["items"], metadata, user)
@@ -236,7 +233,7 @@ def count(index, **kwargs):
236
233
 
237
234
  Result Example:
238
235
  {
239
- "total": 201, # Total results found
236
+ "count": 201, # Total results found
240
237
  }
241
238
  """
242
239
  user = kwargs["user"]
@@ -245,7 +242,7 @@ def count(index, **kwargs):
245
242
  if collection is None:
246
243
  return bad_request(err=f"Not a valid index to search in: {index}")
247
244
 
248
- params, req_data = generate_params(request, [], [])
245
+ params, req_data = generate_params(request, ["timeout"], ["filters"])
249
246
 
250
247
  boolean_fields = ["use_archive"]
251
248
  params.update(
@@ -256,15 +253,15 @@ def count(index, **kwargs):
256
253
  }
257
254
  )
258
255
 
259
- if has_access_control(index):
260
- params.update({"access_control": user["access_control"]})
256
+ access_control = user["access_control"] if has_access_control(index) else None
261
257
 
262
258
  query = req_data.get("query", None)
263
259
  if not query:
264
260
  return bad_request(err="There was no search query.")
265
261
 
262
+ filters = params.pop("filters", [])
266
263
  try:
267
- return ok(collection().count(query, **params))
264
+ return ok(collection().count(query, filters, access_control=access_control))
268
265
  except (SearchException, BadRequestError) as e:
269
266
  return bad_request(err=f"SearchException: {e}")
270
267
 
@@ -1922,7 +1922,7 @@ class ESCollection(Generic[ModelType]):
1922
1922
  search result object that consists of the following:
1923
1923
 
1924
1924
  {
1925
- "total": 123456, # Total number of documents matching the query
1925
+ "count": 123456, # Total number of documents matching the query
1926
1926
  }
1927
1927
 
1928
1928
  :param query: lucene query to search for
@@ -1081,10 +1081,10 @@ class Model:
1081
1081
  Args:
1082
1082
  skip_mappings (bool): Skip over mappings where the real subfield names are unknown.
1083
1083
  """
1084
- if skip_mappings and hasattr(cls, "_odm_field_cache_skip"):
1084
+ if not no_cache and skip_mappings and "_odm_field_cache_skip" in cls.__dict__:
1085
1085
  return cls._odm_field_cache_skip
1086
1086
 
1087
- if not skip_mappings and hasattr(cls, "_odm_field_cache"):
1087
+ if not no_cache and not skip_mappings and "_odm_field_cache" in cls.__dict__:
1088
1088
  return cls._odm_field_cache
1089
1089
 
1090
1090
  out = dict()
@@ -1113,10 +1113,10 @@ class Model:
1113
1113
  def add_namespace(cls, namespace: str, field: _Field, index=None, store=None, description=None):
1114
1114
  recursive_set_name(field, namespace)
1115
1115
 
1116
- if hasattr(cls, "_odm_field_cache_skip"):
1116
+ if "_odm_field_cache_skip" in cls.__dict__:
1117
1117
  cls._odm_field_cache_skip[namespace.rstrip("_")] = field
1118
1118
 
1119
- if hasattr(cls, "_odm_field_cache"):
1119
+ if "_odm_field_cache" in cls.__dict__:
1120
1120
  cls._odm_field_cache[namespace.rstrip("_")] = field
1121
1121
 
1122
1122
  setattr(cls, namespace, field)
@@ -1131,10 +1131,10 @@ class Model:
1131
1131
 
1132
1132
  @classmethod
1133
1133
  def remove_namespace(cls, namespace: str):
1134
- if hasattr(cls, "_odm_field_cache_skip"):
1134
+ if "_odm_field_cache_skip" in cls.__dict__:
1135
1135
  del cls._odm_field_cache_skip[namespace.rstrip("_")]
1136
1136
 
1137
- if hasattr(cls, "_odm_field_cache"):
1137
+ if "_odm_field_cache" in cls.__dict__:
1138
1138
  del cls._odm_field_cache[namespace.rstrip("_")]
1139
1139
 
1140
1140
  delattr(cls, namespace)
@@ -1212,7 +1212,7 @@ class Model:
1212
1212
  include_autogen_note=True,
1213
1213
  defaults=None,
1214
1214
  url_prefix="/howler/odm/class/",
1215
- ) -> dict | str:
1215
+ ) -> str:
1216
1216
  markdown_content = (
1217
1217
  (
1218
1218
  '??? success "Auto-Generated Documentation"\n '
@@ -1524,12 +1524,23 @@ def model(index=None, store=None, description=None, id_field=None):
1524
1524
 
1525
1525
  def _finish_model(cls):
1526
1526
  cls._Model__description = description
1527
- cls._Model__id_field = id_field
1527
+ fields = cls.fields()
1528
1528
 
1529
- if cls._Model__id_field is None:
1529
+ if id_field is None:
1530
1530
  cls._Model__id_field = f"{cls.__name__.lower()}_id"
1531
+ else:
1532
+ if not isinstance(id_field, str):
1533
+ raise HowlerTypeError(f"id_field must be a str, got {type(id_field).__name__}")
1534
+
1535
+ if not FLATTENED_OBJECT_SANITIZER.match(id_field) or id_field in BANNED_FIELDS:
1536
+ raise HowlerValueError(f"Illegal id_field name: {id_field}")
1537
+
1538
+ if id_field not in fields and id_field not in cls.flat_fields():
1539
+ raise HowlerValueError(f"id_field must reference a declared field: {id_field}")
1540
+
1541
+ cls._Model__id_field = id_field
1531
1542
 
1532
- for name, field_data in cls.fields().items():
1543
+ for name, field_data in fields.items():
1533
1544
  if not FIELD_SANITIZER.match(name) or name in BANNED_FIELDS:
1534
1545
  raise HowlerValueError(f"Illegal variable name: {name}")
1535
1546
 
@@ -99,6 +99,7 @@ def search( # noqa: C901
99
99
  timeout: int | None = None,
100
100
  track_total_hits: bool = False,
101
101
  metadata: list[str] | None = None,
102
+ access_control: str | None = None,
102
103
  ) -> SearchResult[dict[str, Any]]:
103
104
  """Search through specified index for a given query. Uses lucene search syntax for query.
104
105
 
@@ -136,6 +137,9 @@ def search( # noqa: C901
136
137
  else:
137
138
  parsed_filters = filters
138
139
 
140
+ if access_control:
141
+ parsed_filters.append(access_control)
142
+
139
143
  if query is None:
140
144
  query = "id:*"
141
145
 
@@ -14,6 +14,12 @@ else:
14
14
  class StrEnum(str, _Enum): # type: ignore[no-redef]
15
15
  """str + Enum backport for Python < 3.11."""
16
16
 
17
+ def __str__(self) -> str:
18
+ return str.__str__(self)
19
+
20
+ def __format__(self, format_spec: str) -> str:
21
+ return str.__format__(self, format_spec)
22
+
17
23
  # typing_extensions.TypedDict supports Generic[T] mixing on Python < 3.11;
18
24
  # the stdlib version does not gain that until 3.11.
19
25
  from typing_extensions import NotRequired, TypedDict # noqa: F401
@@ -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.dev847"
155
+ version = "4.0.0.dev871"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
@@ -1,84 +0,0 @@
1
- """ETag utility module for handling HTTP ETags in Flask responses.
2
-
3
- ETags (Entity Tags) are HTTP headers used for web cache validation and conditional requests.
4
- They help optimize performance by allowing clients to cache responses and only fetch
5
- new data when the resource has actually changed.
6
- """
7
-
8
- import functools
9
- import re
10
-
11
- from flask import Response, request
12
-
13
- from howler.api import not_modified
14
-
15
-
16
- def add_etag(getter, check_if_match=True):
17
- """Decorator to add ETag handling to a Flask response.
18
-
19
- This decorator implements HTTP ETag functionality for API endpoints, enabling:
20
- - Conditional requests using If-Match headers
21
- - Cache validation to prevent unnecessary data transfers
22
- - Version tracking for resources
23
-
24
- Args:
25
- getter: Function that retrieves the object and its version
26
- check_if_match (bool): Whether to check If-Match headers for conditional requests
27
-
28
- Returns:
29
- Decorated function with ETag support
30
- """
31
-
32
- def wrapper(f):
33
- """Inner wrapper function that applies ETag functionality to the decorated function."""
34
-
35
- @functools.wraps(f)
36
- def generate_etag(*args, **kwargs):
37
- """Generate and handle ETags for the HTTP response."""
38
- # Retrieve the object and its version using the provided getter function
39
- # The getter should return (object, version) tuple
40
- obj, version = getter(
41
- kwargs.get("id", kwargs.get("username", None)),
42
- as_odm=True,
43
- version=True,
44
- )
45
-
46
- # Handle conditional requests with If-Match header
47
- # If the client's version matches the current version and it's a GET request
48
- # without metadata parameter, return 304 Not Modified to save bandwidth
49
- if (
50
- check_if_match
51
- and "If-Match" in request.headers
52
- and request.headers["If-Match"] == version
53
- and request.method == "GET"
54
- and "metadata" not in request.args
55
- ):
56
- return not_modified()
57
-
58
- # Extract the resource type from the API path and create a cache key
59
- # e.g., "/api/v1/users/123" becomes "cached_users"
60
- key = re.sub(r"^\/api\/v\d+\/(\w+)\/.+$", r"cached_\1", request.path)
61
- kwargs[key] = obj
62
-
63
- # Call the original function with the cached object and version
64
- values = f(*args, server_version=version, **kwargs)
65
-
66
- # Handle different return value formats from the decorated function
67
- # If there is only one return, it's just the response
68
- if isinstance(values, Response):
69
- # Only add ETag header for successful responses (not 409 Conflict or 400 Bad Request)
70
- if values.status_code != 409 and values.status_code != 400:
71
- values.headers["ETag"] = version
72
- return values
73
-
74
- # If there are two returns, it's the response and the new version
75
- # This happens when the function modifies the resource and returns an updated version
76
- else:
77
- if values[0].status_code != 409 and values[0].status_code != 400:
78
- # Add the new ETag version to successful responses
79
- values[0].headers["ETag"] = values[1]
80
- return values[0]
81
-
82
- return generate_etag
83
-
84
- return wrapper