howler-api 4.0.0.dev1099__tar.gz → 4.0.0.dev1110__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/PKG-INFO +1 -1
  2. howler_api-4.0.0.dev1110/howler/api/v2/fuzzy.py +110 -0
  3. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/app.py +2 -0
  4. howler_api-4.0.0.dev1110/howler/services/fuzzy_service.py +392 -0
  5. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/str_utils.py +1 -1
  6. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/pyproject.toml +1 -1
  7. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/README.md +0 -0
  8. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/__init__.py +0 -0
  9. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/__init__.py +0 -0
  10. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/add_label.py +0 -0
  11. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/add_to_bundle.py +0 -0
  12. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/add_to_case.py +0 -0
  13. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/change_field.py +0 -0
  14. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/demote.py +0 -0
  15. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/example_plugin.py +0 -0
  16. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/prioritization.py +0 -0
  17. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/promote.py +0 -0
  18. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/remove_from_bundle.py +0 -0
  19. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/remove_label.py +0 -0
  20. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/actions/transition.py +0 -0
  21. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/__init__.py +0 -0
  22. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/base.py +0 -0
  23. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/socket.py +0 -0
  24. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/__init__.py +0 -0
  25. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/action.py +0 -0
  26. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/analytic.py +0 -0
  27. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/auth.py +0 -0
  28. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/clue.py +0 -0
  29. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/configs.py +0 -0
  30. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/dossier.py +0 -0
  31. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/help.py +0 -0
  32. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/hit.py +0 -0
  33. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/notebook.py +0 -0
  34. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/overview.py +0 -0
  35. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/search.py +0 -0
  36. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/template.py +0 -0
  37. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/tool.py +0 -0
  38. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/user.py +0 -0
  39. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/utils/__init__.py +0 -0
  40. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/utils/etag.py +0 -0
  41. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/utils/params.py +0 -0
  42. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v1/view.py +0 -0
  43. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v2/__init__.py +0 -0
  44. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v2/case.py +0 -0
  45. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v2/ingest.py +0 -0
  46. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/api/v2/search.py +0 -0
  47. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/README.md +0 -0
  48. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/__init__.py +0 -0
  49. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/classification.py +0 -0
  50. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/classification.yml +0 -0
  51. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/exceptions.py +0 -0
  52. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/loader.py +0 -0
  53. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/logging/__init__.py +0 -0
  54. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/logging/audit.py +0 -0
  55. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/logging/format.py +0 -0
  56. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/net.py +0 -0
  57. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/net_static.py +0 -0
  58. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/random_user.py +0 -0
  59. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/common/swagger.py +0 -0
  60. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/config.py +0 -0
  61. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/cronjobs/__init__.py +0 -0
  62. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/cronjobs/action_queue_worker.py +0 -0
  63. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/cronjobs/correlation.py +0 -0
  64. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/cronjobs/retention.py +0 -0
  65. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/cronjobs/view_cleanup.py +0 -0
  66. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/README.md +0 -0
  67. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/__init__.py +0 -0
  68. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/bulk.py +0 -0
  69. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/collection.py +0 -0
  70. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/constants.py +0 -0
  71. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/exceptions.py +0 -0
  72. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/howler_store.py +0 -0
  73. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/migrations/fix_process.py +0 -0
  74. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/operations.py +0 -0
  75. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/schemas.py +0 -0
  76. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/store.py +0 -0
  77. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/support/__init__.py +0 -0
  78. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/support/build.py +0 -0
  79. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/support/schemas.py +0 -0
  80. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/datastore/types.py +0 -0
  81. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/error.py +0 -0
  82. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/README.md +0 -0
  83. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/__init__.py +0 -0
  84. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/generate_mitre.py +0 -0
  85. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/generate_sigma_rules.py +0 -0
  86. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/generate_tlds.py +0 -0
  87. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/reindex_data.py +0 -0
  88. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/external/wipe_databases.py +0 -0
  89. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/gunicorn_config.py +0 -0
  90. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/healthz.py +0 -0
  91. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/__init__.py +0 -0
  92. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/azure.py +0 -0
  93. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/discover.py +0 -0
  94. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/hit.py +0 -0
  95. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/oauth.py +0 -0
  96. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/search.py +0 -0
  97. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/workflow.py +0 -0
  98. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/helper/ws.py +0 -0
  99. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/README.md +0 -0
  100. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/__init__.py +0 -0
  101. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/base.py +0 -0
  102. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/charter.txt +0 -0
  103. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/constants.py +0 -0
  104. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/helper.py +0 -0
  105. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/howler_enum.py +0 -0
  106. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/mixins.py +0 -0
  107. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/__init__.py +0 -0
  108. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/action.py +0 -0
  109. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/analytic.py +0 -0
  110. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/assemblyline.py +0 -0
  111. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/aws.py +0 -0
  112. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/azure.py +0 -0
  113. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/case.py +0 -0
  114. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/cbs.py +0 -0
  115. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/clue.py +0 -0
  116. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/config.py +0 -0
  117. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/dossier.py +0 -0
  118. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/__init__.py +0 -0
  119. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/agent.py +0 -0
  120. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/autonomous_system.py +0 -0
  121. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/client.py +0 -0
  122. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/cloud.py +0 -0
  123. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/code_signature.py +0 -0
  124. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/container.py +0 -0
  125. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/dns.py +0 -0
  126. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/egress.py +0 -0
  127. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/elf.py +0 -0
  128. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/email.py +0 -0
  129. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/error.py +0 -0
  130. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/event.py +0 -0
  131. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/faas.py +0 -0
  132. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/file.py +0 -0
  133. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/geo.py +0 -0
  134. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/group.py +0 -0
  135. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/hash.py +0 -0
  136. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/host.py +0 -0
  137. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/http.py +0 -0
  138. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/ingress.py +0 -0
  139. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/interface.py +0 -0
  140. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/network.py +0 -0
  141. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/observer.py +0 -0
  142. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/organization.py +0 -0
  143. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/os.py +0 -0
  144. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/pe.py +0 -0
  145. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/process.py +0 -0
  146. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/registry.py +0 -0
  147. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/related.py +0 -0
  148. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/rule.py +0 -0
  149. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/server.py +0 -0
  150. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/threat.py +0 -0
  151. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/tls.py +0 -0
  152. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/url.py +0 -0
  153. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/user.py +0 -0
  154. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/user_agent.py +0 -0
  155. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/ecs/vulnerability.py +0 -0
  156. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/event.py +0 -0
  157. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/gcp.py +0 -0
  158. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/hit.py +0 -0
  159. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/howler_data.py +0 -0
  160. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/lead.py +0 -0
  161. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/localized_label.py +0 -0
  162. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/overview.py +0 -0
  163. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/pivot.py +0 -0
  164. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/record.py +0 -0
  165. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/template.py +0 -0
  166. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/user.py +0 -0
  167. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/models/view.py +0 -0
  168. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/random_data.py +0 -0
  169. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/odm/randomizer.py +0 -0
  170. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/patched.py +0 -0
  171. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/plugins/__init__.py +0 -0
  172. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/plugins/config.py +0 -0
  173. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/__init__.py +0 -0
  174. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/README.md +0 -0
  175. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/__init__.py +0 -0
  176. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/counters.py +0 -0
  177. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/events.py +0 -0
  178. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/hash.py +0 -0
  179. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/lock.py +0 -0
  180. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/queues/__init__.py +0 -0
  181. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/queues/comms.py +0 -0
  182. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/queues/multi.py +0 -0
  183. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/queues/named.py +0 -0
  184. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/queues/priority.py +0 -0
  185. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/set.py +0 -0
  186. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  187. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/security/__init__.py +0 -0
  188. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/security/socket.py +0 -0
  189. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/security/utils.py +0 -0
  190. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/__init__.py +0 -0
  191. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/action_service.py +0 -0
  192. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/analytic_service.py +0 -0
  193. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/auth_service.py +0 -0
  194. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/bundle_compat_service.py +0 -0
  195. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/case_service.py +0 -0
  196. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/comms_service.py +0 -0
  197. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/config_service.py +0 -0
  198. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/correlation_service.py +0 -0
  199. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/docs_service.py +0 -0
  200. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/dossier_service.py +0 -0
  201. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/event_service.py +0 -0
  202. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/hit_service.py +0 -0
  203. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/jwt_service.py +0 -0
  204. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/lucene_service.py +0 -0
  205. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/notebook_service.py +0 -0
  206. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/overview_service.py +0 -0
  207. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/search_service.py +0 -0
  208. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/template_service.py +0 -0
  209. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/user_service.py +0 -0
  210. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/services/viewer_service.py +0 -0
  211. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/telemetry.py +0 -0
  212. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/__init__.py +0 -0
  213. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/annotations.py +0 -0
  214. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/chunk.py +0 -0
  215. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/compat.py +0 -0
  216. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/constants.py +0 -0
  217. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/dict_utils.py +0 -0
  218. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/isotime.py +0 -0
  219. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/list_utils.py +0 -0
  220. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/lucene.py +0 -0
  221. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/path.py +0 -0
  222. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/howler/utils/socket_utils.py +0 -0
  223. {howler_api-4.0.0.dev1099 → howler_api-4.0.0.dev1110}/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.dev1099
