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.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +168 -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/clue.py +99 -0
- howler/api/v1/configs.py +58 -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 +788 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +416 -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 +125 -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/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 +2342 -0
- howler/datastore/constants.py +119 -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 +215 -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 +66 -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 +1543 -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 +24 -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 +609 -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 +331 -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-3.0.0.dev374.dist-info/METADATA +71 -0
- howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
- howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
- 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"])
|
howler/api/v1/configs.py
ADDED
|
@@ -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
|
+
)
|