howler-api 4.1.0.dev1616__tar.gz → 4.1.0.dev1624__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 (238) hide show
  1. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/PKG-INFO +1 -1
  2. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/dossier.py +20 -0
  3. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/base.py +14 -4
  4. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/helper.py +11 -11
  5. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/pivot.py +33 -3
  6. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/random_data.py +51 -0
  7. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/dossier_service.py +63 -14
  8. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/pyproject.toml +93 -1
  9. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/README.md +0 -0
  10. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/__init__.py +0 -0
  11. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/__init__.py +0 -0
  12. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/add_label.py +0 -0
  13. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/add_to_bundle.py +0 -0
  14. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/add_to_case.py +0 -0
  15. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/change_field.py +0 -0
  16. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/demote.py +0 -0
  17. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/example_plugin.py +0 -0
  18. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/prioritization.py +0 -0
  19. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/promote.py +0 -0
  20. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/remove_from_bundle.py +0 -0
  21. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/remove_label.py +0 -0
  22. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/actions/transition.py +0 -0
  23. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/__init__.py +0 -0
  24. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/base.py +0 -0
  25. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/socket.py +0 -0
  26. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/__init__.py +0 -0
  27. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/action.py +0 -0
  28. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/analytic.py +0 -0
  29. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/auth.py +0 -0
  30. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/clue.py +0 -0
  31. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/configs.py +0 -0
  32. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/help.py +0 -0
  33. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/hit.py +0 -0
  34. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/notebook.py +0 -0
  35. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/overview.py +0 -0
  36. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/search.py +0 -0
  37. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/template.py +0 -0
  38. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/tool.py +0 -0
  39. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/user.py +0 -0
  40. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/__init__.py +0 -0
  41. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/etag.py +0 -0
  42. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/params.py +0 -0
  43. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/permission_helper.py +0 -0
  44. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v1/view.py +0 -0
  45. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v2/__init__.py +0 -0
  46. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v2/case.py +0 -0
  47. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v2/fuzzy.py +0 -0
  48. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v2/ingest.py +0 -0
  49. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/api/v2/search.py +0 -0
  50. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/app.py +0 -0
  51. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/README.md +0 -0
  52. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/__init__.py +0 -0
  53. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/classification.py +0 -0
  54. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/classification.yml +0 -0
  55. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/exceptions.py +0 -0
  56. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/loader.py +0 -0
  57. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/logging/__init__.py +0 -0
  58. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/logging/audit.py +0 -0
  59. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/logging/format.py +0 -0
  60. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/net.py +0 -0
  61. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/net_static.py +0 -0
  62. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/random_user.py +0 -0
  63. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/common/swagger.py +0 -0
  64. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/config.py +0 -0
  65. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/cronjobs/__init__.py +0 -0
  66. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/cronjobs/action_queue_worker.py +0 -0
  67. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/cronjobs/correlation.py +0 -0
  68. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/cronjobs/retention.py +0 -0
  69. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/cronjobs/view_cleanup.py +0 -0
  70. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/README.md +0 -0
  71. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/__init__.py +0 -0
  72. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/bulk.py +0 -0
  73. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/collection.py +0 -0
  74. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/constants.py +0 -0
  75. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/exceptions.py +0 -0
  76. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/howler_store.py +0 -0
  77. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/__init__.py +0 -0
  78. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/action_owner.py +0 -0
  79. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/base.py +0 -0
  80. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/fix_process.py +0 -0
  81. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/runner.py +0 -0
  82. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/operations.py +0 -0
  83. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/schemas.py +0 -0
  84. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/store.py +0 -0
  85. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/support/__init__.py +0 -0
  86. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/support/build.py +0 -0
  87. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/support/schemas.py +0 -0
  88. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/types.py +0 -0
  89. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/datastore/utils.py +0 -0
  90. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/error.py +0 -0
  91. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/README.md +0 -0
  92. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/__init__.py +0 -0
  93. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/check_duplicates.py +0 -0
  94. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/generate_mitre.py +0 -0
  95. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/generate_sigma_rules.py +0 -0
  96. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/generate_tlds.py +0 -0
  97. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/reindex_data.py +0 -0
  98. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/run_migrations.py +0 -0
  99. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/external/wipe_databases.py +0 -0
  100. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/gunicorn_config.py +0 -0
  101. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/healthz.py +0 -0
  102. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/__init__.py +0 -0
  103. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/azure.py +0 -0
  104. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/discover.py +0 -0
  105. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/hit.py +0 -0
  106. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/oauth.py +0 -0
  107. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/search.py +0 -0
  108. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/workflow.py +0 -0
  109. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/helper/ws.py +0 -0
  110. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/README.md +0 -0
  111. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/__init__.py +0 -0
  112. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/charter.txt +0 -0
  113. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/constants.py +0 -0
  114. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/howler_enum.py +0 -0
  115. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/mixins.py +0 -0
  116. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/__init__.py +0 -0
  117. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/action.py +0 -0
  118. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/analytic.py +0 -0
  119. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/assemblyline.py +0 -0
  120. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/aws.py +0 -0
  121. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/azure.py +0 -0
  122. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/case.py +0 -0
  123. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/cbs.py +0 -0
  124. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/clue.py +0 -0
  125. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/config.py +0 -0
  126. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/dossier.py +0 -0
  127. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/__init__.py +0 -0
  128. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/agent.py +0 -0
  129. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/autonomous_system.py +0 -0
  130. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/client.py +0 -0
  131. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/cloud.py +0 -0
  132. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/code_signature.py +0 -0
  133. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/container.py +0 -0
  134. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/dns.py +0 -0
  135. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/egress.py +0 -0
  136. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/elf.py +0 -0
  137. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/email.py +0 -0
  138. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/error.py +0 -0
  139. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/event.py +0 -0
  140. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/faas.py +0 -0
  141. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/file.py +0 -0
  142. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/geo.py +0 -0
  143. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/group.py +0 -0
  144. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/hash.py +0 -0
  145. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/host.py +0 -0
  146. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/http.py +0 -0
  147. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/ingress.py +0 -0
  148. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/interface.py +0 -0
  149. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/network.py +0 -0
  150. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/observer.py +0 -0
  151. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/organization.py +0 -0
  152. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/os.py +0 -0
  153. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/pe.py +0 -0
  154. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/process.py +0 -0
  155. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/registry.py +0 -0
  156. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/related.py +0 -0
  157. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/rule.py +0 -0
  158. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/server.py +0 -0
  159. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/threat.py +0 -0
  160. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/tls.py +0 -0
  161. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/url.py +0 -0
  162. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/user.py +0 -0
  163. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/user_agent.py +0 -0
  164. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/vulnerability.py +0 -0
  165. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/event.py +0 -0
  166. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/gcp.py +0 -0
  167. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/hit.py +0 -0
  168. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/howler_data.py +0 -0
  169. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/lead.py +0 -0
  170. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/localized_label.py +0 -0
  171. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/migration.py +0 -0
  172. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/overview.py +0 -0
  173. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/ownership.py +0 -0
  174. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/permission_request.py +0 -0
  175. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/record.py +0 -0
  176. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/template.py +0 -0
  177. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/user.py +0 -0
  178. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/models/view.py +0 -0
  179. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/odm/randomizer.py +0 -0
  180. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/patched.py +0 -0
  181. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/plugins/__init__.py +0 -0
  182. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/plugins/config.py +0 -0
  183. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/__init__.py +0 -0
  184. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/README.md +0 -0
  185. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/__init__.py +0 -0
  186. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/counters.py +0 -0
  187. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/events.py +0 -0
  188. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/hash.py +0 -0
  189. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/lock.py +0 -0
  190. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/__init__.py +0 -0
  191. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/comms.py +0 -0
  192. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/multi.py +0 -0
  193. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/named.py +0 -0
  194. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/priority.py +0 -0
  195. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/set.py +0 -0
  196. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  197. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/security/__init__.py +0 -0
  198. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/security/login.py +0 -0
  199. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/security/socket.py +0 -0
  200. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/security/utils.py +0 -0
  201. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/__init__.py +0 -0
  202. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/action_service.py +0 -0
  203. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/analytic_service.py +0 -0
  204. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/auth_service.py +0 -0
  205. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/bundle_compat_service.py +0 -0
  206. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/case_service.py +0 -0
  207. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/comms_service.py +0 -0
  208. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/config_service.py +0 -0
  209. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/correlation_service.py +0 -0
  210. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/docs_service.py +0 -0
  211. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/event_service.py +0 -0
  212. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/fuzzy_service.py +0 -0
  213. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/hit_service.py +0 -0
  214. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/jwt_service.py +0 -0
  215. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/lucene_service.py +0 -0
  216. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/notebook_service.py +0 -0
  217. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/overview_service.py +0 -0
  218. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/permission_service.py +0 -0
  219. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/search_service.py +0 -0
  220. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/template_service.py +0 -0
  221. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/user_service.py +0 -0
  222. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/services/viewer_service.py +0 -0
  223. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/telemetry.py +0 -0
  224. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/__init__.py +0 -0
  225. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/annotations.py +0 -0
  226. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/chunk.py +0 -0
  227. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/compat.py +0 -0
  228. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/constants.py +0 -0
  229. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/dict_utils.py +0 -0
  230. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/indexes.py +0 -0
  231. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/isotime.py +0 -0
  232. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/list_utils.py +0 -0
  233. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/lucene.py +0 -0
  234. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/net_utils.py +0 -0
  235. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/path.py +0 -0
  236. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/socket_utils.py +0 -0
  237. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/str_utils.py +0 -0
  238. {howler_api-4.1.0.dev1616 → howler_api-4.1.0.dev1624}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 4.1.0.dev1616
