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.
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,231 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import re
6
+ from os import environ
7
+ from typing import Any, Optional, cast
8
+ from urllib.parse import urlparse
9
+
10
+ import elasticsearch
11
+ import elasticsearch.client
12
+
13
+ from howler.datastore.collection import ESCollection
14
+ from howler.datastore.exceptions import DataStoreException
15
+ from howler.odm.models.config import Config
16
+ from howler.odm.models.config import config as _config
17
+
18
+ TRANSPORT_TIMEOUT = int(environ.get("HWL_DATASTORE_TRANSPORT_TIMEOUT", "10"))
19
+
20
+
21
+ class ESStore(object):
22
+ """Elasticsearch multi-index implementation of the ResultStore interface."""
23
+
24
+ DEFAULT_SORT = "id asc"
25
+ DATE_FORMAT = {
26
+ "NOW": "now",
27
+ "YEAR": "y",
28
+ "MONTH": "M",
29
+ "WEEK": "w",
30
+ "DAY": "d",
31
+ "HOUR": "h",
32
+ "MINUTE": "m",
33
+ "SECOND": "s",
34
+ "MILLISECOND": "ms",
35
+ "MICROSECOND": "micros",
36
+ "NANOSECOND": "nanos",
37
+ "SEPARATOR": "||",
38
+ "DATE_END": "Z",
39
+ }
40
+ DATEMATH_MAP = {
41
+ "NOW": "now",
42
+ "YEAR": "y",
43
+ "MONTH": "M",
44
+ "WEEK": "w",
45
+ "DAY": "d",
46
+ "HOUR": "h",
47
+ "MINUTE": "m",
48
+ "SECOND": "s",
49
+ "DATE_END": "Z||",
50
+ }
51
+ ID = "id"
52
+
53
+ def __init__(self, config: Optional[Config] = None, archive_access=True):
54
+ if not config:
55
+ config = _config
56
+
57
+ self._apikey: Optional[tuple[str, str]] = None
58
+ self._username: Optional[str] = None
59
+ self._password: Optional[str] = None
60
+ self._hosts = []
61
+
62
+ for host in config.datastore.hosts:
63
+ self._hosts.append(str(host))
64
+ if os.getenv(f"{host.name.upper()}_HOST_APIKEY_ID", None) is not None:
65
+ self._apikey = (
66
+ os.environ[f"{host.name.upper()}_HOST_APIKEY_ID"],
67
+ os.environ[f"{host.name.upper()}_HOST_APIKEY_SECRET"],
68
+ )
69
+ elif os.getenv(f"{host.name.upper()}_HOST_USERNAME") is not None:
70
+ self._username = os.environ[f"{host.name.upper()}_HOST_USERNAME"]
71
+ self._password = os.environ[f"{host.name.upper()}_HOST_PASSWORD"]
72
+
73
+ self._closed = False
74
+ self._collections: dict[str, ESCollection] = {}
75
+ self._models: dict[str, Any] = {}
76
+ self.validate = True
77
+
78
+ tracer = logging.getLogger("elasticsearch")
79
+ tracer.setLevel(logging.CRITICAL)
80
+
81
+ if self._apikey is not None:
82
+ self.client = elasticsearch.Elasticsearch(
83
+ hosts=self._hosts, # type: ignore
84
+ api_key=self._apikey,
85
+ max_retries=0,
86
+ request_timeout=TRANSPORT_TIMEOUT,
87
+ )
88
+ elif self._username is not None and self._password is not None:
89
+ self.client = elasticsearch.Elasticsearch(
90
+ hosts=self._hosts, # type: ignore
91
+ basic_auth=(self._username, self._password),
92
+ max_retries=0,
93
+ request_timeout=TRANSPORT_TIMEOUT,
94
+ )
95
+ else:
96
+ self.client = elasticsearch.Elasticsearch(
97
+ hosts=self._hosts, # type: ignore
98
+ max_retries=0,
99
+ request_timeout=TRANSPORT_TIMEOUT,
100
+ )
101
+ self.eql = elasticsearch.client.EqlClient(self.client)
102
+ self.archive_access = archive_access
103
+ self.url_path = "elastic"
104
+
105
+ def __enter__(self):
106
+ return self
107
+
108
+ def __exit__(self, ex_type, exc_val, exc_tb):
109
+ self.close()
110
+
111
+ def __str__(self):
112
+ return "{0} - {1}".format(self.__class__.__name__, self._hosts)
113
+
114
+ def __getattr__(self, name) -> ESCollection:
115
+ if not self.validate:
116
+ return ESCollection(self, name, model_class=self._models[name], validate=self.validate)
117
+
118
+ if name not in self._collections:
119
+ self._collections[name] = ESCollection(self, name, model_class=self._models[name], validate=self.validate)
120
+
121
+ return self._collections[name]
122
+
123
+ @property
124
+ def now(self):
125
+ return self.DATE_FORMAT["NOW"]
126
+
127
+ @property
128
+ def ms(self):
129
+ return self.DATE_FORMAT["MILLISECOND"]
130
+
131
+ @property
132
+ def us(self):
133
+ return self.DATE_FORMAT["MICROSECOND"]
134
+
135
+ @property
136
+ def ns(self):
137
+ return self.DATE_FORMAT["NANOSECOND"]
138
+
139
+ @property
140
+ def year(self):
141
+ return self.DATE_FORMAT["YEAR"]
142
+
143
+ @property
144
+ def month(self):
145
+ return self.DATE_FORMAT["MONTH"]
146
+
147
+ @property
148
+ def week(self):
149
+ return self.DATE_FORMAT["WEEK"]
150
+
151
+ @property
152
+ def day(self):
153
+ return self.DATE_FORMAT["DAY"]
154
+
155
+ @property
156
+ def hour(self):
157
+ return self.DATE_FORMAT["HOUR"]
158
+
159
+ @property
160
+ def minute(self):
161
+ return self.DATE_FORMAT["MINUTE"]
162
+
163
+ @property
164
+ def second(self):
165
+ return self.DATE_FORMAT["SECOND"]
166
+
167
+ @property
168
+ def date_separator(self):
169
+ return self.DATE_FORMAT["SEPARATOR"]
170
+
171
+ def connection_reset(self):
172
+ self.client = elasticsearch.Elasticsearch(
173
+ hosts=self._hosts, # type: ignore
174
+ api_key=self._apikey,
175
+ max_retries=0,
176
+ request_timeout=TRANSPORT_TIMEOUT,
177
+ )
178
+ self.eql = elasticsearch.client.EqlClient(self.client)
179
+
180
+ def close(self):
181
+ self._closed = True
182
+ # Flatten the client object so that attempts to access without reconnecting errors hard
183
+ # But 'cast' it so that mypy and other linters don't think that its normal for client to be None
184
+ self.client = cast(elasticsearch.Elasticsearch, None)
185
+ self.eql = cast(elasticsearch.client.EqlClient, None)
186
+
187
+ def get_hosts(self, safe=False):
188
+ if not safe:
189
+ return self._hosts
190
+ else:
191
+ out = []
192
+ for h in self._hosts:
193
+ parsed = urlparse(h)
194
+ out.append(parsed.hostname or parsed.path)
195
+ return out
196
+
197
+ def get_models(self):
198
+ return self._models
199
+
200
+ def is_closed(self):
201
+ return self._closed
202
+
203
+ def ping(self):
204
+ return self.client.ping()
205
+
206
+ def register(self, name: str, model_class=None):
207
+ name_match = re.match(r"[a-z0-9_]*", name)
208
+ if not name_match or name_match.string != name:
209
+ raise DataStoreException(
210
+ "Invalid characters in model name. " "You can only use lower case letters, numbers and underscores."
211
+ )
212
+
213
+ self._models[name] = model_class
214
+
215
+ def to_pydatemath(self, value):
216
+ replace_list = [
217
+ (self.now, self.DATEMATH_MAP["NOW"]),
218
+ (self.year, self.DATEMATH_MAP["YEAR"]),
219
+ (self.month, self.DATEMATH_MAP["MONTH"]),
220
+ (self.week, self.DATEMATH_MAP["WEEK"]),
221
+ (self.day, self.DATEMATH_MAP["DAY"]),
222
+ (self.hour, self.DATEMATH_MAP["HOUR"]),
223
+ (self.minute, self.DATEMATH_MAP["MINUTE"]),
224
+ (self.second, self.DATEMATH_MAP["SECOND"]),
225
+ (self.DATE_FORMAT["DATE_END"], self.DATEMATH_MAP["DATE_END"]),
226
+ ]
227
+
228
+ for x in replace_list:
229
+ value = value.replace(*x)
230
+
231
+ return value
File without changes
@@ -0,0 +1,214 @@
1
+ from typing import Union
2
+
3
+ from howler.common.exceptions import HowlerNotImplementedError, HowlerValueError
4
+ from howler.common.logging import get_logger
5
+ from howler.datastore.constants import ANALYZER_MAPPING, NORMALIZER_MAPPING, TYPE_MAPPING
6
+ from howler.odm import (
7
+ Any,
8
+ Boolean,
9
+ Classification,
10
+ Compound,
11
+ Date,
12
+ FlattenedObject,
13
+ Float,
14
+ Integer,
15
+ Json,
16
+ Keyword,
17
+ List,
18
+ Mapping,
19
+ Optional,
20
+ Text,
21
+ )
22
+
23
+ logger = get_logger(__file__)
24
+
25
+
26
+ def build_mapping(field_data, prefix=None, allow_refuse_implicit=True):
27
+ """The mapping for Elasticsearch based on a python model object."""
28
+ prefix = prefix or []
29
+ mappings = {}
30
+ dynamic = []
31
+
32
+ def set_mapping(temp_field, body):
33
+ body["index"] = temp_field.index
34
+ if body.get("type", "text") != "text":
35
+ body["doc_values"] = temp_field.index
36
+ if temp_field.copyto:
37
+ if len(field.copyto) > 1:
38
+ logger.warning("copyto field larger than 1, only using first entry")
39
+ body["copy_to"] = temp_field.copyto[0]
40
+
41
+ return body
42
+
43
+ # Fill in the sections
44
+ for field in field_data:
45
+ path = prefix + ([field.name] if field.name else [])
46
+ name = ".".join(path)
47
+
48
+ if isinstance(field, Classification):
49
+ mappings[name.strip(".")] = set_mapping(field, {"type": TYPE_MAPPING[field.__class__.__name__]})
50
+ if "." not in name:
51
+ mappings.update(
52
+ {
53
+ "__access_lvl__": {"type": "integer", "index": True},
54
+ "__access_req__": {"type": "keyword", "index": True},
55
+ "__access_grp1__": {"type": "keyword", "index": True},
56
+ "__access_grp2__": {"type": "keyword", "index": True},
57
+ }
58
+ )
59
+
60
+ elif isinstance(field, (Boolean, Integer, Float, Text)):
61
+ mappings[name.strip(".")] = set_mapping(field, {"type": TYPE_MAPPING[field.__class__.__name__]})
62
+
63
+ elif field.__class__ in ANALYZER_MAPPING:
64
+ mappings[name.strip(".")] = set_mapping(
65
+ field,
66
+ {
67
+ "type": TYPE_MAPPING[field.__class__.__name__],
68
+ "analyzer": ANALYZER_MAPPING[field.__class__],
69
+ },
70
+ )
71
+
72
+ elif isinstance(field, (Json, Keyword)):
73
+ es_data_type = TYPE_MAPPING[field.__class__.__name__]
74
+ data: dict[str, Union[str, int]] = {"type": es_data_type}
75
+ if es_data_type == "keyword":
76
+ data["ignore_above"] = 8191 # The maximum always safe value in elasticsearch
77
+ if field.__class__ in NORMALIZER_MAPPING:
78
+ data["normalizer"] = NORMALIZER_MAPPING[field.__class__] # type: ignore
79
+ mappings[name.strip(".")] = set_mapping(field, data)
80
+
81
+ elif isinstance(field, Date):
82
+ mappings[name.strip(".")] = set_mapping(
83
+ field,
84
+ {
85
+ "type": TYPE_MAPPING[field.__class__.__name__],
86
+ "format": "date_optional_time||epoch_millis",
87
+ },
88
+ )
89
+
90
+ elif isinstance(field, FlattenedObject):
91
+ if not field.index or isinstance(field.child_type, Any):
92
+ mappings[name.strip(".")] = {"type": "object", "enabled": False}
93
+ else:
94
+ dynamic.extend(
95
+ build_templates(
96
+ f"{name}.*",
97
+ field.child_type,
98
+ nested_template=True,
99
+ index=field.index,
100
+ )
101
+ )
102
+
103
+ elif isinstance(field, List):
104
+ temp_mappings, temp_dynamic = build_mapping([field.child_type], prefix=path, allow_refuse_implicit=False)
105
+ mappings.update(temp_mappings)
106
+ dynamic.extend(temp_dynamic)
107
+
108
+ elif isinstance(field, Optional):
109
+ temp_mappings, temp_dynamic = build_mapping([field.child_type], prefix=prefix, allow_refuse_implicit=False)
110
+ mappings.update(temp_mappings)
111
+ dynamic.extend(temp_dynamic)
112
+
113
+ elif isinstance(field, Compound):
114
+ temp_mappings, temp_dynamic = build_mapping(
115
+ field.fields().values(), prefix=path, allow_refuse_implicit=False
116
+ )
117
+ mappings.update(temp_mappings)
118
+ dynamic.extend(temp_dynamic)
119
+
120
+ elif isinstance(field, Mapping):
121
+ if not field.index or isinstance(field.child_type, Any):
122
+ mappings[name.strip(".")] = {"type": "object", "enabled": False}
123
+ else:
124
+ dynamic.extend(build_templates(f"{name}.*", field.child_type, index=field.index))
125
+
126
+ elif isinstance(field, Any):
127
+ if field.index:
128
+ raise HowlerValueError(f"Any may not be indexed: {name}")
129
+
130
+ mappings[name.strip(".")] = {
131
+ "type": "keyword",
132
+ "index": False,
133
+ "doc_values": False,
134
+ }
135
+
136
+ else:
137
+ raise HowlerNotImplementedError(f"Unknown type for elasticsearch schema: {field.__class__}")
138
+
139
+ # The final template must match everything and disable indexing
140
+ # this effectively disables dynamic indexing EXCEPT for the templates
141
+ # we have defined
142
+ if not dynamic and allow_refuse_implicit:
143
+ # We cannot use the dynamic type matching if others are in play because they conflict with each other
144
+ # TODO: Find a way to make them work together.
145
+ dynamic.append(
146
+ {
147
+ "refuse_all_implicit_mappings": {
148
+ "match": "*",
149
+ "mapping": {
150
+ "index": False,
151
+ "ignore_malformed": True,
152
+ },
153
+ }
154
+ }
155
+ )
156
+
157
+ return mappings, dynamic
158
+
159
+
160
+ def build_templates(name, field, nested_template=False, index=True) -> list:
161
+ if isinstance(field, (Keyword, Boolean, Integer, Float, Text, Json)):
162
+ if nested_template:
163
+ main_template = {"match": f"{name}", "mapping": {"type": "nested"}}
164
+
165
+ return [{f"nested_{name}": main_template}]
166
+ else:
167
+ field_template = {
168
+ "path_match": name,
169
+ "mapping": {
170
+ "type": TYPE_MAPPING[field.__class__.__name__],
171
+ "index": field.index,
172
+ },
173
+ }
174
+
175
+ if field.copyto:
176
+ if len(field.copyto) > 1:
177
+ logger.warning("copyto field larger than 1, only using first entry")
178
+ field_template["mapping"]["copy_to"] = field.copyto[0]
179
+
180
+ return [{f"{name}_tpl": field_template}]
181
+
182
+ elif isinstance(field, Any) or not index:
183
+ field_template = {
184
+ "path_match": name,
185
+ "mapping": {"type": "keyword", "index": False},
186
+ }
187
+
188
+ if field.index:
189
+ raise HowlerValueError(f"Mapping to Any may not be indexed: {name}")
190
+ return [{f"{name}_tpl": field_template}]
191
+
192
+ elif isinstance(field, (Mapping, List)):
193
+ temp_name = name
194
+ if field.name:
195
+ temp_name = f"{name}.{field.name}"
196
+ return build_templates(temp_name, field.child_type, nested_template=True)
197
+
198
+ elif isinstance(field, Compound):
199
+ temp_name = name
200
+ if field.name:
201
+ temp_name = f"{name}.{field.name}"
202
+
203
+ out = []
204
+ for sub_name, sub_field in field.fields().items():
205
+ sub_name = f"{temp_name}.{sub_name}"
206
+ out.extend(build_templates(sub_name, sub_field))
207
+
208
+ return out
209
+
210
+ elif isinstance(field, Optional):
211
+ return build_templates(name, field.child_type, nested_template=nested_template)
212
+
213
+ else:
214
+ raise HowlerNotImplementedError(f"Unknown type for elasticsearch dynamic mapping: {field.__class__}")
@@ -0,0 +1,90 @@
1
+ default_index = {
2
+ "settings": {
3
+ "analysis": {
4
+ "filter": {
5
+ "text_ws_dsplit": {
6
+ "type": "pattern_replace",
7
+ "pattern": r"(\.)",
8
+ "replacement": " ",
9
+ }
10
+ },
11
+ "analyzer": {
12
+ "string_ci": {
13
+ "type": "custom",
14
+ "tokenizer": "keyword",
15
+ "filter": ["lowercase"],
16
+ },
17
+ "text_fuzzy": {
18
+ "type": "pattern",
19
+ "pattern": r"\s*:\s*",
20
+ "lowercase": False,
21
+ },
22
+ "text_whitespace": {"type": "whitespace"},
23
+ "text_ws_dsplit": {
24
+ "type": "custom",
25
+ "tokenizer": "whitespace",
26
+ "filters": ["text_ws_dsplit"],
27
+ },
28
+ },
29
+ "normalizer": {
30
+ "lowercase_normalizer": {
31
+ "type": "custom",
32
+ "char_filter": [],
33
+ "filter": ["lowercase"],
34
+ }
35
+ },
36
+ }
37
+ },
38
+ "mappings": {},
39
+ }
40
+
41
+ default_mapping = {
42
+ "dynamic": True,
43
+ "properties": {
44
+ "__text__": {"type": "text"},
45
+ },
46
+ }
47
+
48
+ default_dynamic_strings = {
49
+ "strings_as_keywords": {
50
+ "match_mapping_type": "string",
51
+ "mapping": {"type": "keyword", "ignore_above": 8191},
52
+ }
53
+ }
54
+
55
+ default_dynamic_templates = [
56
+ {"int": {"path_match": "*_i", "mapping": {"type": "integer", "store": True}}},
57
+ {"ints": {"path_match": "*_is", "mapping": {"type": "integer", "store": True}}},
58
+ {"long": {"path_match": "*_l", "mapping": {"type": "long", "store": True}}},
59
+ {"longs": {"path_match": "*_ls", "mapping": {"type": "long", "store": True}}},
60
+ {"double": {"path_match": "*_d", "mapping": {"type": "float", "store": True}}},
61
+ {"doubles": {"path_match": "*_ds", "mapping": {"type": "float", "store": True}}},
62
+ {"float": {"path_match": "*_f", "mapping": {"type": "float", "store": True}}},
63
+ {"floats": {"path_match": "*_fs", "mapping": {"type": "float", "store": True}}},
64
+ {"string": {"path_match": "*_s", "mapping": {"type": "keyword", "store": True}}},
65
+ {"strings": {"path_match": "*_ss", "mapping": {"type": "keyword", "store": True}}},
66
+ {"text": {"path_match": "*_t", "mapping": {"type": "text", "store": True}}},
67
+ {"texts": {"path_match": "*_ts", "mapping": {"type": "text", "store": True}}},
68
+ {"boolean": {"path_match": "*_b", "mapping": {"type": "boolean", "store": True}}},
69
+ {"booleans": {"path_match": "*_bs", "mapping": {"type": "boolean", "store": True}}},
70
+ {
71
+ "date": {
72
+ "path_match": "*_dt",
73
+ "mapping": {
74
+ "type": "date",
75
+ "format": "date_optional_time||epoch_millis",
76
+ "store": True,
77
+ },
78
+ }
79
+ },
80
+ {
81
+ "date": {
82
+ "path_match": "*_dts",
83
+ "mapping": {
84
+ "type": "date",
85
+ "format": "date_optional_time||epoch_millis",
86
+ "store": True,
87
+ },
88
+ }
89
+ },
90
+ ]
@@ -0,0 +1,22 @@
1
+ from typing import Any, TypedDict, Union
2
+
3
+ from howler.odm import Model
4
+ from howler.odm.models.hit import Hit
5
+
6
+
7
+ class SearchResult(TypedDict):
8
+ """Search Results for an Elastic search query.
9
+
10
+ TypedDict's are used in the same way as a normal Dict, but provides typing for InteliSense
11
+ """
12
+
13
+ offset: int
14
+ rows: int
15
+ total: int
16
+ items: list[Union[Model, dict[str, Any]]]
17
+
18
+
19
+ class HitSearchResult(SearchResult):
20
+ """Hit specific typing for Elastic search query"""
21
+
22
+ items: list[Union[Hit, dict[str, Any]]] # type: ignore[misc]
howler/error.py ADDED
@@ -0,0 +1,91 @@
1
+ from sys import exc_info
2
+ from traceback import format_tb
3
+
4
+ from flask import Blueprint, request
5
+ from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized
6
+
7
+ from howler.api import bad_request, forbidden, internal_error, not_found, unauthorized
8
+ from howler.common.exceptions import AccessDeniedException, AuthenticationException
9
+ from howler.common.logging import get_logger, log_with_traceback
10
+ from howler.common.logging.audit import AUDIT
11
+ from howler.config import config
12
+
13
+ errors = Blueprint("errors", __name__)
14
+
15
+ logger = get_logger(__file__)
16
+
17
+
18
+ ######################################
19
+ # Custom Error page
20
+ @errors.app_errorhandler(400)
21
+ def handle_400(e):
22
+ """Handle bad request errors"""
23
+ if isinstance(e, BadRequest):
24
+ error_message = "No data block provided or data block not in JSON format.'"
25
+ else:
26
+ error_message = str(e)
27
+ return bad_request(err=error_message)
28
+
29
+
30
+ @errors.app_errorhandler(401)
31
+ def handle_401(e):
32
+ """Handle unauthorized errors"""
33
+ if isinstance(e, Unauthorized):
34
+ msg = e.description
35
+ else:
36
+ msg = str(e)
37
+
38
+ data = {
39
+ "oauth_providers": [name for name in config.auth.oauth.providers.keys()],
40
+ "allow_userpass_login": config.auth.internal.enabled,
41
+ }
42
+ res = unauthorized(data, err=msg)
43
+ res.set_cookie("XSRF-TOKEN", "", max_age=0, secure=True, httponly=True, samesite="Strict")
44
+ return res
45
+
46
+
47
+ @errors.app_errorhandler(403)
48
+ def handle_403(e):
49
+ """Handle bad forbidden errors"""
50
+ if isinstance(e, Forbidden):
51
+ error_message = e.description
52
+ else:
53
+ error_message = str(e)
54
+
55
+ trace = exc_info()[2]
56
+ if AUDIT:
57
+ uname = "(None)"
58
+ ip = request.remote_addr
59
+
60
+ log_with_traceback(trace, f"Access Denied. (U:{uname} - IP:{ip}) [{error_message}]", audit=True)
61
+
62
+ config_block = {
63
+ "auth": {
64
+ "allow_apikeys": config.auth.allow_apikeys,
65
+ }
66
+ }
67
+ return forbidden(config_block, err=f"Access Denied ({request.path}) [{error_message}]")
68
+
69
+
70
+ @errors.app_errorhandler(404)
71
+ def handle_404(_):
72
+ """Handle not found errors"""
73
+ return not_found(err=f"Api does not exist ({request.path})")
74
+
75
+
76
+ @errors.app_errorhandler(500)
77
+ def handle_500(e):
78
+ """Handle internal server errors"""
79
+ if isinstance(e.original_exception, AccessDeniedException):
80
+ return handle_403(e.original_exception)
81
+
82
+ if isinstance(e.original_exception, AuthenticationException):
83
+ return handle_401(e.original_exception)
84
+
85
+ oe = e.original_exception or e
86
+
87
+ trace = exc_info()[2]
88
+ log_with_traceback(trace, "Exception", is_exception=True)
89
+
90
+ message = "".join(["\n"] + format_tb(exc_info()[2]) + ["%s: %s\n" % (oe.__class__.__name__, str(oe))]).rstrip("\n")
91
+ return internal_error(err=message)
File without changes