3
+ Version: 4.0.0.dev1110
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,110 @@
1
+ from flask import request
2
+
3
+ from howler.api import bad_request, make_subapi_blueprint, ok
4
+ from howler.common.logging import get_logger
5
+ from howler.common.swagger import generate_swagger_docs
6
+ from howler.datastore.exceptions import SearchException
7
+ from howler.helper.search import has_access_control
8
+ from howler.security import api_login
9
+ from howler.services import fuzzy_service
10
+
11
+ SUB_API = "fuzzy"
12
+ fuzzy_api = make_subapi_blueprint(SUB_API, api_version=2)
13
+ fuzzy_api._doc = "Perform fuzzy plain-text searches across indexes" # type: ignore
14
+
15
+ logger = get_logger(__file__)
16
+
17
+
18
+ @generate_swagger_docs()
19
+ @fuzzy_api.route("/search", methods=["POST"])
20
+ @api_login(required_priv=["R"])
21
+ def fuzzy_search(**kwargs): # noqa: C901
22
+ """Perform a plain-text fuzzy search across hits, events, and cases.
23
+
24
+ Accepts a plain string (word, IP, domain, URL, username, email, hash) and
25
+ returns ranked results across specified indexes.
26
+
27
+ Variables:
28
+ None
29
+
30
+ Arguments:
31
+ None
32
+
33
+ Data Block:
34
+ {
35
+ "query": "192.168.1.1", # Plain text search query (required)
36
+ "indexes": ["hit", "event", "case"], # Indexes to search (optional, defaults to all)
37
+ "filters": ["howler.status:open"], # Additional filters (optional)
38
+ "offset": 0, # Offset into results (optional, default 0)
39
+ "rows": 100, # Number of results (optional, default 100)
40
+ "track_total_hits": false # Track total hits (optional, default false)
41
+ }
42
+
43
+ Result Example:
44
+ {
45
+ "total": 42,
46
+ "offset": 0,
47
+ "rows": 42,
48
+ "items": [
49
+ {
50
+ "__index": "hit",
51
+ "_score": 12.5,
52
+ "howler": {"id": "abc123", ...},
53
+ ...
54
+ }
55
+ ]
56
+ }
57
+ """
58
+ user = kwargs["user"]
59
+
60
+ req_data = request.get_json(silent=True)
61
+ if not req_data:
62
+ return bad_request(err="Request body is required.")
63
+
64
+ query = req_data.get("query", "").strip()
65
+ if not query:
66
+ return bad_request(err="Search query is required and cannot be empty.")
67
+
68
+ # Parse indexes
69
+ indexes = req_data.get("indexes", ["hit", "event", "case"])
70
+ if isinstance(indexes, str):
71
+ indexes = [idx.strip() for idx in indexes.split(",") if idx.strip()]
72
+
73
+ # Validate indexes
74
+ for idx in indexes:
75
+ if idx not in fuzzy_service.VALID_INDEXES:
76
+ return bad_request(err=f"Invalid index: {idx}. Must be one of: hit, event, case")
77
+
78
+ if not indexes:
79
+ return bad_request(err="At least one index must be specified.")
80
+
81
+ filters = req_data.get("filters", None)
82
+ if isinstance(filters, str):
83
+ filters = [filters]
84
+
85
+ try:
86
+ offset = int(req_data.get("offset", 0))
87
+ rows = int(req_data.get("rows", 100))
88
+ except (TypeError, ValueError):
89
+ return bad_request(err="'offset' and 'rows' must be integers.")
90
+ track_total_hits = bool(req_data.get("track_total_hits", False))
91
+
92
+ # Apply access control if indexes are access controlled
93
+ access_control = None
94
+ if has_access_control(indexes):
95
+ access_control = user["access_control"]
96
+
97
+ try:
98
+ result = fuzzy_service.fuzzy_search(
99
+ indexes=indexes,
100
+ query=query,
101
+ filters=filters,
102
+ offset=offset,
103
+ rows=rows,
104
+ track_total_hits=track_total_hits,
105
+ access_control=access_control,
106
+ )
107
+ except SearchException as e:
108
+ return bad_request(err=str(e))
109
+
110
+ return ok(result)
@@ -49,6 +49,7 @@ from howler.api.v1.user import user_api
49
49
  from howler.api.v1.view import view_api
