howler-api 2.10.0.dev51__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 (194) hide show
  1. howler_api-2.10.0.dev51/PKG-INFO +71 -0
  2. howler_api-2.10.0.dev51/README.md +9 -0
  3. howler_api-2.10.0.dev51/howler/__init__.py +0 -0
  4. howler_api-2.10.0.dev51/howler/actions/__init__.py +154 -0
  5. howler_api-2.10.0.dev51/howler/actions/add_label.py +111 -0
  6. howler_api-2.10.0.dev51/howler/actions/add_to_bundle.py +159 -0
  7. howler_api-2.10.0.dev51/howler/actions/change_field.py +76 -0
  8. howler_api-2.10.0.dev51/howler/actions/demote.py +160 -0
  9. howler_api-2.10.0.dev51/howler/actions/example_plugin.py +104 -0
  10. howler_api-2.10.0.dev51/howler/actions/prioritization.py +93 -0
  11. howler_api-2.10.0.dev51/howler/actions/promote.py +147 -0
  12. howler_api-2.10.0.dev51/howler/actions/remove_from_bundle.py +133 -0
  13. howler_api-2.10.0.dev51/howler/actions/remove_label.py +111 -0
  14. howler_api-2.10.0.dev51/howler/actions/transition.py +200 -0
  15. howler_api-2.10.0.dev51/howler/api/__init__.py +249 -0
  16. howler_api-2.10.0.dev51/howler/api/base.py +88 -0
  17. howler_api-2.10.0.dev51/howler/api/socket.py +114 -0
  18. howler_api-2.10.0.dev51/howler/api/v1/__init__.py +97 -0
  19. howler_api-2.10.0.dev51/howler/api/v1/action.py +417 -0
  20. howler_api-2.10.0.dev51/howler/api/v1/analytic.py +748 -0
  21. howler_api-2.10.0.dev51/howler/api/v1/auth.py +382 -0
  22. howler_api-2.10.0.dev51/howler/api/v1/borealis.py +100 -0
  23. howler_api-2.10.0.dev51/howler/api/v1/configs.py +55 -0
  24. howler_api-2.10.0.dev51/howler/api/v1/dossier.py +246 -0
  25. howler_api-2.10.0.dev51/howler/api/v1/help.py +28 -0
  26. howler_api-2.10.0.dev51/howler/api/v1/hit.py +1177 -0
  27. howler_api-2.10.0.dev51/howler/api/v1/notebook.py +82 -0
  28. howler_api-2.10.0.dev51/howler/api/v1/overview.py +191 -0
  29. howler_api-2.10.0.dev51/howler/api/v1/search.py +645 -0
  30. howler_api-2.10.0.dev51/howler/api/v1/template.py +206 -0
  31. howler_api-2.10.0.dev51/howler/api/v1/tool.py +184 -0
  32. howler_api-2.10.0.dev51/howler/api/v1/user.py +421 -0
  33. howler_api-2.10.0.dev51/howler/api/v1/utils/__init__.py +0 -0
  34. howler_api-2.10.0.dev51/howler/api/v1/utils/etag.py +46 -0
  35. howler_api-2.10.0.dev51/howler/api/v1/view.py +296 -0
  36. howler_api-2.10.0.dev51/howler/app.py +226 -0
  37. howler_api-2.10.0.dev51/howler/common/README.md +144 -0
  38. howler_api-2.10.0.dev51/howler/common/__init__.py +0 -0
  39. howler_api-2.10.0.dev51/howler/common/classification.py +979 -0
  40. howler_api-2.10.0.dev51/howler/common/classification.yml +107 -0
  41. howler_api-2.10.0.dev51/howler/common/exceptions.py +167 -0
  42. howler_api-2.10.0.dev51/howler/common/hexdump.py +48 -0
  43. howler_api-2.10.0.dev51/howler/common/iprange.py +171 -0
  44. howler_api-2.10.0.dev51/howler/common/loader.py +154 -0
  45. howler_api-2.10.0.dev51/howler/common/logging/__init__.py +241 -0
  46. howler_api-2.10.0.dev51/howler/common/logging/audit.py +138 -0
  47. howler_api-2.10.0.dev51/howler/common/logging/format.py +38 -0
  48. howler_api-2.10.0.dev51/howler/common/net.py +79 -0
  49. howler_api-2.10.0.dev51/howler/common/net_static.py +1494 -0
  50. howler_api-2.10.0.dev51/howler/common/random_user.py +316 -0
  51. howler_api-2.10.0.dev51/howler/common/swagger.py +117 -0
  52. howler_api-2.10.0.dev51/howler/config.py +64 -0
  53. howler_api-2.10.0.dev51/howler/cronjobs/__init__.py +28 -0
  54. howler_api-2.10.0.dev51/howler/cronjobs/retention.py +61 -0
  55. howler_api-2.10.0.dev51/howler/cronjobs/rules.py +274 -0
  56. howler_api-2.10.0.dev51/howler/datastore/README.md +114 -0
  57. howler_api-2.10.0.dev51/howler/datastore/__init__.py +0 -0
  58. howler_api-2.10.0.dev51/howler/datastore/bulk.py +72 -0
  59. howler_api-2.10.0.dev51/howler/datastore/collection.py +2322 -0
  60. howler_api-2.10.0.dev51/howler/datastore/constants.py +117 -0
  61. howler_api-2.10.0.dev51/howler/datastore/exceptions.py +41 -0
  62. howler_api-2.10.0.dev51/howler/datastore/howler_store.py +134 -0
  63. howler_api-2.10.0.dev51/howler/datastore/migrations/fix_process.py +41 -0
  64. howler_api-2.10.0.dev51/howler/datastore/operations.py +130 -0
  65. howler_api-2.10.0.dev51/howler/datastore/schemas.py +90 -0
  66. howler_api-2.10.0.dev51/howler/datastore/store.py +212 -0
  67. howler_api-2.10.0.dev51/howler/datastore/support/__init__.py +0 -0
  68. howler_api-2.10.0.dev51/howler/datastore/support/build.py +214 -0
  69. howler_api-2.10.0.dev51/howler/datastore/support/schemas.py +90 -0
  70. howler_api-2.10.0.dev51/howler/datastore/types.py +22 -0
  71. howler_api-2.10.0.dev51/howler/error.py +91 -0
  72. howler_api-2.10.0.dev51/howler/external/__init__.py +0 -0
  73. howler_api-2.10.0.dev51/howler/external/generate_mitre.py +96 -0
  74. howler_api-2.10.0.dev51/howler/external/generate_sigma_rules.py +31 -0
  75. howler_api-2.10.0.dev51/howler/external/generate_tlds.py +47 -0
  76. howler_api-2.10.0.dev51/howler/external/reindex_data.py +46 -0
  77. howler_api-2.10.0.dev51/howler/external/wipe_databases.py +58 -0
  78. howler_api-2.10.0.dev51/howler/gunicorn_config.py +25 -0
  79. howler_api-2.10.0.dev51/howler/healthz.py +47 -0
  80. howler_api-2.10.0.dev51/howler/helper/__init__.py +0 -0
  81. howler_api-2.10.0.dev51/howler/helper/azure.py +50 -0
  82. howler_api-2.10.0.dev51/howler/helper/discover.py +59 -0
  83. howler_api-2.10.0.dev51/howler/helper/hit.py +236 -0
  84. howler_api-2.10.0.dev51/howler/helper/oauth.py +247 -0
  85. howler_api-2.10.0.dev51/howler/helper/search.py +94 -0
  86. howler_api-2.10.0.dev51/howler/helper/workflow.py +110 -0
  87. howler_api-2.10.0.dev51/howler/helper/ws.py +378 -0
  88. howler_api-2.10.0.dev51/howler/odm/README.md +102 -0
  89. howler_api-2.10.0.dev51/howler/odm/__init__.py +1 -0
  90. howler_api-2.10.0.dev51/howler/odm/base.py +1510 -0
  91. howler_api-2.10.0.dev51/howler/odm/charter.txt +146 -0
  92. howler_api-2.10.0.dev51/howler/odm/helper.py +415 -0
  93. howler_api-2.10.0.dev51/howler/odm/howler_enum.py +25 -0
  94. howler_api-2.10.0.dev51/howler/odm/models/__init__.py +0 -0
  95. howler_api-2.10.0.dev51/howler/odm/models/action.py +33 -0
  96. howler_api-2.10.0.dev51/howler/odm/models/analytic.py +90 -0
  97. howler_api-2.10.0.dev51/howler/odm/models/assemblyline.py +48 -0
  98. howler_api-2.10.0.dev51/howler/odm/models/aws.py +23 -0
  99. howler_api-2.10.0.dev51/howler/odm/models/azure.py +16 -0
  100. howler_api-2.10.0.dev51/howler/odm/models/cbs.py +44 -0
  101. howler_api-2.10.0.dev51/howler/odm/models/config.py +395 -0
  102. howler_api-2.10.0.dev51/howler/odm/models/dossier.py +33 -0
  103. howler_api-2.10.0.dev51/howler/odm/models/ecs/__init__.py +0 -0
  104. howler_api-2.10.0.dev51/howler/odm/models/ecs/agent.py +17 -0
  105. howler_api-2.10.0.dev51/howler/odm/models/ecs/autonomous_system.py +16 -0
  106. howler_api-2.10.0.dev51/howler/odm/models/ecs/client.py +149 -0
  107. howler_api-2.10.0.dev51/howler/odm/models/ecs/cloud.py +141 -0
  108. howler_api-2.10.0.dev51/howler/odm/models/ecs/code_signature.py +27 -0
  109. howler_api-2.10.0.dev51/howler/odm/models/ecs/container.py +32 -0
  110. howler_api-2.10.0.dev51/howler/odm/models/ecs/dns.py +62 -0
  111. howler_api-2.10.0.dev51/howler/odm/models/ecs/egress.py +10 -0
  112. howler_api-2.10.0.dev51/howler/odm/models/ecs/elf.py +74 -0
  113. howler_api-2.10.0.dev51/howler/odm/models/ecs/email.py +122 -0
  114. howler_api-2.10.0.dev51/howler/odm/models/ecs/error.py +14 -0
  115. howler_api-2.10.0.dev51/howler/odm/models/ecs/event.py +140 -0
  116. howler_api-2.10.0.dev51/howler/odm/models/ecs/faas.py +24 -0
  117. howler_api-2.10.0.dev51/howler/odm/models/ecs/file.py +84 -0
  118. howler_api-2.10.0.dev51/howler/odm/models/ecs/geo.py +30 -0
  119. howler_api-2.10.0.dev51/howler/odm/models/ecs/group.py +18 -0
  120. howler_api-2.10.0.dev51/howler/odm/models/ecs/hash.py +16 -0
  121. howler_api-2.10.0.dev51/howler/odm/models/ecs/host.py +17 -0
  122. howler_api-2.10.0.dev51/howler/odm/models/ecs/http.py +37 -0
  123. howler_api-2.10.0.dev51/howler/odm/models/ecs/ingress.py +12 -0
  124. howler_api-2.10.0.dev51/howler/odm/models/ecs/interface.py +21 -0
  125. howler_api-2.10.0.dev51/howler/odm/models/ecs/network.py +30 -0
  126. howler_api-2.10.0.dev51/howler/odm/models/ecs/observer.py +45 -0
  127. howler_api-2.10.0.dev51/howler/odm/models/ecs/organization.py +12 -0
  128. howler_api-2.10.0.dev51/howler/odm/models/ecs/os.py +21 -0
  129. howler_api-2.10.0.dev51/howler/odm/models/ecs/pe.py +17 -0
  130. howler_api-2.10.0.dev51/howler/odm/models/ecs/process.py +216 -0
  131. howler_api-2.10.0.dev51/howler/odm/models/ecs/registry.py +26 -0
  132. howler_api-2.10.0.dev51/howler/odm/models/ecs/related.py +45 -0
  133. howler_api-2.10.0.dev51/howler/odm/models/ecs/rule.py +51 -0
  134. howler_api-2.10.0.dev51/howler/odm/models/ecs/server.py +24 -0
  135. howler_api-2.10.0.dev51/howler/odm/models/ecs/threat.py +247 -0
  136. howler_api-2.10.0.dev51/howler/odm/models/ecs/tls.py +58 -0
  137. howler_api-2.10.0.dev51/howler/odm/models/ecs/url.py +51 -0
  138. howler_api-2.10.0.dev51/howler/odm/models/ecs/user.py +57 -0
  139. howler_api-2.10.0.dev51/howler/odm/models/ecs/user_agent.py +20 -0
  140. howler_api-2.10.0.dev51/howler/odm/models/ecs/vulnerability.py +41 -0
  141. howler_api-2.10.0.dev51/howler/odm/models/gcp.py +16 -0
  142. howler_api-2.10.0.dev51/howler/odm/models/hit.py +365 -0
  143. howler_api-2.10.0.dev51/howler/odm/models/howler_data.py +326 -0
  144. howler_api-2.10.0.dev51/howler/odm/models/lead.py +33 -0
  145. howler_api-2.10.0.dev51/howler/odm/models/localized_label.py +13 -0
  146. howler_api-2.10.0.dev51/howler/odm/models/overview.py +16 -0
  147. howler_api-2.10.0.dev51/howler/odm/models/pivot.py +49 -0
  148. howler_api-2.10.0.dev51/howler/odm/models/template.py +24 -0
  149. howler_api-2.10.0.dev51/howler/odm/models/user.py +83 -0
  150. howler_api-2.10.0.dev51/howler/odm/models/view.py +34 -0
  151. howler_api-2.10.0.dev51/howler/odm/random_data.py +830 -0
  152. howler_api-2.10.0.dev51/howler/odm/randomizer.py +606 -0
  153. howler_api-2.10.0.dev51/howler/patched.py +5 -0
  154. howler_api-2.10.0.dev51/howler/remote/__init__.py +0 -0
  155. howler_api-2.10.0.dev51/howler/remote/datatypes/README.md +355 -0
  156. howler_api-2.10.0.dev51/howler/remote/datatypes/__init__.py +98 -0
  157. howler_api-2.10.0.dev51/howler/remote/datatypes/counters.py +63 -0
  158. howler_api-2.10.0.dev51/howler/remote/datatypes/events.py +66 -0
  159. howler_api-2.10.0.dev51/howler/remote/datatypes/hash.py +206 -0
  160. howler_api-2.10.0.dev51/howler/remote/datatypes/lock.py +42 -0
  161. howler_api-2.10.0.dev51/howler/remote/datatypes/queues/__init__.py +0 -0
  162. howler_api-2.10.0.dev51/howler/remote/datatypes/queues/comms.py +59 -0
  163. howler_api-2.10.0.dev51/howler/remote/datatypes/queues/multi.py +32 -0
  164. howler_api-2.10.0.dev51/howler/remote/datatypes/queues/named.py +93 -0
  165. howler_api-2.10.0.dev51/howler/remote/datatypes/queues/priority.py +215 -0
  166. howler_api-2.10.0.dev51/howler/remote/datatypes/set.py +118 -0
  167. howler_api-2.10.0.dev51/howler/remote/datatypes/user_quota_tracker.py +54 -0
  168. howler_api-2.10.0.dev51/howler/security/__init__.py +255 -0
  169. howler_api-2.10.0.dev51/howler/security/socket.py +108 -0
  170. howler_api-2.10.0.dev51/howler/security/utils.py +185 -0
  171. howler_api-2.10.0.dev51/howler/services/__init__.py +0 -0
  172. howler_api-2.10.0.dev51/howler/services/action_service.py +64 -0
  173. howler_api-2.10.0.dev51/howler/services/analytic_service.py +97 -0
  174. howler_api-2.10.0.dev51/howler/services/auth_service.py +325 -0
  175. howler_api-2.10.0.dev51/howler/services/config_service.py +127 -0
  176. howler_api-2.10.0.dev51/howler/services/dossier_service.py +119 -0
  177. howler_api-2.10.0.dev51/howler/services/event_service.py +93 -0
  178. howler_api-2.10.0.dev51/howler/services/hit_service.py +654 -0
  179. howler_api-2.10.0.dev51/howler/services/jwt_service.py +158 -0
  180. howler_api-2.10.0.dev51/howler/services/lucene_service.py +278 -0
  181. howler_api-2.10.0.dev51/howler/services/notebook_service.py +133 -0
  182. howler_api-2.10.0.dev51/howler/services/user_service.py +332 -0
  183. howler_api-2.10.0.dev51/howler/utils/__init__.py +0 -0
  184. howler_api-2.10.0.dev51/howler/utils/annotations.py +28 -0
  185. howler_api-2.10.0.dev51/howler/utils/chunk.py +38 -0
  186. howler_api-2.10.0.dev51/howler/utils/dict_utils.py +145 -0
  187. howler_api-2.10.0.dev51/howler/utils/isotime.py +17 -0
  188. howler_api-2.10.0.dev51/howler/utils/list_utils.py +11 -0
  189. howler_api-2.10.0.dev51/howler/utils/lucene.py +57 -0
  190. howler_api-2.10.0.dev51/howler/utils/path.py +27 -0
  191. howler_api-2.10.0.dev51/howler/utils/socket_utils.py +61 -0
  192. howler_api-2.10.0.dev51/howler/utils/str_utils.py +256 -0
  193. howler_api-2.10.0.dev51/howler/utils/uid.py +47 -0
  194. howler_api-2.10.0.dev51/pyproject.toml +269 -0
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.3
2
+ Name: howler-api
3
+ Version: 2.10.0.dev51
4
+ Summary: Howler - API server
5
+ License: MIT
6
+ Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
7
+ Author: Canadian Centre for Cyber Security
8
+ Author-email: howler@cyber.gc.ca
9
+ Maintainer: Matthew Rafuse
10
+ Maintainer-email: matthew.rafuse@cyber.gc.ca
11
+ Requires-Python: >=3.9.17,<4.0.0
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Dist: apscheduler (==3.10.4)
22
+ Requires-Dist: authlib (>=1.6.0,<2.0.0)
23
+ Requires-Dist: azure-identity (==1.16.1)
24
+ Requires-Dist: azure-storage-blob (==12.14.1)
25
+ Requires-Dist: chardet (==5.1.0)
26
+ Requires-Dist: chevron (==0.14.0)
27
+ Requires-Dist: elastic-apm[flask] (>=6.22.0,<7.0.0)
28
+ Requires-Dist: elasticsearch (==8.6.1)
29
+ Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
30
+ Requires-Dist: flask (==2.2.5)
31
+ Requires-Dist: flask-caching (==2.0.2)
32
+ Requires-Dist: gevent (==23.9.1)
33
+ Requires-Dist: gunicorn (==23.0.0)
34
+ Requires-Dist: luqum (>=1.0.0,<2.0.0)
35
+ Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
36
+ Requires-Dist: netifaces (==0.11.0)
37
+ Requires-Dist: packaging (<25.0)
38
+ Requires-Dist: passlib (==1.7.4)
39
+ Requires-Dist: prometheus-client (==0.17.1)
40
+ Requires-Dist: pydantic (>=2.11.4,<3.0.0)
41
+ Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
42
+ Requires-Dist: pydash (>=8.0.5,<9.0.0)
43
+ Requires-Dist: pyjwt (==2.6.0)
44
+ Requires-Dist: pyroute2-core (==0.6.13)
45
+ Requires-Dist: pysftp (==0.2.9)
46
+ Requires-Dist: pysigma (==0.11.17)
47
+ Requires-Dist: pysigma-backend-elasticsearch (>=1.1.2,<2.0.0)
48
+ Requires-Dist: python-baseconv (==1.2.2)
49
+ Requires-Dist: python-datemath (==3.0.3)
50
+ Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
51
+ Requires-Dist: pyyaml (==6.0.2)
52
+ Requires-Dist: redis (==4.5.4)
53
+ Requires-Dist: requests (==2.32.2)
54
+ Requires-Dist: typing-extensions (>=4.12.2,<5.0.0)
55
+ Requires-Dist: validators (>=0.34.0,<0.35.0)
56
+ Requires-Dist: wsproto (==1.2.0)
57
+ Project-URL: Documentation, https://cybercentrecanada.github.io/howler-docs/developer/backend/
58
+ Project-URL: Homepage, https://cybercentrecanada.github.io/howler-docs/
59
+ Project-URL: Repository, https://github.com/CybercentreCanada/howler-api
60
+ Description-Content-Type: text/markdown
61
+
62
+ # Howler API
63
+
64
+ ## Introduction
65
+
66
+ Howler is an application that allows analysts to triage hits and alerts. It provides a way for analysts to efficiently review and analyze alerts generated by different analytics and detections.
67
+
68
+ ## Contributing
69
+
70
+ See [CONTRIBUTING.md](doc/CONTRIBUTING.md).
71
+
@@ -0,0 +1,9 @@
1
+ # Howler API
2
+
3
+ ## Introduction
4
+
5
+ Howler is an application that allows analysts to triage hits and alerts. It provides a way for analysts to efficiently review and analyze alerts generated by different analytics and detections.
6
+
7
+ ## Contributing
8
+
9
+ See [CONTRIBUTING.md](doc/CONTRIBUTING.md).
File without changes
@@ -0,0 +1,154 @@
1
+ import importlib
2
+ import os
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ from howler.common.logging import get_logger
8
+ from howler.config import config
9
+ from howler.odm.models.user import User
10
+
11
+ logger = get_logger(__file__)
12
+
13
+ PLUGIN_PATH = Path(os.environ.get("HWL_PLUGIN_DIRECTORY", "/etc/howler/plugins"))
14
+
15
+
16
+ def __sanitize_specification(spec: dict[str, Any]) -> dict[str, Any]:
17
+ """Adapt the specification for use in the UI
18
+
19
+ Args:
20
+ spec (dict[str, Any]): The raw specification
21
+
22
+ Returns:
23
+ dict[str, Any]: The sanitized specification for use in the UI
24
+ """
25
+ return {
26
+ **spec,
27
+ "description": {
28
+ **spec["description"],
29
+ "long": re.sub(r"\n +(request_id|query).+", "", spec["description"]["long"])
30
+ .replace("\n ", "\n")
31
+ .replace("Args:", "Args:\n"),
32
+ },
33
+ "steps": [{**step, "args": {k: list(v) for k, v in step["args"].items()}} for step in spec["steps"]],
34
+ }
35
+
36
+
37
+ def __sanitize_report(report: list[dict[str, Any]]) -> list[dict[str, Any]]:
38
+ """Deduplicate identical entries with different queries
39
+
40
+ Args:
41
+ report (list[dict[str, Any]]): The unsanitized, verbose report
42
+
43
+ Returns:
44
+ list[dict[str, Any]]: The sanitized, concise report
45
+ """
46
+ by_message: dict[str, Any] = {}
47
+
48
+ for entry in report:
49
+ # if these three keys match, we should merge the queries that use them both. For example, when multiple hits
50
+ # fail to transition for the same reason.
51
+ key = f"{entry['title']}==={entry['message']}==={entry['outcome']}"
52
+
53
+ if key in by_message:
54
+ by_message[key].append(f'({entry["query"]})')
55
+ else:
56
+ by_message[key] = [f'({entry["query"]})']
57
+
58
+ sanitized: list[dict[str, Any]] = []
59
+ for key, queries in by_message.items():
60
+ (title, message, outcome) = key.split("===")
61
+
62
+ sanitized.append(
63
+ {
64
+ "query": " OR ".join(queries),
65
+ "outcome": outcome,
66
+ "title": title,
67
+ "message": message,
68
+ }
69
+ )
70
+
71
+ return sanitized
72
+
73
+
74
+ def execute(
75
+ operation_id: str,
76
+ query: str,
77
+ user: User,
78
+ request_id: Optional[str] = None,
79
+ **kwargs,
80
+ ) -> list[dict[str, Any]]:
81
+ """Execute a specification
82
+
83
+ Args:
84
+ operation_id (str): The id of the operation to run
85
+ query (str): The query to run this action on
86
+ user (dict[str, Any]): The user running this action
87
+ request_id (str, None): A user-provided ID, can be used to track the progress of their excecution via websockets
88
+
89
+ Returns:
90
+ list[dict[str, Any]]: A report on the execution
91
+ """
92
+ try:
93
+ automation = importlib.import_module(f"howler.actions.{operation_id}")
94
+ except Exception as e:
95
+ logger.critical("Error when importing %s - %s", operation_id, e)
96
+
97
+ return [
98
+ {
99
+ "query": query,
100
+ "outcome": "error",
101
+ "title": "Unknown Action",
102
+ "message": f"The operation ID provided ({operation_id}) does not match any enabled operations.",
103
+ }
104
+ ]
105
+
106
+ missing_roles = set(automation.specification()["roles"]) - set(user["type"])
107
+ if missing_roles:
108
+ return [
109
+ {
110
+ "query": query,
111
+ "outcome": "error",
112
+ "title": "Insufficient permissions",
113
+ "message": (
114
+ f"The operation ID provided ({operation_id}) requires permissions you do not have "
115
+ f"({', '.join(missing_roles)}). Contact HOWLER Support for more information."
116
+ ),
117
+ }
118
+ ]
119
+
120
+ report = automation.execute(query=query, request_id=request_id, user=user, **kwargs)
121
+
122
+ return __sanitize_report(report)
123
+
124
+
125
+ def specifications() -> list[dict[str, Any]]:
126
+ """A list of specifications for the available operations
127
+
128
+ Returns:
129
+ list[dict[str, Any]]: A list of specifications
130
+ """
131
+ specifications = []
132
+
133
+ module_paths = {"howler": Path(__file__).parent}
134
+
135
+ for plugin in config.core.plugins:
136
+ plugin_module_path = PLUGIN_PATH / plugin / "actions"
137
+ if plugin_module_path.exists():
138
+ module_paths[plugin] = plugin_module_path
139
+
140
+ for module_name, module_path in module_paths.items():
141
+ for module in (
142
+ _file
143
+ for _file in module_path.iterdir()
144
+ if _file.suffix == ".py" and _file.name not in ["__init__.py", "example_plugin.py"]
145
+ ):
146
+ try:
147
+ automation = importlib.import_module(f"{module_name}.actions.{module.stem}")
148
+
149
+ specifications.append(__sanitize_specification(automation.specification()))
150
+
151
+ except Exception: # pragma: no cover
152
+ logger.exception("Error when initializing %s", module)
153
+
154
+ return specifications
@@ -0,0 +1,111 @@
1
+ from typing import Optional
2
+
3
+ from howler.common.loader import datastore
4
+ from howler.datastore.operations import OdmHelper
5
+ from howler.odm.models.action import VALID_TRIGGERS
6
+ from howler.odm.models.hit import Hit
7
+ from howler.odm.models.howler_data import Label
8
+ from howler.utils.str_utils import sanitize_lucene_query
9
+
10
+ hit_helper = OdmHelper(Hit)
11
+
12
+ OPERATION_ID = "add_label"
13
+
14
+ CATEGORIES = list(Label.fields().keys())
15
+
16
+
17
+ def execute(query: str, category: str = "generic", label: Optional[str] = None, **kwargs):
18
+ """Add a label to a hit.
19
+
20
+ Args:
21
+ query (str): The query on which to apply this automation.
22
+ category (str, optional): The category of label to add. Defaults to "generic".
23
+ label (str): The label content. Defaults to None.
24
+ """
25
+ if category not in CATEGORIES:
26
+ return [
27
+ {
28
+ "query": query,
29
+ "outcome": "error",
30
+ "title": "Invalid Category",
31
+ "message": f"'{category}' is not a valid category.",
32
+ }
33
+ ]
34
+
35
+ if not label:
36
+ return [
37
+ {
38
+ "query": query,
39
+ "outcome": "error",
40
+ "title": "Invalid Label",
41
+ "message": "Label cannot be empty.",
42
+ }
43
+ ]
44
+
45
+ report = []
46
+
47
+ ds = datastore()
48
+
49
+ skipped_hits = ds.hit.search(
50
+ f"({query}) AND howler.labels.{category}:{sanitize_lucene_query((label))}",
51
+ fl="howler.id",
52
+ )["items"]
53
+
54
+ if len(skipped_hits) > 0:
55
+ report.append(
56
+ {
57
+ "query": f"howler.id:({' OR '.join(h.howler.id for h in skipped_hits)})",
58
+ "outcome": "skipped",
59
+ "title": "Skipped Hit with Label",
60
+ "message": f"These hits already have the label {label}.",
61
+ }
62
+ )
63
+
64
+ try:
65
+ ds.hit.update_by_query(
66
+ query,
67
+ [hit_helper.list_add(f"howler.labels.{category}", label, if_missing=True)],
68
+ )
69
+
70
+ report.append(
71
+ {
72
+ "query": query,
73
+ "outcome": "success",
74
+ "title": "Executed Successfully",
75
+ "message": f"Label '{label}' added to category '{category}' for all matching hits.",
76
+ }
77
+ )
78
+ except Exception as e:
79
+ report.append(
80
+ {
81
+ "query": query,
82
+ "outcome": "error",
83
+ "title": "Failed to Execute",
84
+ "message": f"Unknown exception occurred: {str(e)}",
85
+ }
86
+ )
87
+
88
+ return report
89
+
90
+
91
+ def specification():
92
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
93
+ return {
94
+ "id": OPERATION_ID,
95
+ "title": "Add Label",
96
+ "priority": 8,
97
+ "i18nKey": "operations.add_label",
98
+ "description": {
99
+ "short": "Add a label to a hit",
100
+ "long": execute.__doc__,
101
+ },
102
+ "roles": ["automation_basic"],
103
+ "steps": [
104
+ {
105
+ "args": {"category": [], "label": []},
106
+ "options": {"category": CATEGORIES},
107
+ "validation": {"warn": {"query": "howler.labels.$category:$label"}},
108
+ }
109
+ ],
110
+ "triggers": VALID_TRIGGERS,
111
+ }
@@ -0,0 +1,159 @@
1
+ from typing import Optional
2
+
3
+ from howler.common.loader import datastore
4
+ from howler.datastore.operations import OdmHelper
5
+ from howler.odm.models.action import VALID_TRIGGERS
6
+ from howler.odm.models.hit import Hit
7
+ from howler.services import hit_service
8
+ from howler.utils.str_utils import sanitize_lucene_query
9
+
10
+ hit_helper = OdmHelper(Hit)
11
+
12
+ OPERATION_ID = "add_to_bundle"
13
+
14
+
15
+ def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
16
+ """Add a set of hits matching the query to the specified bundle.
17
+
18
+ Args:
19
+ query (str): The query containing the matching hits
20
+ bundle_id (str): The `howler.id` of the bundle to add the hits to.
21
+ """
22
+ report = []
23
+
24
+ if not bundle_id:
25
+ return [
26
+ {
27
+ "query": query,
28
+ "outcome": "error",
29
+ "title": "Invalid Bundle ID",
30
+ "message": "Bundle ID cannot be empty.",
31
+ }
32
+ ]
33
+
34
+ try:
35
+ bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
36
+ if not bundle_hit or not bundle_hit.howler.is_bundle:
37
+ report.append(
38
+ {
39
+ "query": query,
40
+ "outcome": "error",
41
+ "title": "Invalid Bundle",
42
+ "message": f"Either a hit with ID {bundle_id} does not exist, or it is not a bundle.",
43
+ }
44
+ )
45
+ return report
46
+
47
+ ds = datastore()
48
+
49
+ skipped_hits_bundles = ds.hit.search(
50
+ f"({query}) AND howler.is_bundle:true",
51
+ fl="howler.id",
52
+ )["items"]
53
+
54
+ if len(skipped_hits_bundles) > 0:
55
+ report.append(
56
+ {
57
+ "query": f"({query}) AND howler.is_bundle:true",
58
+ "outcome": "skipped",
59
+ "title": "Skipped Bundles",
60
+ "message": "Bundles cannot be added to a bundle.",
61
+ }
62
+ )
63
+
64
+ skipped_hits_already_added = ds.hit.search(
65
+ f"({query}) AND (howler.bundles:{sanitize_lucene_query(bundle_id)})",
66
+ fl="howler.id",
67
+ )["items"]
68
+
69
+ if len(skipped_hits_already_added) > 0:
70
+ report.append(
71
+ {
72
+ "query": f"({query}) AND (howler.bundles:{sanitize_lucene_query(bundle_id)})",
73
+ "outcome": "skipped",
74
+ "title": "Skipped Hits",
75
+ "message": "These hits have already been added to the specified bundle.",
76
+ }
77
+ )
78
+
79
+ safe_query = f"({query}) AND (-howler.bundles:({sanitize_lucene_query(bundle_id)}) AND howler.is_bundle:false)"
80
+ matching_hits = ds.hit.search(safe_query)["items"]
81
+ if len(matching_hits) < 1:
82
+ report.append(
83
+ {
84
+ "query": safe_query,
85
+ "outcome": "skipped",
86
+ "title": "No Matching Hits",
87
+ "message": "There were no hits matching this query.",
88
+ }
89
+ )
90
+ return report
91
+
92
+ ds.hit.update_by_query(
93
+ safe_query,
94
+ [hit_helper.list_add("howler.bundles", sanitize_lucene_query(bundle_id), if_missing=True)],
95
+ )
96
+
97
+ operations = [
98
+ hit_helper.list_add(
99
+ "howler.hits",
100
+ hit["howler"]["id"],
101
+ if_missing=True,
102
+ )
103
+ for hit in matching_hits
104
+ ]
105
+
106
+ operations.append(hit_helper.update("howler.bundle_size", len(operations)))
107
+ hit_service.update_hit(
108
+ bundle_id,
109
+ operations,
110
+ )
111
+ bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
112
+ report.append(
113
+ {
114
+ "query": safe_query.replace("-howler.bundles", "howler.bundles"),
115
+ "outcome": "success",
116
+ "title": "Executed Successfully",
117
+ "message": "The specified bundle has had all matching hits added.",
118
+ }
119
+ )
120
+ except Exception as e:
121
+ report.append(
122
+ {
123
+ "query": query,
124
+ "outcome": "error",
125
+ "title": "Failed to Execute",
126
+ "message": f"Unknown exception occurred: {str(e)}",
127
+ }
128
+ )
129
+
130
+ return report
131
+
132
+
133
+ def specification():
134
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
135
+ return {
136
+ "id": OPERATION_ID,
137
+ "title": "Add to Bundle",
138
+ "priority": 6,
139
+ "i18nKey": f"operations.{OPERATION_ID}",
140
+ "description": {
141
+ "short": "Add a set of hits to a bundle",
142
+ "long": execute.__doc__,
143
+ },
144
+ "roles": ["automation_basic"],
145
+ "steps": [
146
+ {
147
+ "args": {"bundle_id": []},
148
+ "options": {},
149
+ "validation": {
150
+ "warn": {"query": "howler.bundles:($bundle_id) OR howler.is_bundle:true"},
151
+ "error": {
152
+ "query": "howler.id:$bundle_id AND howler.is_bundle:false",
153
+ "message": "The bundle id given must be a bundle.",
154
+ },
155
+ },
156
+ }
157
+ ],
158
+ "triggers": VALID_TRIGGERS,
159
+ }
@@ -0,0 +1,76 @@
1
+ from howler.common.loader import datastore
2
+ from howler.datastore.operations import OdmHelper
3
+ from howler.odm.models.action import VALID_TRIGGERS
4
+ from howler.odm.models.hit import Hit
5
+
6
+ hit_helper = OdmHelper(Hit)
7
+
8
+ OPERATION_ID = "change_field"
9
+
10
+
11
+ def execute(query: str, field: str, value: str, **kwargs):
12
+ """Change one of the fields of a hit
13
+
14
+ Args:
15
+ query (str): The query to run this action on
16
+ field (str): The field to update.
17
+ value (str): The value to set it to. Must be a string.
18
+ """
19
+ if field not in Hit.flat_fields():
20
+ return [
21
+ {
22
+ "query": query,
23
+ "outcome": "error",
24
+ "title": "Invalid field",
25
+ "message": (f"Field '{field}' does not exist. You must pick a valid entry from the howler index."),
26
+ }
27
+ ]
28
+
29
+ report = []
30
+
31
+ try:
32
+ datastore().hit.update_by_query(
33
+ query,
34
+ [hit_helper.update(field, value)],
35
+ )
36
+
37
+ report.append(
38
+ {
39
+ "query": query,
40
+ "outcome": "success",
41
+ "title": "Executed Successfully",
42
+ "message": f"Field '{field}' updated to value '{value}' for all matching hits.",
43
+ }
44
+ )
45
+ except Exception as e:
46
+ report.append(
47
+ {
48
+ "query": query,
49
+ "outcome": "error",
50
+ "title": "Failed to Execute",
51
+ "message": str(e),
52
+ }
53
+ )
54
+
55
+ return report
56
+
57
+
58
+ def specification():
59
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
60
+ return {
61
+ "id": OPERATION_ID,
62
+ "title": "Change Field",
63
+ "i18nKey": f"operations.{OPERATION_ID}",
64
+ "description": {
65
+ "short": "Change one of the fields of a hit",
66
+ "long": execute.__doc__,
67
+ },
68
+ "roles": ["automation_advanced", "admin"],
69
+ "steps": [
70
+ {
71
+ "args": {"field": [], "value": []},
72
+ "options": {"field": list(Hit.flat_fields().keys())},
73
+ }
74
+ ],
75
+ "triggers": VALID_TRIGGERS,
76
+ }