howler-api 3.0.0.dev374__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (198) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +168 -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/clue.py +99 -0
  21. howler/api/v1/configs.py +58 -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 +788 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +416 -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 +125 -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/loader.py +154 -0
  41. howler/common/logging/__init__.py +241 -0
  42. howler/common/logging/audit.py +138 -0
  43. howler/common/logging/format.py +38 -0
  44. howler/common/net.py +79 -0
  45. howler/common/net_static.py +1494 -0
  46. howler/common/random_user.py +316 -0
  47. howler/common/swagger.py +117 -0
  48. howler/config.py +64 -0
  49. howler/cronjobs/__init__.py +29 -0
  50. howler/cronjobs/retention.py +61 -0
  51. howler/cronjobs/rules.py +274 -0
  52. howler/cronjobs/view_cleanup.py +88 -0
  53. howler/datastore/README.md +112 -0
  54. howler/datastore/__init__.py +0 -0
  55. howler/datastore/bulk.py +72 -0
  56. howler/datastore/collection.py +2342 -0
  57. howler/datastore/constants.py +119 -0
  58. howler/datastore/exceptions.py +41 -0
  59. howler/datastore/howler_store.py +105 -0
  60. howler/datastore/migrations/fix_process.py +41 -0
  61. howler/datastore/operations.py +130 -0
  62. howler/datastore/schemas.py +90 -0
  63. howler/datastore/store.py +231 -0
  64. howler/datastore/support/__init__.py +0 -0
  65. howler/datastore/support/build.py +215 -0
  66. howler/datastore/support/schemas.py +90 -0
  67. howler/datastore/types.py +22 -0
  68. howler/error.py +91 -0
  69. howler/external/__init__.py +0 -0
  70. howler/external/generate_mitre.py +96 -0
  71. howler/external/generate_sigma_rules.py +31 -0
  72. howler/external/generate_tlds.py +47 -0
  73. howler/external/reindex_data.py +66 -0
  74. howler/external/wipe_databases.py +58 -0
  75. howler/gunicorn_config.py +25 -0
  76. howler/healthz.py +47 -0
  77. howler/helper/__init__.py +0 -0
  78. howler/helper/azure.py +50 -0
  79. howler/helper/discover.py +59 -0
  80. howler/helper/hit.py +236 -0
  81. howler/helper/oauth.py +247 -0
  82. howler/helper/search.py +92 -0
  83. howler/helper/workflow.py +110 -0
  84. howler/helper/ws.py +378 -0
  85. howler/odm/README.md +102 -0
  86. howler/odm/__init__.py +1 -0
  87. howler/odm/base.py +1543 -0
  88. howler/odm/charter.txt +146 -0
  89. howler/odm/helper.py +416 -0
  90. howler/odm/howler_enum.py +25 -0
  91. howler/odm/models/__init__.py +0 -0
  92. howler/odm/models/action.py +33 -0
  93. howler/odm/models/analytic.py +90 -0
  94. howler/odm/models/assemblyline.py +48 -0
  95. howler/odm/models/aws.py +23 -0
  96. howler/odm/models/azure.py +16 -0
  97. howler/odm/models/cbs.py +44 -0
  98. howler/odm/models/config.py +558 -0
  99. howler/odm/models/dossier.py +33 -0
  100. howler/odm/models/ecs/__init__.py +0 -0
  101. howler/odm/models/ecs/agent.py +17 -0
  102. howler/odm/models/ecs/autonomous_system.py +16 -0
  103. howler/odm/models/ecs/client.py +149 -0
  104. howler/odm/models/ecs/cloud.py +141 -0
  105. howler/odm/models/ecs/code_signature.py +27 -0
  106. howler/odm/models/ecs/container.py +32 -0
  107. howler/odm/models/ecs/dns.py +62 -0
  108. howler/odm/models/ecs/egress.py +10 -0
  109. howler/odm/models/ecs/elf.py +74 -0
  110. howler/odm/models/ecs/email.py +122 -0
  111. howler/odm/models/ecs/error.py +14 -0
  112. howler/odm/models/ecs/event.py +140 -0
  113. howler/odm/models/ecs/faas.py +24 -0
  114. howler/odm/models/ecs/file.py +84 -0
  115. howler/odm/models/ecs/geo.py +30 -0
  116. howler/odm/models/ecs/group.py +18 -0
  117. howler/odm/models/ecs/hash.py +16 -0
  118. howler/odm/models/ecs/host.py +17 -0
  119. howler/odm/models/ecs/http.py +37 -0
  120. howler/odm/models/ecs/ingress.py +12 -0
  121. howler/odm/models/ecs/interface.py +21 -0
  122. howler/odm/models/ecs/network.py +30 -0
  123. howler/odm/models/ecs/observer.py +45 -0
  124. howler/odm/models/ecs/organization.py +12 -0
  125. howler/odm/models/ecs/os.py +21 -0
  126. howler/odm/models/ecs/pe.py +17 -0
  127. howler/odm/models/ecs/process.py +216 -0
  128. howler/odm/models/ecs/registry.py +26 -0
  129. howler/odm/models/ecs/related.py +45 -0
  130. howler/odm/models/ecs/rule.py +51 -0
  131. howler/odm/models/ecs/server.py +24 -0
  132. howler/odm/models/ecs/threat.py +247 -0
  133. howler/odm/models/ecs/tls.py +58 -0
  134. howler/odm/models/ecs/url.py +51 -0
  135. howler/odm/models/ecs/user.py +57 -0
  136. howler/odm/models/ecs/user_agent.py +20 -0
  137. howler/odm/models/ecs/vulnerability.py +41 -0
  138. howler/odm/models/gcp.py +16 -0
  139. howler/odm/models/hit.py +356 -0
  140. howler/odm/models/howler_data.py +328 -0
  141. howler/odm/models/lead.py +24 -0
  142. howler/odm/models/localized_label.py +13 -0
  143. howler/odm/models/overview.py +16 -0
  144. howler/odm/models/pivot.py +40 -0
  145. howler/odm/models/template.py +24 -0
  146. howler/odm/models/user.py +83 -0
  147. howler/odm/models/view.py +34 -0
  148. howler/odm/random_data.py +888 -0
  149. howler/odm/randomizer.py +609 -0
  150. howler/patched.py +5 -0
  151. howler/plugins/__init__.py +25 -0
  152. howler/plugins/config.py +123 -0
  153. howler/remote/__init__.py +0 -0
  154. howler/remote/datatypes/README.md +355 -0
  155. howler/remote/datatypes/__init__.py +98 -0
  156. howler/remote/datatypes/counters.py +63 -0
  157. howler/remote/datatypes/events.py +66 -0
  158. howler/remote/datatypes/hash.py +206 -0
  159. howler/remote/datatypes/lock.py +42 -0
  160. howler/remote/datatypes/queues/__init__.py +0 -0
  161. howler/remote/datatypes/queues/comms.py +59 -0
  162. howler/remote/datatypes/queues/multi.py +32 -0
  163. howler/remote/datatypes/queues/named.py +93 -0
  164. howler/remote/datatypes/queues/priority.py +215 -0
  165. howler/remote/datatypes/set.py +118 -0
  166. howler/remote/datatypes/user_quota_tracker.py +54 -0
  167. howler/security/__init__.py +253 -0
  168. howler/security/socket.py +108 -0
  169. howler/security/utils.py +185 -0
  170. howler/services/__init__.py +0 -0
  171. howler/services/action_service.py +111 -0
  172. howler/services/analytic_service.py +128 -0
  173. howler/services/auth_service.py +323 -0
  174. howler/services/config_service.py +128 -0
  175. howler/services/dossier_service.py +252 -0
  176. howler/services/event_service.py +93 -0
  177. howler/services/hit_service.py +893 -0
  178. howler/services/jwt_service.py +158 -0
  179. howler/services/lucene_service.py +286 -0
  180. howler/services/notebook_service.py +119 -0
  181. howler/services/overview_service.py +44 -0
  182. howler/services/template_service.py +45 -0
  183. howler/services/user_service.py +331 -0
  184. howler/utils/__init__.py +0 -0
  185. howler/utils/annotations.py +28 -0
  186. howler/utils/chunk.py +38 -0
  187. howler/utils/dict_utils.py +200 -0
  188. howler/utils/isotime.py +17 -0
  189. howler/utils/list_utils.py +11 -0
  190. howler/utils/lucene.py +77 -0
  191. howler/utils/path.py +27 -0
  192. howler/utils/socket_utils.py +61 -0
  193. howler/utils/str_utils.py +256 -0
  194. howler/utils/uid.py +47 -0
  195. howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
  196. howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
  197. howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
  198. howler_api-3.0.0.dev374.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
