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
howler/api/v1/auth.py ADDED
@@ -0,0 +1,382 @@
1
+ import typing
2
+ from datetime import datetime, timedelta
3
+ from typing import Any, Optional
4
+ from urllib.parse import urlparse
5
+
6
+ from authlib.integrations.base_client import OAuthError
7
+ from flask import current_app, request
8
+ from passlib.hash import bcrypt
9
+
10
+ import howler.services.auth_service as auth_service
11
+ import howler.services.user_service as user_service
12
+ from howler.api import (
13
+ bad_request,
14
+ forbidden,
15
+ internal_error,
16
+ make_subapi_blueprint,
17
+ no_content,
18
+ not_found,
19
+ ok,
20
+ unauthorized,
21
+ )
22
+ from howler.common.exceptions import (
23
+ AccessDeniedException,
24
+ AuthenticationException,
25
+ HowlerException,
26
+ HowlerValueError,
27
+ InvalidDataException,
28
+ )
29
+ from howler.common.loader import datastore
30
+ from howler.common.logging import get_logger
31
+ from howler.common.swagger import generate_swagger_docs
32
+ from howler.config import config
33
+ from howler.odm.models.user import User
34
+ from howler.security import api_login
35
+ from howler.security.utils import generate_random_secret
36
+ from howler.services import jwt_service
37
+ from howler.utils.str_utils import default_string_value
38
+
39
+ logger = get_logger(__file__)
40
+
41
+
42
+ SUB_API = "auth"
43
+ auth_api = make_subapi_blueprint(SUB_API, api_version=1)
44
+ auth_api._doc = "Allow user to authenticate to the web server"
45
+
46
+ logger = get_logger(__file__)
47
+
48
+
49
+ @generate_swagger_docs()
50
+ @auth_api.route("/apikey", methods=["POST"])
51
+ @api_login(audit=False)
52
+ def add_apikey(**kwargs): # noqa: C901
53
+ """Add an API Key for the currently logged in user with given privileges
54
+
55
+ Variables:
56
+ name => Name of the API key
57
+ priv => Requested privileges
58
+ expiry_dates => API key expiry date
59
+
60
+ Arguments:
61
+ None
62
+
63
+ Data Block:
64
+ {
65
+ "name": "apikey", # The username to authenticate
66
+ "priv": "priv", # The access priv of API key
67
+ "expiry_date": "Expiry Date", # The API key expiry date (optional)
68
+ }
69
+
70
+ Result Example:
71
+ {
72
+ "apikey": <ramdomly_generated_password>
73
+ }
74
+ """
75
+ user = kwargs["user"]
76
+ storage = datastore()
77
+ user_data = storage.user.get_if_exists(user["uname"])
78
+ apikey_data = request.json
79
+ if not isinstance(apikey_data, dict):
80
+ return bad_request(err="Invalid data format")
81
+
82
+ if apikey_data["name"] in user_data.apikeys:
83
+ return bad_request(err=f"APIKey '{apikey_data['name']}' already exists")
84
+
85
+ privs: list[str] = [p for p in apikey_data["priv"]]
86
+
87
+ if any(p for p in privs if p not in ["R", "W", "E", "I"]):
88
+ return bad_request(
89
+ err="APIKey contains permissions that do not exist. Please provide a subset of [R, W, E, I]."
90
+ )
91
+
92
+ if "E" in privs and not config.auth.allow_extended_apikeys:
93
+ return bad_request(err="Extended permissions are disabled.")
94
+
95
+ if "E" in privs and "I" in privs:
96
+ return bad_request(err="Extended permission is not allowed on impersonation keys.")
97
+
98
+ expiry_date = apikey_data.get("expiry_date", None)
99
+ max_expiry = None
100
+ if config.auth.max_apikey_duration_amount and config.auth.max_apikey_duration_unit:
101
+ if not expiry_date:
102
+ return bad_request(err="API keys must have an expiry date.")
103
+
104
+ max_expiry = datetime.now() + timedelta(
105
+ **{str(config.auth.max_apikey_duration_unit): config.auth.max_apikey_duration_amount}
106
+ )
107
+
108
+ if config.auth.oauth.strict_apikeys:
109
+ auth_header: Optional[str] = request.headers.get("Authorization", None)
110
+
111
+ if auth_header and auth_header.startswith("Bearer") and "." in auth_header:
112
+ oauth_token = auth_header.split(" ")[1]
113
+ data = jwt_service.decode(
114
+ oauth_token,
115
+ validate_audience=False,
116
+ options={"verify_signature": False},
117
+ )
118
+ max_expiry = datetime.fromtimestamp(data["exp"])
119
+
120
+ if expiry_date:
121
+ try:
122
+ expiry = datetime.fromisoformat(expiry_date.replace("Z", ""))
123
+ except (ValueError, TypeError):
124
+ return bad_request(err="Invalid expiry date format. Please use ISO format.")
125
+
126
+ if max_expiry and max_expiry < expiry:
127
+ return bad_request(err=f"Expiry date must be before {max_expiry.isoformat()}.")
128
+
129
+ try:
130
+ random_pass = generate_random_secret(length=50)
131
+ key_name = apikey_data["name"] if "I" not in privs else f"impersonate_{apikey_data['name']}"
132
+
133
+ new_key = {
134
+ "password": bcrypt.hash(random_pass),
135
+ "agents": apikey_data.get("agents", []),
136
+ "acl": privs,
137
+ }
138
+
139
+ if expiry_date:
140
+ new_key["expiry_date"] = expiry.isoformat()
141
+
142
+ user_data.apikeys[key_name] = new_key
143
+ except HowlerException as e:
144
+ return bad_request(err=e.message)
145
+
146
+ storage.user.save(user["uname"], user_data)
147
+
148
+ return ok({"apikey": f"{key_name}:{random_pass}"})
149
+
150
+
151
+ @auth_api.route("/apikey/<name>", methods=["DELETE"])
152
+ @api_login(audit=False)
153
+ def delete_apikey(name, **kwargs):
154
+ """Delete an API Key matching specified name for the currently logged in user
155
+
156
+ Variables:
157
+ name => Name of the API key
158
+
159
+ Arguments:
160
+ None
161
+
162
+ Result Example:
163
+ {
164
+ "success": True
165
+ }
166
+ """
167
+ user = kwargs["user"]
168
+ storage = datastore()
169
+ user_data: User = storage.user.get_if_exists(user["uname"])
170
+
171
+ if name not in user_data.apikeys:
172
+ return not_found("Api key does not exist")
173
+
174
+ user_data.apikeys.pop(name)
175
+ storage.user.save(user["uname"], user_data)
176
+
177
+ return no_content()
178
+
179
+
180
+ @auth_api.route("/login", methods=["GET", "POST"])
181
+ def login(**_): # noqa: C901
182
+ """Log the user into the system, in one of three ways.
183
+
184
+ 1. Username/Password Authentication
185
+ 2. Username/API Key Authentication
186
+ 3. OAuth Login flow
187
+ (See here: https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow)
188
+
189
+ Variables:
190
+ None
191
+
192
+ Arguments:
193
+ NOTE: The arguments are used only when completing the OAuth authorization flow.
194
+
195
+ provider => The provider of the OAuth code.
196
+ state => Random state used in the OAuth authentication flow.
197
+ code => The code provided by the OAuth provider used to exchange for an access token.
198
+
199
+ Data Block:
200
+ {
201
+ "user": "user", # The username to authenticate as (optional)
202
+ "password": "password", # The password used to authenticate (optional)
203
+ "apikey": "devkey:user", # The apikey used ot authenticate (optional)
204
+ "oauth_provider": "keycloak" # The oauth provider initiate an OAuth Authorization Flow with (optional)
205
+ }
206
+
207
+ Result Example:
208
+ {
209
+ # Profile picture for the user
210
+ "avatar": "data:image/png;base64, ...",
211
+ # Username of the authenticated user
212
+ "username": "user",
213
+ # Different privileges that the user will get for this session
214
+ "privileges": ["R", "W"],
215
+ # A token generated by us the user can use to authenticate with howler
216
+ "app_token": "asdfsd876opqwm465a89sdf4",
217
+ # A JSON Web Access Token generated by an OAuth provider to authenticate with them
218
+ "access_token": "<JWT>",
219
+ }
220
+ """
221
+ data: dict[str, Any]
222
+ if request.is_json and len(request.data) > 0:
223
+ data = request.json # type: ignore
224
+ else:
225
+ data = request.values
226
+
227
+ # Get the ip the request came from - used in logging later
228
+ ip = request.headers.get("X-Forwarded-For", request.remote_addr)
229
+
230
+ # Get the data from the request
231
+ # TODO: Figure out how to fix this inconsistency
232
+ oauth_provider = data.get("provider", data.get("oauth_provider", None))
233
+ user = data.get("user", None)
234
+ password = data.get("password", None)
235
+ apikey = data.get("apikey", None)
236
+
237
+ # These variables are what will eventually be returned, if authentication is successful
238
+ logged_in_uname = None
239
+ access_token = None
240
+ refresh_token = data.get("refresh_token", None)
241
+ priv: Optional[list[str]] = []
242
+
243
+ try:
244
+ # First, we'll try oauth
245
+ if oauth_provider:
246
+ if not config.auth.oauth.enabled:
247
+ raise InvalidDataException("OAuth is disabled.") # noqa: TRY301
248
+
249
+ oauth = current_app.extensions.get("authlib.integrations.flask_client")
250
+ if not oauth: # pragma: no cover
251
+ logger.critical("Authlib integration missing!")
252
+ raise HowlerValueError()
253
+
254
+ provider = oauth.create_client(oauth_provider)
255
+
256
+ if not provider:
257
+ logger.critical("OAuth client failed to create!")
258
+ raise HowlerValueError()
259
+
260
+ # This means that they want to start the oauth process, so we'll redirect them to their chosen provider
261
+ if "code" not in request.args and not refresh_token:
262
+ referer = request.headers.get("Referer", None)
263
+ uri = urlparse(referer if referer else request.host_url)
264
+ port_portion = ":" + str(uri.port) if uri.port else ""
265
+ redirect_uri = f"{uri.scheme}://{uri.hostname}{port_portion}/login?provider={oauth_provider}"
266
+ return provider.authorize_redirect(redirect_uri=redirect_uri, nonce=request.args.get("nonce", None))
267
+
268
+ # At this point we know the code exists, so we're good to use that to exchange for an JSON Web Token with
269
+ # user data in it. token_data contains the access token, expiry, refresh token, and id token,
270
+ # in JWT format: https://jwt.io/
271
+
272
+ oauth_provider_config = config.auth.oauth.providers[oauth_provider]
273
+
274
+ # We need to figure out what information the provider already has, and provide whatever it doesn't.
275
+ # Without this step, the provider will try and send the client_id and/or secret *twice*, leading to an
276
+ # error.
277
+ kwargs = {}
278
+
279
+ # Does the provider have the client id? If not provide it
280
+ if not provider.client_id:
281
+ kwargs["client_id"] = default_string_value(
282
+ oauth_provider_config.client_id,
283
+ env_name=f"{oauth_provider.upper()}_CLIENT_ID",
284
+ )
285
+
286
+ if not kwargs["client_id"]:
287
+ logger.critical("client id not set! Cannot complete oauth")
288
+ raise HowlerValueError()
289
+
290
+ # Does the provider have the client secret? If not provide it
291
+ if not provider.client_secret:
292
+ kwargs["client_secret"] = default_string_value(
293
+ oauth_provider_config.client_secret,
294
+ env_name=f"{oauth_provider.upper()}_CLIENT_SECRET",
295
+ )
296
+
297
+ if not kwargs["client_secret"]:
298
+ logger.critical("client secret not set! Cannot complete oauth")
299
+ raise HowlerValueError()
300
+
301
+ if refresh_token is not None:
302
+ token_data = provider.fetch_access_token(
303
+ refresh_token=refresh_token,
304
+ grant_type="refresh_token",
305
+ **kwargs,
306
+ )
307
+ else:
308
+ # Finally, ask for the access token with whatever info the provider needs
309
+ token_data = provider.authorize_access_token(**kwargs)
310
+
311
+ access_token = token_data.get("access_token", None)
312
+ refresh_token = token_data.get("refresh_token", None)
313
+
314
+ # Get a useful dict of user data from the web token
315
+ cur_user = user_service.parse_user_data(
316
+ token_data, oauth_provider, skip_setup=False, access_token=access_token
317
+ )
318
+
319
+ logged_in_uname = cur_user["uname"]
320
+
321
+ priv = ["R", "W", "E"]
322
+
323
+ # No oauth provider was specified, so we fall back to user/pass or user/apikey
324
+ elif user and (password or apikey):
325
+ if password and apikey:
326
+ raise InvalidDataException("Cannot specify password and API key.") # noqa: TRY301
327
+
328
+ user_data, priv = auth_service.basic_auth(
329
+ f"{user}:{password or apikey}",
330
+ is_base64=False,
331
+ # No need to validate for api keys if we know they provided a password, and vice versa
332
+ skip_apikey=bool(password),
333
+ skip_password=bool(apikey),
334
+ )
335
+
336
+ if not user_data:
337
+ raise AuthenticationException("User does not exist, or authentication was invalid") # noqa: TRY301
338
+
339
+ logged_in_uname = user_data["uname"]
340
+
341
+ else:
342
+ raise AuthenticationException("Not enough information to proceed with authentication") # noqa: TRY301
343
+
344
+ # For sanity's sake, we throw exceptions throughout the authentication code and simply catch the exceptions here to
345
+ # return the corresponding HTTP Code to the user
346
+ except (OAuthError, AuthenticationException) as err:
347
+ logger.warning(f"Authentication failure. (U:{user} - IP:{ip}) [{err}]")
348
+ return unauthorized(err=str(err))
349
+
350
+ except AccessDeniedException as err:
351
+ logger.warning(f"Authorization failure. (U:{user} - IP:{ip}) [{err}]")
352
+ return forbidden(err=err.message)
353
+
354
+ except InvalidDataException as err:
355
+ return bad_request(err=err.message or str(err))
356
+
357
+ except HowlerException:
358
+ logger.exception(f"Internal Authentication Error. (U:{user} - IP:{ip})")
359
+ return internal_error(
360
+ err="Unhandled exception occured while Authenticating. Contact your administrator.",
361
+ )
362
+
363
+ logger.info(f"Login successful. (U:{logged_in_uname} - IP:{ip})")
364
+
365
+ xsrf_token = generate_random_secret()
366
+
367
+ # Generate the token this user can use to authenticate from now on
368
+
369
+ if access_token:
370
+ app_token = access_token
371
+ else:
372
+ app_token = f"{logged_in_uname}:{auth_service.create_token(logged_in_uname, typing.cast(list[str], priv))}"
373
+
374
+ return ok(
375
+ {
376
+ "app_token": app_token,
377
+ "provider": oauth_provider,
378
+ "refresh_token": refresh_token,
379
+ "privileges": priv,
380
+ },
381
+ cookies={"XSRF-TOKEN": xsrf_token},
382
+ )
@@ -0,0 +1,101 @@
1
+ import sys
2
+ import time
3
+ from typing import Callable, Optional
4
+
5
+ import elasticapm
6
+ import requests
7
+ from flask import request
8
+
9
+ from howler.api import bad_gateway, make_subapi_blueprint, ok
10
+ from howler.common.exceptions import AuthenticationException
11
+ from howler.common.logging import get_logger
12
+ from howler.common.swagger import generate_swagger_docs
13
+ from howler.config import cache, config
14
+ from howler.plugins import get_plugins
15
+ from howler.security import api_login
16
+
17
+ SUB_API = "borealis"
18
+ borealis_api = make_subapi_blueprint(SUB_API, api_version=1)
19
+ borealis_api._doc = "Proxy enrichment requests to borealis"
20
+
21
+ logger = get_logger(__file__)
22
+
23
+
24
+ def skip_cache(*args):
25
+ "Function to skip cache in testing mode"
26
+ return "pytest" in sys.modules
27
+
28
+
29
+ @cache.memoize(15 * 60, unless=skip_cache)
30
+ def get_token(access_token: str) -> str:
31
+ """Get a borealis token based on the current howler token"""
32
+ get_borealis_token: Optional[Callable[[str], str]] = None
33
+
34
+ for plugin in get_plugins():
35
+ if get_borealis_token := plugin.modules.token_functions.get("borealis", None):
36
+ break
37
+
38
+ if get_borealis_token:
39
+ borealis_access_token = get_borealis_token(access_token)
40
+ else:
41
+ logger.info("No custom borealis token logic provided, continuing with howler credentials")
42
+ borealis_access_token = access_token
43
+
44
+ return borealis_access_token
45
+
46
+
47
+ @generate_swagger_docs()
48
+ @borealis_api.route("/<path:path>", methods=["GET", "POST"])
49
+ @api_login(required_priv=["R"], required_method=["oauth"])
50
+ def proxy_to_borealis(path, **kwargs):
51
+ """Proxy enrichment requests to Borealis
52
+
53
+ Variables:
54
+ None
55
+
56
+ Arguments:
57
+ None
58
+
59
+ Data Block:
60
+ Any
61
+
62
+ Result Example:
63
+ Borealis Responses
64
+ """
65
+ logger.info(
66
+ "Proxying borealis request to path %s/%s?%s", config.core.borealis.url, path, request.query_string.decode()
67
+ )
68
+
69
+ auth_data: Optional[str] = request.headers.get("Authorization", None, type=str)
70
+
71
+ if not auth_data:
72
+ raise AuthenticationException("No Authorization header present")
73
+
74
+ auth_token = auth_data.split(" ")[1]
75
+
76
+ borealis_token = get_token(auth_token)
77
+
78
+ start = time.perf_counter()
79
+ with elasticapm.capture_span("borealis", span_type="http"):
80
+ if request.method.lower() == "get":
81
+ response = requests.get(
82
+ f"{config.core.borealis.url}/{path}",
83
+ headers={"Authorization": f"Bearer {borealis_token}", "Accept": "application/json"},
84
+ params=request.args.to_dict(),
85
+ timeout=5 * 60,
86
+ )
87
+ else:
88
+ response = requests.post(
89
+ f"{config.core.borealis.url}/{path}",
90
+ json=request.json,
91
+ headers={"Authorization": f"Bearer {borealis_token}", "Accept": "application/json"},
92
+ params=request.args.to_dict(),
93
+ timeout=5 * 60,
94
+ )
95
+
96
+ logger.debug(f"Request to borealis completed in {round(time.perf_counter() - start)}ms")
97
+
98
+ if not response.ok:
99
+ return bad_gateway(response.json(), err="Something went wrong when connecting to borealis")
100
+
101
+ return ok(response.json()["api_response"])
@@ -0,0 +1,55 @@
1
+ from flask import request
2
+
3
+ import howler.services.config_service as config_service
4
+ from howler.api import make_subapi_blueprint, ok
5
+ from howler.common.swagger import generate_swagger_docs
6
+ from howler.security.utils import get_disco_url
7
+
8
+ SUB_API = "configs"
9
+ config_api = make_subapi_blueprint(SUB_API, api_version=1)
10
+ config_api._doc = "Read configuration data about the system"
11
+
12
+
13
+ @generate_swagger_docs()
14
+ @config_api.route("/", methods=["GET"])
15
+ def configs(**kwargs):
16
+ """Return all of the configuration information about the deployment.
17
+
18
+ Variables:
19
+ None
20
+
21
+ Arguments:
22
+ None
23
+
24
+ Result Example:
25
+ {
26
+ "lookups": {
27
+ "status": [],
28
+ "scrutiny": [],
29
+ "escalation": [],
30
+ "assessment": []
31
+ },
32
+ "configuration": { # Configuration block
33
+ "auth": { # Authentication Configuration
34
+ "allow_apikeys": True, # Are APIKeys allowed for the user
35
+ "allow_extended_apikeys": True, # Allow user to generate extended access API Keys
36
+ },
37
+ "system": { # System Configuration
38
+ "type": "production", # Type of deployment
39
+ "version": "4.1" # Howler version
40
+ },
41
+ "ui": { # UI Configuration
42
+ "apps": [], # List of apps shown in the apps switcher
43
+ }
44
+ },
45
+ "c12nDef": {}, # Classification definition block
46
+ "indexes": {}, # Search indexes definitions
47
+ }
48
+
49
+ """
50
+ return ok(
51
+ config_service.get_configuration(
52
+ user=kwargs.get("user", None),
53
+ discovery_url=get_disco_url(request.environ.get("HTTP_REFERER")),
54
+ )
55
+ )