3
+ Version: 4.1.0.dev1624
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -93,6 +93,26 @@ def create_dossier(**kwargs):
93
93
  return internal_error(err="An unknown error occured when creating the dossier.")
94
94
 
95
95
 
96
+ @generate_swagger_docs()
97
+ @dossier_api.route("/groups", methods=["GET"])
98
+ @api_login(required_priv=["R"])
99
+ def get_pivot_groups(user: User, **kwargs):
100
+ """Suggest existing pivot group paths matching a prefix
101
+
102
+ Variables:
103
+ None
104
+
105
+ Optional Arguments:
106
+ prefix => A case-insensitive prefix to filter suggestions by
107
+
108
+ Result Example:
109
+ [
110
+ "network/dns" # Up to 25 matching group paths, used to autocomplete the pivot group field
111
+ ]
112
+ """
113
+ return ok(dossier_service.get_pivot_groups(request.args.get("prefix", "", type=str), username=user.uname))
114
+
115
+
96
116
  @generate_swagger_docs()
97
117
  @dossier_api.route("/<id>", methods=["GET"])
98
118
  @api_login(required_priv=["R"])
@@ -32,6 +32,7 @@ from howler.common import loader
32
32
  from howler.common.exceptions import HowlerKeyError, HowlerNotImplementedError, HowlerTypeError, HowlerValueError
