howler-api 2.13.0.dev329__py3-none-any.whl

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.

Potentially problematic release.


This version of howler-api might be problematic. Click here for more details.

Files changed (200) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +167 -0
  3. howler/actions/add_label.py +111 -0
  4. howler/actions/add_to_bundle.py +159 -0
  5. howler/actions/change_field.py +76 -0
  6. howler/actions/demote.py +160 -0
  7. howler/actions/example_plugin.py +104 -0
  8. howler/actions/prioritization.py +93 -0
  9. howler/actions/promote.py +147 -0
  10. howler/actions/remove_from_bundle.py +133 -0
  11. howler/actions/remove_label.py +111 -0
  12. howler/actions/transition.py +200 -0
  13. howler/api/__init__.py +249 -0
  14. howler/api/base.py +88 -0
  15. howler/api/socket.py +114 -0
  16. howler/api/v1/__init__.py +97 -0
  17. howler/api/v1/action.py +372 -0
  18. howler/api/v1/analytic.py +748 -0
  19. howler/api/v1/auth.py +382 -0
  20. howler/api/v1/borealis.py +101 -0
  21. howler/api/v1/configs.py +55 -0
  22. howler/api/v1/dossier.py +222 -0
  23. howler/api/v1/help.py +28 -0
  24. howler/api/v1/hit.py +1181 -0
  25. howler/api/v1/notebook.py +82 -0
  26. howler/api/v1/overview.py +191 -0
  27. howler/api/v1/search.py +715 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +414 -0
  31. howler/api/v1/utils/__init__.py +0 -0
  32. howler/api/v1/utils/etag.py +84 -0
  33. howler/api/v1/view.py +288 -0
  34. howler/app.py +235 -0
  35. howler/common/README.md +144 -0
  36. howler/common/__init__.py +0 -0
  37. howler/common/classification.py +979 -0
  38. howler/common/classification.yml +107 -0
  39. howler/common/exceptions.py +167 -0
  40. howler/common/hexdump.py +48 -0
  41. howler/common/iprange.py +171 -0
  42. howler/common/loader.py +154 -0
  43. howler/common/logging/__init__.py +241 -0
  44. howler/common/logging/audit.py +138 -0
  45. howler/common/logging/format.py +38 -0
  46. howler/common/net.py +79 -0
  47. howler/common/net_static.py +1494 -0
  48. howler/common/random_user.py +316 -0
  49. howler/common/swagger.py +117 -0
  50. howler/config.py +64 -0
  51. howler/cronjobs/__init__.py +29 -0
  52. howler/cronjobs/retention.py +61 -0
  53. howler/cronjobs/rules.py +274 -0
  54. howler/cronjobs/view_cleanup.py +88 -0
  55. howler/datastore/README.md +112 -0
  56. howler/datastore/__init__.py +0 -0
  57. howler/datastore/bulk.py +72 -0
  58. howler/datastore/collection.py +2327 -0
  59. howler/datastore/constants.py +117 -0
  60. howler/datastore/exceptions.py +41 -0
  61. howler/datastore/howler_store.py +105 -0
  62. howler/datastore/migrations/fix_process.py +41 -0
  63. howler/datastore/operations.py +130 -0
  64. howler/datastore/schemas.py +90 -0
  65. howler/datastore/store.py +231 -0
  66. howler/datastore/support/__init__.py +0 -0
  67. howler/datastore/support/build.py +214 -0
  68. howler/datastore/support/schemas.py +90 -0
  69. howler/datastore/types.py +22 -0
  70. howler/error.py +91 -0
  71. howler/external/__init__.py +0 -0
  72. howler/external/generate_mitre.py +96 -0
  73. howler/external/generate_sigma_rules.py +31 -0
  74. howler/external/generate_tlds.py +47 -0
  75. howler/external/reindex_data.py +46 -0
  76. howler/external/wipe_databases.py +58 -0
  77. howler/gunicorn_config.py +25 -0
  78. howler/healthz.py +47 -0
  79. howler/helper/__init__.py +0 -0
  80. howler/helper/azure.py +50 -0
  81. howler/helper/discover.py +59 -0
  82. howler/helper/hit.py +236 -0
  83. howler/helper/oauth.py +247 -0
  84. howler/helper/search.py +92 -0
  85. howler/helper/workflow.py +110 -0
  86. howler/helper/ws.py +378 -0
  87. howler/odm/README.md +102 -0
  88. howler/odm/__init__.py +1 -0
  89. howler/odm/base.py +1504 -0
  90. howler/odm/charter.txt +146 -0
  91. howler/odm/helper.py +416 -0
  92. howler/odm/howler_enum.py +25 -0
  93. howler/odm/models/__init__.py +0 -0
  94. howler/odm/models/action.py +33 -0
  95. howler/odm/models/analytic.py +90 -0
  96. howler/odm/models/assemblyline.py +48 -0
  97. howler/odm/models/aws.py +23 -0
  98. howler/odm/models/azure.py +16 -0
  99. howler/odm/models/cbs.py +44 -0
  100. howler/odm/models/config.py +558 -0
  101. howler/odm/models/dossier.py +33 -0
  102. howler/odm/models/ecs/__init__.py +0 -0
  103. howler/odm/models/ecs/agent.py +17 -0
  104. howler/odm/models/ecs/autonomous_system.py +16 -0
  105. howler/odm/models/ecs/client.py +149 -0
  106. howler/odm/models/ecs/cloud.py +141 -0
  107. howler/odm/models/ecs/code_signature.py +27 -0
  108. howler/odm/models/ecs/container.py +32 -0
  109. howler/odm/models/ecs/dns.py +62 -0
  110. howler/odm/models/ecs/egress.py +10 -0
  111. howler/odm/models/ecs/elf.py +74 -0
  112. howler/odm/models/ecs/email.py +122 -0
  113. howler/odm/models/ecs/error.py +14 -0
  114. howler/odm/models/ecs/event.py +140 -0
  115. howler/odm/models/ecs/faas.py +24 -0
  116. howler/odm/models/ecs/file.py +84 -0
  117. howler/odm/models/ecs/geo.py +30 -0
  118. howler/odm/models/ecs/group.py +18 -0
  119. howler/odm/models/ecs/hash.py +16 -0
  120. howler/odm/models/ecs/host.py +17 -0
  121. howler/odm/models/ecs/http.py +37 -0
  122. howler/odm/models/ecs/ingress.py +12 -0
  123. howler/odm/models/ecs/interface.py +21 -0
  124. howler/odm/models/ecs/network.py +30 -0
  125. howler/odm/models/ecs/observer.py +45 -0
  126. howler/odm/models/ecs/organization.py +12 -0
  127. howler/odm/models/ecs/os.py +21 -0
  128. howler/odm/models/ecs/pe.py +17 -0
  129. howler/odm/models/ecs/process.py +216 -0
  130. howler/odm/models/ecs/registry.py +26 -0
  131. howler/odm/models/ecs/related.py +45 -0
  132. howler/odm/models/ecs/rule.py +51 -0
  133. howler/odm/models/ecs/server.py +24 -0
  134. howler/odm/models/ecs/threat.py +247 -0
  135. howler/odm/models/ecs/tls.py +58 -0
  136. howler/odm/models/ecs/url.py +51 -0
  137. howler/odm/models/ecs/user.py +57 -0
  138. howler/odm/models/ecs/user_agent.py +20 -0
  139. howler/odm/models/ecs/vulnerability.py +41 -0
  140. howler/odm/models/gcp.py +16 -0
  141. howler/odm/models/hit.py +356 -0
  142. howler/odm/models/howler_data.py +328 -0
  143. howler/odm/models/lead.py +33 -0
  144. howler/odm/models/localized_label.py +13 -0
  145. howler/odm/models/overview.py +16 -0
  146. howler/odm/models/pivot.py +40 -0
  147. howler/odm/models/template.py +24 -0
  148. howler/odm/models/user.py +83 -0
  149. howler/odm/models/view.py +34 -0
  150. howler/odm/random_data.py +888 -0
  151. howler/odm/randomizer.py +606 -0
  152. howler/patched.py +5 -0
  153. howler/plugins/__init__.py +25 -0
  154. howler/plugins/config.py +123 -0
  155. howler/remote/__init__.py +0 -0
  156. howler/remote/datatypes/README.md +355 -0
  157. howler/remote/datatypes/__init__.py +98 -0
  158. howler/remote/datatypes/counters.py +63 -0
  159. howler/remote/datatypes/events.py +66 -0
  160. howler/remote/datatypes/hash.py +206 -0
  161. howler/remote/datatypes/lock.py +42 -0
  162. howler/remote/datatypes/queues/__init__.py +0 -0
  163. howler/remote/datatypes/queues/comms.py +59 -0
  164. howler/remote/datatypes/queues/multi.py +32 -0
  165. howler/remote/datatypes/queues/named.py +93 -0
  166. howler/remote/datatypes/queues/priority.py +215 -0
  167. howler/remote/datatypes/set.py +118 -0
  168. howler/remote/datatypes/user_quota_tracker.py +54 -0
  169. howler/security/__init__.py +253 -0
  170. howler/security/socket.py +108 -0
  171. howler/security/utils.py +185 -0
  172. howler/services/__init__.py +0 -0
  173. howler/services/action_service.py +111 -0
  174. howler/services/analytic_service.py +128 -0
  175. howler/services/auth_service.py +323 -0
  176. howler/services/config_service.py +128 -0
  177. howler/services/dossier_service.py +252 -0
  178. howler/services/event_service.py +93 -0
  179. howler/services/hit_service.py +893 -0
  180. howler/services/jwt_service.py +158 -0
  181. howler/services/lucene_service.py +286 -0
  182. howler/services/notebook_service.py +119 -0
  183. howler/services/overview_service.py +44 -0
  184. howler/services/template_service.py +45 -0
  185. howler/services/user_service.py +330 -0
  186. howler/utils/__init__.py +0 -0
  187. howler/utils/annotations.py +28 -0
  188. howler/utils/chunk.py +38 -0
  189. howler/utils/dict_utils.py +200 -0
  190. howler/utils/isotime.py +17 -0
  191. howler/utils/list_utils.py +11 -0
  192. howler/utils/lucene.py +77 -0
  193. howler/utils/path.py +27 -0
  194. howler/utils/socket_utils.py +61 -0
  195. howler/utils/str_utils.py +256 -0
  196. howler/utils/uid.py +47 -0
  197. howler_api-2.13.0.dev329.dist-info/METADATA +71 -0
  198. howler_api-2.13.0.dev329.dist-info/RECORD +200 -0
  199. howler_api-2.13.0.dev329.dist-info/WHEEL +4 -0
  200. howler_api-2.13.0.dev329.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,97 @@
