howler-api 4.1.0.dev1468__tar.gz → 4.1.0.dev1502__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 (228) hide show
  1. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/PKG-INFO +1 -1
  2. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/base.py +1 -1
  3. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/socket.py +1 -1
  4. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/__init__.py +1 -1
  5. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/action.py +1 -1
  6. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/analytic.py +1 -1
  7. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/auth.py +1 -1
  8. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/clue.py +1 -1
  9. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/dossier.py +1 -1
  10. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/help.py +1 -1
  11. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/hit.py +1 -1
  12. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/notebook.py +1 -1
  13. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/overview.py +1 -1
  14. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/search.py +1 -1
  15. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/template.py +1 -1
  16. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/tool.py +1 -1
  17. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/user.py +1 -1
  18. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/view.py +1 -1
  19. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/__init__.py +1 -1
  20. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/case.py +1 -1
  21. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/fuzzy.py +1 -1
  22. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/ingest.py +1 -1
  23. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/search.py +1 -1
  24. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/action_queue_worker.py +5 -3
  25. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/config.py +13 -1
  26. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/security/utils.py +10 -0
  27. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/action_service.py +59 -9
  28. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/auth_service.py +44 -1
  29. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/correlation_service.py +1 -1
  30. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/hit_service.py +5 -2
  31. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/jwt_service.py +21 -0
  32. howler_api-4.1.0.dev1502/howler/utils/__init__.py +0 -0
  33. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/pyproject.toml +3 -3
  34. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/README.md +0 -0
  35. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/__init__.py +0 -0
  36. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/__init__.py +0 -0
  37. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/add_label.py +0 -0
  38. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/add_to_bundle.py +0 -0
  39. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/add_to_case.py +0 -0
  40. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/change_field.py +0 -0
  41. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/demote.py +0 -0
  42. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/example_plugin.py +0 -0
  43. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/prioritization.py +0 -0
  44. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/promote.py +0 -0
  45. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/remove_from_bundle.py +0 -0
  46. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/remove_label.py +0 -0
  47. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/transition.py +0 -0
  48. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/__init__.py +0 -0
  49. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/configs.py +0 -0
  50. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/utils/__init__.py +0 -0
  51. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/utils/etag.py +0 -0
  52. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/utils/params.py +0 -0
  53. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/app.py +0 -0
  54. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/README.md +0 -0
  55. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/__init__.py +0 -0
  56. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/classification.py +0 -0
  57. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/classification.yml +0 -0
  58. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/exceptions.py +0 -0
  59. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/loader.py +0 -0
  60. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/logging/__init__.py +0 -0
  61. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/logging/audit.py +0 -0
  62. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/logging/format.py +0 -0
  63. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/net.py +0 -0
  64. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/net_static.py +0 -0
  65. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/random_user.py +0 -0
  66. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/swagger.py +0 -0
  67. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/config.py +0 -0
  68. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/__init__.py +0 -0
  69. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/correlation.py +0 -0
  70. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/retention.py +0 -0
  71. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/view_cleanup.py +0 -0
  72. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/README.md +0 -0
  73. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/__init__.py +0 -0
  74. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/bulk.py +0 -0
  75. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/collection.py +0 -0
  76. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/constants.py +0 -0
  77. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/exceptions.py +0 -0
  78. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/howler_store.py +0 -0
  79. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/migrations/fix_process.py +0 -0
  80. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/operations.py +0 -0
  81. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/schemas.py +0 -0
  82. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/store.py +0 -0
  83. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/support/__init__.py +0 -0
  84. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/support/build.py +0 -0
  85. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/support/schemas.py +0 -0
  86. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/types.py +0 -0
  87. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/utils.py +0 -0
  88. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/error.py +0 -0
  89. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/README.md +0 -0
  90. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/__init__.py +0 -0
  91. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/check_duplicates.py +0 -0
  92. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/generate_mitre.py +0 -0
  93. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/generate_sigma_rules.py +0 -0
  94. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/generate_tlds.py +0 -0
  95. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/reindex_data.py +0 -0
  96. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/wipe_databases.py +0 -0
  97. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/gunicorn_config.py +0 -0
  98. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/healthz.py +0 -0
  99. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/__init__.py +0 -0
  100. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/azure.py +0 -0
  101. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/discover.py +0 -0
  102. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/hit.py +0 -0
  103. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/oauth.py +0 -0
  104. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/search.py +0 -0
  105. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/workflow.py +0 -0
  106. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/ws.py +0 -0
  107. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/README.md +0 -0
  108. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/__init__.py +0 -0
  109. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/base.py +0 -0
  110. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/charter.txt +0 -0
  111. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/constants.py +0 -0
  112. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/helper.py +0 -0
  113. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/howler_enum.py +0 -0
  114. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/mixins.py +0 -0
  115. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/__init__.py +0 -0
  116. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/action.py +0 -0
  117. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/analytic.py +0 -0
  118. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/assemblyline.py +0 -0
  119. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/aws.py +0 -0
  120. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/azure.py +0 -0
  121. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/case.py +0 -0
  122. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/cbs.py +0 -0
  123. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/clue.py +0 -0
  124. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/dossier.py +0 -0
  125. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/__init__.py +0 -0
  126. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/agent.py +0 -0
  127. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/autonomous_system.py +0 -0
  128. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/client.py +0 -0
  129. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/cloud.py +0 -0
  130. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/code_signature.py +0 -0
  131. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/container.py +0 -0
  132. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/dns.py +0 -0
  133. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/egress.py +0 -0
  134. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/elf.py +0 -0
  135. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/email.py +0 -0
  136. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/error.py +0 -0
  137. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/event.py +0 -0
  138. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/faas.py +0 -0
  139. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/file.py +0 -0
  140. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/geo.py +0 -0
  141. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/group.py +0 -0
  142. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/hash.py +0 -0
  143. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/host.py +0 -0
  144. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/http.py +0 -0
  145. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/ingress.py +0 -0
  146. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/interface.py +0 -0
  147. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/network.py +0 -0
  148. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/observer.py +0 -0
  149. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/organization.py +0 -0
  150. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/os.py +0 -0
  151. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/pe.py +0 -0
  152. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/process.py +0 -0
  153. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/registry.py +0 -0
  154. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/related.py +0 -0
  155. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/rule.py +0 -0
  156. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/server.py +0 -0
  157. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/threat.py +0 -0
  158. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/tls.py +0 -0
  159. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/url.py +0 -0
  160. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/user.py +0 -0
  161. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/user_agent.py +0 -0
  162. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/vulnerability.py +0 -0
  163. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/event.py +0 -0
  164. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/gcp.py +0 -0
  165. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/hit.py +0 -0
  166. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/howler_data.py +0 -0
  167. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/lead.py +0 -0
  168. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/localized_label.py +0 -0
  169. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/overview.py +0 -0
  170. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/pivot.py +0 -0
  171. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/record.py +0 -0
  172. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/template.py +0 -0
  173. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/user.py +0 -0
  174. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/view.py +0 -0
  175. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/random_data.py +0 -0
  176. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/randomizer.py +0 -0
  177. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/patched.py +0 -0
  178. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/plugins/__init__.py +0 -0
  179. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/plugins/config.py +0 -0
  180. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/__init__.py +0 -0
  181. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/README.md +0 -0
  182. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/__init__.py +0 -0
  183. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/counters.py +0 -0
  184. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/events.py +0 -0
  185. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/hash.py +0 -0
  186. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/lock.py +0 -0
  187. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/__init__.py +0 -0
  188. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/comms.py +0 -0
  189. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/multi.py +0 -0
  190. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/named.py +0 -0
  191. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/priority.py +0 -0
  192. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/set.py +0 -0
  193. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  194. {howler_api-4.1.0.dev1468/howler/services → howler_api-4.1.0.dev1502/howler/security}/__init__.py +0 -0
  195. /howler_api-4.1.0.dev1468/howler/security/__init__.py → /howler_api-4.1.0.dev1502/howler/security/login.py +0 -0
  196. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/security/socket.py +0 -0
  197. {howler_api-4.1.0.dev1468/howler/utils → howler_api-4.1.0.dev1502/howler/services}/__init__.py +0 -0
  198. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/analytic_service.py +0 -0
  199. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/bundle_compat_service.py +0 -0
  200. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/case_service.py +0 -0
  201. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/comms_service.py +0 -0
  202. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/config_service.py +0 -0
  203. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/docs_service.py +0 -0
  204. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/dossier_service.py +0 -0
  205. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/event_service.py +0 -0
  206. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/fuzzy_service.py +0 -0
  207. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/lucene_service.py +0 -0
  208. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/notebook_service.py +0 -0
  209. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/overview_service.py +0 -0
  210. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/search_service.py +0 -0
  211. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/template_service.py +0 -0
  212. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/user_service.py +0 -0
  213. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/viewer_service.py +0 -0
  214. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/telemetry.py +0 -0
  215. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/annotations.py +0 -0
  216. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/chunk.py +0 -0
  217. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/compat.py +0 -0
  218. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/constants.py +0 -0
  219. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/dict_utils.py +0 -0
  220. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/indexes.py +0 -0
  221. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/isotime.py +0 -0
  222. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/list_utils.py +0 -0
  223. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/lucene.py +0 -0
  224. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/net_utils.py +0 -0
  225. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/path.py +0 -0
  226. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/socket_utils.py +0 -0
  227. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/str_utils.py +0 -0
  228. {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/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.dev1468
3
+ Version: 4.1.0.dev1502
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -1,7 +1,7 @@
1
1
  from flask import Blueprint, current_app, request
2
2
 
3
3
  from howler.api import API_PREFIX, ok
4
- from howler.security import api_login
4
+ from howler.security.login import api_login
5
5
 
6
6
  api = Blueprint("api", __name__, url_prefix=API_PREFIX)
7
7
 
@@ -11,7 +11,7 @@ import howler.services.viewer_service as viewer_service
11
11
  from howler.api import ok, unauthorized
12
12
  from howler.common.logging import get_logger
13
13
  from howler.helper.ws import ConnectionClosed, Server
14
- from howler.security import api_login
14
+ from howler.security.login import api_login
15
15
  from howler.security.socket import websocket_auth, ws_response
16
16
  from howler.utils.socket_utils import check_action
17
17
 
@@ -1,7 +1,7 @@
1
1
  from flask import Blueprint
2
2
 
3
3
  from howler.api import ok
4
- from howler.security import api_login
4
+ from howler.security.login import api_login
5
5
  from howler.services import docs_service
6
6
 
7
7
  API_PREFIX = "/api/v1"
@@ -12,7 +12,7 @@ from howler.common.swagger import generate_swagger_docs
12
12
  from howler.config import CLASSIFICATION
13
13
  from howler.odm.models.action import Action
14
14
  from howler.odm.models.user import User
15
- from howler.security import api_login
15
+ from howler.security.login import api_login
16
16
  from howler.services import action_service
17
17
 
18
18
  SUB_API = "action"
@@ -20,7 +20,7 @@ from howler.datastore.exceptions import DataStoreException
20
20
  from howler.datastore.operations import OdmHelper
21
21
  from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
22
22
  from howler.odm.models.user import User
23
- from howler.security import api_login
23
+ from howler.security.login import api_login
24
24
  from howler.services import analytic_service, user_service
25
25
 
26
26
  MAX_COMMENT_LEN = 5000
@@ -31,7 +31,7 @@ from howler.common.logging import get_logger
31
31
  from howler.common.swagger import generate_swagger_docs
32
32
  from howler.config import config
33
33
  from howler.odm.models.user import ApiKey, User
34
- from howler.security import api_login
34
+ from howler.security.login import api_login
35
35
  from howler.security.utils import generate_random_secret
36
36
  from howler.services import jwt_service
37
37
  from howler.utils.str_utils import default_string_value
@@ -10,7 +10,7 @@ from howler.common.logging import get_logger
10
10
  from howler.common.swagger import generate_swagger_docs
11
11
  from howler.config import cache, config
12
12
  from howler.plugins import get_plugins
13
- from howler.security import api_login
13
+ from howler.security.login import api_login
14
14
  from howler.utils.constants import TESTING
15
15
 
16
16
  SUB_API = "clue"
@@ -13,7 +13,7 @@ from howler.common.logging import get_logger
13
13
  from howler.common.swagger import generate_swagger_docs
14
14
  from howler.odm.models.dossier import Dossier
15
15
  from howler.odm.models.user import User
16
- from howler.security import api_login
16
+ from howler.security.login import api_login
17
17
  from howler.services import dossier_service
18
18
 
19
19
  SUB_API = "dossier"
@@ -1,7 +1,7 @@
1
1
  from howler.api import make_subapi_blueprint, ok
2
2
  from howler.common.swagger import generate_swagger_docs
3
3
  from howler.config import CLASSIFICATION
4
- from howler.security import api_login
4
+ from howler.security.login import api_login
5
5
 
6
6
  SUB_API = "help"
7
7
  classification_definition = CLASSIFICATION.get_parsed_classification_definition()
@@ -29,7 +29,7 @@ from howler.helper.workflow import WorkflowException
29
29
  from howler.odm.models.hit import Hit
30
30
  from howler.odm.models.howler_data import Comment, HitOperationType, HitStatusTransition
31
31
  from howler.odm.models.user import User
32
- from howler.security import api_login
32
+ from howler.security.login import api_login
33
33
  from howler.services import action_service, analytic_service, comms_service, correlation_service, hit_service
34
34
  from howler.utils.constants import DEBUG_FORCE_REFRESH
35
35
  from howler.utils.str_utils import sanitize_lucene_query
@@ -2,7 +2,7 @@ from flask import request
2
2
 
3
3
  from howler.api import bad_request, internal_error, make_subapi_blueprint, ok
4
4
  from howler.common.swagger import generate_swagger_docs
5
- from howler.security import api_login
5
+ from howler.security.login import api_login
6
6
  from howler.services import notebook_service
7
7
 
8
8
  SUB_API = "notebook"
@@ -17,7 +17,7 @@ from howler.common.logging import get_logger
17
17
  from howler.common.swagger import generate_swagger_docs
18
18
  from howler.odm.models.overview import Overview
19
19
  from howler.odm.models.user import User
20
- from howler.security import api_login
20
+ from howler.security.login import api_login
21
21
  from howler.utils.str_utils import sanitize_lucene_query
22
22
 
23
23
  SUB_API = "overview"
@@ -16,7 +16,7 @@ from howler.common.swagger import generate_swagger_docs
16
16
  from howler.datastore.exceptions import SearchException
17
17
  from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
18
18
  from howler.odm.models.user import User
19
- from howler.security import api_login
19
+ from howler.security.login import api_login
20
20
  from howler.services import hit_service, lucene_service
21
21
  from howler.utils.net_utils import generate_params
22
22
 
@@ -18,7 +18,7 @@ from howler.common.swagger import generate_swagger_docs
18
18
  from howler.datastore.operations import OdmHelper
19
19
  from howler.odm.models.template import Template
20
20
  from howler.odm.models.user import User
21
- from howler.security import api_login
21
+ from howler.security.login import api_login
22
22
  from howler.utils.str_utils import sanitize_lucene_query
23
23
 
24
24
  SUB_API = "template"
@@ -12,7 +12,7 @@ from howler.datastore.operations import OdmHelper
12
12
  from howler.odm.base import _Field
13
13
  from howler.odm.models.hit import Hit
14
14
  from howler.odm.models.user import User
15
- from howler.security import api_login
15
+ from howler.security.login import api_login
16
16
  from howler.services import action_service, analytic_service, correlation_service, hit_service
17
17
  from howler.utils.constants import DEBUG_FORCE_REFRESH
18
18
  from howler.utils.dict_utils import flatten
@@ -21,7 +21,7 @@ from howler.common.swagger import generate_swagger_docs
21
21
  from howler.config import config
22
22
  from howler.helper.oauth import fetch_groups
23
23
  from howler.odm.models.user import User
24
- from howler.security import api_login
24
+ from howler.security.login import api_login
25
25
  from howler.security.utils import check_password_requirements, get_password_hash, get_password_requirement_message
26
26
 
27
27
  SUB_API = "user"
@@ -12,7 +12,7 @@ from howler.common.swagger import generate_swagger_docs
12
12
  from howler.datastore.exceptions import SearchException
13
13
  from howler.odm.models.user import User
14
14
  from howler.odm.models.view import View
15
- from howler.security import api_login
15
+ from howler.security.login import api_login
16
16
 
17
17
  SUB_API = "view"
18
18
  view_api = make_subapi_blueprint(SUB_API, api_version=1)
@@ -1,7 +1,7 @@
1
1
  from flask import Blueprint
2
2
 
3
3
  from howler.api import ok
4
- from howler.security import api_login
4
+ from howler.security.login import api_login
5
5
  from howler.services import docs_service
6
6
 
7
7
  API_PREFIX = "/api/v2"
@@ -12,7 +12,7 @@ from howler.common.swagger import generate_swagger_docs
12
12
  from howler.datastore.exceptions import DataStoreException
13
13
  from howler.odm.models.case import Case, CaseItem
14
14
  from howler.odm.models.user import User
15
- from howler.security import api_login
15
+ from howler.security.login import api_login
16
16
  from howler.services import case_service
17
17
 
18
18
  SUB_API = "case"
@@ -6,7 +6,7 @@ from howler.common.logging.audit import audit
6
6
  from howler.common.swagger import generate_swagger_docs
7
7
  from howler.datastore.exceptions import SearchException
8
8
  from howler.helper.search import has_access_control
9
- from howler.security import api_login
9
+ from howler.security.login import api_login
10
10
  from howler.services import fuzzy_service
11
11
 
12
12
  SUB_API = "fuzzy"
@@ -18,7 +18,7 @@ from howler.datastore.operations import OdmHelper, OdmUpdateOperation
18
18
  from howler.odm.models.event import Event
19
19
  from howler.odm.models.hit import Hit
20
20
  from howler.odm.models.user import User
21
- from howler.security import api_login
21
+ from howler.security.login import api_login
22
22
  from howler.services import correlation_service, event_service, hit_service
23
23
  from howler.utils.dict_utils import flatten
24
24
 
@@ -15,7 +15,7 @@ from howler.common.swagger import generate_swagger_docs
15
15
  from howler.datastore.exceptions import SearchException
16
16
  from howler.helper.search import get_collection, has_access_control
17
17
  from howler.odm.models.user import User
18
- from howler.security import api_login
18
+ from howler.security.login import api_login
19
19
  from howler.services import hit_service, lucene_service, search_service
20
20
  from howler.services.search_service import SensitiveUserFieldsException
21
21
  from howler.utils.net_utils import generate_params
@@ -50,16 +50,18 @@ def run_worker(trigger: str) -> None: # pragma: no cover – long-running loop,
50
50
  if item is not None:
51
51
  batch.append(item)
52
52
 
53
- if len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE:
53
+ if len(batch) > 0 and (len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE):
54
54
  logger.info("Batch size: %s", len(batch))
55
55
 
56
56
  if len(batch) >= BATCH_SIZE or (item is None and batch):
57
57
  finalized_batch = [*batch]
58
58
  batch = []
59
- logger.debug("Processing action batch of %d item(s) for trigger=%s", len(batch), trigger)
59
+ logger.debug("Processing action batch of %d item(s) for trigger=%s", len(finalized_batch), trigger)
60
60
  try:
61
61
  process_action_batch(trigger, finalized_batch)
62
- logger.info("Action batch complete: %d item(s) processed for trigger=%s", len(batch), trigger)
62
+ logger.info(
63
+ "Action batch complete: %d item(s) processed for trigger=%s", len(finalized_batch), trigger
64
+ )
63
65
  except Exception:
64
66
  logger.exception("Error processing action batch for trigger=%s", trigger)
65
67
  except Exception:
@@ -4,7 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  from typing import Literal, Optional
6
6
 
7
- from pydantic import BaseModel, Field
7
+ from pydantic import BaseModel, Field, field_validator
8
8
  from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource
9
9
 
10
10
  from howler.common.logging.format import HWL_DATE_FORMAT, HWL_LOG_FORMAT
@@ -476,6 +476,10 @@ class System(BaseModel):
476
476
  """
477
477
 
478
478
  type: Literal["production", "staging", "development"] = Field(default="development", description="Type of system")
479
+ encryption_key: str | None = Field(
480
+ default=None,
481
+ description="32-byte secret used to encrypt JWE payloads such as queued authorization tokens.",
482
+ )
479
483
  retention: Retention = Retention()
480
484
  "Retention Configuration"
481
485
  view_cleanup: ViewCleanup = ViewCleanup()
@@ -485,6 +489,14 @@ class System(BaseModel):
485
489
  action_queue: ActionQueue = ActionQueue()
486
490
  "Action Queue Worker Configuration"
487
491
 
492
+ @field_validator("encryption_key")
493
+ @classmethod
494
+ def validate_encryption_key(cls, value: str | None) -> str | None:
495
+ if value is not None and len(value.encode("utf-8")) != 32:
496
+ raise ValueError("System encryption_key must be exactly 32 bytes when UTF-8 encoded")
497
+
498
+ return value
499
+
488
500
 
489
501
  class UI(BaseModel):
490
502
  """User interface and web server configuration.
@@ -7,6 +7,7 @@ from urllib.parse import urlparse
7
7
  from opentelemetry import trace
8
8
  from passlib.hash import bcrypt
9
9
 
10
+ from howler.common.exceptions import HowlerValueError
10
11
  from howler.config import config
11
12
 
12
13
  tracer = trace.get_tracer(__name__)
@@ -185,3 +186,12 @@ def get_disco_url(host_url: Optional[str]):
185
186
  return config.discovery.url
186
187
  else:
187
188
  return config.discovery.url
189
+
190
+
191
+ def encryption_key() -> bytes:
192
+ """Return the configured encryption key for JWE payloads."""
193
+ secret_key = config.system.encryption_key
194
+ if secret_key is None:
195
+ raise HowlerValueError("System encryption_key must be configured before encrypting tokens")
196
+
197
+ return secret_key.encode("utf-8")
@@ -1,8 +1,9 @@
1
1
  import json
2
2
  from collections import defaultdict
3
+ from importlib.util import find_spec
3
4
  from typing import Any, Optional, TypedDict
4
5
 
5
- from flask import Response
6
+ from flask import Response, has_request_context, request
6
7
 
7
8
  from howler import actions
8
9
  from howler.api import bad_request
@@ -14,6 +15,7 @@ from howler.config import config
14
15
  from howler.odm.models.action import VALID_TRIGGERS, Action
15
16
  from howler.odm.models.user import User
16
17
  from howler.remote.datatypes.queues.named import NamedQueue
18
+ from howler.services import auth_service
17
19
  from howler.utils.constants import TESTING
18
20
  from howler.utils.str_utils import sanitize_lucene_query
19
21
 
@@ -25,12 +27,30 @@ class TriggeredAction(TypedDict):
25
27
 
26
28
  hit_ids: list[str]
27
29
  uname: str | None
30
+ auth_token: str | None
28
31
 
29
32
 
30
33
  # Per-trigger persistent queues for buffering action execution requests.
31
34
  _action_queues: dict[str, NamedQueue[TriggeredAction]] = {}
32
35
 
33
36
 
37
+ def _is_builtin_operation(operation_id: str) -> bool:
38
+ """Return whether an operation is implemented by Howler's built-in actions package."""
39
+ return find_spec(f"howler.actions.{operation_id}") is not None
40
+
41
+
42
+ def _request_auth_token() -> str | None:
43
+ """Extract the credential portion of the current request's authorization header."""
44
+ if not has_request_context():
45
+ return None
46
+
47
+ authorization = request.headers.get("Authorization")
48
+ if authorization is None or " " not in authorization:
49
+ return None
50
+
51
+ return authorization.split(" ", maxsplit=1)[1]
52
+
53
+
34
54
  def get_action_queue(trigger: str) -> NamedQueue[TriggeredAction]:
35
55
  """Return the action queue for *trigger*, creating it on first use.
36
56
 
@@ -51,7 +71,7 @@ def get_action_queue(trigger: str) -> NamedQueue[TriggeredAction]:
51
71
  return _action_queues[trigger]
52
72
 
53
73
 
54
- def enqueue_action_execution(hit_ids: list[str], trigger: str = "create", user: Optional[User] = None) -> None:
74
+ def enqueue_action_execution(hit_ids: str | list[str], trigger: str = "create", user: Optional[User] = None) -> None:
55
75
  """Buffer action execution by pushing to a Redis queue.
56
76
 
57
77
  When the action queue is disabled in configuration, falls back to
@@ -65,25 +85,44 @@ def enqueue_action_execution(hit_ids: list[str], trigger: str = "create", user:
65
85
  if not hit_ids:
66
86
  return
67
87
 
88
+ if not isinstance(hit_ids, list):
89
+ hit_ids = [hit_ids]
90
+
68
91
  if trigger not in VALID_TRIGGERS:
69
92
  raise HowlerValueError(f"Invalid trigger {trigger!r}. Must be one of {VALID_TRIGGERS}")
70
93
 
94
+ auth_token = _request_auth_token()
95
+
71
96
  if not config.system.action_queue.enabled:
97
+ logger.debug(
98
+ "Executing actions for trigger %s on hit id(s) [%s] with user %s",
99
+ trigger,
100
+ ", ".join(hit_ids),
101
+ user.uname if user else "NO_USER",
102
+ )
103
+
72
104
  query = f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in hit_ids)})"