+ )
howler/api/v1/clue.py ADDED
@@ -0,0 +1,99 @@
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 = "clue"
18
+ clue_api = make_subapi_blueprint(SUB_API, api_version=1)
19
+ clue_api._doc = "Proxy enrichment requests to clue"
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 clue token based on the current howler token"""
32
+ get_clue_token: Optional[Callable[[str], str]] = None
33
+
34
+ for plugin in get_plugins():
35
+ if get_clue_token := plugin.modules.token_functions.get("clue", None):
36
+ break
37
+
38
+ if get_clue_token:
39
+ clue_access_token = get_clue_token(access_token)
40
+ else:
41
+ logger.info("No custom clue token logic provided, continuing with howler credentials")
42
+ clue_access_token = access_token
43
+
44
+ return clue_access_token
45
+
46
+
47
+ @generate_swagger_docs()
48
+ @clue_api.route("/<path:path>", methods=["GET", "POST"])
49
+ @api_login(required_priv=["R"], required_method=["oauth"])
50
+ def proxy_to_clue(path, **kwargs):
51
+ """Proxy enrichment requests to Clue
52
+
53
+ Variables:
54
+ None
55
+
56
+ Arguments:
57
+ None
58
+
59
+ Data Block:
60
+ Any
61
+
62
+ Result Example:
63
+ Clue Responses
64
+ """
65
+ logger.info("Proxying clue request to path %s/%s?%s", config.core.clue.url, path, request.query_string.decode())
66
+
67
+ auth_data: Optional[str] = request.headers.get("Authorization", None, type=str)
68
+
69
+ if not auth_data:
70
+ raise AuthenticationException("No Authorization header present")
71
+
72
+ auth_token = auth_data.split(" ")[1]
73
+
74
+ clue_token = get_token(auth_token)
75
+
76
+ start = time.perf_counter()
77
+ with elasticapm.capture_span("clue", span_type="http"):
78
+ if request.method.lower() == "get":
79
+ response = requests.get(
80
+ f"{config.core.clue.url}/{path}",
81
+ headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
82
+ params=request.args.to_dict(),
83
+ timeout=5 * 60,
84
+ )
85
+ else:
86
+ response = requests.post(
87
+ f"{config.core.clue.url}/{path}",
88
+ json=request.json,
89
+ headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
90
+ params=request.args.to_dict(),
91
+ timeout=5 * 60,
92
+ )
93
+
94
+ logger.debug(f"Request to clue completed in {round(time.perf_counter() - start)}ms")
95
+
96
+ if not response.ok:
97
+ return bad_gateway(response.json(), err="Something went wrong when connecting to clue")
98
+
99
+ return ok(response.json()["api_response"])
@@ -0,0 +1,58 @@
1
+ from typing import cast
2
+
3
+ from flask import request
4
+
5
+ import howler.services.config_service as config_service
6
+ from howler.api import make_subapi_blueprint, ok
7
+ from howler.common.swagger import generate_swagger_docs
8
+ from howler.odm.models.user import User
9
+ from howler.security.utils import get_disco_url
10
+
11
+ SUB_API = "configs"
12
+ config_api = make_subapi_blueprint(SUB_API, api_version=1)
13
+ config_api._doc = "Read configuration data about the system"
14
+
15
+
16
+ @generate_swagger_docs()
17
+ @config_api.route("/", methods=["GET"])
18
+ def configs(**kwargs):
19
+ """Return all of the configuration information about the deployment.
20
+
21
+ Variables:
22
+ None
23
+
24
+ Arguments:
25
+ None
26
+
27
+ Result Example:
28
+ {
29
+ "lookups": {
30
+ "status": [],
31
+ "scrutiny": [],
32
+ "escalation": [],
33
+ "assessment": []
34
+ },
35
+ "configuration": { # Configuration block
36
+ "auth": { # Authentication Configuration
37
+ "allow_apikeys": True, # Are APIKeys allowed for the user
38
+ "allow_extended_apikeys": True, # Allow user to generate extended access API Keys
39
+ },
40
+ "system": { # System Configuration
41
+ "type": "production", # Type of deployment
42
+ "version": "4.1" # Howler version
43
+ },
44
+ "ui": { # UI Configuration
45
+ "apps": [], # List of apps shown in the apps switcher
46
+ }
47
+ },
48
+ "c12nDef": {}, # Classification definition block
49
+ "indexes": {}, # Search indexes definitions
50
+ }
51
+
52
+ """
53
+ return ok(
54
+ config_service.get_configuration(
55
+ user=cast(User | None, kwargs.get("user", None)),
56
+ discovery_url=get_disco_url(request.environ.get("HTTP_REFERER")),
57
+ )
58
+ )