1
+ from textwrap import dedent
2
+
3
+ from flask import Blueprint, current_app, request
4
+
5
+ from howler.api import ok
6
+ from howler.security import api_login
7
+
8
+ API_PREFIX = "/api/v1"
9
+ apiv1 = Blueprint("apiv1", __name__, url_prefix=API_PREFIX)
10
+ apiv1._doc = "Api Documentation Version 1" # type: ignore[attr-defined]
11
+
12
+
13
+ @apiv1.route("/")
14
+ @api_login(audit=False, required_priv=["R", "W"], required_type=["user", "admin"])
15
+ def get_api_documentation(**kwargs):
16
+ """Full API doc.
17
+
18
+ Loop through all registered API paths and display their documentation.
19
+ Returns a list of API definition.
20
+
21
+ Variables:
22
+ None
23
+
24
+ Arguments:
25
+ None
26
+
27
+ Result Example:
28
+ [
29
+ {
30
+ 'name': "Api Doc", # Name of the api
31
+ 'path': "/api/path/<variable>/", # API path
32
+ 'ui_only': false, # Is UI only API
33
+ 'methods': ["GET", "POST"], # Allowed HTTP methods
34
+ 'description': "API doc.", # API documentation
35
+ 'id': "api_doc", # Unique ID for the API
36
+ 'function': "apiv1.api_doc", # Function called in the code
37
+ 'protected': False, # Does the API require login?
38
+ 'required_type': ['user'], # Type of users allowed to use API
39
+ 'complete' : True # Is the API stable?
40
+ },
41
+ ]
42
+ """
43
+ user_types = kwargs["user"]["type"]
44
+
45
+ api_blueprints = {}
46
+ api_list = []
47
+ for rule in current_app.url_map.iter_rules():
48
+ if rule.rule.startswith(request.path):
49
+ methods = [item for item in (rule.methods or []) if item != "OPTIONS" and item != "HEAD"]
50
+
51
+ func = current_app.view_functions[rule.endpoint]
52
+ required_type = func.__dict__.get("required_type", ["user"])
53
+
54
+ for u_type in user_types:
55
+ if u_type in required_type:
56
+ doc_string = func.__doc__
57
+ func_title = " ".join(
58
+ [x.capitalize() for x in rule.endpoint[rule.endpoint.rindex(".") + 1 :].split("_")]
59
+ )
60
+ blueprint = rule.endpoint[: rule.endpoint.rindex(".")]
61
+ if blueprint == "apiv1":
62
+ blueprint = "documentation"
63
+
64
+ if blueprint not in api_blueprints:
65
+ try:
66
+ doc = current_app.blueprints[rule.endpoint[: rule.endpoint.rindex(".")]]._doc # type: ignore[attr-defined]
67
+ except Exception:
68
+ doc = ""
69
+
70
+ api_blueprints[blueprint] = doc
71
+
72
+ if doc_string:
73
+ description = dedent(doc_string)
74
+ else:
75
+ description = "[INCOMPLETE]\n\nTHIS API HAS NOT BEEN DOCUMENTED YET!"
76
+
77
+ api_id = rule.endpoint.replace("apiv1.", "").replace(".", "_")
78
+
79
+ api_list.append(
80
+ {
81
+ "protected": func.__dict__.get("protected", False),
82
+ "required_type": sorted(required_type),
83
+ "name": func_title,
84
+ "id": api_id,
85
+ "function": f"api.v1.{rule.endpoint}",
86
+ "path": rule.rule,
87
+ "ui_only": rule.rule.startswith("%sui/" % request.path),
88
+ "methods": sorted(methods),
89
+ "description": description,
90
+ "complete": "[INCOMPLETE]" not in description,
91
+ "required_priv": func.__dict__.get("required_priv", []),
92
+ }
93
+ )
94
+
95
+ break
96
+
97
+ return ok({"apis": api_list, "blueprints": api_blueprints})
@@ -0,0 +1,372 @@
1
+ import json
2
+
3
+ from flask import Response, request
4
+
5
+ import howler.actions as actions
6
+ from howler.api import bad_request, created, forbidden, internal_error, make_subapi_blueprint, no_content, not_found, ok
7
+ from howler.common.exceptions import HowlerException
8
+ from howler.common.loader import datastore
9
+ from howler.common.logging.audit import audit
10
+ from howler.common.swagger import generate_swagger_docs
11
+ from howler.config import CLASSIFICATION
12
+ from howler.odm.models.action import Action
13
+ from howler.odm.models.user import User
14
+ from howler.security import api_login
15
+ from howler.services import action_service
16
+
17
+ SUB_API = "action"
18
+ classification_definition = CLASSIFICATION.get_parsed_classification_definition()
19
+
20
+ action_api = make_subapi_blueprint(SUB_API, api_version=1)
21
+ action_api._doc = "Endpoints relating to bulk actions and automation"
22
+
23
+
24
+ @generate_swagger_docs()
25
+ @action_api.route("/")
26
+ @api_login(audit=False, check_xsrf_token=False, required_type=["automation_basic"])
27
+ def get_actions(**_) -> Response:
28
+ """Get a list of existing actions
29
+
30
+ Variables:
31
+ None
32
+
33
+ Optional Arguments:
34
+ None
35
+
36
+ Result Example:
37
+ [
38
+ ...actions # A list of actions the user can see
39
+ ]
40
+ """
41
+ return ok(datastore().action.search("*:*", as_obj=False)["items"])
42
+
43
+
44
+ @generate_swagger_docs()
45
+ @action_api.route("/", methods=["POST"])
46
+ @api_login(audit=False, check_xsrf_token=False, required_type=["automation_basic"])
47
+ def add_action(user: User, **_) -> Response:
48
+ """Create a new action
49
+
50
+ Variables:
51
+ None
52
+
53
+ Optional Arguments:
54
+ None
55
+
56
+ Data Block:
57
+ {
58
+ "name": "New Action", # An action name (human readable)
59
+ "query": "howler.id:*", # The query to execute when triggering this action
60
+ "operations": [ # A list of operations to execute
61
+ {
62
+ "operation_id": "add_label", # The id of the operation to run
63
+ "data_json": "{'category': 'generic', 'label': 'assigned'}" # Various requisite values for the operation
64
+ }
65
+ ]
66
+ }
67
+
68
+ Result Example:
69
+ {
70
+ ...action # The saved action data
71
+ }
72
+ """
73
+ new_action = request.json
74
+
75
+ if new_action is None:
76
+ return bad_request(err="You must specify an action")
77
+
78
+ if error := action_service.validate_action(new_action):
79
+ return error
80
+
81
+ try:
82
+ new_action["owner_id"] = user.uname
83
+
84
+ action_obj = Action(new_action)
85
+
86
+ ds = datastore()
87
+ ds.action.save(action_obj.action_id, action_obj)
88
+ ds.action.commit()
89
+ except HowlerException as e:
90
+ return bad_request(err=str(e))
91
+
92
+ return created(action_obj)
93
+
94
+
95
+ @generate_swagger_docs()
96
+ @action_api.route("/<id>", methods=["PUT", "PATCH"])
97
+ @api_login(
98
+ audit=False,
99
+ check_xsrf_token=False,
100
+ required_type=["automation_basic"],
101
+ )
102
+ def update_action(id: str, user: User, **_) -> Response:
103
+ """Update an existing action
104
+
105
+ Variables:
106
+ id => id of the aciton to update
107
+
108
+ Optional Arguments:
109
+ None
110
+
111
+ Data Block:
112
+ {
113
+ "name": "New Action", # An action name (human readable)
114
+ "query": "howler.id:*", # The query to execute when triggering this action
115
+ "actions": [ # A list of actions to execute
116
+ {
117
+ "operation_id": "add_label", # The id of the action to run
118
+ "data_json": "{ 'category': 'generic', 'label': 'assigned' }" # Various requisite values for the action
119
+ }
120
+ ]
121
+ }
122
+
123
+ Result Example:
124
+ {
125
+ ...action # The saved action data
126
+ }
127
+ """
128
+ updated_action = request.json
129
+ if not isinstance(updated_action, dict):
130
+ return bad_request(err="Incorrect data structure!")
131
+
132
+ ds = datastore()
133
+
134
+ existing_action = ds.action.get(id, as_obj=False)
135
+
136
+ if not existing_action:
137
+ return not_found(err="The specified automation does not exist")
138
+
139
+ if "automation_advanced" not in user.type and updated_action.get("triggers", []) != existing_action.get(
140
+ "triggers", []
141
+ ):
142
+ return forbidden(err="Updating triggers requires the role 'automation_advanced'.")
143
+
144
+ updated_action = {
145
+ **existing_action,
146
+ **updated_action,
147
+ "action_id": existing_action["action_id"],
148
+ }
149
+
150
+ if error := action_service.validate_action(updated_action):
151
+ return error
152
+
153
+ try:
154
+ action_obj = Action(updated_action)
155
+ action_obj.action_id = id
156
+
157
+ ds.action.save(action_obj.action_id, action_obj)
158
+ ds.action.commit()
159
+ except HowlerException as e:
160
+ return bad_request(err=str(e))
161
+
162
+ return ok(action_obj)
163
+
164
+
165
+ @generate_swagger_docs()
166
+ @action_api.route("/<id>", methods=["DELETE"])
167
+ @api_login(audit=True, check_xsrf_token=False, required_type=["automation_basic"])
168
+ def delete_action(id: str, user: User, **kwargs) -> Response:
169
+ """Delete an existing action
170
+
171
+ Variables:
172
+ id => The id of the action to delete
173
+
174
+ Optional Arguments:
175
+ None
176
+
177
+ Result Example:
178
+ None
179
+ """
180
+ ds = datastore()
181
+
182
+ result = ds.action.search(f"action_id:{id}", rows=1)
183
+
184
+ if not result["total"]:
185
+ return not_found(err="Action does not exist")
186
+
187
+ action: Action = result["items"][0]
188
+
189
+ if action.owner_id != user.uname and "admin" not in user.type:
190
+ return forbidden(err="You do not have the permissions necessary to delete this action.")
191
+
192
+ try:
193
+ ds.action.delete(id)
194
+ ds.action.commit()
195
+
196
+ return no_content()
197
+ except HowlerException as e:
198
+ return internal_error(err=str(e))
199
+
200
+
201
+ @generate_swagger_docs()
202
+ @action_api.route("/<id>/execute", methods=["POST"])
203
+ @api_login(audit=True, check_xsrf_token=False, required_type=["automation_basic"])
204
+ def execute_action(id: str, **kwargs) -> Response:
205
+ """Execute one or more actions on a given query
206
+
207
+ Variables:
208
+ id => The id of the action to execute
209
+
210
+ Optional Arguments:
211
+ None
212
+
213
+ Data Block:
214
+ {
215
+ "request_id": "abc123", # An id used to identify the request in websocket updates
216
+ "query": "howler.id:*" # An optional override query
217
+ }
218
+
219
+ Result Example:
220
+ {
221
+ "add_label": [ # Each entry corresponds to a given action ID
222
+ {
223
+ "query": "howler.id:*", # The query this portion of the report applies to
224
+ "title": "Execution Succeeeded", # The title of this section of the report
225
+ "message": "Label successfully added to 42 hits" # A longer explanation of this portion
226
+ }
227
+ ]
228
+ }
229
+ """
230
+ execute_req = request.json
231
+ if not isinstance(execute_req, dict):
232
+ return bad_request(err="Incorrect data structure!")
233
+
234
+ action: Action = datastore().action.get(id)
235
+
236
+ if not action:
237
+ return not_found(err="The specified action does not exist")
238
+
239
+ reports: dict[str, list[dict]] = {}
240
+ current_user = kwargs.get("user", None)
241
+
242
+ for operation in action.operations:
243
+ op_data = json.loads(operation["data_json"])
244
+
245
+ query = execute_req.get("query", action.query) or action.query
246
+
247
+ audit(
248
+ [],
249
+ {
250
+ **kwargs,
251
+ "query": query,
252
+ "operation_id": operation.operation_id,
253
+ **op_data,
254
+ },
255
+ current_user["uname"] if current_user is not None else "unknown",
256
+ current_user,
257
+ execute_action,
258
+ )
259
+
260
+ report = actions.execute(
261
+ operation_id=operation.operation_id,
262
+ request_id=execute_req["request_id"],
263
+ query=query,
264
+ user=current_user,
265
+ **op_data,
266
+ )
267
+
268
+ if operation.operation_id not in reports:
269
+ reports[operation.operation_id] = []
270
+
271
+ reports[operation.operation_id].extend(report)
272
+
273
+ return ok(reports)
274
+
275
+
276
+ @generate_swagger_docs()
277
+ @action_api.route("/operations")
278
+ @api_login(audit=False, check_xsrf_token=False, required_type=["automation_basic"])
279
+ def get_operations(**_) -> Response:
280
+ """Get a list of operations the user can run on a query
281
+
282
+ Variables:
283
+ None
284
+
285
+ Optional Arguments:
286
+ None
287
+
288
+ Result Example:
289
+ [
290
+ ...operations # A list of specifications for the operations the user can use
291
+ ]
292
+ """
293
+ return ok(actions.specifications())
294
+
295
+
296
+ @generate_swagger_docs()
297
+ @action_api.route("/execute", methods=["POST"])
298
+ @api_login(audit=True, check_xsrf_token=False, required_type=["automation_basic"])
299
+ def execute_operations(**kwargs) -> Response:
300
+ """Execute one or more operations on a given query
301
+
302
+ Variables:
303
+ None
304
+
305
+ Optional Arguments:
306
+ None
307
+
308
+ Data Block:
309
+ {
310
+ "query": "howler.id:*", # The query to run
311
+ "request_id": "abc123", # An id used to identify the request in websocket updates
312
+ "operations": [ # A list of operations to execute
313
+ {
314
+ "operation_id": "add_label", # The id of the action to run
315
+ "data_json": { "category": "generic", "label": "assigned" } # Various requisite values for the action
316
+ }
317
+ ]
318
+ }
319
+
320
+ Result Example:
321
+ {
322
+ "add_label": [ # Each entry corresponds to a given operation ID
323
+ {
324
+ "query": "howler.id:*", # The query this portion of the report applies to
325
+ "title": "Execution Succeeeded", # The title of this section of the report
326
+ "message": "Label successfully added to 42 hits" # A longer explanation of this portion
327
+ }
328
+ ]
329
+ }
330
+ """
331
+ execute_req = request.json
332
+ if not isinstance(execute_req, dict):
333
+ return bad_request(err="Incorrect data structure!")
334
+
335
+ reports: dict[str, list[dict]] = {}
336
+ current_user = kwargs.get("user", None)
337
+ operations = execute_req["operations"]
338
+
339
+ operation_ids = [o["operation_id"] for o in operations]
340
+ if len(operation_ids) != len(set(operation_ids)):
341
+ return bad_request(err="You must have a maximum of one operation of each type in request.")
342
+
343
+ for operation in operations:
344
+ op_data = json.loads(operation["data_json"])
345
+
346
+ audit(
347
+ [],
348
+ {
349
+ **kwargs,
350
+ "query": execute_req["query"],
351
+ "operation_id": operation["operation_id"],
352
+ **op_data,
353
+ },
354
+ current_user["uname"] if current_user is not None else "unknown",
355
+ current_user,
356
+ execute_operations,
357
+ )
358
+
359
+ report = actions.execute(
360
+ operation_id=operation["operation_id"],
361
+ request_id=execute_req["request_id"],
362
+ query=execute_req["query"],
363
+ user=current_user,
364
+ **op_data,
365
+ )
366
+
367
+ if operation["operation_id"] not in reports:
368
+ reports[operation["operation_id"]] = []
369
+
370
+ reports[operation["operation_id"]].extend(report)
371
+
372
+ return ok(reports)