73
- bulk_execute_on_query(query, trigger=trigger, user=user)
105
+ bulk_execute_on_query(query, trigger=trigger, user=user, auth_token=auth_token)
74
106
  return
75
107
 
76
108
  try:
109
+ logger.debug(
110
+ "Enqueuing actions for trigger %s on hit id(s) [%s] with user %s",
111
+ trigger,
112
+ ", ".join(hit_ids),
113
+ user.uname if user else "NO_USER",
114
+ )
77
115
  get_action_queue(trigger).push(
78
116
  {
79
117
  "hit_ids": hit_ids,
80
118
  "uname": user.uname if user else None,
119
+ "auth_token": auth_service.encrypt_token(auth_token),
81
120
  }
82
121
  )
83
122
  except Exception:
84
123
  logger.exception("Failed to enqueue action execution, falling back to direct execution")
85
124
  query = f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in hit_ids)})"
86
- bulk_execute_on_query(query, trigger=trigger, user=user)
125
+ bulk_execute_on_query(query, trigger=trigger, user=user, auth_token=auth_token)
87
126
 
88
127
 
89
128
  def process_action_batch(trigger: str, items: list[TriggeredAction]) -> None:
@@ -99,21 +138,26 @@ def process_action_batch(trigger: str, items: list[TriggeredAction]) -> None:
99
138
  if not items:
