howler-api 2.13.0.dev329__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of howler-api might be problematic. Click here for more details.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +167 -0
- howler/actions/add_label.py +111 -0
- howler/actions/add_to_bundle.py +159 -0
- howler/actions/change_field.py +76 -0
- howler/actions/demote.py +160 -0
- howler/actions/example_plugin.py +104 -0
- howler/actions/prioritization.py +93 -0
- howler/actions/promote.py +147 -0
- howler/actions/remove_from_bundle.py +133 -0
- howler/actions/remove_label.py +111 -0
- howler/actions/transition.py +200 -0
- howler/api/__init__.py +249 -0
- howler/api/base.py +88 -0
- howler/api/socket.py +114 -0
- howler/api/v1/__init__.py +97 -0
- howler/api/v1/action.py +372 -0
- howler/api/v1/analytic.py +748 -0
- howler/api/v1/auth.py +382 -0
- howler/api/v1/borealis.py +101 -0
- howler/api/v1/configs.py +55 -0
- howler/api/v1/dossier.py +222 -0
- howler/api/v1/help.py +28 -0
- howler/api/v1/hit.py +1181 -0
- howler/api/v1/notebook.py +82 -0
- howler/api/v1/overview.py +191 -0
- howler/api/v1/search.py +715 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +414 -0
- howler/api/v1/utils/__init__.py +0 -0
- howler/api/v1/utils/etag.py +84 -0
- howler/api/v1/view.py +288 -0
- howler/app.py +235 -0
- howler/common/README.md +144 -0
- howler/common/__init__.py +0 -0
- howler/common/classification.py +979 -0
- howler/common/classification.yml +107 -0
- howler/common/exceptions.py +167 -0
- howler/common/hexdump.py +48 -0
- howler/common/iprange.py +171 -0
- howler/common/loader.py +154 -0
- howler/common/logging/__init__.py +241 -0
- howler/common/logging/audit.py +138 -0
- howler/common/logging/format.py +38 -0
- howler/common/net.py +79 -0
- howler/common/net_static.py +1494 -0
- howler/common/random_user.py +316 -0
- howler/common/swagger.py +117 -0
- howler/config.py +64 -0
- howler/cronjobs/__init__.py +29 -0
- howler/cronjobs/retention.py +61 -0
- howler/cronjobs/rules.py +274 -0
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +112 -0
- howler/datastore/__init__.py +0 -0
- howler/datastore/bulk.py +72 -0
- howler/datastore/collection.py +2327 -0
- howler/datastore/constants.py +117 -0
- howler/datastore/exceptions.py +41 -0
- howler/datastore/howler_store.py +105 -0
- howler/datastore/migrations/fix_process.py +41 -0
- howler/datastore/operations.py +130 -0
- howler/datastore/schemas.py +90 -0
- howler/datastore/store.py +231 -0
- howler/datastore/support/__init__.py +0 -0
- howler/datastore/support/build.py +214 -0
- howler/datastore/support/schemas.py +90 -0
- howler/datastore/types.py +22 -0
- howler/error.py +91 -0
- howler/external/__init__.py +0 -0
- howler/external/generate_mitre.py +96 -0
- howler/external/generate_sigma_rules.py +31 -0
- howler/external/generate_tlds.py +47 -0
- howler/external/reindex_data.py +46 -0
- howler/external/wipe_databases.py +58 -0
- howler/gunicorn_config.py +25 -0
- howler/healthz.py +47 -0
- howler/helper/__init__.py +0 -0
- howler/helper/azure.py +50 -0
- howler/helper/discover.py +59 -0
- howler/helper/hit.py +236 -0
- howler/helper/oauth.py +247 -0
- howler/helper/search.py +92 -0
- howler/helper/workflow.py +110 -0
- howler/helper/ws.py +378 -0
- howler/odm/README.md +102 -0
- howler/odm/__init__.py +1 -0
- howler/odm/base.py +1504 -0
- howler/odm/charter.txt +146 -0
- howler/odm/helper.py +416 -0
- howler/odm/howler_enum.py +25 -0
- howler/odm/models/__init__.py +0 -0
- howler/odm/models/action.py +33 -0
- howler/odm/models/analytic.py +90 -0
- howler/odm/models/assemblyline.py +48 -0
- howler/odm/models/aws.py +23 -0
- howler/odm/models/azure.py +16 -0
- howler/odm/models/cbs.py +44 -0
- howler/odm/models/config.py +558 -0
- howler/odm/models/dossier.py +33 -0
- howler/odm/models/ecs/__init__.py +0 -0
- howler/odm/models/ecs/agent.py +17 -0
- howler/odm/models/ecs/autonomous_system.py +16 -0
- howler/odm/models/ecs/client.py +149 -0
- howler/odm/models/ecs/cloud.py +141 -0
- howler/odm/models/ecs/code_signature.py +27 -0
- howler/odm/models/ecs/container.py +32 -0
- howler/odm/models/ecs/dns.py +62 -0
- howler/odm/models/ecs/egress.py +10 -0
- howler/odm/models/ecs/elf.py +74 -0
- howler/odm/models/ecs/email.py +122 -0
- howler/odm/models/ecs/error.py +14 -0
- howler/odm/models/ecs/event.py +140 -0
- howler/odm/models/ecs/faas.py +24 -0
- howler/odm/models/ecs/file.py +84 -0
- howler/odm/models/ecs/geo.py +30 -0
- howler/odm/models/ecs/group.py +18 -0
- howler/odm/models/ecs/hash.py +16 -0
- howler/odm/models/ecs/host.py +17 -0
- howler/odm/models/ecs/http.py +37 -0
- howler/odm/models/ecs/ingress.py +12 -0
- howler/odm/models/ecs/interface.py +21 -0
- howler/odm/models/ecs/network.py +30 -0
- howler/odm/models/ecs/observer.py +45 -0
- howler/odm/models/ecs/organization.py +12 -0
- howler/odm/models/ecs/os.py +21 -0
- howler/odm/models/ecs/pe.py +17 -0
- howler/odm/models/ecs/process.py +216 -0
- howler/odm/models/ecs/registry.py +26 -0
- howler/odm/models/ecs/related.py +45 -0
- howler/odm/models/ecs/rule.py +51 -0
- howler/odm/models/ecs/server.py +24 -0
- howler/odm/models/ecs/threat.py +247 -0
- howler/odm/models/ecs/tls.py +58 -0
- howler/odm/models/ecs/url.py +51 -0
- howler/odm/models/ecs/user.py +57 -0
- howler/odm/models/ecs/user_agent.py +20 -0
- howler/odm/models/ecs/vulnerability.py +41 -0
- howler/odm/models/gcp.py +16 -0
- howler/odm/models/hit.py +356 -0
- howler/odm/models/howler_data.py +328 -0
- howler/odm/models/lead.py +33 -0
- howler/odm/models/localized_label.py +13 -0
- howler/odm/models/overview.py +16 -0
- howler/odm/models/pivot.py +40 -0
- howler/odm/models/template.py +24 -0
- howler/odm/models/user.py +83 -0
- howler/odm/models/view.py +34 -0
- howler/odm/random_data.py +888 -0
- howler/odm/randomizer.py +606 -0
- howler/patched.py +5 -0
- howler/plugins/__init__.py +25 -0
- howler/plugins/config.py +123 -0
- howler/remote/__init__.py +0 -0
- howler/remote/datatypes/README.md +355 -0
- howler/remote/datatypes/__init__.py +98 -0
- howler/remote/datatypes/counters.py +63 -0
- howler/remote/datatypes/events.py +66 -0
- howler/remote/datatypes/hash.py +206 -0
- howler/remote/datatypes/lock.py +42 -0
- howler/remote/datatypes/queues/__init__.py +0 -0
- howler/remote/datatypes/queues/comms.py +59 -0
- howler/remote/datatypes/queues/multi.py +32 -0
- howler/remote/datatypes/queues/named.py +93 -0
- howler/remote/datatypes/queues/priority.py +215 -0
- howler/remote/datatypes/set.py +118 -0
- howler/remote/datatypes/user_quota_tracker.py +54 -0
- howler/security/__init__.py +253 -0
- howler/security/socket.py +108 -0
- howler/security/utils.py +185 -0
- howler/services/__init__.py +0 -0
- howler/services/action_service.py +111 -0
- howler/services/analytic_service.py +128 -0
- howler/services/auth_service.py +323 -0
- howler/services/config_service.py +128 -0
- howler/services/dossier_service.py +252 -0
- howler/services/event_service.py +93 -0
- howler/services/hit_service.py +893 -0
- howler/services/jwt_service.py +158 -0
- howler/services/lucene_service.py +286 -0
- howler/services/notebook_service.py +119 -0
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/services/user_service.py +330 -0
- howler/utils/__init__.py +0 -0
- howler/utils/annotations.py +28 -0
- howler/utils/chunk.py +38 -0
- howler/utils/dict_utils.py +200 -0
- howler/utils/isotime.py +17 -0
- howler/utils/list_utils.py +11 -0
- howler/utils/lucene.py +77 -0
- howler/utils/path.py +27 -0
- howler/utils/socket_utils.py +61 -0
- howler/utils/str_utils.py +256 -0
- howler/utils/uid.py +47 -0
- howler_api-2.13.0.dev329.dist-info/METADATA +71 -0
- howler_api-2.13.0.dev329.dist-info/RECORD +200 -0
- howler_api-2.13.0.dev329.dist-info/WHEEL +4 -0
- howler_api-2.13.0.dev329.dist-info/entry_points.txt +8 -0
howler/api/v1/view.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from flask import request
|
|
2
|
+
from mergedeep.mergedeep import merge
|
|
3
|
+
|
|
4
|
+
from howler.api import bad_request, created, forbidden, make_subapi_blueprint, no_content, not_found, ok
|
|
5
|
+
from howler.common.exceptions import HowlerException
|
|
6
|
+
from howler.common.loader import datastore
|
|
7
|
+
from howler.common.logging import get_logger
|
|
8
|
+
from howler.common.swagger import generate_swagger_docs
|
|
9
|
+
from howler.datastore.exceptions import SearchException
|
|
10
|
+
from howler.odm.models.user import User
|
|
11
|
+
from howler.odm.models.view import View
|
|
12
|
+
from howler.security import api_login
|
|
13
|
+
|
|
14
|
+
SUB_API = "view"
|
|
15
|
+
view_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
16
|
+
view_api._doc = "Manage the different views created for filtering hits"
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__file__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@generate_swagger_docs()
|
|
22
|
+
@view_api.route("/", methods=["GET"])
|
|
23
|
+
@api_login(required_priv=["R"])
|
|
24
|
+
def get_views(user: User, **kwargs):
|
|
25
|
+
"""Get a list of views the user can use to filter hits
|
|
26
|
+
|
|
27
|
+
Variables:
|
|
28
|
+
None
|
|
29
|
+
|
|
30
|
+
Optional Arguments:
|
|
31
|
+
None
|
|
32
|
+
|
|
33
|
+
Result Example:
|
|
34
|
+
[
|
|
35
|
+
...views # A list of views the user can use
|
|
36
|
+
]
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
return ok(
|
|
40
|
+
datastore().view.search(
|
|
41
|
+
f"type:global OR owner:({user['uname']} OR none)", as_obj=False, rows=1000, sort="title asc"
|
|
42
|
+
)["items"]
|
|
43
|
+
)
|
|
44
|
+
except ValueError as e:
|
|
45
|
+
return bad_request(err=str(e))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@generate_swagger_docs()
|
|
49
|
+
@view_api.route("/", methods=["POST"])
|
|
50
|
+
@api_login(required_priv=["R", "W"])
|
|
51
|
+
def create_view(**kwargs):
|
|
52
|
+
"""Create a new view
|
|
53
|
+
|
|
54
|
+
Variables:
|
|
55
|
+
None
|
|
56
|
+
|
|
57
|
+
Optional Arguments:
|
|
58
|
+
None
|
|
59
|
+
|
|
60
|
+
Data Block:
|
|
61
|
+
{
|
|
62
|
+
"title": "New View" # The name of this view
|
|
63
|
+
"query": "howler.id:*" # The query to run
|
|
64
|
+
"type": "global" # The type of view - personal or global
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Result Example:
|
|
68
|
+
{
|
|
69
|
+
...view # The new view data
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
view_data = request.json
|
|
73
|
+
if not isinstance(view_data, dict):
|
|
74
|
+
return bad_request(err="Invalid data format")
|
|
75
|
+
|
|
76
|
+
if "title" not in view_data:
|
|
77
|
+
return bad_request(err="You must specify a title when creating a view.")
|
|
78
|
+
|
|
79
|
+
if "query" not in view_data:
|
|
80
|
+
return bad_request(err="You must specify a query when creating a view.")
|
|
81
|
+
|
|
82
|
+
if "type" not in view_data:
|
|
83
|
+
return bad_request(err="You must specify a type when creating a view.")
|
|
84
|
+
|
|
85
|
+
storage = datastore()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Make sure the query is valid
|
|
89
|
+
storage.hit.search(view_data["query"])
|
|
90
|
+
|
|
91
|
+
view = View(view_data)
|
|
92
|
+
|
|
93
|
+
view.owner = kwargs["user"]["uname"]
|
|
94
|
+
|
|
95
|
+
if view.type == "personal":
|
|
96
|
+
current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
|
|
97
|
+
|
|
98
|
+
current_user["favourite_views"] = current_user.get("favourite_views", []) + [view.view_id]
|
|
99
|
+
|
|
100
|
+
storage.user.save(current_user["uname"], current_user)
|
|
101
|
+
|
|
102
|
+
storage.view.save(view.view_id, view)
|
|
103
|
+
return created(view)
|
|
104
|
+
except SearchException:
|
|
105
|
+
return bad_request(err="You must use a valid query when creating a view.")
|
|
106
|
+
except HowlerException as e:
|
|
107
|
+
return bad_request(err=str(e))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@generate_swagger_docs()
|
|
111
|
+
@view_api.route("/<view_id>", methods=["DELETE"])
|
|
112
|
+
@api_login(required_priv=["W"])
|
|
113
|
+
def delete_view(view_id: str, user: User, **kwargs):
|
|
114
|
+
"""Delete a view
|
|
115
|
+
|
|
116
|
+
Variables:
|
|
117
|
+
view_id => The id of the view to delete
|
|
118
|
+
|
|
119
|
+
Optional Arguments:
|
|
120
|
+
None
|
|
121
|
+
|
|
122
|
+
Data Block:
|
|
123
|
+
None
|
|
124
|
+
|
|
125
|
+
Result Example:
|
|
126
|
+
{
|
|
127
|
+
"success": true # Did the deletion succeed?
|
|
128
|
+
}
|
|
129
|
+
"""
|
|
130
|
+
storage = datastore()
|
|
131
|
+
|
|
132
|
+
existing_view: View = storage.view.get_if_exists(view_id)
|
|
133
|
+
if not existing_view:
|
|
134
|
+
return not_found(err="This view does not exist")
|
|
135
|
+
|
|
136
|
+
if existing_view.owner != user.uname and "admin" not in user.type:
|
|
137
|
+
return forbidden(err="You cannot delete a view unless you are an administrator, or the owner.")
|
|
138
|
+
|
|
139
|
+
if existing_view.type == "readonly":
|
|
140
|
+
return forbidden(err="You cannot delete built-in views.")
|
|
141
|
+
|
|
142
|
+
success = storage.view.delete(view_id)
|
|
143
|
+
|
|
144
|
+
storage.view.commit()
|
|
145
|
+
|
|
146
|
+
return no_content({"success": success})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@generate_swagger_docs()
|
|
150
|
+
@view_api.route("/<view_id>", methods=["PUT"])
|
|
151
|
+
@api_login(required_priv=["R", "W"])
|
|
152
|
+
def update_view(view_id: str, user: User, **kwargs):
|
|
153
|
+
"""Update a view
|
|
154
|
+
|
|
155
|
+
Variables:
|
|
156
|
+
view_id => The view_id of the view to modify
|
|
157
|
+
|
|
158
|
+
Optional Arguments:
|
|
159
|
+
None
|
|
160
|
+
|
|
161
|
+
Data Block:
|
|
162
|
+
{
|
|
163
|
+
"title": "New View Name" # The name of this view
|
|
164
|
+
"query": "howler.id:*" # The query to run
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Result Example:
|
|
168
|
+
{
|
|
169
|
+
...view # The updated view data
|
|
170
|
+
}
|
|
171
|
+
"""
|
|
172
|
+
storage = datastore()
|
|
173
|
+
|
|
174
|
+
new_data = request.json
|
|
175
|
+
if not isinstance(new_data, dict):
|
|
176
|
+
return bad_request(err="Invalid data format")
|
|
177
|
+
|
|
178
|
+
if set(new_data.keys()) & {"view_id", "owner"}:
|
|
179
|
+
return bad_request(err="You cannot change the owner or id of a view.")
|
|
180
|
+
|
|
181
|
+
existing_view: View = storage.view.get_if_exists(view_id)
|
|
182
|
+
if not existing_view:
|
|
183
|
+
return not_found(err="This view does not exist")
|
|
184
|
+
|
|
185
|
+
if existing_view.type == "readonly":
|
|
186
|
+
return forbidden(err="You cannot edit a built-in view.")
|
|
187
|
+
|
|
188
|
+
if existing_view.type == "personal" and existing_view.owner != user.uname:
|
|
189
|
+
return forbidden(err="You cannot update a personal view that is not owned by you.")
|
|
190
|
+
|
|
191
|
+
if existing_view.type == "global" and existing_view.owner != user.uname and "admin" not in user.type:
|
|
192
|
+
return forbidden(err="Only the owner of a view and administrators can edit a global view.")
|
|
193
|
+
|
|
194
|
+
new_view = View(merge({}, existing_view.as_primitives(), new_data))
|
|
195
|
+
|
|
196
|
+
storage.view.save(new_view.view_id, new_view)
|
|
197
|
+
|
|
198
|
+
storage.view.commit()
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if "query" in new_data:
|
|
202
|
+
# Make sure the query is valid
|
|
203
|
+
storage.hit.search(new_data["query"])
|
|
204
|
+
|
|
205
|
+
return ok(storage.view.get_if_exists(existing_view.view_id, as_obj=False))
|
|
206
|
+
except SearchException:
|
|
207
|
+
return bad_request(err="You must use a valid query when updating a view.")
|
|
208
|
+
except HowlerException as e:
|
|
209
|
+
return bad_request(err=str(e))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@generate_swagger_docs()
|
|
213
|
+
@view_api.route("/<view_id>/favourite", methods=["POST"])
|
|
214
|
+
@api_login(required_priv=["R", "W"])
|
|
215
|
+
def set_as_favourite(view_id: str, **kwargs):
|
|
216
|
+
"""Add a view to a list of the user's favourites
|
|
217
|
+
|
|
218
|
+
Variables:
|
|
219
|
+
view_id => The id of the view to add as a favourite
|
|
220
|
+
|
|
221
|
+
Optional Arguments:
|
|
222
|
+
None
|
|
223
|
+
|
|
224
|
+
Data Block:
|
|
225
|
+
{} # Empty
|
|
226
|
+
|
|
227
|
+
Result Example:
|
|
228
|
+
{
|
|
229
|
+
"success": True # If the operation succeeded
|
|
230
|
+
}
|
|
231
|
+
"""
|
|
232
|
+
storage = datastore()
|
|
233
|
+
|
|
234
|
+
existing_view: View = storage.view.get_if_exists(view_id)
|
|
235
|
+
if not existing_view:
|
|
236
|
+
return not_found(err="This view does not exist")
|
|
237
|
+
|
|
238
|
+
if existing_view.type != "global" and (
|
|
239
|
+
existing_view.owner != kwargs["user"]["uname"] and existing_view.owner != "none"
|
|
240
|
+
):
|
|
241
|
+
return forbidden(err="You can only favourite global views, or views owned by you.")
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
|
|
245
|
+
|
|
246
|
+
current_user["favourite_views"] = list(set(current_user.get("favourite_views", []) + [view_id]))
|
|
247
|
+
|
|
248
|
+
storage.user.save(current_user["uname"], current_user)
|
|
249
|
+
|
|
250
|
+
return ok()
|
|
251
|
+
except ValueError as e:
|
|
252
|
+
return bad_request(err=str(e))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@generate_swagger_docs()
|
|
256
|
+
@view_api.route("/<view_id>/favourite", methods=["DELETE"])
|
|
257
|
+
@api_login(required_priv=["R", "W"])
|
|
258
|
+
def remove_as_favourite(view_id: str, **kwargs):
|
|
259
|
+
"""Remove a view from a list of the user's favourites
|
|
260
|
+
|
|
261
|
+
Variables:
|
|
262
|
+
id => The id of the view to remove as a favourite
|
|
263
|
+
|
|
264
|
+
Optional Arguments:
|
|
265
|
+
None
|
|
266
|
+
|
|
267
|
+
Result Example:
|
|
268
|
+
{
|
|
269
|
+
"success": True # If the operation succeeded
|
|
270
|
+
}
|
|
271
|
+
"""
|
|
272
|
+
storage = datastore()
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
|
|
276
|
+
|
|
277
|
+
current_favourites: list[str] = current_user.get("favourite_views", [])
|
|
278
|
+
|
|
279
|
+
if view_id not in current_favourites:
|
|
280
|
+
return not_found(err="View is not favourited.")
|
|
281
|
+
|
|
282
|
+
current_user["favourite_views"] = [favourite for favourite in current_favourites if favourite != view_id]
|
|
283
|
+
|
|
284
|
+
storage.user.save(current_user["uname"], current_user)
|
|
285
|
+
|
|
286
|
+
return no_content()
|
|
287
|
+
except ValueError as e:
|
|
288
|
+
return bad_request(err=str(e))
|
howler/app.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
from howler.plugins import get_plugins
|
|
8
|
+
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
# We append the plugin directory for howler to the python part
|
|
12
|
+
PLUGIN_PATH = Path(os.environ.get("HWL_PLUGIN_DIRECTORY", "/etc/howler/plugins"))
|
|
13
|
+
sys.path.insert(0, str(PLUGIN_PATH))
|
|
14
|
+
|
|
15
|
+
from howler.odm.models.config import config
|
|
16
|
+
|
|
17
|
+
if config.ui.debug and PLUGIN_PATH.exists():
|
|
18
|
+
for _plugin in PLUGIN_PATH.iterdir():
|
|
19
|
+
sys.path.append(
|
|
20
|
+
str(Path(os.path.realpath(_plugin)) / f"../.venv/lib/python3.{sys.version_info.minor}/site-packages")
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any, cast
|
|
25
|
+
|
|
26
|
+
from authlib.integrations.flask_client import OAuth
|
|
27
|
+
from elasticapm.contrib.flask import ElasticAPM
|
|
28
|
+
from flasgger import Swagger
|
|
29
|
+
from flask import Flask
|
|
30
|
+
from flask.blueprints import Blueprint
|
|
31
|
+
from flask.logging import default_handler
|
|
32
|
+
from prometheus_client import make_wsgi_app
|
|
33
|
+
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
|
34
|
+
|
|
35
|
+
from howler.api.base import api
|
|
36
|
+
from howler.api.socket import socket_api
|
|
37
|
+
from howler.api.v1 import apiv1
|
|
38
|
+
from howler.api.v1.action import action_api
|
|
39
|
+
from howler.api.v1.analytic import analytic_api
|
|
40
|
+
from howler.api.v1.auth import auth_api
|
|
41
|
+
from howler.api.v1.configs import config_api
|
|
42
|
+
from howler.api.v1.dossier import dossier_api
|
|
43
|
+
from howler.api.v1.help import help_api
|
|
44
|
+
from howler.api.v1.hit import hit_api
|
|
45
|
+
from howler.api.v1.overview import overview_api
|
|
46
|
+
from howler.api.v1.search import search_api
|
|
47
|
+
from howler.api.v1.template import template_api
|
|
48
|
+
from howler.api.v1.tool import tool_api
|
|
49
|
+
from howler.api.v1.user import user_api
|
|
50
|
+
from howler.api.v1.view import view_api
|
|
51
|
+
from howler.common.logging import get_logger
|
|
52
|
+
from howler.config import (
|
|
53
|
+
DEBUG,
|
|
54
|
+
HWL_UNSECURED_UI,
|
|
55
|
+
HWL_USE_JOB_SYSTEM,
|
|
56
|
+
HWL_USE_REST_API,
|
|
57
|
+
HWL_USE_WEBSOCKET_API,
|
|
58
|
+
SECRET_KEY,
|
|
59
|
+
cache,
|
|
60
|
+
config,
|
|
61
|
+
)
|
|
62
|
+
from howler.cronjobs import setup_jobs
|
|
63
|
+
from howler.error import errors
|
|
64
|
+
from howler.healthz import healthz
|
|
65
|
+
|
|
66
|
+
logger = get_logger(__file__)
|
|
67
|
+
|
|
68
|
+
app = Flask(
|
|
69
|
+
"howler-api",
|
|
70
|
+
static_url_path="/api/static",
|
|
71
|
+
static_folder=config.ui.static_folder,
|
|
72
|
+
)
|
|
73
|
+
# Disable strict check on trailing slashes for endpoints
|
|
74
|
+
app.url_map.strict_slashes = False
|
|
75
|
+
app.config["JSON_SORT_KEYS"] = False
|
|
76
|
+
|
|
77
|
+
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/metrics": make_wsgi_app()}) # type: ignore[method-assign]
|
|
78
|
+
|
|
79
|
+
swagger_template = {
|
|
80
|
+
"info": {
|
|
81
|
+
"title": "Howler API",
|
|
82
|
+
"description": (
|
|
83
|
+
"Howler is an application that allows analysts to triage hits and alerts. It provides a way for "
|
|
84
|
+
"analysts to efficiently review and analyze alerts generated by different analytics and detections."
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
swagger = Swagger(
|
|
89
|
+
app,
|
|
90
|
+
template=swagger_template,
|
|
91
|
+
config={
|
|
92
|
+
"headers": [],
|
|
93
|
+
"static_url_path": "/api/swagger_static",
|
|
94
|
+
"specs": [
|
|
95
|
+
{
|
|
96
|
+
"endpoint": "apispec_v1",
|
|
97
|
+
"route": "/api/apispec_v1.json",
|
|
98
|
+
"rule_filter": lambda rule: True, # all in
|
|
99
|
+
"model_filter": lambda tag: True, # all in
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"specs_route": "/api/docs",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
cache.init_app(app)
|
|
107
|
+
|
|
108
|
+
app.logger.setLevel(60) # This completely turns off the flask logger
|
|
109
|
+
if HWL_UNSECURED_UI:
|
|
110
|
+
app.config.update(SESSION_COOKIE_SECURE=False, SECRET_KEY=SECRET_KEY, PREFERRED_URL_SCHEME="http")
|
|
111
|
+
else:
|
|
112
|
+
app.config.update(SESSION_COOKIE_SECURE=True, SECRET_KEY=SECRET_KEY, PREFERRED_URL_SCHEME="https")
|
|
113
|
+
|
|
114
|
+
app.register_blueprint(errors)
|
|
115
|
+
app.register_blueprint(healthz)
|
|
116
|
+
|
|
117
|
+
if HWL_USE_REST_API or DEBUG:
|
|
118
|
+
logger.debug("Enabled REST API")
|
|
119
|
+
app.register_blueprint(action_api)
|
|
120
|
+
app.register_blueprint(analytic_api)
|
|
121
|
+
app.register_blueprint(api)
|
|
122
|
+
app.register_blueprint(apiv1)
|
|
123
|
+
app.register_blueprint(auth_api)
|
|
124
|
+
app.register_blueprint(config_api)
|
|
125
|
+
app.register_blueprint(help_api)
|
|
126
|
+
app.register_blueprint(hit_api)
|
|
127
|
+
app.register_blueprint(search_api)
|
|
128
|
+
app.register_blueprint(template_api)
|
|
129
|
+
app.register_blueprint(overview_api)
|
|
130
|
+
app.register_blueprint(tool_api)
|
|
131
|
+
app.register_blueprint(user_api)
|
|
132
|
+
app.register_blueprint(view_api)
|
|
133
|
+
app.register_blueprint(dossier_api)
|
|
134
|
+
|
|
135
|
+
if config.core.notebook.enabled:
|
|
136
|
+
from howler.api.v1.notebook import notebook_api
|
|
137
|
+
|
|
138
|
+
logger.debug("Enabled Notebook Integration")
|
|
139
|
+
app.register_blueprint(notebook_api)
|
|
140
|
+
|
|
141
|
+
if config.core.borealis.enabled:
|
|
142
|
+
from howler.api.v1.borealis import borealis_api
|
|
143
|
+
|
|
144
|
+
logger.debug("Enabled Borealis Integration")
|
|
145
|
+
app.register_blueprint(borealis_api)
|
|
146
|
+
|
|
147
|
+
logger.info("Checking plugins for additional routes")
|
|
148
|
+
for plugin in get_plugins():
|
|
149
|
+
if not plugin.modules.routes:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
for route in cast(list[Blueprint], plugin.modules.routes):
|
|
153
|
+
logger.info("Enabling additional endpoint: %s", route.url_prefix)
|
|
154
|
+
app.register_blueprint(route)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
else:
|
|
158
|
+
logger.info("Disabled REST API")
|
|
159
|
+
|
|
160
|
+
if HWL_USE_WEBSOCKET_API or DEBUG:
|
|
161
|
+
logger.debug("Enabled Websocket API")
|
|
162
|
+
app.register_blueprint(socket_api)
|
|
163
|
+
else:
|
|
164
|
+
logger.info("Disabled Websocket API")
|
|
165
|
+
|
|
166
|
+
if HWL_USE_JOB_SYSTEM or DEBUG:
|
|
167
|
+
setup_jobs()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Setup OAuth providers
|
|
171
|
+
if config.auth.oauth.enabled:
|
|
172
|
+
providers = []
|
|
173
|
+
for name, provider in config.auth.oauth.providers.items():
|
|
174
|
+
p: dict[str, Any] = provider.model_dump()
|
|
175
|
+
|
|
176
|
+
# Set provider name
|
|
177
|
+
p["name"] = name
|
|
178
|
+
|
|
179
|
+
# Remove howler specific fields from oAuth config
|
|
180
|
+
p.pop("auto_create", None)
|
|
181
|
+
p.pop("auto_sync", None)
|
|
182
|
+
p.pop("user_get", None)
|
|
183
|
+
p.pop("auto_properties", None)
|
|
184
|
+
p.pop("uid_regex", None)
|
|
185
|
+
p.pop("uid_format", None)
|
|
186
|
+
p.pop("user_groups", None)
|
|
187
|
+
p.pop("user_groups_data_field", None)
|
|
188
|
+
p.pop("user_groups_name_field", None)
|
|
189
|
+
p.pop("app_provider", None)
|
|
190
|
+
|
|
191
|
+
# Add the provider to the list of providers
|
|
192
|
+
providers.append(p)
|
|
193
|
+
|
|
194
|
+
if providers:
|
|
195
|
+
oauth = OAuth()
|
|
196
|
+
for p in providers:
|
|
197
|
+
oauth.register(**p)
|
|
198
|
+
oauth.init_app(app)
|
|
199
|
+
|
|
200
|
+
# Setup logging
|
|
201
|
+
app.logger.setLevel(logger.getEffectiveLevel())
|
|
202
|
+
app.logger.removeHandler(default_handler)
|
|
203
|
+
if logger.parent:
|
|
204
|
+
for ph in logger.parent.handlers:
|
|
205
|
+
app.logger.addHandler(ph)
|
|
206
|
+
|
|
207
|
+
# Setup APMs
|
|
208
|
+
if config.core.metrics.apm_server.server_url is not None:
|
|
209
|
+
logger.info(f"Exporting application metrics to: {config.core.metrics.apm_server.server_url}")
|
|
210
|
+
ElasticAPM(
|
|
211
|
+
app,
|
|
212
|
+
server_url=config.core.metrics.apm_server.server_url,
|
|
213
|
+
service_name="howler_api",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
wlog = logging.getLogger("werkzeug")
|
|
217
|
+
wlog.setLevel(logging.WARNING)
|
|
218
|
+
if logger.parent: # pragma: no cover
|
|
219
|
+
for h in logger.parent.handlers:
|
|
220
|
+
wlog.addHandler(h)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main():
|
|
224
|
+
"""Main application function"""
|
|
225
|
+
app.jinja_env.cache = {}
|
|
226
|
+
app.run(
|
|
227
|
+
host="0.0.0.0", # noqa: S104
|
|
228
|
+
debug=DEBUG,
|
|
229
|
+
port=int(os.getenv("FLASK_RUN_PORT", os.getenv("PORT", 5000))),
|
|
230
|
+
extra_files=os.environ.get("FLASK_RUN_EXTRA_FILES", "").split(":"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
howler/common/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Utility Functions
|
|
2
|
+
|
|
3
|
+
The `howler/common` folder provides the utility functions for the library. Each file inside this folder will be explained in this README.
|
|
4
|
+
|
|
5
|
+
## chunk.py
|
|
6
|
+
|
|
7
|
+
Has utilities to transform list of items into list of tuples grouping sets of X items together.
|
|
8
|
+
|
|
9
|
+
- `chunk(list)`: The chunk function return a generator of tuples.
|
|
10
|
+
|
|
11
|
+
- `chunked_list(list)`:
|
|
12
|
+
|
|
13
|
+
The chunked_list goes through all the items and returns a list of all the tuples.
|
|
14
|
+
|
|
15
|
+
chunked_list([1,2,3,4,5,6,7,8], 2): [(1,2), (3,4), (5,6), (7,8)]
|
|
16
|
+
|
|
17
|
+
## classification.py
|
|
18
|
+
|
|
19
|
+
This file, in conjunction to it's default configuration file `classification.yml`, provide support for handling classifications in the system (Access control). It is fully configurable and the configuration definition is provided in-line in the `classification.yml` file.
|
|
20
|
+
|
|
21
|
+
### Classification object
|
|
22
|
+
|
|
23
|
+
The classification object provides the different methods to parse, normalize and compare classification strings. Here are some notable functions you will likely be using:
|
|
24
|
+
|
|
25
|
+
- `list_all_combinations()`: This function returns all possible classification strings that the current `classification.yml` file supports.
|
|
26
|
+
- `get_access_control_parts()`: This functions splits the classification string in parts to be used in a lucene query.
|
|
27
|
+
- `intersect_user_classification(c12n_1, c12n_2)`: This function takes two classification strings and generate the highest classification that both strings share in common.
|
|
28
|
+
- `is_accessible(user_c12n, target_c12n)`: This function verifies if a user's maximum classification give them access to see a certain target classification.
|
|
29
|
+
- `max_classification(c12n_1, c12n_2)`: This function returns the highest possible classification by mixing both classifications.
|
|
30
|
+
- `min_classification(c12n_1, c12n_2)`: This function returns the minimum possible classification by mixing both classifications.
|
|
31
|
+
|
|
32
|
+
## dict_utils.py
|
|
33
|
+
|
|
34
|
+
This file provides utility functions to merge dictionaries together, find the differences between dictionaries or change the ways its keys are displayed.
|
|
35
|
+
|
|
36
|
+
- `strip_nulls(dict)`: This function remove all keys that are null in the dictionary rescursively.
|
|
37
|
+
- `recursive_update(dict, update_dict)`: This function recursively applied the update_dict values to the original dict that was provided
|
|
38
|
+
- `get_recursive_delta(d1, d2)`: This function generate a delta dictionary that tells you which keys changed to which values if you go from d1 to d2.
|
|
39
|
+
- `flatten/unflatten(dict)`:
|
|
40
|
+
|
|
41
|
+
The flatten function take a multiple level deep dictionary and transforms it into a single level dictionary by preserving the key space using a dotted notation:
|
|
42
|
+
|
|
43
|
+
{a: {b: 1} }: {a.b: 1}
|
|
44
|
+
|
|
45
|
+
Where as the unflatten does the invert by taking the dotted notation and transforming it back to it's original multiple level dictionary.
|
|
46
|
+
|
|
47
|
+
## hexdump.py
|
|
48
|
+
|
|
49
|
+
This file provide functions to take a binary data blob and transform it into and hexadecimal dump of its bytes. The `dump(buf)` function only outputs the bytes where as the `hexdump(buf)` function outputs also the offsets and some trimmed down ascii representation.
|
|
50
|
+
|
|
51
|
+
`dump("HTTP/1.1 404 Not\r\nCont")`
|
|
52
|
+
|
|
53
|
+
48 54 54 50 2F 31 2E 31 20 34 30 34 20 4E 6F 74 20 46 6F 75 6E 64 0D 0A 43 6F 6E 74
|
|
54
|
+
|
|
55
|
+
`hexdump("HTTP/1.1 404 Not\r\nCont")`
|
|
56
|
+
|
|
57
|
+
00000000: 48 54 54 50 2F 31 2E 31 20 34 30 34 20 4E 6F 74 HTTP/1.1 404 Not
|
|
58
|
+
00000010: 20 46 6F 75 6E 64 0D 0A 43 6F 6E 74 Found..Cont
|
|
59
|
+
|
|
60
|
+
## iprange.py
|
|
61
|
+
|
|
62
|
+
This file provides you with a RangeTable class that let's you determine if and IP is part of a certain CIDR definition. It also provides to quick function that let you determine if an IP is in a private CIDR (`is_private(ip)`) or if an IP is in a reserved CIDR (`is_reserved(ip)`).
|
|
63
|
+
|
|
64
|
+
*Note*: only IPV4 IPs are supported.
|
|
65
|
+
|
|
66
|
+
## isotime.py
|
|
67
|
+
|
|
68
|
+
This file provides you which methods to transform date into strings or epoch values. It support local, ISO and epoch time. It also makes sure that the local and ISO time get up to a microsecond precision.
|
|
69
|
+
|
|
70
|
+
Here are the support date operation:
|
|
71
|
+
|
|
72
|
+
- Get current time functions:
|
|
73
|
+
- `now()` -> Current epoch time
|
|
74
|
+
- `now_as_iso()` -> Current iso time
|
|
75
|
+
- `now_as_local()` -> Current local time
|
|
76
|
+
- Tranformation functions:
|
|
77
|
+
- `epoch_to_iso(date)`
|
|
78
|
+
- `epoch_to_local(date)`
|
|
79
|
+
- `local_to_epoch(date)`
|
|
80
|
+
- `local_to_iso(date)`
|
|
81
|
+
- `iso_to_epoch(date)`
|
|
82
|
+
- `iso_to_local(date)`
|
|
83
|
+
|
|
84
|
+
## loader.py
|
|
85
|
+
|
|
86
|
+
This file provide helper function for components that require external configuration files: Classification engine, datastore and remote datatypes.
|
|
87
|
+
|
|
88
|
+
- `get_classification()`: returns a pre-configured classification object.
|
|
89
|
+
- `get_config()`: returns the current classification of the system.
|
|
90
|
+
- `get_datastore()`: returns an Howler datastore using the config form the get_config() output.
|
|
91
|
+
|
|
92
|
+
## log.py/logformat.py
|
|
93
|
+
|
|
94
|
+
This file provides an `init_logger()` function that will setup logging in your app using the configuration file and formats it using the format found in logformat.py
|
|
95
|
+
|
|
96
|
+
## memory_zip.py
|
|
97
|
+
|
|
98
|
+
Provides an interface file to create zip files in memory.
|
|
99
|
+
|
|
100
|
+
## net.py/net_static.py
|
|
101
|
+
|
|
102
|
+
Provide multiple function to validate ip/port/domains and the get networking information about the current host.
|
|
103
|
+
|
|
104
|
+
- Validation:
|
|
105
|
+
- `is_valid_port(port)`
|
|
106
|
+
- `is_valid_domain(domain)`
|
|
107
|
+
- `is_valid_ip(ip)`
|
|
108
|
+
- `is_valid_email(email)`
|
|
109
|
+
- `is_ip_in_network(ip, cidr)`
|
|
110
|
+
- Network information:
|
|
111
|
+
- `get_hostname()`
|
|
112
|
+
- `get_mac_address()`
|
|
113
|
+
- `get_route_to(ip)`
|
|
114
|
+
- `get_host_ip()`
|
|
115
|
+
- `get_host_default_gateway()`
|
|
116
|
+
|
|
117
|
+
## random_user.py
|
|
118
|
+
|
|
119
|
+
Generate random usernames base of a list of nouns and adjectives.
|
|
120
|
+
|
|
121
|
+
## security.py
|
|
122
|
+
|
|
123
|
+
Provide secure function to generate/validate passwords and API keys of users.
|
|
124
|
+
|
|
125
|
+
- Password generation/validation:
|
|
126
|
+
- `get_password_hash(password)`: returns the hash of a plaintext password
|
|
127
|
+
- `verify_password(password, hash)`: verifies if a password matches a hash
|
|
128
|
+
- `get_random_password()`: generates a random password
|
|
129
|
+
- `check_password_requirements(password)`: Check if a password meets the minimum requirements
|
|
130
|
+
|
|
131
|
+
## str_utils.py
|
|
132
|
+
|
|
133
|
+
Provide functions to safely manipulate and transform strings.
|
|
134
|
+
|
|
135
|
+
- `safe_str(buf)`: Make sure to safely encode bytes into uft-8 string or change the current string encoding to utf-8
|
|
136
|
+
- `translate_str(buf)`: Try to guess the current encoding of a string or a byte buffer
|
|
137
|
+
- `truncate(buf)`: Make sure a string does not exceed a certain length by adding ellipses.
|
|
138
|
+
|
|
139
|
+
## uid.py
|
|
140
|
+
|
|
141
|
+
Generate random ID in a format shorter then UUID and more double click friendly
|
|
142
|
+
|
|
143
|
+
- `get_random_id()`: Generate a, base62 based + 22 character, collision free random ID
|
|
144
|
+
- `get_id_from_data(data)`: Generate an ID base of the provided data
|
|
File without changes
|