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.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +167 -0
- howler/actions/add_label.py +111 -0
- howler/actions/add_to_bundle.py +159 -0
- howler/actions/change_field.py +76 -0
- howler/actions/demote.py +160 -0
- howler/actions/example_plugin.py +104 -0
- howler/actions/prioritization.py +93 -0
- howler/actions/promote.py +147 -0
- howler/actions/remove_from_bundle.py +133 -0
- howler/actions/remove_label.py +111 -0
- howler/actions/transition.py +200 -0
- howler/api/__init__.py +249 -0
- howler/api/base.py +88 -0
- howler/api/socket.py +114 -0
- howler/api/v1/__init__.py +97 -0
- howler/api/v1/action.py +372 -0
- howler/api/v1/analytic.py +748 -0
- howler/api/v1/auth.py +382 -0
- howler/api/v1/borealis.py +101 -0
- howler/api/v1/configs.py +55 -0
- howler/api/v1/dossier.py +222 -0
- howler/api/v1/help.py +28 -0
- howler/api/v1/hit.py +1181 -0
- howler/api/v1/notebook.py +82 -0
- howler/api/v1/overview.py +191 -0
- howler/api/v1/search.py +715 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +414 -0
- howler/api/v1/utils/__init__.py +0 -0
- howler/api/v1/utils/etag.py +84 -0
- howler/api/v1/view.py +288 -0
- howler/app.py +235 -0
- howler/common/README.md +144 -0
- howler/common/__init__.py +0 -0
- howler/common/classification.py +979 -0
- howler/common/classification.yml +107 -0
- howler/common/exceptions.py +167 -0
- howler/common/hexdump.py +48 -0
- howler/common/iprange.py +171 -0
- howler/common/loader.py +154 -0
- howler/common/logging/__init__.py +241 -0
- howler/common/logging/audit.py +138 -0
- howler/common/logging/format.py +38 -0
- howler/common/net.py +79 -0
- howler/common/net_static.py +1494 -0
- howler/common/random_user.py +316 -0
- howler/common/swagger.py +117 -0
- howler/config.py +64 -0
- howler/cronjobs/__init__.py +29 -0
- howler/cronjobs/retention.py +61 -0
- howler/cronjobs/rules.py +274 -0
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +112 -0
- howler/datastore/__init__.py +0 -0
- howler/datastore/bulk.py +72 -0
- howler/datastore/collection.py +2327 -0
- howler/datastore/constants.py +117 -0
- howler/datastore/exceptions.py +41 -0
- howler/datastore/howler_store.py +105 -0
- howler/datastore/migrations/fix_process.py +41 -0
- howler/datastore/operations.py +130 -0
- howler/datastore/schemas.py +90 -0
- howler/datastore/store.py +231 -0
- howler/datastore/support/__init__.py +0 -0
- howler/datastore/support/build.py +214 -0
- howler/datastore/support/schemas.py +90 -0
- howler/datastore/types.py +22 -0
- howler/error.py +91 -0
- howler/external/__init__.py +0 -0
- howler/external/generate_mitre.py +96 -0
- howler/external/generate_sigma_rules.py +31 -0
- howler/external/generate_tlds.py +47 -0
- howler/external/reindex_data.py +46 -0
- howler/external/wipe_databases.py +58 -0
- howler/gunicorn_config.py +25 -0
- howler/healthz.py +47 -0
- howler/helper/__init__.py +0 -0
- howler/helper/azure.py +50 -0
- howler/helper/discover.py +59 -0
- howler/helper/hit.py +236 -0
- howler/helper/oauth.py +247 -0
- howler/helper/search.py +92 -0
- howler/helper/workflow.py +110 -0
- howler/helper/ws.py +378 -0
- howler/odm/README.md +102 -0
- howler/odm/__init__.py +1 -0
- howler/odm/base.py +1504 -0
- howler/odm/charter.txt +146 -0
- howler/odm/helper.py +416 -0
- howler/odm/howler_enum.py +25 -0
- howler/odm/models/__init__.py +0 -0
- howler/odm/models/action.py +33 -0
- howler/odm/models/analytic.py +90 -0
- howler/odm/models/assemblyline.py +48 -0
- howler/odm/models/aws.py +23 -0
- howler/odm/models/azure.py +16 -0
- howler/odm/models/cbs.py +44 -0
- howler/odm/models/config.py +558 -0
- howler/odm/models/dossier.py +33 -0
- howler/odm/models/ecs/__init__.py +0 -0
- howler/odm/models/ecs/agent.py +17 -0
- howler/odm/models/ecs/autonomous_system.py +16 -0
- howler/odm/models/ecs/client.py +149 -0
- howler/odm/models/ecs/cloud.py +141 -0
- howler/odm/models/ecs/code_signature.py +27 -0
- howler/odm/models/ecs/container.py +32 -0
- howler/odm/models/ecs/dns.py +62 -0
- howler/odm/models/ecs/egress.py +10 -0
- howler/odm/models/ecs/elf.py +74 -0
- howler/odm/models/ecs/email.py +122 -0
- howler/odm/models/ecs/error.py +14 -0
- howler/odm/models/ecs/event.py +140 -0
- howler/odm/models/ecs/faas.py +24 -0
- howler/odm/models/ecs/file.py +84 -0
- howler/odm/models/ecs/geo.py +30 -0
- howler/odm/models/ecs/group.py +18 -0
- howler/odm/models/ecs/hash.py +16 -0
- howler/odm/models/ecs/host.py +17 -0
- howler/odm/models/ecs/http.py +37 -0
- howler/odm/models/ecs/ingress.py +12 -0
- howler/odm/models/ecs/interface.py +21 -0
- howler/odm/models/ecs/network.py +30 -0
- howler/odm/models/ecs/observer.py +45 -0
- howler/odm/models/ecs/organization.py +12 -0
- howler/odm/models/ecs/os.py +21 -0
- howler/odm/models/ecs/pe.py +17 -0
- howler/odm/models/ecs/process.py +216 -0
- howler/odm/models/ecs/registry.py +26 -0
- howler/odm/models/ecs/related.py +45 -0
- howler/odm/models/ecs/rule.py +51 -0
- howler/odm/models/ecs/server.py +24 -0
- howler/odm/models/ecs/threat.py +247 -0
- howler/odm/models/ecs/tls.py +58 -0
- howler/odm/models/ecs/url.py +51 -0
- howler/odm/models/ecs/user.py +57 -0
- howler/odm/models/ecs/user_agent.py +20 -0
- howler/odm/models/ecs/vulnerability.py +41 -0
- howler/odm/models/gcp.py +16 -0
- howler/odm/models/hit.py +356 -0
- howler/odm/models/howler_data.py +328 -0
- howler/odm/models/lead.py +33 -0
- howler/odm/models/localized_label.py +13 -0
- howler/odm/models/overview.py +16 -0
- howler/odm/models/pivot.py +40 -0
- howler/odm/models/template.py +24 -0
- howler/odm/models/user.py +83 -0
- howler/odm/models/view.py +34 -0
- howler/odm/random_data.py +888 -0
- howler/odm/randomizer.py +606 -0
- howler/patched.py +5 -0
- howler/plugins/__init__.py +25 -0
- howler/plugins/config.py +123 -0
- howler/remote/__init__.py +0 -0
- howler/remote/datatypes/README.md +355 -0
- howler/remote/datatypes/__init__.py +98 -0
- howler/remote/datatypes/counters.py +63 -0
- howler/remote/datatypes/events.py +66 -0
- howler/remote/datatypes/hash.py +206 -0
- howler/remote/datatypes/lock.py +42 -0
- howler/remote/datatypes/queues/__init__.py +0 -0
- howler/remote/datatypes/queues/comms.py +59 -0
- howler/remote/datatypes/queues/multi.py +32 -0
- howler/remote/datatypes/queues/named.py +93 -0
- howler/remote/datatypes/queues/priority.py +215 -0
- howler/remote/datatypes/set.py +118 -0
- howler/remote/datatypes/user_quota_tracker.py +54 -0
- howler/security/__init__.py +253 -0
- howler/security/socket.py +108 -0
- howler/security/utils.py +185 -0
- howler/services/__init__.py +0 -0
- howler/services/action_service.py +111 -0
- howler/services/analytic_service.py +128 -0
- howler/services/auth_service.py +323 -0
- howler/services/config_service.py +128 -0
- howler/services/dossier_service.py +252 -0
- howler/services/event_service.py +93 -0
- howler/services/hit_service.py +893 -0
- howler/services/jwt_service.py +158 -0
- howler/services/lucene_service.py +286 -0
- howler/services/notebook_service.py +119 -0
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/services/user_service.py +330 -0
- howler/utils/__init__.py +0 -0
- howler/utils/annotations.py +28 -0
- howler/utils/chunk.py +38 -0
- howler/utils/dict_utils.py +200 -0
- howler/utils/isotime.py +17 -0
- howler/utils/list_utils.py +11 -0
- howler/utils/lucene.py +77 -0
- howler/utils/path.py +27 -0
- howler/utils/socket_utils.py +61 -0
- howler/utils/str_utils.py +256 -0
- howler/utils/uid.py +47 -0
- howler_api-2.13.0.dev329.dist-info/METADATA +71 -0
- howler_api-2.13.0.dev329.dist-info/RECORD +200 -0
- howler_api-2.13.0.dev329.dist-info/WHEEL +4 -0
- howler_api-2.13.0.dev329.dist-info/entry_points.txt +8 -0
howler/api/v1/search.py
ADDED
|
@@ -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}")
|