100
139
  return
101
140
 
102
- groups: dict[str | None, list[str]] = defaultdict(list)
141
+ groups: dict[tuple[str | None, str | None], list[str]] = defaultdict(list)
103
142
 
104
143
  for item in items:
105
144
  uname = item.get("uname")
145
+ try:
146
+ auth_token = auth_service.decrypt_token(item.get("auth_token"))
147
+ except Exception:
148
+ logger.exception("Unable to decrypt queued authorization token for user=%s", uname)
149
+ auth_token = None
106
150
 
107
- groups[uname].extend(item["hit_ids"])
151
+ groups[(uname, auth_token)].extend(item["hit_ids"])
108
152
 
109
- for uname, hit_ids in groups.items():
153
+ for (uname, auth_token), hit_ids in groups.items():
110
154
  # Deduplicate IDs within a batch group
111
155
  unique_ids = list(dict.fromkeys(hit_ids))
112
156
  query = f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in unique_ids)})"
113
157
 
114
158
  try:
115
159
  user = datastore().user.get(uname)
116
- bulk_execute_on_query(query, trigger=trigger, user=user)
160
+ bulk_execute_on_query(query, trigger=trigger, user=user, auth_token=auth_token)
117
161
  except Exception:
118
162
  logger.exception("Error processing action batch for trigger=%s user=%s", trigger, uname)
