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/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.clue.enabled:
142
+ from howler.api.v1.clue import clue_api
143
+
144
+ logger.debug("Enabled Clue Integration")
145
+ app.register_blueprint(clue_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()
@@ -0,0 +1,125 @@
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
+ ## isotime.py
48
+
49
+ 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.
50
+
51
+ Here are the support date operation:
52
+
53
+ - Get current time functions:
54
+ - `now()` -> Current epoch time
55
+ - `now_as_iso()` -> Current iso time
56
+ - `now_as_local()` -> Current local time
57
+ - Tranformation functions:
58
+ - `epoch_to_iso(date)`
59
+ - `epoch_to_local(date)`
60
+ - `local_to_epoch(date)`
61
+ - `local_to_iso(date)`
62
+ - `iso_to_epoch(date)`
63
+ - `iso_to_local(date)`
64
+
65
+ ## loader.py
66
+
67
+ This file provide helper function for components that require external configuration files: Classification engine, datastore and remote datatypes.
68
+
69
+ - `get_classification()`: returns a pre-configured classification object.
70
+ - `get_config()`: returns the current classification of the system.
71
+ - `get_datastore()`: returns an Howler datastore using the config form the get_config() output.
72
+
73
+ ## log.py/logformat.py
74
+
75
+ 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
76
+
77
+ ## memory_zip.py
78
+
79
+ Provides an interface file to create zip files in memory.
80
+
81
+ ## net.py/net_static.py
82
+
83
+ Provide multiple function to validate ip/port/domains and the get networking information about the current host.
84
+
85
+ - Validation:
86
+ - `is_valid_port(port)`
87
+ - `is_valid_domain(domain)`
88
+ - `is_valid_ip(ip)`
89
+ - `is_valid_email(email)`
90
+ - `is_ip_in_network(ip, cidr)`
91
+ - Network information:
92
+ - `get_hostname()`
93
+ - `get_mac_address()`
94
+ - `get_route_to(ip)`
95
+ - `get_host_ip()`
96
+ - `get_host_default_gateway()`
97
+
98
+ ## random_user.py
99
+
100
+ Generate random usernames base of a list of nouns and adjectives.
101
+
102
+ ## security.py
103
+
104
+ Provide secure function to generate/validate passwords and API keys of users.
105
+
106
+ - Password generation/validation:
107
+ - `get_password_hash(password)`: returns the hash of a plaintext password
108
+ - `verify_password(password, hash)`: verifies if a password matches a hash
109
+ - `get_random_password()`: generates a random password
110
+ - `check_password_requirements(password)`: Check if a password meets the minimum requirements
111
+
112
+ ## str_utils.py
113
+
114
+ Provide functions to safely manipulate and transform strings.
115
+
116
+ - `safe_str(buf)`: Make sure to safely encode bytes into uft-8 string or change the current string encoding to utf-8
117
+ - `translate_str(buf)`: Try to guess the current encoding of a string or a byte buffer
118
+ - `truncate(buf)`: Make sure a string does not exceed a certain length by adding ellipses.
119
+
120
+ ## uid.py
121
+
122
+ Generate random ID in a format shorter then UUID and more double click friendly
123
+
124
+ - `get_random_id()`: Generate a, base62 based + 22 character, collision free random ID
125
+ - `get_id_from_data(data)`: Generate an ID base of the provided data
File without changes