50
50
  from howler.api.v2 import apiv2
51
51
  from howler.api.v2.case import case_api
52
+ from howler.api.v2.fuzzy import fuzzy_api
52
53
  from howler.api.v2.ingest import ingest_api
53
54
  from howler.api.v2.search import search_api as v2_search_api
54
55
  from howler.common.logging import get_logger
@@ -143,6 +144,7 @@ if HWL_USE_REST_API or DEBUG:
143
144
  # v2
144
145
  app.register_blueprint(apiv2)
145
146
  app.register_blueprint(case_api)
147
+ app.register_blueprint(fuzzy_api)
146
148
  app.register_blueprint(ingest_api)
147
149
  app.register_blueprint(v2_search_api)
148
150
 
@@ -0,0 +1,392 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from functools import lru_cache
5
+ from typing import Any
6
+
7
+ import elasticsearch
8
+ from elasticsearch import Elasticsearch
9
+
10
+ from howler import odm
11
+ from howler.common.loader import APP_NAME, datastore
12
+ from howler.datastore.exceptions import SearchException, SearchRetryException
13
+ from howler.datastore.types import SearchResult
14
+ from howler.odm.base import (
15
+ DOMAIN_ONLY_REGEX,
16
+ EMAIL_REGEX,
17
+ IP,
18
+ IP_ONLY_REGEX,
19
+ MD5_REGEX,
20
+ SHA1_REGEX,
21
+ SHA256_REGEX,
22
+ List,
23
+ Optional,
24
+ Text,
25
+ _Field,
26
+ )
27
+ from howler.services.search_service import _normalize_indexes
28
+ from howler.utils.str_utils import sanitize_lucene_query
29
+
30
+ DEFAULT_OFFSET = 0
31
+ DEFAULT_ROW_SIZE = 100
32
+ VALID_INDEXES = {"hit", "event", "case"}
33
+
34
+
35
+ def _escape_query_string(query: str) -> str:
36
+ """Escape special Lucene characters from a raw query string."""
37
+ return sanitize_lucene_query(query)
38
+
39
+
40
+ # Compiled regexes from ODM base for token type detection
41
+ _IP_RE = re.compile(IP_ONLY_REGEX)
42
+ _MD5_RE = re.compile(MD5_REGEX, re.IGNORECASE)
43
+ _SHA1_RE = re.compile(SHA1_REGEX, re.IGNORECASE)
44
+ _SHA256_RE = re.compile(SHA256_REGEX, re.IGNORECASE)
45
+ _EMAIL_RE = re.compile(EMAIL_REGEX)
46
+ _DOMAIN_RE = re.compile(DOMAIN_ONLY_REGEX)
47
+ # FULL_URI is too permissive for token detection (matches bare domains).
48
+ # Use a scheme-prefix check to identify URLs specifically.
49
+ _URL_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+\-.]*://", re.IGNORECASE)
50
+
51
+ # Field boosts by index
52
+ FIELD_BOOSTS: dict[str, dict[str, int]] = {
53
+ "hit": {
54
+ "howler.id": 5,
55
+ "howler.outline.threat": 4,
56
+ "howler.outline.target": 3,
57
+ "related.ip": 4,
58
+ "related.user": 3,
59
+ "related.uri": 3,
60
+ "related.hosts": 3,
61
+ "related.hash": 3,
62
+ "related.signature": 2,
63
+ "message": 1,
64
+ },
65
+ "event": {
66
+ "howler.id": 5,
67
+ "source.ip": 4,
68
+ "destination.ip": 4,
69
+ "dns.domain": 3,
70
+ "url.domain": 3,
71
+ "http.host": 3,
72
+ "related.ip": 2,
73
+ "related.user": 2,
74
+ "related.uri": 2,
75
+ "related.hosts": 2,
76
+ "related.hash": 2,
77
+ "message": 1,
78
+ },
79
+ "case": {
80
+ "case_id": 5,
81
+ "indicators": 4,
82
+ "targets": 4,
83
+ "items.value": 3,
84
+ "items.path": 2,
85
+ },
86
+ }
87
+
88
+
89
+ def _detect_token_type(token: str) -> str:
90
+ """Detect the type of a search token using ODM-defined patterns.
91
+
92
+ Returns one of: 'ip', 'md5', 'sha1', 'sha256', 'email', 'url', 'domain', 'text'
93
+ """
94
+ if _IP_RE.match(token):
95
+ return "ip"
96
+ if _MD5_RE.match(token):
97
+ return "md5"
98
+ if _SHA1_RE.match(token):
99
+ return "sha1"
100
+ if _SHA256_RE.match(token):
101
+ return "sha256"
102
+ if _EMAIL_RE.match(token):
103
+ return "email"
104
+ if _URL_RE.match(token):
105
+ return "url"
106
+ if _DOMAIN_RE.match(token):
107
+ return "domain"
108
+ return "text"
109
+
110
+
111
+ def _get_fields_for_index(index: str) -> list[str]:
112
+ """Get boosted field strings for a given index."""
113
+ boosts = FIELD_BOOSTS.get(index, {})
114
+ return [f"{field}^{boost}" for field, boost in boosts.items()]
115
+
116
+
117
+ def _resolve_field_type(field: _Field) -> type:
118
+ """Unwrap Optional/List wrappers to get the leaf field type."""
119
+ if isinstance(field, Optional):
120
+ return _resolve_field_type(field.child_type)
121
+ if isinstance(field, List):
122
+ return _resolve_field_type(field.child_type)
123
+ return type(field)
124
+
125
+
126
+ @lru_cache(maxsize=1)
127
+ def _classify_boosted_fields() -> dict[str, str]:
128
+ """Classify each field in FIELD_BOOSTS by its Elasticsearch type using ODM model introspection.
129
+
130
+ Inspects the flat_fields() of each model to determine the ES mapping type.
131
+ Fields declared as odm.IP() map to ES 'ip' type.
132
+ Fields declared as odm.Text() map to ES 'text' type.
133
+ All others (Keyword and subtypes) map to ES 'keyword' type.
134
+
135
+ Returns:
136
+ A dict mapping field name to one of 'ip', 'text', or 'keyword'.
137
+ """
138
+ from howler.odm.models.case import Case
139
+ from howler.odm.models.event import Event
140
+ from howler.odm.models.hit import Hit
141
+
142
+ model_map: dict[str, type[odm.Model]] = {
143
+ "hit": Hit,
144
+ "event": Event,
145
+ "case": Case,
146
+ }
147
+
148
+ field_types: dict[str, str] = {}
149
+ for index, fields in FIELD_BOOSTS.items():
150
+ model_class = model_map.get(index, None)
151
+ if not model_class:
152
+ continue
153
+
154
+ flat = model_class.flat_fields()
155
+ for field_name in fields:
156
+ if field_name in field_types:
157
+ continue
158
+ field_def = flat.get(field_name)
159
+ if not field_def:
160
+ field_types[field_name] = "keyword"
161
+ continue
162
+ leaf_type = _resolve_field_type(field_def)
163
+ if issubclass(leaf_type, IP):
164
+ field_types[field_name] = "ip"
165
+ elif issubclass(leaf_type, Text):
166
+ field_types[field_name] = "text"
167
+ else:
168
+ field_types[field_name] = "keyword"
169
+
170
+ return field_types
171
+
172
+
173
+ def _get_ip_typed_fields() -> set[str]:
174
+ """Return the set of fields classified as IP type."""
175
+ return {f for f, t in _classify_boosted_fields().items() if t == "ip"}
176
+
177
+
178
+ def build_fuzzy_query( # noqa: C901
179
+ query: str,
180
+ indexes: list[str],
181
+ filters: list[str] | None = None,
182
+ access_control: str | None = None,
183
+ ) -> dict[str, Any]:
184
+ """Build an Elasticsearch query body for fuzzy/plain-text search.
185
+
186
+ Args:
187
+ q: The search string (plain text, IP, hash, domain, etc.)
188
+ indexes: List of index names to search across.
189
+ filters: Additional filter queries.
190
+ access_control: Access control filter string.
191
+
192
+ Returns:
193
+ An Elasticsearch query body dict.
194
+ """
195
+ query = query.strip()
196
+ token_type = _detect_token_type(query)
197
+
198
+ # Collect all boosted fields across requested indexes
199
+ all_fields: list[str] = []
200
+ for index in indexes:
201
+ all_fields.extend(_get_fields_for_index(index))
202
+
203
+ # De-duplicate fields (keep highest boost if duplicate base field)
204
+ field_boost_map: dict[str, int] = {}
205
+ for field_str in all_fields:
206
+ if "^" in field_str:
207
+ field, boost_str = field_str.rsplit("^", 1)
208
+ boost = int(boost_str)
209
+ else:
210
+ field = field_str
211
+ boost = 1
212
+ if field not in field_boost_map or field_boost_map[field] < boost:
213
+ field_boost_map[field] = boost
214
+
215
+ # Partition fields by ES type using ODM model introspection.
216
+ # - ip: must use term queries (no fuzzy/phrase support)
217
+ # - text: supports all query types including phrase_prefix
218
+ # - keyword: supports best_fields and phrase, but NOT phrase_prefix
219
+ field_classes = _classify_boosted_fields()
220
+ ip_fields: dict[str, int] = {}
221
+ text_fields: dict[str, int] = {}
222
+ keyword_fields: dict[str, int] = {}
223
+ for f, b in field_boost_map.items():
224
+ es_type = field_classes.get(f, "keyword")
225
+ if es_type == "ip":
226
+ ip_fields[f] = b
227
+ elif es_type == "text":
228
+ text_fields[f] = b
229
+ else:
230
+ keyword_fields[f] = b
231
+
232
+ # Fields safe for multi_match best_fields / phrase (text + keyword)
233
+ searchable_field_list = [f"{f}^{b}" for f, b in {**text_fields, **keyword_fields}.items()]
234
+ # Fields safe for phrase_prefix (text only)
235
+ text_field_list = [f"{f}^{b}" for f, b in text_fields.items()]
236
+
237
+ should_clauses: list[dict[str, Any]] = []
238
+
239
+ # Broad catch-all: query_string across all indexed fields at low boost.
240
+ # This ensures every field in the schema is searched (e.g. agent.type),
241
+ # while the boosted multi_match clauses below rank high-value fields higher.
242
+ escaped_q = sanitize_lucene_query(query)
243
+
244
+ if token_type == "ip": # noqa: S105
245
+ # For IP tokens, use term queries on IP fields and best_fields on boosted text/keyword fields
246
+ for field, boost in ip_fields.items():
247
+ should_clauses.append({"term": {field: {"value": query, "boost": boost}}})
248
+ if searchable_field_list:
249
+ should_clauses.append(
250
+ {"multi_match": {"query": query, "fields": searchable_field_list, "type": "best_fields"}}
251
+ )
252
+ should_clauses.append({"query_string": {"query": escaped_q, "default_field": "*", "boost": 0.5}})
253
+ elif token_type in ("md5", "sha1", "sha256"):
254
+ # Exact match for hashes — boosted fields + catch-all
255
+ if searchable_field_list:
256
+ should_clauses.append(
257
+ {"multi_match": {"query": query, "fields": searchable_field_list, "type": "best_fields"}}
258
+ )
259
+ should_clauses.append({"query_string": {"query": escaped_q, "default_field": "*", "boost": 0.5}})
260
+ elif token_type in ("email", "url", "domain"):
261
+ # Use phrase matching on boosted fields + catch-all
262
+ if searchable_field_list:
263
+ should_clauses.append({"multi_match": {"query": query, "fields": searchable_field_list, "type": "phrase"}})
264
+ should_clauses.append(
265
+ {"multi_match": {"query": query, "fields": searchable_field_list, "type": "best_fields"}}
266
+ )
267
+ should_clauses.append({"query_string": {"query": escaped_q, "default_field": "*", "boost": 0.5}})
268
+ else:
269
+ # General text - boosted fuzzy match + catch-all across all fields
270
+ if searchable_field_list:
271
+ should_clauses.append(
272
+ {
273
+ "multi_match": {
274
+ "query": query,
275
+ "fields": searchable_field_list,
276
+ "type": "best_fields",
277
+ "fuzziness": "AUTO",
278
+ }
279
+ }
280
+ )
281
+ # phrase_prefix only works on text fields, not keyword
282
+ if len(query) >= 3 and text_field_list:
283
+ should_clauses.append(
284
+ {"multi_match": {"query": query, "fields": text_field_list, "type": "phrase_prefix"}}
285
+ )
286
+ should_clauses.append(
287
+ {"query_string": {"query": f"*{escaped_q}*", "default_field": "*", "boost": 0.5, "analyze_wildcard": True}}
288
+ )
289
+
290
+ # Build filter clauses
291
+ filter_clauses: list[dict[str, Any]] = []
292
+ if filters:
293
+ for f in filters:
294
+ filter_clauses.append({"query_string": {"query": f}})
295
+ if access_control:
296
+ filter_clauses.append({"query_string": {"query": access_control}})
297
+
298
+ query_body: dict[str, Any] = {
299
+ "query": {
300
+ "bool": {
301
+ "should": should_clauses,
302
+ "minimum_should_match": 1,
303
+ }
304
+ }
305
+ }
306
+
307
+ if filter_clauses:
308
+ query_body["query"]["bool"]["filter"] = filter_clauses
309
+
310
+ return query_body
311
+
312
+
313
+ def fuzzy_search(
314
+ indexes: list[str],
315
+ query: str,
316
+ filters: list[str] | None = None,
317
+ offset: int = DEFAULT_OFFSET,
318
+ rows: int = DEFAULT_ROW_SIZE,
319
+ track_total_hits: bool = False,
320
+ access_control: str | None = None,
321
+ ) -> SearchResult[dict[str, Any]]:
322
+ """Perform a fuzzy/plain-text search across multiple indexes.
323
+
324
+ Args:
325
+ indexes: List of index names (hit, event, case).
326
+ q: Plain text search query.
327
+ filters: Additional filter queries.
328
+ offset: Offset into results.
329
+ rows: Number of results to return.
330
+ track_total_hits: Whether to track total hits beyond 10000.
331
+ access_control: Access control filter string.
332
+
333
+ Returns:
334
+ A SearchResult containing matched items with __index and _score.
335
+ """
336
+ # Validate indexes
337
+ for idx in indexes:
338
+ if idx not in VALID_INDEXES:
339
+ raise SearchException(f"Invalid index for fuzzy search: {idx}")
340
+
341
+ client: Elasticsearch = datastore().ds.client
342
+ parsed_indexes = _normalize_indexes(indexes)
343
+
344
+ query_body = build_fuzzy_query(query, indexes, filters, access_control)
345
+
346
+ params: dict[str, Any] = {}
347
+ if track_total_hits:
348
+ params["track_total_hits"] = True
349
+
350
+ try:
351
+ result = client.search(
352
+ index=parsed_indexes,
353
+ sort=[{"_score": "desc"}],
354
+ from_=offset,
355
+ size=rows,
356
+ **params,
357
+ **query_body,
358
+ )
359
+ except (elasticsearch.exceptions.ConnectionError, elasticsearch.exceptions.ConnectionTimeout) as error:
360
+ raise SearchRetryException(f"indexes: {parsed_indexes}, query: {query}, error: {str(error)}") from error
361
+ except (elasticsearch.exceptions.TransportError, elasticsearch.exceptions.RequestError) as error:
362
+ raise SearchException(str(error)) from error
363
+ except Exception as error:
364
+ raise SearchException(f"indexes: {parsed_indexes}, query: {query}, error: {str(error)}") from error
365
+
366
+ total = result.get("hits", {}).get("total", {}).get("value", 0)
367
+ hits = result.get("hits", {}).get("hits", [])
368
+
369
+ items = _format_items_with_score(hits)
370
+
371
+ response: SearchResult[dict[str, Any]] = {
372
+ "offset": int(offset),
373
+ "rows": len(items),
374
+ "total": int(total),
375
+ "items": items,
376
+ }
377
+
378
+ return response
379
+
380
+
381
+ def _format_items_with_score(hits: list[dict[str, Any]]) -> list[dict[str, Any]]:
382
+ """Format Elasticsearch hits including _score in the output."""
383
+ items: list[dict[str, Any]] = []
384
+ for hit in hits:
385
+ source = hit.get("_source")
386
+ if source:
387
+ raw_index = hit.get("_index", None)
388
+ if raw_index:
389
+ source["__index"] = raw_index.replace(f"{APP_NAME}-", "").replace("_hot", "")
390
+ source["_score"] = hit.get("_score", 0)
391
+ items.append(source)
392
+ return items
@@ -203,7 +203,7 @@ def get_parent_key(key: str) -> str:
203
203
 
204
204
  def sanitize_lucene_query(query: str):
205
205
  """Take in a given string, and escape it to ensure it is safe to search on via lucene"""
206
- query = re.sub(r'([\^"~*?:\\/()[\]{}\-!])', r"\\\1", query)
206
+ query = re.sub(r'([\^"~*?:\\/()[\]{}\-!+])', r"\\\1", query)
207
207
 
208
208
  return query.replace("&&", "\\&&").replace("||", "\\||")
209
209
 
@@ -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.dev1099"
155
+ version = "4.0.0.dev1110"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",