33
33
  from howler.common.net import is_valid_domain, is_valid_ip
34
34
  from howler.odm.howler_enum import HowlerEnum
35
+ from howler.utils.compat import Self
35
36
  from howler.utils.compat import StrEnum as PyStrEnum
36
37
  from howler.utils.dict_utils import flatten, recursive_update
37
38
  from howler.utils.isotime import now_as_iso
@@ -284,12 +285,21 @@ class Keyword(_Field):
284
285
  Examples: file hashes, service names, document ids
285
286
  """
286
287
 
287
- def check(self, value, context=[], **kwargs):
288
+ def __init__(self, *args, coerce=True, **kwargs):
289
+ super().__init__(*args, **kwargs)
290
+ self.coerce = coerce
291
+
292
+ def check(self: Self, value: Any | None, context=[], **kwargs):
288
293
  # We have a special case for bytes here due to how often strings and bytes
289
294
  # get mixed up in python apis
290
295
  if self.optional and value is None:
291
296
  return None
292
297
 
298
+ if not (self.coerce or isinstance(value, str)):
299
+ raise HowlerValueError(
300
+ f"[{'.'.join(context) or self.name}] Provided value is not a string, and coerce is set to false"
301
+ )
302
+
293
303
  if isinstance(value, bytes):
294
304
  raise HowlerValueError(f"[{'.'.join(context) or self.name}] Keyword doesn't accept bytes values")
295
305
 
@@ -422,7 +432,7 @@ class IP(Keyword):
422
432
  super().__init__(*args, **kwargs)
423
433
  self.validation_regex = re.compile(IP_ONLY_REGEX)
424
434
 
425
- def check(self, value, context=[], **kwargs):
435
+ def check(self: Self, value: str, context: list[str] = [], **kwargs):
426
436
  if not value:
427
437
  return None
428
438
 
@@ -465,7 +475,7 @@ class Email(Keyword):
465
475
  super().__init__(*args, **kwargs)
466
476
  self.validation_regex = re.compile(EMAIL_REGEX)
467
477
 
468
- def check(self, value, context=[], **kwargs):
478
+ def check(self: Self, value: str, context: list[str] = [], **kwargs):
469
479
  if not value:
470
480
  return None
471
481
 
@@ -489,7 +499,7 @@ class URI(Keyword):
489
499
  super().__init__(*args, **kwargs)
490
500
  self.validation_regex = re.compile(FULL_URI)
491
501
 
492
- def check(self, value, context=[], **kwargs):
502
+ def check(self: Self, value: str, context: list[str] = [], **kwargs):
493
503
  if not value:
494
504
  return None
495
505
 
@@ -1,9 +1,8 @@
1
1
  import json
2
- import random
3
2
  from datetime import datetime, timedelta
4
3
  from hashlib import md5
5
4
  from math import ceil
6
- from random import choice, randint, sample
5
+ from random import choice, choices, gauss, randint, random, sample
7
6
  from typing import Any, cast
8
7
 
9
8
  from howler.common.logging import get_logger
@@ -55,13 +54,13 @@ def generate_useful_hit( # noqa: C901
55
54
  if CLASSIFICATION.enforce:
56
55
  hit.classification = CLASSIFICATION.UNRESTRICTED
57
56
 
58
- rand_seed = random.random()
57
+ rand_seed = random()
59
58
 
60
59
  timestamp = datetime.now() - timedelta(
61
60
  days=round(rand_seed * 30),
62
- hours=min(max(round(random.gauss(14, 3)), 0), 23),
63
- minutes=random.randint(0, 59),
64
- seconds=random.randint(0, 59),
61
+ hours=min(max(round(gauss(14, 3)), 0), 23),
62
+ minutes=randint(0, 59),
63
+ seconds=randint(0, 59),
65
64
  )
66
65
 
67
66
  hit.event.created = timestamp.isoformat() + "Z"
@@ -251,7 +250,7 @@ def generate_useful_hit( # noqa: C901
251
250
  "icon": app["name"],
252
251
  }
253
252
  )
254
- for app in random.choices(APPS, k=5)
253
+ for app in choices(APPS, k=5)
255
254
  )
256
255
  except IndexError:
257
256
  pass
@@ -362,13 +361,13 @@ def generate_useful_event( # noqa: C901
362
361
  "Create a random, useful/cogent event for synthetic data"
363
362
  event: Event = random_model_obj(cast(Model, Event))
364
363
 
365
- rand_seed = random.random()
364
+ rand_seed = random()
366
365
 
367
366
  timestamp = datetime.now() - timedelta(
368
367
  days=round(rand_seed * 30),
369
- hours=min(max(round(random.gauss(14, 3)), 0), 23),
370
- minutes=random.randint(0, 59),
371
- seconds=random.randint(0, 59),
368
+ hours=min(max(round(gauss(14, 3)), 0), 23),
369
+ minutes=randint(0, 59),
370
+ seconds=randint(0, 59),
372
371
  )
373
372
 
374
373
  event.event.created = timestamp.isoformat() + "Z"
@@ -518,6 +517,7 @@ def generate_useful_dossier(users: list[User]) -> Dossier:
518
517
  "label": {"en": get_random_word(), "fr": get_random_word()},
519
518
  "value": f"https://google.com/search?q={{{{test{i}}}}} and {{{{custom_value}}}}",
520
519
  "format": "link",
520
+ "group": choice([None, "parent/child", "test/group"]),
521
521
  "mappings": [
522
522
  {"key": f"test{i}", "field": choice(list(Hit.flat_fields().keys()))},
523
523
  {"key": "custom_value", "field": "custom", "custom_value": get_random_word()},
@@ -1,7 +1,8 @@
1
1
  # mypy: ignore-errors
2
- from typing import Optional
2
+ from typing import Any, Optional
3
3
 
4
4
  from howler import odm
5
+ from howler.common.exceptions import HowlerValueError
5
6
  from howler.odm.models.localized_label import LocalizedLabel
6
7
 
7
8
 
@@ -21,14 +22,35 @@ class Mapping(odm.Model):
21
22
  )
22
23
 
23
24
 
25
+ def validate_group(group: str | Any) -> None:
26
+ """Validate a slash-separated pivot group path."""
27
+ if group is None:
28
+ return
29
+
30
+ if group == "":
31
+ raise HowlerValueError("Group cannot be an empty string")
32
+
33
+ if not isinstance(group, str):
34
+ raise HowlerValueError('Data "group" should be a slash-separated string.')
35
+
36
+ if any(not section for section in group.split("/")):
37
+ raise HowlerValueError("A group must consist of a relative path with no empty segments (e.g. a/b/c)")
38
+
39
+
24
40
  @odm.model(
25
41
  index=False,
26
42
  store=True,
27
43
  description="The dossier object stores individual tabs/fields for a given alert.",
28
44
  )
29
45
  class Pivot(odm.Model):
30
- icon: Optional[str] = odm.Text(
31
- description="An optional icon to use in the tab display for this dossier.", optional=True
46
+ icon: str = odm.Text(
47
+ description="An optional icon to use in the tab display for this dossier.", default="material-symbols:link"
48
+ )
49
+ group: str | None = odm.Keyword(
50
+ description="The folder path this pivot should be presented in when viewed in the UI.",
51
+ optional=True,
52
+ index=True,
53
+ coerce=False,
32
54
  )
33
55
  label: LocalizedLabel = odm.Compound(LocalizedLabel, description="Labels for the pivot in the UI.")
34
56
  value: str = odm.Keyword(description="The link/plugin information to pivot on.")
@@ -38,3 +60,11 @@ class Pivot(odm.Model):
38
60
  default=[],
39
61
  description="A list of the mappings to use when activating a pivot.",
40
62
  )
63
+
64
+ def __init__(self, data: dict = None, *args, **kwargs):
65
+ super().__init__(data, *args, **kwargs)
66
+
67
+ validate_group(self.group)
68
+
69
+ if len(self.mappings) != len({mapping.key for mapping in self.mappings}):
70
+ raise HowlerValueError("One of your pivots has duplicate keys set.")
@@ -36,10 +36,12 @@ from howler.odm.base import Keyword
36
36
  from howler.odm.helper import generate_useful_case, generate_useful_dossier, generate_useful_event, generate_useful_hit
37
37
  from howler.odm.models.action import Action
38
38
  from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
39
+ from howler.odm.models.dossier import Dossier
39
40
  from howler.odm.models.ecs.event import EVENT_CATEGORIES
40
41
  from howler.odm.models.hit import Hit
41
42
  from howler.odm.models.howler_data import Assessment, Escalation, Scrutiny, Status
42
43
  from howler.odm.models.overview import Overview
44
+ from howler.odm.models.pivot import Pivot
43
45
  from howler.odm.models.template import Template
44
46
  from howler.odm.models.user import User
45
47
  from howler.odm.models.view import View
@@ -881,6 +883,7 @@ def wipe_actions(ds: HowlerDatastore):
881
883
  def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
882
884
  "Create random dossiers"
883
885
  users = ds.user.search("*:*")["items"]
886
+ grouped_paths = ("network", "network/dns", "network/http")
884
887
  for index in range(num_dossiers):
885
888
  dossier = generate_useful_dossier(users)
886
889
  ownership_data = _get_ownership_data(users, dossier.owner)
@@ -890,8 +893,56 @@ def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
890
893
  dossier.type = "personal"
891
894
  elif index == 1:
892
895
  dossier.type = "global"
896
+
897
+ # Keep one dossier's random pivot data intact, while ensuring the mock data
898
+ # always includes shared and nested groups for the grouped-pivot UI.
899
+ if index > 0:
900
+ grouped_path = grouped_paths[(index - 1) % len(grouped_paths)]
901
+ for pivot in dossier.pivots:
902
+ pivot.group = grouped_path
903
+
893
904
  ds.dossier.save(dossier.dossier_id, dossier)
894
905
 
906
+ password_checker_dossier = Dossier(
907
+ {
908
+ "title": "Password Checker Pivot Folders",
909
+ "query": 'howler.analytic:"Password Checker"',
910
+ "type": "global",
911
+ **_get_ownership_data(users, "admin"),
912
+ "leads": [],
913
+ }
914
+ )
915
+ pivot_groups = [
916
+ None,
917
+ "credentials",
918
+ "credentials/hash",
919
+ "credentials/metadata",
920
+ "validation",
921
+ "validation/length",
922
+ "validation/complexity",
923
+ "validation/reuse",
924
+ "exposure",
925
+ "exposure/breach",
926
+ "exposure/darkweb",
927
+ "exposure/thirdparty",
928
+ "recommendations",
929
+ "recommendations/rotation",
930
+ "recommendations/monitoring",
931
+ ]
932
+ for index, pivot_group in enumerate(pivot_groups, start=1):
933
+ password_checker_dossier.pivots.append(
934
+ Pivot(
935
+ {
936
+ "label": {"en": f"Password Check {index}", "fr": f"Verification mot de passe {index}"},
937
+ "value": "https://password-checker.example.test/{{pivot_value}}",
938
+ "format": "link",
939
+ "group": pivot_group,
940
+ "mappings": [{"key": "pivot_value", "field": "howler.id"}],
941
+ }
942
+ )
943
+ )
944
+
945
+ ds.dossier.save(password_checker_dossier.dossier_id, password_checker_dossier)
895
946
  ds.dossier.commit()
896
947
 
897
948
 
@@ -9,13 +9,20 @@ from typing import Any, Literal, Optional, cast, overload
9
9
 
10
10
  from mergedeep.mergedeep import merge
11
11
 
12
- from howler.common.exceptions import ForbiddenException, HowlerException, InvalidDataException, NotFoundException
12
+ from howler.common.exceptions import (
13
+ ForbiddenException,
14
+ HowlerException,
15
+ HowlerValueError,
16
+ InvalidDataException,
17
+ NotFoundException,
18
+ )
13
19
  from howler.common.loader import datastore
14
20
  from howler.common.logging import get_logger
15
21
  from howler.datastore.exceptions import SearchException
16
22
  from howler.odm.models.dossier import Dossier
17
23
  from howler.odm.models.user import User
18
24
  from howler.services import lucene_service
25
+ from howler.utils.str_utils import sanitize_lucene_query
19
26
 
20
27
  logger = get_logger(__file__)
21
28
 
@@ -28,6 +35,12 @@ PERMITTED_KEYS = {
28
35
  "type",
29
36
  }
30
37
 
38
+ # Cap the number of group suggestions returned, so the endpoint can't be used to dump the entire group list
39
+ MAX_GROUP_SUGGESTIONS = 25
40
+ GROUP_SUGGESTION_PAGE_SIZE = 100
41
+ # Group suggestions are a convenience feature, so bound aggregation work for broad prefixes.
42
+ MAX_GROUP_SUGGESTION_PAGES = 5
43
+
31
44
 
32
45
  def exists(dossier_id: str) -> bool:
33
46
  """Check if a dossier exists in the datastore.
