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,715 @@
1
+ from typing import Any, Union
2
+
3
+ from elasticsearch import BadRequestError
4
+ from flask import request
5
+ from sigma.backends.elasticsearch import LuceneBackend
6
+ from sigma.rule import SigmaRule
7
+ from werkzeug.exceptions import BadRequest
8
+ from yaml.scanner import ScannerError
9
+
10
+ from howler.api import bad_request, make_subapi_blueprint, ok
11
+ from howler.common.loader import datastore
12
+ from howler.common.logging import get_logger
13
+ from howler.common.swagger import generate_swagger_docs
14
+ from howler.datastore.exceptions import SearchException
15
+ from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
16
+ from howler.security import api_login
17
+ from howler.services import hit_service
18
+
19
+ SUB_API = "search"
20
+ search_api = make_subapi_blueprint(SUB_API, api_version=1)
21
+ search_api._doc = "Perform search queries"
22
+
23
+ logger = get_logger(__file__)
24
+
25
+
26
+ def generate_params(request, fields, multi_fields, params=None):
27
+ """Generate a list of parameters, combining the request data and the query arguments"""
28
+ # I hate you, python
29
+ if params is None:
30
+ params = {}
31
+
32
+ if request.method == "POST":
33
+ try:
34
+ req_data = request.json
35
+ except BadRequest:
36
+ req_data = {"query": "*:*"}
37
+
38
+ params = {
39
+ **params,
40
+ **{k: req_data[k] for k in fields if k in req_data},
41
+ **{k: req_data[k] for k in multi_fields if k in req_data},
42
+ }
43
+
44
+ else:
45
+ req_data = request.args
46
+ params = {
47
+ **params,
48
+ **{k: req_data[k] for k in fields if k in req_data},
49
+ **{k: req_data.getlist(k, None) for k in multi_fields if k in req_data},
50
+ }
51
+
52
+ return params, req_data
53
+
54
+
55
+ @generate_swagger_docs()
56
+ @search_api.route("/<index>", methods=["GET", "POST"])
57
+ @api_login(required_priv=["R"])
58
+ def search(index, **kwargs):
59
+ """Search through specified index for a given query. Uses lucene search syntax for query.
60
+
61
+ Variables:
62
+ index => Index to search in (hit, user,...)
63
+
64
+ Arguments:
65
+ query => Query to search for
66
+
67
+ Optional Arguments:
68
+ deep_paging_id => ID of the next page or * to start deep paging
69
+ filters => List of additional filter queries limit the data
70
+ offset => Offset in the results
71
+ rows => Number of results per page
72
+ sort => How to sort the results (not available in deep paging)
73
+ fl => List of fields to return
74
+ timeout => Maximum execution time (ms)
75
+ use_archive => Allow access to the datastore achive (Default: False)
76
+ track_total_hits => Track the total number of query matches, instead of stopping at 10000 (Default: False)
77
+ metadata => A list of additional features to be added to the result alongside the raw results
78
+
79
+ Data Block:
80
+ # Note that the data block is for POST requests only!
81
+ {"query": "query", # Query to search for
82
+ "offset": 0, # Offset in the results
83
+ "rows": 100, # Max number of results
84
+ "sort": "field asc", # How to sort the results
85
+ "fl": "id,score", # List of fields to return
86
+ "timeout": 1000, # Maximum execution time (ms)
87
+ "filters": ['fq'], # List of additional filter queries limit the data
88
+ "metadata": ["dossiers"]} # List of additional features to add to the search
89
+
90
+
91
+ Result Example:
92
+ {"total": 201, # Total results found
93
+ "offset": 0, # Offset in the result list
94
+ "rows": 100, # Number of results returned
95
+ "next_deep_paging_id": "asX3f...342", # ID to pass back for the next page during deep paging
96
+ "items": []} # List of results
97
+ """
98
+ user = kwargs["user"]
99
+ collection = get_collection(index, user)
100
+ default_sort = get_default_sort(index, user)
101
+
102
+ if collection is None or default_sort is None:
103
+ return bad_request(err=f"Not a valid index to search in: {index}")
104
+
105
+ fields = [
106
+ "offset",
107
+ "rows",
108
+ "sort",
109
+ "fl",
110
+ "timeout",
111
+ "deep_paging_id",
112
+ "track_total_hits",
113
+ ]
114
+ multi_fields = ["filters", "metadata"]
115
+ boolean_fields = ["use_archive"]
116
+
117
+ params, req_data = generate_params(request, fields, multi_fields)
118
+
119
+ params.update(
120
+ {
121
+ k: str(req_data.get(k, "false")).lower() in ["true", ""]
122
+ for k in boolean_fields
123
+ if req_data.get(k, None) is not None
124
+ }
125
+ )
126
+
127
+ if has_access_control(index):
128
+ params.update({"access_control": user["access_control"]})
129
+
130
+ params["as_obj"] = False
131
+ params.update({"sort": (params.get("sort", None) or default_sort).split(",")})
132
+
133
+ query = req_data.get("query", None)
134
+ if not query:
135
+ return bad_request(err="There was no search query.")
136
+
137
+ try:
138
+ metadata = params.pop("metadata", [])
139
+ result = collection().search(query, **params)
140
+
141
+ if index == "hit" and len(metadata) > 0:
142
+ hit_service.augment_metadata(result["items"], metadata, user)
143
+
144
+ return ok(result)
145
+ except (SearchException, BadRequestError) as e:
146
+ return bad_request(err=f"SearchException: {e}")
147
+
148
+
149
+ @generate_swagger_docs()
150
+ @search_api.route("/<index>/eql", methods=["GET", "POST"])
151
+ @api_login(required_priv=["R"])
152
+ def eql_search(index, **kwargs):
153
+ """Search through specified index for a given EQL query. Uses EQL search syntax for query.
154
+
155
+ Variables:
156
+ index => Index to search in (hit, user,...)
157
+
158
+ Arguments:
159
+ eql_query => EQL Query to search for
160
+
161
+ Optional Arguments:
162
+ filters => List of additional filter queries limit the data, written in lucene
163
+ fl => Comma-separated list of fields to return
164
+ rows => Number of results per page
165
+ timeout => Maximum execution time (ms)
166
+
167
+ Data Block:
168
+ # Note that the data block is for POST requests only!
169
+ {"eql_query": "query", # EQL Query to search for
170
+ "rows": 100, # Max number of results
171
+ "fl": "id,score", # List of fields to return
172
+ "timeout": 1000, # Maximum execution time (ms)
173
+ "filters": ['fq']} # List of additional filter queries limit the data
174
+
175
+
176
+ Result Example:
177
+ {"total": 201, # Total results found
178
+ "offset": 0, # Offset in the result list
179
+ "rows": 100, # Number of results returned
180
+ "items": []} # List of results
181
+ """
182
+ user = kwargs["user"]
183
+ collection = get_collection(index, user)
184
+
185
+ if collection is None:
186
+ return bad_request(err=f"Not a valid index to search in: {index}")
187
+
188
+ fields = [
189
+ "eql_query",
190
+ "fl",
191
+ "rows",
192
+ "timeout",
193
+ ]
194
+ multi_fields = ["filters"]
195
+
196
+ params, req_data = generate_params(request, fields, multi_fields)
197
+
198
+ if has_access_control(index):
199
+ params.update({"access_control": user["access_control"]})
200
+
201
+ params["as_obj"] = False
202
+
203
+ eql_query = req_data.get("eql_query", None)
204
+ if not eql_query:
205
+ return bad_request(err="There was no EQL search query.")
206
+
207
+ try:
208
+ return ok(collection().raw_eql_search(**params))
209
+ except (SearchException, BadRequestError) as e:
210
+ logger.error("SearchException: %s", str(e), exc_info=True)
211
+ return bad_request(err=f"SearchException: {e}")
212
+
213
+
214
+ @generate_swagger_docs()
215
+ @search_api.route("/<index>/sigma", methods=["GET", "POST"])
216
+ @api_login(required_priv=["R"])
217
+ def sigma_search(index, **kwargs):
218
+ """Search through specified index using a given sigma rule. Uses sigma rule syntax for query.
219
+
220
+ Variables:
221
+ index => Index to search in (hit, user,...)
222
+
223
+ Arguments:
224
+ sigma => Sigma rule to search on
225
+
226
+ Optional Arguments:
227
+ filters => List of additional filter queries limit the data, written in lucene
228
+ fl => Comma-separated list of fields to return
229
+ rows => Number of results per page
230
+ timeout => Maximum execution time (ms)
231
+
232
+ Data Block:
233
+ # Note that the data block is for POST requests only!
234
+ {"sigma": "sigma yaml", # Sigma Rule to search for
235
+ "rows": 100, # Max number of results
236
+ "fl": "id,score", # List of fields to return
237
+ "timeout": 1000, # Maximum execution time (ms)
238
+ "filters": ['fq']} # List of additional filter queries limit the data
239
+
240
+
241
+ Result Example:
242
+ {"total": 201, # Total results found
243
+ "offset": 0, # Offset in the result list
244
+ "rows": 100, # Number of results returned
245
+ "items": []} # List of results
246
+ """
247
+ user = kwargs["user"]
248
+ collection = get_collection(index, user)
249
+ default_sort = get_default_sort(index, user)
250
+
251
+ if collection is None or default_sort is None:
252
+ return bad_request(err=f"Not a valid index to search in: {index}")
253
+
254
+ fields = [
255
+ "offset",
256
+ "rows",
257
+ "sort",
258
+ "fl",
259
+ "timeout",
260
+ "deep_paging_id",
261
+ "track_total_hits",
262
+ ]
263
+ multi_fields = ["filters"]
264
+ boolean_fields = ["use_archive"]
265
+
266
+ params, req_data = generate_params(request, fields, multi_fields)
267
+
268
+ params.update(
269
+ {
270
+ k: str(req_data.get(k, "false")).lower() in ["true", ""]
271
+ for k in boolean_fields
272
+ if req_data.get(k, None) is not None
273
+ }
274
+ )
275
+
276
+ if has_access_control(index):
277
+ params.update({"access_control": user["access_control"]})
278
+
279
+ params["as_obj"] = False
280
+ params.update({"sort": (params.get("sort", None) or default_sort).split(",")})
281
+
282
+ sigma = req_data.get("sigma", None)
283
+ if not sigma:
284
+ return bad_request(err="There was no sigma rule.")
285
+
286
+ try:
287
+ rule = SigmaRule.from_yaml(sigma)
288
+ except ScannerError as e:
289
+ return bad_request(err=f"Error when parsing yaml: {e.problem} {e.problem_mark}")
290
+
291
+ es_collection = collection()
292
+
293
+ lucene_queries = LuceneBackend(index_names=[es_collection.index_name]).convert_rule(rule)
294
+
295
+ try:
296
+ return ok(es_collection.search("*:*", **params, filters=[*params.get("filters", []), *lucene_queries]))
297
+ except (SearchException, BadRequestError) as e:
298
+ logger.error("SearchException: %s", str(e), exc_info=True)
299
+ return bad_request(err=f"SearchException: {e}")
300
+
301
+
302
+ @generate_swagger_docs()
303
+ @search_api.route("/grouped/<index>/<group_field>", methods=["GET", "POST"])
304
+ @api_login(required_priv=["R"])
305
+ def group_search(index, group_field, **kwargs):
306
+ """Search for a given query and groups the data based on a specific field. Uses lucene search syntax.
307
+
308
+ Variables:
309
+ index => Index to search in (hit, user,...)
310
+ group_field => Field to group on
311
+
312
+ Optional Arguments:
313
+ group_sort => How to sort the results inside the group
314
+ limit => Maximum number of results return for each groups
315
+ query => Query to search for
316
+ filters => List of additional filter queries limit the data
317
+ offset => Offset in the results
318
+ rows => Max number of results
319
+ sort => How to sort the results
320
+ fl => List of fields to return
321
+
322
+ Data Block:
323
+ # Note that the data block is for POST requests only!
324
+ {"group_sort": "score desc",
325
+ "limit": 10,
326
+ "query": "query",
327
+ "offset": 0,
328
+ "rows": 100,
329
+ "sort": "field asc",
330
+ "fl": "id,score",
331
+ "filters": ['fq']}
332
+
333
+
334
+ Result Example:
335
+ {
336
+ "total": 201, # Total results found
337
+ "offset": 0, # Offset in the result list
338
+ "rows": 100, # Number of results returned
339
+ "items": [], # List of results
340
+ "sequences": [], # List of matching sequences
341
+ }
342
+ """
343
+ user = kwargs["user"]
344
+ collection = get_collection(index, user)
345
+ default_sort = get_default_sort(index, user)
346
+ if collection is None or default_sort is None:
347
+ return bad_request(err=f"Not a valid index to search in: {index}")
348
+
349
+ fields = ["group_sort", "limit", "query", "offset", "rows", "sort", "fl"]
350
+ multi_fields = ["filters"]
351
+
352
+ params = generate_params(request, fields, multi_fields)[0]
353
+
354
+ if has_access_control(index):
355
+ params.update({"access_control": user["access_control"]})
356
+
357
+ params["as_obj"] = False
358
+ params.setdefault("sort", default_sort)
359
+
360
+ if not group_field:
361
+ return bad_request(err="The field to group on was not specified.")
362
+
363
+ try:
364
+ return ok(collection().grouped_search(group_field, **params))
365
+ except (SearchException, BadRequestError) as e:
366
+ logger.error("SearchException: %s", str(e), exc_info=True)
367
+ return bad_request(err=f"SearchException: {e}")
368
+
369
+
370
+ # noinspection PyUnusedLocal
371
+ @generate_swagger_docs()
372
+ @search_api.route("/fields/<index>", methods=["GET"])
373
+ @api_login(required_priv=["R"])
374
+ def list_index_fields(index, **kwargs):
375
+ """List all available fields for a given index
376
+
377
+ Variables:
378
+ index => Which specific index you want to know the fields for
379
+
380
+
381
+ Arguments:
382
+ None
383
+
384
+ Result Example:
385
+ {
386
+ "<<FIELD_NAME>>": { # For a given field
387
+ indexed: True, # Is the field indexed
388
+ stored: False, # Is the field stored
389
+ type: string # What type of data in the field
390
+ },
391
+ ...
392
+
393
+ }
394
+ """
395
+ user = kwargs["user"]
396
+ collection = get_collection(index, user)
397
+ if collection is not None:
398
+ return ok(collection().fields())
399
+ elif index == "ALL":
400
+ return ok(list_all_fields("admin" in user["type"]))
401
+ else:
402
+ return bad_request(err=f"Not a valid index to search in: {index}")
403
+
404
+
405
+ @generate_swagger_docs()
406
+ @search_api.route("/count/<index>", methods=["GET", "POST"])
407
+ @api_login(required_priv=["R"])
408
+ def count(index, **kwargs):
409
+ """Returns number of documents matching a query. Uses lucene search syntax for query.
410
+
411
+ Variables:
412
+ index => Index to search in (hit, user,...)
413
+
414
+ Arguments:
415
+ query => Query to search for
416
+
417
+ Optional Arguments:
418
+ filters => List of additional filter queries limit the data
419
+ timeout => Maximum execution time (ms)
420
+ use_archive => Allow access to the datastore achive (Default: False)
421
+
422
+ Data Block:
423
+ # Note that the data block is for POST requests only!
424
+ {
425
+ "query": "query", # Query to search for
426
+ "timeout": 1000, # Maximum execution time (ms)
427
+ }
428
+
429
+
430
+ Result Example:
431
+ {
432
+ "total": 201, # Total results found
433
+ }
434
+ """
435
+ user = kwargs["user"]
436
+ collection = get_collection(index, user)
437
+
438
+ if collection is None:
439
+ return bad_request(err=f"Not a valid index to search in: {index}")
440
+
441
+ params, req_data = generate_params(request, [], [])
442
+
443
+ boolean_fields = ["use_archive"]
444
+ params.update(
445
+ {
446
+ k: str(req_data.get(k, "false")).lower() in ["true", ""]
447
+ for k in boolean_fields
448
+ if req_data.get(k, None) is not None
449
+ }
450
+ )
451
+
452
+ if has_access_control(index):
453
+ params.update({"access_control": user["access_control"]})
454
+
455
+ query = req_data.get("query", None)
456
+ if not query:
457
+ return bad_request(err="There was no search query.")
458
+
459
+ try:
460
+ return ok(collection().count(query, **params))
461
+ except (SearchException, BadRequestError) as e:
462
+ return bad_request(err=f"SearchException: {e}")
463
+
464
+
465
+ @generate_swagger_docs()
466
+ @search_api.route("/facet/<index>", methods=["GET", "POST"])
467
+ @api_login(required_priv=["R"])
468
+ def facet(index, **kwargs):
469
+ """Perform field analysis on the selected fields. (Also known as facetting in lucene).
470
+
471
+ This essentially counts the number of instances a field is seen with each specific
472
+ values where the documents matches the specified queries.
473
+
474
+ Variables:
475
+ index => Index to search in (hit, user,...)
476
+
477
+ Optional Arguments:
478
+ query => Query to search for
479
+ mincount => Minimum item count for the fieldvalue to be returned
480
+ rows => The max number of fieldvalues to return
481
+ filters => Additional query to limit to output
482
+ fields => Field to analyse
483
+
484
+ Data Block:
485
+ # Note that the data block is for POST requests only!
486
+ {"fields": ["howler.id", ...]
487
+ "query": "id:*",
488
+ "mincount": "10",
489
+ "rows": "10",
490
+ "filters": ['fq']}
491
+
492
+ Result Example:
493
+ {
494
+ "howler.id": { # Facetting results
495
+ "value_0": 2,
496
+ ...
497
+ "value_N": 19,
498
+ },
499
+ ...
500
+ }
501
+ """
502
+ user = kwargs["user"]
503
+ collection = get_collection(index, user)
504
+ if collection is None:
505
+ return bad_request(err=f"Not a valid index to search in: {index}")
506
+
507
+ fields = ["query", "mincount", "rows"]
508
+ multi_fields = ["filters", "fields"]
509
+
510
+ params = generate_params(request, fields, multi_fields)[0]
511
+
512
+ if has_access_control(index):
513
+ params.update({"access_control": user["access_control"]})
514
+
515
+ try:
516
+ fields = params.pop("fields")
517
+ facet_result: dict[str, dict[str, Any]] = {}
518
+ for field in fields:
519
+ if field not in collection().fields():
520
+ logger.warning("Invalid field %s requested for faceting, skipping", field)
521
+ continue
522
+
523
+ facet_result[field] = collection().facet(field, **params)
524
+
525
+ return ok(facet_result)
526
+ except (SearchException, BadRequestError) as e:
527
+ logger.error("SearchException: %s", str(e), exc_info=True)
528
+ return bad_request(err=f"SearchException: {e}")
529
+
530
+
531
+ @generate_swagger_docs()
532
+ @search_api.route("/facet/<index>/<field>", methods=["GET", "POST"])
533
+ @api_login(required_priv=["R"])
534
+ def facet_field(index, field, **kwargs):
535
+ """Perform field analysis on the selected field. (Also known as facetting in lucene).
536
+
537
+ This essentially counts the number of instances a field is seen with each specific
538
+ values where the documents matches the specified queries.
539
+
540
+ Variables:
541
+ index => Index to search in (hit, user,...)
542
+ field => Field to analyse
543
+
544
+ Optional Arguments:
545
+ query => Query to search for
546
+ mincount => Minimum item count for the fieldvalue to be returned
547
+ rows => The max number of fieldvalues to return
548
+ filters => Additional query to limit to output
549
+
550
+ Data Block:
551
+ # Note that the data block is for POST requests only!
552
+ {"query": "id:*",
553
+ "mincount": "10",
554
+ "rows": "10",
555
+ "filters": ['fq']}
556
+
557
+ Result Example:
558
+ { # Facetting results
559
+ "value_0": 2,
560
+ ...
561
+ "value_N": 19,
562
+ }
563
+ """
564
+ user = kwargs["user"]
565
+ collection = get_collection(index, user)
566
+ if collection is None:
567
+ return bad_request(err=f"Not a valid index to search in: {index}")
568
+
569
+ field_info = collection().fields().get(field, None)
570
+ if field_info is None:
571
+ return bad_request(err=f"Field '{field}' is not a valid field in index: {index}")
572
+
573
+ fields = ["query", "mincount", "rows"]
574
+ multi_fields = ["filters"]
575
+
576
+ params = generate_params(request, fields, multi_fields)[0]
577
+
578
+ if has_access_control(index):
579
+ params.update({"access_control": user["access_control"]})
580
+
581
+ try:
582
+ return ok(collection().facet(field, **params))
583
+ except (SearchException, BadRequestError) as e:
584
+ logger.error("SearchException: %s", str(e), exc_info=True)
585
+ return bad_request(err=f"SearchException: {e}")
586
+
587
+
588
+ @generate_swagger_docs()
589
+ @search_api.route("/histogram/<index>/<field>", methods=["GET", "POST"])
590
+ @api_login(required_priv=["R"])
591
+ def histogram(index, field, **kwargs):
592
+ """Generate an histogram based on a time or and int field using a specific gap size
593
+
594
+ Variables:
595
+ index => Index to search in (hit, user,...)
596
+ field => Field to generate the histogram from
597
+
598
+ Optional Arguments:
599
+ query => Query to search for
600
+ mincount => Minimum item count for the fieldvalue to be returned
601
+ filters => Additional query to limit to output
602
+ start => Value at which to start creating the histogram
603
+ * Defaults: 0 or now-1d
604
+ end => Value at which to end the histogram. Defaults: 2000 or now
605
+ gap => Size of each step in the histogram. Defaults: 100 or +1h
606
+
607
+ Data Block:
608
+ # Note that the data block is for POST requests only!
609
+ {"query": "id:*",
610
+ "mincount": "10",
611
+ "filters": ['fq'],
612
+ "start": 0,
613
+ "end": 100,
614
+ "gap": 10}
615
+
616
+ Result Example:
617
+ { # Histogram results
618
+ "step_0": 2,
619
+ ...
620
+ "step_N": 19,
621
+ }
622
+ """
623
+ fields = ["query", "mincount", "start", "end", "gap"]
624
+ multi_fields = ["filters"]
625
+ user = kwargs["user"]
626
+
627
+ collection = get_collection(index, user)
628
+ if collection is None:
629
+ return bad_request(err=f"Not a valid index to search in: {index}")
630
+
631
+ # Get fields default values
632
+ field_info = collection().fields().get(field, None)
633
+ params: dict[str, Union[str, int]] = {}
634
+ if field_info is None:
635
+ return bad_request(err=f"Field '{field}' is not a valid field in index: {index}")
636
+ elif field_info["type"] == "integer":
637
+ params = {"start": 0, "end": 2000, "gap": 100}
638
+ elif field_info["type"] == "date":
639
+ storage = datastore()
640
+ params = {
641
+ "start": f"{storage.ds.now}-1{storage.ds.day}",
642
+ "end": f"{storage.ds.now}",
643
+ "gap": f"+1{storage.ds.hour}",
644
+ }
645
+ else:
646
+ err_msg = f"Field '{field}' is of type '{field_info['type']}'. Only 'integer' or 'date' are acceptable."
647
+ return bad_request(err=err_msg)
648
+
649
+ # Load API variables
650
+ params = generate_params(request, fields, multi_fields, params)[0]
651
+
652
+ # Make sure access control is enforced
653
+ if has_access_control(index):
654
+ params.update({"access_control": user["access_control"]})
655
+
656
+ try:
657
+ return ok(collection().histogram(field, **params))
658
+ except (SearchException, BadRequestError) as e:
659
+ logger.error("SearchException: %s", str(e), exc_info=True)
660
+ return bad_request(err=f"SearchException: {e}")
661
+
662
+
663
+ @generate_swagger_docs()
664
+ @search_api.route("/stats/<index>/<int_field>", methods=["GET", "POST"])
665
+ @api_login(required_priv=["R"])
666
+ def stats(index, int_field, **kwargs):
667
+ """Perform statistical analysis of an integer field to get its min, max, average and count values
668
+
669
+ Variables:
670
+ index => Index to search in (hit, user,...)
671
+ int_field => Integer field to analyse
672
+
673
+ Optional Arguments:
674
+ query => Query to search for
675
+ filters => Additional query to limit to output
676
+
677
+ Data Block:
678
+ # Note that the data block is for POST requests only!
679
+ {"query": "id:*",
680
+ "filters": ['fq']}
681
+
682
+ Result Example:
683
+ { # Stats results
684
+ "count": 1, # Number of times this field is seen
685
+ "min": 1, # Minimum value
686
+ "max": 1, # Maximum value
687
+ "avg": 1, # Average value
688
+ "sum": 1 # Sum of all values
689
+ }
690
+ """
691
+ user = kwargs["user"]
692
+ collection = get_collection(index, user)
693
+ if collection is None:
694
+ return bad_request(err=f"Not a valid index to search in: {index}")
695
+
696
+ field_info = collection().fields().get(int_field, None)
697
+ if field_info is None:
698
+ return bad_request(err=f"Field '{int_field}' is not a valid field in index: {index}")
699
+
700
+ if field_info["type"] not in ["integer", "float"]:
701
+ return bad_request(err=f"Field '{int_field}' is not a numeric field.")
702
+
703
+ fields = ["query"]
704
+ multi_fields = ["filters"]
705
+
706
+ params = generate_params(request, fields, multi_fields)[0]
707
+
708
+ if has_access_control(index):
709
+ params.update({"access_control": user["access_control"]})
710
+
711
+ try:
712
+ return ok(collection().stats(int_field, **params))
713
+ except (SearchException, BadRequestError) as e:
714
+ logger.error("SearchException: %s", str(e), exc_info=True)
715
+ return bad_request(err=f"SearchException: {e}")