howler-api 2.11.0.dev185__tar.gz → 2.11.0.dev217__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 (198) hide show
  1. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/PKG-INFO +1 -1
  2. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/auth.py +1 -1
  3. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/dossier.py +4 -28
  4. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/hit.py +11 -7
  5. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/search.py +18 -9
  6. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/user.py +2 -2
  7. howler_api-2.11.0.dev217/howler/api/v1/utils/etag.py +84 -0
  8. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/helper.py +4 -1
  9. howler_api-2.11.0.dev217/howler/services/dossier_service.py +252 -0
  10. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/hit_service.py +296 -70
  11. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/lucene_service.py +14 -7
  12. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/lucene.py +22 -2
  13. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/pyproject.toml +1 -1
  14. howler_api-2.11.0.dev185/howler/api/v1/utils/etag.py +0 -46
  15. howler_api-2.11.0.dev185/howler/services/dossier_service.py +0 -119
  16. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/README.md +0 -0
  17. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/__init__.py +0 -0
  18. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/__init__.py +0 -0
  19. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/add_label.py +0 -0
  20. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/add_to_bundle.py +0 -0
  21. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/change_field.py +0 -0
  22. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/demote.py +0 -0
  23. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/example_plugin.py +0 -0
  24. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/prioritization.py +0 -0
  25. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/promote.py +0 -0
  26. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/remove_from_bundle.py +0 -0
  27. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/remove_label.py +0 -0
  28. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/transition.py +0 -0
  29. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/__init__.py +0 -0
  30. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/base.py +0 -0
  31. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/socket.py +0 -0
  32. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/__init__.py +0 -0
  33. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/action.py +0 -0
  34. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/analytic.py +0 -0
  35. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/borealis.py +0 -0
  36. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/configs.py +0 -0
  37. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/help.py +0 -0
  38. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/notebook.py +0 -0
  39. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/overview.py +0 -0
  40. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/template.py +0 -0
  41. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/tool.py +0 -0
  42. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/utils/__init__.py +0 -0
  43. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/view.py +0 -0
  44. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/app.py +0 -0
  45. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/README.md +0 -0
  46. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/__init__.py +0 -0
  47. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/classification.py +0 -0
  48. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/classification.yml +0 -0
  49. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/exceptions.py +0 -0
  50. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/hexdump.py +0 -0
  51. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/iprange.py +0 -0
  52. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/loader.py +0 -0
  53. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/logging/__init__.py +0 -0
  54. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/logging/audit.py +0 -0
  55. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/logging/format.py +0 -0
  56. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/net.py +0 -0
  57. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/net_static.py +0 -0
  58. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/random_user.py +0 -0
  59. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/swagger.py +0 -0
  60. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/config.py +0 -0
  61. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/cronjobs/__init__.py +0 -0
  62. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/cronjobs/retention.py +0 -0
  63. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/cronjobs/rules.py +0 -0
  64. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/README.md +0 -0
  65. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/__init__.py +0 -0
  66. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/bulk.py +0 -0
  67. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/collection.py +0 -0
  68. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/constants.py +0 -0
  69. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/exceptions.py +0 -0
  70. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/howler_store.py +0 -0
  71. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/migrations/fix_process.py +0 -0
  72. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/operations.py +0 -0
  73. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/schemas.py +0 -0
  74. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/store.py +0 -0
  75. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/support/__init__.py +0 -0
  76. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/support/build.py +0 -0
  77. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/support/schemas.py +0 -0
  78. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/types.py +0 -0
  79. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/error.py +0 -0
  80. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/__init__.py +0 -0
  81. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/generate_mitre.py +0 -0
  82. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/generate_sigma_rules.py +0 -0
  83. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/generate_tlds.py +0 -0
  84. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/reindex_data.py +0 -0
  85. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/wipe_databases.py +0 -0
  86. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/gunicorn_config.py +0 -0
  87. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/healthz.py +0 -0
  88. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/__init__.py +0 -0
  89. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/azure.py +0 -0
  90. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/discover.py +0 -0
  91. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/hit.py +0 -0
  92. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/oauth.py +0 -0
  93. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/search.py +0 -0
  94. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/workflow.py +0 -0
  95. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/ws.py +0 -0
  96. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/README.md +0 -0
  97. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/__init__.py +0 -0
  98. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/base.py +0 -0
  99. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/charter.txt +0 -0
  100. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/howler_enum.py +0 -0
  101. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/__init__.py +0 -0
  102. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/action.py +0 -0
  103. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/analytic.py +0 -0
  104. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/assemblyline.py +0 -0
  105. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/aws.py +0 -0
  106. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/azure.py +0 -0
  107. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/cbs.py +0 -0
  108. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/config.py +0 -0
  109. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/dossier.py +0 -0
  110. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/__init__.py +0 -0
  111. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/agent.py +0 -0
  112. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/autonomous_system.py +0 -0
  113. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/client.py +0 -0
  114. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/cloud.py +0 -0
  115. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/code_signature.py +0 -0
  116. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/container.py +0 -0
  117. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/dns.py +0 -0
  118. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/egress.py +0 -0
  119. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/elf.py +0 -0
  120. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/email.py +0 -0
  121. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/error.py +0 -0
  122. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/event.py +0 -0
  123. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/faas.py +0 -0
  124. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/file.py +0 -0
  125. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/geo.py +0 -0
  126. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/group.py +0 -0
  127. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/hash.py +0 -0
  128. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/host.py +0 -0
  129. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/http.py +0 -0
  130. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/ingress.py +0 -0
  131. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/interface.py +0 -0
  132. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/network.py +0 -0
  133. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/observer.py +0 -0
  134. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/organization.py +0 -0
  135. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/os.py +0 -0
  136. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/pe.py +0 -0
  137. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/process.py +0 -0
  138. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/registry.py +0 -0
  139. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/related.py +0 -0
  140. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/rule.py +0 -0
  141. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/server.py +0 -0
  142. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/threat.py +0 -0
  143. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/tls.py +0 -0
  144. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/url.py +0 -0
  145. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/user.py +0 -0
  146. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/user_agent.py +0 -0
  147. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/vulnerability.py +0 -0
  148. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/gcp.py +0 -0
  149. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/hit.py +0 -0
  150. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/howler_data.py +0 -0
  151. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/lead.py +0 -0
  152. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/localized_label.py +0 -0
  153. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/overview.py +0 -0
  154. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/pivot.py +0 -0
  155. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/template.py +0 -0
  156. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/user.py +0 -0
  157. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/view.py +0 -0
  158. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/random_data.py +0 -0
  159. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/randomizer.py +0 -0
  160. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/patched.py +0 -0
  161. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/plugins/__init__.py +0 -0
  162. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/plugins/config.py +0 -0
  163. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/__init__.py +0 -0
  164. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/README.md +0 -0
  165. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/__init__.py +0 -0
  166. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/counters.py +0 -0
  167. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/events.py +0 -0
  168. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/hash.py +0 -0
  169. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/lock.py +0 -0
  170. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/__init__.py +0 -0
  171. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/comms.py +0 -0
  172. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/multi.py +0 -0
  173. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/named.py +0 -0
  174. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/priority.py +0 -0
  175. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/set.py +0 -0
  176. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  177. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/security/__init__.py +0 -0
  178. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/security/socket.py +0 -0
  179. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/security/utils.py +0 -0
  180. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/__init__.py +0 -0
  181. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/action_service.py +0 -0
  182. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/analytic_service.py +0 -0
  183. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/auth_service.py +0 -0
  184. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/config_service.py +0 -0
  185. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/event_service.py +0 -0
  186. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/jwt_service.py +0 -0
  187. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/notebook_service.py +0 -0
  188. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/user_service.py +0 -0
  189. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/__init__.py +0 -0
  190. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/annotations.py +0 -0
  191. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/chunk.py +0 -0
  192. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/dict_utils.py +0 -0
  193. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/isotime.py +0 -0
  194. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/list_utils.py +0 -0
  195. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/path.py +0 -0
  196. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/socket_utils.py +0 -0
  197. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/str_utils.py +0 -0
  198. {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: howler-api
3
- Version: 2.11.0.dev185
3
+ Version: 2.11.0.dev217
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -131,7 +131,7 @@ def add_apikey(**kwargs): # noqa: C901
131
131
  key_name = apikey_data["name"] if "I" not in privs else f"impersonate_{apikey_data['name']}"
132
132
 
133
133
  new_key = {
134
- "password": bcrypt.encrypt(random_pass),
134
+ "password": bcrypt.hash(random_pass),
135
135
  "agents": apikey_data.get("agents", []),
136
136
  "acl": privs,
137
137
  }
@@ -1,15 +1,6 @@
1
1
  from flask import request
2
2
 
3
- from howler.api import (
4
- bad_request,
5
- created,
6
- forbidden,
7
- internal_error,
8
- make_subapi_blueprint,
9
- no_content,
10
- not_found,
11
- ok,
12
- )
3
+ from howler.api import bad_request, created, forbidden, internal_error, make_subapi_blueprint, no_content, not_found, ok
13
4
  from howler.common.exceptions import ForbiddenException, HowlerException, InvalidDataException, NotFoundException
14
5
  from howler.common.loader import datastore
15
6
  from howler.common.logging import get_logger
@@ -17,7 +8,7 @@ from howler.common.swagger import generate_swagger_docs
17
8
  from howler.odm.models.dossier import Dossier
18
9
  from howler.odm.models.user import User
19
10
  from howler.security import api_login
20
- from howler.services import dossier_service, lucene_service
11
+ from howler.services import dossier_service
21
12
 
22
13
  SUB_API = "dossier"
23
14
  dossier_api = make_subapi_blueprint(SUB_API, api_version=1)
@@ -141,29 +132,14 @@ def get_dossier_for_hit(id: str, user: User, **kwargs):
141
132
  """
142
133
  storage = datastore()
143
134
  try:
144
- response = storage.hit.search(f"howler.id:{id}", rows=1)
135
+ response = storage.hit.search(f"howler.id:{id}", rows=1, as_obj=False)
145
136
 
146
137
  if response["total"] < 1:
147
138
  return not_found(err="Hit does not exist.")
148
139
 
149
140
  hit = response["items"][0]
150
141
 
151
- results: list[Dossier] = storage.dossier.search(
152
- "dossier_id:*",
153
- as_obj=True,
154
- rows=1000,
155
- )["items"]
156
-
157
- matching_dossiers: list[Dossier] = []
158
- for dossier in results:
159
- if dossier.query is None:
160
- matching_dossiers.append(dossier)
161
- continue
162
-
163
- if lucene_service.match(dossier.query, hit.as_primitives()):
164
- matching_dossiers.append(dossier)
165
-
166
- return ok(matching_dossiers)
142
+ return ok(dossier_service.get_matching_dossiers(hit))
167
143
  except ValueError as e:
168
144
  return bad_request(err=str(e))
169
145
 
@@ -17,11 +17,7 @@ from howler.api import (
17
17
  ok,
18
18
  )
19
19
  from howler.api.v1.utils.etag import add_etag
20
- from howler.common.exceptions import (
21
- HowlerException,
22
- HowlerValueError,
23
- InvalidDataException,
24
- )
20
+ from howler.common.exceptions import HowlerException, HowlerValueError, InvalidDataException
25
21
  from howler.common.loader import datastore
26
22
  from howler.common.logging import get_logger
27
23
  from howler.common.swagger import generate_swagger_docs
@@ -252,7 +248,7 @@ def validate_hits(**kwargs):
252
248
  @generate_swagger_docs()
253
249
  @hit_api.route("/<id>", methods=["GET"])
254
250
  @api_login(audit=False, required_priv=["R"])
255
- @add_etag(getter=hit_service.get_hit, check_if_match=False)
251
+ @add_etag(getter=hit_service.get_hit)
256
252
  def get_hit(id: str, server_version: str, **kwargs):
257
253
  """Get a hit.
258
254
 
@@ -265,11 +261,19 @@ def get_hit(id: str, server_version: str, **kwargs):
265
261
  Result Example:
266
262
  https://github.com/CybercentreCanada/howler-api/blob/main/howler/odm/models/hit.py
267
263
  """
268
- hit = cast(Optional[Hit], kwargs.get("cached_hit"))
264
+ hit = cast(Optional[Any], kwargs.get("cached_hit"))
269
265
 
270
266
  if not hit:
271
267
  return not_found(err="Hit %s does not exist" % id)
272
268
 
269
+ if "metadata" in request.args:
270
+ metadata = (request.args.get("metadata", type=str) or "").split(",")
271
+
272
+ hit = hit.as_primitives()
273
+
274
+ if len(metadata) > 0:
275
+ hit_service.augment_metadata(hit, metadata, kwargs["user"])
276
+
273
277
  return ok(hit), server_version
274
278
 
275
279
 
@@ -14,6 +14,7 @@ from howler.common.swagger import generate_swagger_docs
14
14
  from howler.datastore.exceptions import SearchException
15
15
  from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
16
16
  from howler.security import api_login
17
+ from howler.services import hit_service
17
18
 
18
19
  SUB_API = "search"
19
20
  search_api = make_subapi_blueprint(SUB_API, api_version=1)
@@ -73,16 +74,18 @@ def search(index, **kwargs):
73
74
  timeout => Maximum execution time (ms)
74
75
  use_archive => Allow access to the datastore achive (Default: False)
75
76
  track_total_hits => Track the total number of query matches, instead of stopping at 10000 (Default: False)
77
+ metadata => A list of additional features to be added to the result alongside the raw results
76
78
 
77
79
  Data Block:
78
80
  # Note that the data block is for POST requests only!
79
- {"query": "query", # Query to search for
80
- "offset": 0, # Offset in the results
81
- "rows": 100, # Max number of results
82
- "sort": "field asc", # How to sort the results
83
- "fl": "id,score", # List of fields to return
84
- "timeout": 1000, # Maximum execution time (ms)
85
- "filters": ['fq']} # List of additional filter queries limit the data
81
+ {"query": "query", # Query to search for
82
+ "offset": 0, # Offset in the results
83
+ "rows": 100, # Max number of results
84
+ "sort": "field asc", # How to sort the results
85
+ "fl": "id,score", # List of fields to return
86
+ "timeout": 1000, # Maximum execution time (ms)
87
+ "filters": ['fq'], # List of additional filter queries limit the data
88
+ "metadata": ["dossiers"]} # List of additional features to add to the search
86
89
 
87
90
 
88
91
  Result Example:
@@ -108,7 +111,7 @@ def search(index, **kwargs):
108
111
  "deep_paging_id",
109
112
  "track_total_hits",
110
113
  ]
111
- multi_fields = ["filters"]
114
+ multi_fields = ["filters", "metadata"]
112
115
  boolean_fields = ["use_archive"]
113
116
 
114
117
  params, req_data = generate_params(request, fields, multi_fields)
@@ -132,7 +135,13 @@ def search(index, **kwargs):
132
135
  return bad_request(err="There was no search query.")
133
136
 
134
137
  try:
135
- return ok(collection().search(query, **params))
138
+ metadata = params.pop("metadata", [])
139
+ result = collection().search(query, **params)
140
+
141
+ if index == "hit" and len(metadata) > 0:
142
+ hit_service.augment_metadata(result["items"], metadata, user)
143
+
144
+ return ok(result)
136
145
  except (SearchException, BadRequestError) as e:
137
146
  return bad_request(err=f"SearchException: {e}")
138
147
 
@@ -145,7 +145,7 @@ def add_user_account(username, **_):
145
145
  @generate_swagger_docs()
146
146
  @user_api.route("/<username>", methods=["GET"])
147
147
  @api_login(audit=False, required_priv=["R"])
148
- @add_etag(getter=user_service.get_user, check_if_match=False)
148
+ @add_etag(getter=user_service.get_user, check_if_match=True)
149
149
  def get_user_account(username: str, server_version: Optional[str] = None, **kwargs):
150
150
  """Load the user account information.
151
151
 
@@ -327,7 +327,7 @@ def get_user_avatar(username, **_):
327
327
  resp.headers["ETag"] = sha256(avatar.encode("utf-8")).hexdigest()
328
328
  return resp
329
329
  else:
330
- return not_found(err="No avatar for specified user")
330
+ return no_content()
331
331
 
332
332
 
333
333
  @generate_swagger_docs()
@@ -0,0 +1,84 @@
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
@@ -143,8 +143,9 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
143
143
  hit.howler.labels.mitigation = []
144
144
  hit.howler.labels.operation = []
145
145
  hit.howler.labels.threat = []
146
+ hit.howler.labels.tuning = []
146
147
 
147
- label_type = ceil(rand_seed * 6)
148
+ label_type = ceil(rand_seed * 7)
148
149
  if label_type == 1:
149
150
  hit.howler.labels.campaign = ["Bad event 2023-07"]
150
151
  elif label_type == 2:
@@ -155,6 +156,8 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
155
156
  hit.howler.labels.mitigation = ["Blocked: google.com"]
156
157
  elif label_type == 5:
157
158
  hit.howler.labels.operation = ["OP_HOWLER"]
159
+ elif label_type == 6:
160
+ hit.howler.labels.tuning = ["Tune example"]
158
161
  else:
159
162
  hit.howler.labels.threat = ["Bad Mojo"]
160
163
 
@@ -0,0 +1,252 @@
1
+ """Dossier service module for managing security investigation dossiers.
2
+
3
+ This module provides functionality for creating, updating, retrieving, and managing
4
+ dossiers - collections of security alerts and investigation data organized by analysts.
5
+ Dossiers can be personal (private to the creator) or global (shared with the team).
6
+ """
7
+
8
+ from typing import Any, Optional, cast
9
+
10
+ from mergedeep.mergedeep import merge
11
+
12
+ from howler.common.exceptions import ForbiddenException, HowlerException, InvalidDataException, NotFoundException
13
+ from howler.common.loader import datastore
14
+ from howler.common.logging import get_logger
15
+ from howler.datastore.exceptions import SearchException
16
+ from howler.odm.models.dossier import Dossier
17
+ from howler.odm.models.user import User
18
+ from howler.services import lucene_service
19
+
20
+ logger = get_logger(__file__)
21
+
22
+ # Define which fields are allowed to be updated in a dossier, preventing unauthorized modification of sensitive fields
23
+ PERMITTED_KEYS = {
24
+ "title",
25
+ "query",
26
+ "leads",
27
+ "pivots",
28
+ "type",
29
+ "owner",
30
+ }
31
+
32
+
33
+ def exists(dossier_id: str) -> bool:
34
+ """Check if a dossier exists in the datastore.
35
+
36
+ Args:
37
+ dossier_id: Unique identifier for the dossier
38
+
39
+ Returns:
40
+ True if the dossier exists, False otherwise
41
+ """
42
+ return datastore().dossier.exists(dossier_id)
43
+
44
+
45
+ def get_dossier(
46
+ id: str,
47
+ as_odm: bool = False,
48
+ version: bool = False,
49
+ ) -> Dossier:
50
+ """Retrieve a dossier from the datastore.
51
+
52
+ Args:
53
+ id: Unique identifier for the dossier
54
+ as_odm: Whether to return as ODM object (True) or dictionary (False)
55
+ version: Whether to include version information in the response
56
+
57
+ Returns:
58
+ Dossier object or dictionary containing dossier data
59
+
60
+ Raises:
61
+ NotFoundException: If the dossier doesn't exist
62
+ """
63
+ return datastore().dossier.get_if_exists(key=id, as_obj=as_odm, version=version)
64
+
65
+
66
+ def create_dossier(dossier_data: Optional[Any], username: str) -> Dossier: # noqa: C901
67
+ """Create a new dossier in the datastore.
68
+
69
+ This function validates the input data, ensures the query is valid by testing it
70
+ against the hit collection, and creates a new dossier with the specified parameters.
71
+
72
+ Args:
73
+ dossier_data: Dictionary containing dossier configuration data
74
+ username: Username of the user creating the dossier
75
+
76
+ Returns:
77
+ Newly created Dossier object
78
+
79
+ Raises:
80
+ InvalidDataException: If data format is invalid, required fields are missing,
81
+ or the query is invalid
82
+ HowlerException: If there's an error during dossier creation
83
+ """
84
+ # Validate input data format
85
+ if not isinstance(dossier_data, dict):
86
+ raise InvalidDataException("Invalid data format")
87
+
88
+ # Validate required fields for dossier creation
89
+ if "title" not in dossier_data:
90
+ raise InvalidDataException("You must specify a title when creating a dossier.")
91
+
92
+ if "query" not in dossier_data:
93
+ raise InvalidDataException("You must specify a query when creating a dossier.")
94
+
95
+ if "type" not in dossier_data:
96
+ raise InvalidDataException("You must specify a type when creating a dossier.")
97
+
98
+ storage = datastore()
99
+
100
+ try:
101
+ # Validate the Lucene query by attempting to search with it
102
+ # This ensures the query syntax is correct before saving the dossier
103
+ if query := dossier_data.get("query", None):
104
+ storage.hit.search(query)
105
+
106
+ if "owner" not in dossier_data:
107
+ dossier_data["owner"] = username
108
+
109
+ dossier = Dossier(dossier_data)
110
+
111
+ # Validate pivot configurations to ensure no duplicate mapping keys
112
+ for pivot in dossier.pivots:
113
+ if len(pivot.mappings) != len(set(mapping.key for mapping in pivot.mappings)):
114
+ raise InvalidDataException("One of your pivots has duplicate keys set.")
115
+
116
+ # Ensure the owner is set to the current user (security measure)
117
+ dossier.owner = username
118
+
119
+ # Save the dossier to the datastore
120
+ storage.dossier.save(dossier.dossier_id, dossier)
121
+
122
+ # Commit the transaction to persist changes
123
+ storage.dossier.commit()
124
+
125
+ return dossier
126
+ except SearchException:
127
+ # Handle invalid Lucene query syntax
128
+ raise InvalidDataException("You must use a valid query when creating a dossier.")
129
+ except HowlerException as e:
130
+ # Handle other application-specific errors
131
+ raise InvalidDataException(str(e))
132
+
133
+
134
+ def update_dossier(dossier_id: str, dossier_data: dict[str, Any], user: User) -> Dossier: # noqa: C901
135
+ """Update one or more properties of a dossier in the database.
136
+
137
+ This function enforces access control rules and validates data before updating.
138
+ Personal dossiers can only be updated by their owners or admins.
139
+ Global dossiers can only be updated by their owners or admins.
140
+
141
+ Args:
142
+ dossier_id: Unique identifier of the dossier to update
143
+ dossier_data: Dictionary containing fields to update
144
+ user: User object representing the requesting user
145
+
146
+ Returns:
147
+ Updated Dossier object
148
+
149
+ Raises:
150
+ NotFoundException: If the dossier doesn't exist
151
+ InvalidDataException: If invalid fields are provided or data is malformed
152
+ ForbiddenException: If user lacks permission to update the dossier
153
+ """
154
+ # Verify the dossier exists before attempting to update
155
+ if not exists(dossier_id):
156
+ raise NotFoundException(f"Dossier with id '{dossier_id}' does not exist.")
157
+
158
+ # Validate that only permitted fields are being updated
159
+ # This prevents unauthorized modification of sensitive fields
160
+ if set(dossier_data.keys()) - PERMITTED_KEYS:
161
+ raise InvalidDataException(f"Only {', '.join(PERMITTED_KEYS)} can be updated.")
162
+
163
+ storage = datastore()
164
+
165
+ # Retrieve the existing dossier for access control checks
166
+ existing_dossier: Dossier = get_dossier(dossier_id, as_odm=True)
167
+
168
+ # Enforce access control for personal dossiers
169
+ # Only the owner or admin users can modify personal dossiers
170
+ if existing_dossier.type == "personal" and existing_dossier.owner != user.uname and "admin" not in user.type:
171
+ raise ForbiddenException("You cannot update a personal dossier that is not owned by you.")
172
+
173
+ # Enforce access control for global dossiers
174
+ # Only the owner or admin users can modify global dossiers
175
+ if existing_dossier.type == "global" and existing_dossier.owner != user.uname and "admin" not in user.type:
176
+ raise ForbiddenException("Only the owner of a dossier and administrators can edit a global dossier.")
177
+
178
+ # Validate pivot configurations if they're being updated
179
+ # Ensure no duplicate mapping keys exist within any pivot
180
+ if "pivots" in dossier_data:
181
+ for pivot in dossier_data["pivots"]:
182
+ if len(pivot["mappings"]) != len(set(mapping["key"] for mapping in pivot["mappings"])):
183
+ raise InvalidDataException("One of your pivots has duplicate keys set.")
184
+
185
+ try:
186
+ # Validate the Lucene query if it's being updated
187
+ if "query" in dossier_data:
188
+ # Test the query against the hit index to ensure it's valid
189
+ storage.hit.search(dossier_data["query"])
190
+
191
+ # Merge the new data with existing dossier data
192
+ new_data = Dossier(merge({}, existing_dossier.as_primitives(), dossier_data))
193
+
194
+ storage.dossier.save(dossier_id, new_data)
195
+
196
+ # Commit the transaction to persist changes
197
+ storage.dossier.commit()
198
+
199
+ return new_data
200
+ except SearchException:
201
+ # Handle invalid Lucene query syntax
202
+ raise InvalidDataException("You must use a valid query when updating a dossier.")
203
+ except (HowlerException, TypeError) as e:
204
+ # Log the error for debugging purposes
205
+ logger.exception("Error when updating dossier.")
206
+ # Provide a user-friendly error message while preserving the original exception
207
+ raise InvalidDataException("We were unable to update the dossier.", cause=e) from e
208
+
209
+
210
+ def get_matching_dossiers(hit: dict[str, Any], dossiers: Optional[list[dict[str, Any]]] = None):
211
+ """Get a list of dossiers that match a specific security alert/hit.
212
+
213
+ This function evaluates each dossier's query against the provided hit data
214
+ to determine which dossiers are relevant to the security event.
215
+
216
+ Args:
217
+ hit: Dictionary containing security alert/hit data to match against
218
+ dossiers: Optional list of dossiers to check. If None, all dossiers
219
+ will be retrieved from the datastore
220
+
221
+ Returns:
222
+ List of dossier dictionaries that match the provided hit
223
+
224
+ Note:
225
+ This function uses Lucene query matching to determine relevance.
226
+ Dossiers with no query are assumed to match all hits.
227
+ """
228
+ # Retrieve all dossiers if none provided
229
+ if dossiers is None:
230
+ dossiers: list[dict[str, Any]] = datastore().dossier.search(
231
+ "dossier_id:*",
232
+ as_obj=False,
233
+ # TODO: Eventually implement caching here
234
+ rows=1000,
235
+ )["items"]
236
+
237
+ matching_dossiers: list[dict[str, Any]] = []
238
+
239
+ # Evaluate each dossier against the hit data
240
+ for dossier in cast(list[dict[str, Any]], dossiers):
241
+ # Dossiers without queries match all hits by default
242
+ # This allows for catch-all dossiers that collect all security events
243
+ if "query" not in dossier or dossier["query"] is None:
244
+ matching_dossiers.append(dossier)
245
+ continue
246
+
247
+ # Use Lucene service to check if the hit matches the dossier's query
248
+ # This determines if the security event is relevant to this investigation
249
+ if lucene_service.match(dossier["query"], hit):
250
+ matching_dossiers.append(dossier)
251
+
252
+ return matching_dossiers