119
163
 
@@ -153,7 +197,9 @@ def validate_action(new_action: Any) -> Optional[Response]: # noqa: C901
153
197
  return None
154
198
 
155
199
 
156
- def bulk_execute_on_query(query: str, trigger: str = "create", user: Optional[User] = None):
200
+ def bulk_execute_on_query(
201
+ query: str, trigger: str = "create", user: Optional[User] = None, auth_token: str | None = None
202
+ ):
157
203
  """Execute the operations specified in registered actions on the given query"""
158
204
  storage = datastore()
159
205
 
@@ -195,6 +241,10 @@ def bulk_execute_on_query(query: str, trigger: str = "create", user: Optional[Us
195
241
  if not user:
196
242
  raise NotImplementedError("Running actions without a user object is not currently supported")
197
243
 
244
+ if auth_token is not None and not _is_builtin_operation(operation.operation_id):
245
+ logger.debug("Appending auth token to external operation %s", operation.operation_id)
246
+ parsed_data["auth_token"] = auth_token
247
+
198
248
  report = actions.execute(
199
249
  operation_id=operation.operation_id,
200
250
  query=intersected_query,
@@ -1,9 +1,12 @@
1
1
  import base64
2
2
  import hashlib
3
3
  import hmac
4
+ import os
4
5
  from datetime import datetime
5
6
  from typing import Optional, Union
6
7
 
8
+ import jwt
9
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
7
10
  from flask import request
8
11
  from opentelemetry import trace
9
12
 
@@ -22,7 +25,7 @@ from howler.odm.models.user import User
22
25
  from howler.remote.datatypes import retry_call
23
26
  from howler.remote.datatypes.queues.named import NamedQueue
24
27
  from howler.remote.datatypes.set import ExpiringSet
25
- from howler.security.utils import generate_random_secret, verify_password
28
+ from howler.security.utils import encryption_key, generate_random_secret, verify_password
26
29
 
27
30
  logger = get_logger(__file__)
28
31
  tracer = trace.get_tracer(__name__)
@@ -39,6 +42,46 @@ _APIKEY_CACHE_TTL: int = 600 # 10 minutes
39
42
  # Derive the HMAC key from the configured secret
40
43
  _HMAC_KEY: bytes = hashlib.sha256(config.auth.hmac_secret_key.encode()).digest()
41
44
 
45
+ _NON_JWT_TOKEN_PREFIX = "aesgcm:" # noqa: S105
46
+ _AES_GCM_NONCE_LENGTH = 12
47
+
48
+
49
+ def _is_jwt(token: str) -> bool:
50
+ """Return whether *token* has the compact three-segment JWT format."""
51
+ if token.count(".") != 2:
52
+ return False
53
+
54
+ try:
55
+ return "alg" in jwt.get_unverified_header(token)
56
+ except jwt.InvalidTokenError:
57
+ return False
58
+
59
+
60
+ def encrypt_token(token: str | None) -> str | None:
61
+ """Encrypt an authorization token before it is serialized into Redis."""
62
+ if token is None:
63
+ return None
64
+
65
+ if _is_jwt(token):
66
+ return jwt_service.encrypt_token(token)
67
+
68
+ nonce = os.urandom(_AES_GCM_NONCE_LENGTH)
69
+ ciphertext = AESGCM(encryption_key()).encrypt(nonce, token.encode("utf-8"), None)
70
+ return _NON_JWT_TOKEN_PREFIX + base64.urlsafe_b64encode(nonce + ciphertext).decode("ascii")
71
+
72
+
73
+ def decrypt_token(encrypted_token: str | None) -> str | None:
74
+ """Decrypt a queued authorization token after reading it from Redis."""
75
+ if encrypted_token is None:
76
+ return None
77
+
78
+ if not encrypted_token.startswith(_NON_JWT_TOKEN_PREFIX):
79
+ return jwt_service.decrypt_token(encrypted_token)
80
+
81
+ payload = base64.urlsafe_b64decode(encrypted_token.removeprefix(_NON_JWT_TOKEN_PREFIX))
82
+ nonce, ciphertext = payload[:_AES_GCM_NONCE_LENGTH], payload[_AES_GCM_NONCE_LENGTH:]
83
+ return AESGCM(encryption_key()).decrypt(nonce, ciphertext, None).decode("utf-8")
84
+
42
85
 
43
86
  def _apikey_cache_key(username: str, key_name: str) -> str:
44
87
  """Build the Redis key that identifies a cached apikey verification.
@@ -345,7 +345,7 @@ def run_worker() -> None: # pragma: no cover – long-running loop, tested via
345
345
  if item is not None:
346
346
  batch.append(item)
347
347
 
348
- if len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE:
348
+ if len(batch) > 0 and (len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE):
349
349
  logger.info("Batch size: %s", len(batch))
350
350
 
351
351
  if len(batch) >= BATCH_SIZE or (item is None and batch):
@@ -684,7 +684,7 @@ def transition_hit(
684
684
  workflow: Workflow = get_hit_workflow()
685
685
 
686
686
  # Log hit that will be transitioned
687
- logger.debug("Transitioning (%s)", hit)
687
+ logger.debug("Transitioning hit %s with transition %s", hit["howler"]["id"], transition)
688
688
 
689
689
  # Process each hit (primary + children) with the workflow transition
690
690
  hit_status = hit["howler"]["status"]
@@ -727,7 +727,7 @@ def transition_hit(
727
727
  datastore().hit.commit()
728
728
 
729
729
  # Enqueue action execution for all hits
730
- action_service.enqueue_action_execution([hit_id], trigger=trigger, user=user)
730
+ action_service.enqueue_action_execution(hit_id, trigger=trigger, user=user)
731
731
 
732
732
  # Emit events for processed hit to notify other systems
733
733
  updated_hit, new_version = datastore().hit.get(hit_id, as_obj=True, version=True)
@@ -736,6 +736,9 @@ def transition_hit(
736
736
  )
737
737
 
738
738
  updated_hit, new_version = datastore().hit.get(hit_id, as_obj=True, version=True)
739
+ else:
740
+ logger.debug("Transition %s does not require executing bulk actions", transition)
741
+
739
742
  return updated_hit, new_version
740
743
 
741
744
 
@@ -6,14 +6,35 @@ from typing import Any, Optional, cast
6
6
 
7
7
  import jwt
8
8
  import requests
9
+ from authlib.jose import JsonWebEncryption
9
10
  from jwt.api_jwk import PyJWK
10
11
 
11
12
  from howler.common.exceptions import ForbiddenException, HowlerKeyError, HowlerValueError
12
13
  from howler.common.logging import get_logger
13
14
  from howler.config import cache, config
15
+ from howler.security.utils import encryption_key
14
16
 
15
17
  logger = get_logger(__file__)
16
18
 
19
+ _jwe = JsonWebEncryption()
20
+ _JWE_HEADER = {"alg": "A256GCMKW", "enc": "A256GCM"}
21
+
22
+
23
+ def encrypt_token(auth_token: str | None) -> str | None:
24
+ """Encrypt an authorization token before it is serialized into Redis."""
25
+ if auth_token is None:
26
+ return None
27
+
28
+ return _jwe.serialize_compact(_JWE_HEADER, auth_token.encode("utf-8"), encryption_key()).decode("ascii")
29
+
30
+
31
+ def decrypt_token(encrypted_auth_token: str | None) -> str | None:
32
+ """Decrypt a queued authorization token for in-memory action execution."""
33
+ if encrypted_auth_token is None:
34
+ return None
35
+
36
+ return _jwe.deserialize_compact(encrypted_auth_token, encryption_key())["payload"].decode("utf-8")
37
+
17
38
 
18
39
  def get_jwk(access_token: str) -> PyJWK:
19
40
  """Get the JSON Web Key associated with the given JWT"""
File without changes
@@ -60,7 +60,7 @@ module = ["howler.odm.base", "test", "test.*"]
60
60
  ignore_errors = true
61
61
 
62
62
  [[tool.mypy.overrides]]
63
- module = "howler.security"
63
+ module = "howler.security.login"
64
64
  disable_error_code = "attr-defined"
65
65
 
66
66
  [[tool.mypy.overrides]]
@@ -133,7 +133,7 @@ suppress-none-returning = true
133
133
  "build_scripts/*" = ["D", "ANN", "S", "N818", "T20", "TRY"]
134
134
  "howler/app.py" = ["E402"]
135
135
  "howler/odm/random*.py" = ["C901", "S105", "S311", "E402"]
136
- "howler/security/__init__.py" = ["TRY301"]
136
+ "howler/security/login.py" = ["TRY301"]
137
137
  "howler/external/*.py" = ["T20"]
138
138
  "howler/odm/helper.py" = ["S311"]
139
139
  "howler/common/classification.py" = ["D", "ANN", "C901", "TRY"]
@@ -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.1.0.dev1468"
155
+ version = "4.1.0.dev1502"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",