@@ -140,11 +153,6 @@ def create_dossier( # noqa: C901
140
153
 
141
154
  dossier = Dossier(dossier_data)
142
155
 
143
- # Validate pivot configurations to ensure no duplicate mapping keys
144
- for pivot in dossier.pivots:
145
- if len(pivot.mappings) != len(set(mapping.key for mapping in pivot.mappings)):
146
- raise InvalidDataException("One of your pivots has duplicate keys set.")
147
-
148
156
  # Ensure the owner is set to the current user.
149
157
  dossier.owner = username
150
158
 
@@ -208,14 +216,6 @@ def update_dossier( # noqa: C901
208
216
  "You cannot update a dossier that is not owned by you, or you are not an administrator of."
209
217
  )
210
218
 
211
- # Validate pivot configurations if they're being updated
212
- # Ensure no duplicate mapping keys exist within any pivot
213
- if "pivots" in dossier_data:
214
- for pivot in dossier_data["pivots"]:
215
- mappings = pivot.get("mappings") or []
216
- if len(mappings) != len(set(mapping.get("key") for mapping in mappings)):
217
- raise InvalidDataException("One of your pivots has duplicate keys set.")
218
-
219
219
  try:
220
220
  # Validate the Lucene query if it's being updated
221
221
  if "query" in dossier_data:
@@ -231,6 +231,8 @@ def update_dossier( # noqa: C901
231
231
  except SearchException:
232
232
  # Handle invalid Lucene query syntax
233
233
  raise InvalidDataException("You must use a valid query when updating a dossier.")
234
+ except HowlerValueError as e:
235
+ raise InvalidDataException(str(e), cause=e) from e
234
236
  except (HowlerException, TypeError) as e:
235
237
  # Log the error for debugging purposes
236
238
  logger.exception("Error when updating dossier.")
@@ -238,6 +240,53 @@ def update_dossier( # noqa: C901
238
240
  raise InvalidDataException("We were unable to update the dossier.", cause=e) from e
239
241
 
240
242
 
243
+ def get_pivot_groups(prefix: str, username: str) -> list[str]:
244
+ """Suggest existing pivot group paths matching a prefix, so users can reuse groups already in use.
245
+
246
+ Args:
247
+ prefix: Case-insensitive prefix to filter suggestions by.
248
+ username: The requesting user, used to scope which dossiers are visible.
249
+
250
+ Returns:
251
+ Up to MAX_GROUP_SUGGESTIONS unique matching group paths from the visible dossier search, sorted alphabetically.
252
+ Suggestions are best-effort and scan up to MAX_GROUP_SUGGESTION_PAGES matching aggregation pages.
253
+ """
254
+ groups: set[str] = set()
255
+ lowered_prefix: str = (prefix or "").lower()
256
+ after: dict[str, str] | None = None
257
+
258
+ for _ in range(MAX_GROUP_SUGGESTION_PAGES):
259
+ composite: dict[str, Any] = {
260
+ "size": GROUP_SUGGESTION_PAGE_SIZE,
261
+ "sources": [{"group": {"terms": {"field": "pivots.group"}}}],
262
+ }
263
+ if after:
264
+ composite["after"] = after
265
+
266
+ group_filter: dict[str, Any] = {"match_all": {}}
267
+ if prefix:
268
+ group_filter = {"prefix": {"pivots.group": {"value": prefix, "case_insensitive": True}}}
269
+
270
+ result = datastore().dossier.search(
271
+ f'(type:global OR owner:("{sanitize_lucene_query(username)}" OR none))',
272
+ rows=0,
273
+ aggregations=[("pivot_groups", {"filter": group_filter, "aggs": {"groups": {"composite": composite}}})],
274
+ )
275
+ aggregation = result["aggregations"]["pivot_groups"]["groups"]
276
+
277
+ for bucket in aggregation["buckets"]:
278
+ group = bucket["key"]["group"]
279
+ if not group.lower().startswith(lowered_prefix):
280
+ continue
281
+ groups.add(group)
282
+
283
+ after = aggregation.get("after_key")
284
+ if not after:
285
+ break
286
+
287
+ return sorted(groups)[:MAX_GROUP_SUGGESTIONS]
288
+
289
+
241
290
  def get_matching_dossiers(
242
291
  hit: dict[str, Any], dossiers: list[dict[str, Any]] | None = None, username: str | None = None
243
292
  ):
@@ -76,9 +76,101 @@ module = "requests"
76
76
  ignore_missing_imports = true
77
77
 
78
78
  [tool.pyright]
79
+ exclude = [
80
+ "**/build",
81
+ "**/.venv",
82
+ "**/node_modules",
83
+ "**/__pycache__",
84
+ "**/.*",
85
+ ]
86
+ ignore = [
87
+ "**/build",
88
+ "**/.venv",
89
+ ]
90
+ reportMissingImports = "error"
91
+ reportOptionalMemberAccess = "error"
92
+ strictDictionaryInference = true
93
+ strictListInference = true
79
94
  venvPath = "."
80
95
  venv = ".venv"
81
96
 
97
+ [[tool.pyright.executionEnvironments]]
98
+ extraPaths = ["."]
99
+ reportArgumentType = "none"
100
+ reportAssignmentType = "none"
101
+ root = "howler/odm/models"
102
+
103
+ [[tool.pyright.executionEnvironments]]
104
+ extraPaths = ["."]
105
+ reportArgumentType = "information"
106
+ reportAttributeAccessIssue = "information"
107
+ reportOptionalCall = "information"
108
+ root = "howler/datastore"
109
+
110
+ [[tool.pyright.executionEnvironments]]
111
+ extraPaths = ["."]
112
+ reportOptionalMemberAccess = "information"
113
+ reportPossiblyUnboundVariable = "information"
114
+ root = "howler/odm/helper.py"
115
+
116
+ [[tool.pyright.executionEnvironments]]
117
+ extraPaths = ["."]
118
+ reportPossiblyUnboundVariable = "information"
119
+ root = "howler/odm/random_data.py"
120
+
121
+ [[tool.pyright.executionEnvironments]]
122
+ extraPaths = ["."]
123
+ reportArgumentType = "information"
124
+ reportAttributeAccessIssue = "information"
125
+ reportIncompatibleMethodOverride = "information"
126
+ reportOperatorIssue = "information"
127
+ reportOptionalMemberAccess = "information"
128
+ reportPossiblyUnboundVariable = "information"
129
+ root = "howler/odm/base.py"
130
+
131
+ [[tool.pyright.executionEnvironments]]
132
+ extraPaths = ["."]
133
+ reportAttributeAccessIssue = "information"
134
+ root = "howler/security/login.py"
135
+
136
+ [[tool.pyright.executionEnvironments]]
137
+ extraPaths = ["."]
138
+ reportArgumentType = "information"
139
+ reportOperatorIssue = "information"
140
+ reportOptionalMemberAccess = "information"
141
+ reportPossiblyUnboundVariable = "information"
142
+ root = "howler/helper/ws.py"
143
+
144
+ [[tool.pyright.executionEnvironments]]
145
+ extraPaths = ["."]
146
+ reportArgumentType = "information"
147
+ reportIncompatibleMethodOverride = "information"
148
+ reportReturnType = "information"
149
+ root = "howler/remote/datatypes"
150
+
151
+ [[tool.pyright.executionEnvironments]]
152
+ extraPaths = ["."]
153
+ reportArgumentType = "none"
154
+ root = "howler/common/classification.py"
155
+
156
+ [[tool.pyright.executionEnvironments]]
157
+ extraPaths = ["."]
158
+ reportArgumentType = "none"
159
+ reportAttributeAccessIssue = "none"
160
+ reportCallIssue = "none"
161
+ reportGeneralTypeIssues = "none"
162
+ reportInvalidTypeForm = "none"
163
+ reportOperatorIssue = "none"
164
+ reportOptionalIterable = "none"
165
+ reportOptionalMemberAccess = "none"
166
+ reportOptionalOperand = "none"
167
+ reportOptionalSubscript = "none"
168
+ reportPossiblyUnboundVariable = "none"
169
+ reportReturnType = "none"
170
+ reportTypedDictNotRequiredAccess = "none"
171
+ reportUnusedExpression = "none"
172
+ root = "test"
173
+
82
174
  [tool.ruff]
83
175
  line-length = 120
84
176
  indent-width = 4
@@ -152,7 +244,7 @@ suppress-none-returning = true
152
244
  [tool.poetry]
153
245
  package-mode = true
154
246
  name = "howler-api"
155
- version = "4.1.0.dev1616"
247
+ version = "4.1.0.dev1624"
156
248
  description = "Howler - API server"
157
249
  authors = [
158
250
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",