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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import logging.handlers
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from traceback import format_exception
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
8
|
+
|
|
9
|
+
from flask import request
|
|
10
|
+
from typing_extensions import override # type: ignore
|
|
11
|
+
|
|
12
|
+
from howler.common import loader
|
|
13
|
+
from howler.common.logging.format import (
|
|
14
|
+
HWL_DATE_FORMAT,
|
|
15
|
+
HWL_JSON_FORMAT,
|
|
16
|
+
HWL_LOG_FORMAT,
|
|
17
|
+
HWL_SYSLOG_FORMAT,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from howler.odm.models.config import Config
|
|
22
|
+
|
|
23
|
+
LOG_LEVEL_MAP = {
|
|
24
|
+
"DEBUG": logging.DEBUG,
|
|
25
|
+
"INFO": logging.INFO,
|
|
26
|
+
"WARNING": logging.WARNING,
|
|
27
|
+
"ERROR": logging.ERROR,
|
|
28
|
+
"CRITICAL": logging.CRITICAL,
|
|
29
|
+
"DISABLED": 60,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DEBUG = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JsonFormatter(logging.Formatter):
|
|
36
|
+
"""logging Formatter to output in JSON"""
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
def formatMessage(self, record):
|
|
40
|
+
if record.exc_info:
|
|
41
|
+
record.exc_text = self.formatException(record.exc_info)
|
|
42
|
+
record.exc_info = None
|
|
43
|
+
|
|
44
|
+
if record.exc_text:
|
|
45
|
+
record.message += "\n" + record.exc_text
|
|
46
|
+
record.exc_text = None
|
|
47
|
+
|
|
48
|
+
record.message = json.dumps(record.message)
|
|
49
|
+
return self._style.format(record)
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def formatException(self, exc_info):
|
|
53
|
+
return "".join(format_exception(*exc_info))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def init_log_to_file(logger: logging.Logger, log_level: int, name: str, config: "Config"):
|
|
57
|
+
"""Initialize file-based logging"""
|
|
58
|
+
if not os.path.isdir(config.logging.log_directory):
|
|
59
|
+
logger.warning(
|
|
60
|
+
"Log directory does not exist. Will try to create %s",
|
|
61
|
+
config.logging.log_directory,
|
|
62
|
+
)
|
|
63
|
+
os.makedirs(config.logging.log_directory)
|
|
64
|
+
|
|
65
|
+
if log_level <= logging.DEBUG:
|
|
66
|
+
dbg_file_handler = logging.handlers.RotatingFileHandler(
|
|
67
|
+
os.path.join(config.logging.log_directory, f"{name}.dbg"),
|
|
68
|
+
maxBytes=10485760,
|
|
69
|
+
backupCount=5,
|
|
70
|
+
)
|
|
71
|
+
dbg_file_handler.setLevel(logging.DEBUG)
|
|
72
|
+
if config.logging.log_as_json:
|
|
73
|
+
dbg_file_handler.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
|
|
74
|
+
else:
|
|
75
|
+
dbg_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
|
|
76
|
+
logger.addHandler(dbg_file_handler)
|
|
77
|
+
|
|
78
|
+
if log_level <= logging.INFO:
|
|
79
|
+
op_file_handler = logging.handlers.RotatingFileHandler(
|
|
80
|
+
os.path.join(config.logging.log_directory, f"{name}.log"),
|
|
81
|
+
maxBytes=10485760,
|
|
82
|
+
backupCount=5,
|
|
83
|
+
)
|
|
84
|
+
op_file_handler.setLevel(logging.INFO)
|
|
85
|
+
if config.logging.log_as_json:
|
|
86
|
+
op_file_handler.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
|
|
87
|
+
else:
|
|
88
|
+
op_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
|
|
89
|
+
logger.addHandler(op_file_handler)
|
|
90
|
+
|
|
91
|
+
if log_level <= logging.ERROR:
|
|
92
|
+
err_file_handler = logging.handlers.RotatingFileHandler(
|
|
93
|
+
os.path.join(config.logging.log_directory, f"{name}.err"),
|
|
94
|
+
maxBytes=10485760,
|
|
95
|
+
backupCount=5,
|
|
96
|
+
)
|
|
97
|
+
err_file_handler.setLevel(logging.ERROR)
|
|
98
|
+
if config.logging.log_as_json:
|
|
99
|
+
err_file_handler.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
|
|
100
|
+
else:
|
|
101
|
+
err_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
|
|
102
|
+
err_file_handler.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
|
|
103
|
+
logger.addHandler(err_file_handler)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def init_logging(name: str, log_level: Optional[int] = None):
|
|
107
|
+
"""Initialize the logger"""
|
|
108
|
+
from howler.config import config
|
|
109
|
+
|
|
110
|
+
logger = logging.getLogger(loader.APP_NAME)
|
|
111
|
+
|
|
112
|
+
# Test if we've initialized the log handler already.
|
|
113
|
+
if len(logger.handlers) != 0:
|
|
114
|
+
return logger.getChild(name)
|
|
115
|
+
|
|
116
|
+
if name.startswith(f"{loader.APP_NAME}."):
|
|
117
|
+
name = name[len(loader.APP_NAME) + 1 :]
|
|
118
|
+
|
|
119
|
+
config.logging.log_to_console = config.logging.log_to_console or config.ui.debug
|
|
120
|
+
|
|
121
|
+
if log_level is None:
|
|
122
|
+
log_level = LOG_LEVEL_MAP[config.logging.log_level]
|
|
123
|
+
|
|
124
|
+
logging.root.setLevel(logging.CRITICAL)
|
|
125
|
+
logger.setLevel(log_level)
|
|
126
|
+
|
|
127
|
+
if config.logging.log_level == "DISABLED":
|
|
128
|
+
# While log_level is set to disable, we will not create any handlers
|
|
129
|
+
return logger.getChild(name)
|
|
130
|
+
|
|
131
|
+
if config.logging.log_to_file:
|
|
132
|
+
init_log_to_file(logger, log_level, name, config)
|
|
133
|
+
|
|
134
|
+
if config.logging.log_to_console:
|
|
135
|
+
console = logging.StreamHandler()
|
|
136
|
+
if config.logging.log_as_json:
|
|
137
|
+
console.setFormatter(JsonFormatter(HWL_JSON_FORMAT))
|
|
138
|
+
else:
|
|
139
|
+
console.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
|
|
140
|
+
logger.addHandler(console)
|
|
141
|
+
|
|
142
|
+
if config.logging.log_to_syslog and config.logging.syslog_host and config.logging.syslog_port:
|
|
143
|
+
syslog_handler = logging.handlers.SysLogHandler(
|
|
144
|
+
address=(config.logging.syslog_host, config.logging.syslog_port)
|
|
145
|
+
)
|
|
146
|
+
syslog_handler.formatter = logging.Formatter(HWL_SYSLOG_FORMAT)
|
|
147
|
+
logger.addHandler(syslog_handler)
|
|
148
|
+
|
|
149
|
+
logger.debug("Logger ready!")
|
|
150
|
+
return logger.getChild(name)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_logger(name: str = "default") -> logging.Logger:
|
|
154
|
+
"""Get a logger with a useful name given a filename"""
|
|
155
|
+
name = re.sub(r".+(howler|test)/", "", name).replace("/", ".").replace(".__init__", "").replace(".py", "")
|
|
156
|
+
name = re.sub(r"^api\.?", "", name)
|
|
157
|
+
logger = init_logging("api")
|
|
158
|
+
if name:
|
|
159
|
+
logger = logger.getChild(name)
|
|
160
|
+
return logger
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_traceback_info(tb):
|
|
164
|
+
"""Prase the traceback information for a given traceback"""
|
|
165
|
+
tb_list = []
|
|
166
|
+
tb_id = 0
|
|
167
|
+
last_ui = None
|
|
168
|
+
while tb is not None:
|
|
169
|
+
f = tb.tb_frame
|
|
170
|
+
line_no = tb.tb_lineno
|
|
171
|
+
tb_list.append((f, line_no))
|
|
172
|
+
tb = tb.tb_next
|
|
173
|
+
if "/ui/" in f.f_code.co_filename:
|
|
174
|
+
last_ui = tb_id
|
|
175
|
+
tb_id += 1
|
|
176
|
+
|
|
177
|
+
if last_ui is not None:
|
|
178
|
+
tb_frame, line = tb_list[last_ui]
|
|
179
|
+
user = tb_frame.f_locals.get("kwargs", {}).get("user", None)
|
|
180
|
+
|
|
181
|
+
if not user:
|
|
182
|
+
temp = tb_frame.f_locals.get("_", {})
|
|
183
|
+
if isinstance(temp, dict):
|
|
184
|
+
user = temp.get("user", None)
|
|
185
|
+
|
|
186
|
+
if not user:
|
|
187
|
+
user = tb_frame.f_locals.get("user", None)
|
|
188
|
+
|
|
189
|
+
if not user:
|
|
190
|
+
user = tb_frame.f_locals.get("impersonator", None)
|
|
191
|
+
|
|
192
|
+
if user:
|
|
193
|
+
return user, tb_frame.f_code.co_filename, tb_frame.f_code.co_name, line
|
|
194
|
+
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def __dumb_log(log, msg, is_exception=False):
|
|
201
|
+
"""Dumb logger for use with log_with_traceback"""
|
|
202
|
+
args: Union[str, bytes] = request.query_string
|
|
203
|
+
if isinstance(args, bytes):
|
|
204
|
+
args = args.decode()
|
|
205
|
+
|
|
206
|
+
if args:
|
|
207
|
+
args = f"?{args}"
|
|
208
|
+
|
|
209
|
+
message = f"{msg} - {request.path}{args}"
|
|
210
|
+
if is_exception:
|
|
211
|
+
log.exception(message)
|
|
212
|
+
else:
|
|
213
|
+
log.warning(message)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def log_with_traceback(traceback, msg, is_exception=False, audit=False):
|
|
217
|
+
"""Log a message along with the stacktrace"""
|
|
218
|
+
log = get_logger("traceback") if not audit else logging.getLogger("howler.api.audit")
|
|
219
|
+
|
|
220
|
+
tb_info = get_traceback_info(traceback)
|
|
221
|
+
if tb_info:
|
|
222
|
+
tb_user, tb_file, tb_function, tb_line_no = tb_info
|
|
223
|
+
args: Optional[Union[str, bytes]] = request.query_string
|
|
224
|
+
if args:
|
|
225
|
+
args = f"?{args if isinstance(args, str) else args.decode()}"
|
|
226
|
+
else:
|
|
227
|
+
args = ""
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
message = (
|
|
231
|
+
f'{tb_user["uname"]} [{tb_user["classification"]}] :: {msg} - {tb_file}:{tb_function}:{tb_line_no}'
|
|
232
|
+
f'[{os.environ.get("HOWLER_VERSION", "0.0.0.dev0")}] ({request.path}{args})'
|
|
233
|
+
)
|
|
234
|
+
if is_exception:
|
|
235
|
+
log.exception(message)
|
|
236
|
+
else:
|
|
237
|
+
log.warning(message)
|
|
238
|
+
except Exception:
|
|
239
|
+
__dumb_log(log, msg, is_exception=is_exception)
|
|
240
|
+
else:
|
|
241
|
+
__dumb_log(log, msg, is_exception=is_exception)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from flask import request
|
|
6
|
+
|
|
7
|
+
from howler.common.logging.format import HWL_AUDIT_FORMAT, HWL_DATE_FORMAT, HWL_ISO_DATE_FORMAT, HWL_LOG_FORMAT
|
|
8
|
+
from howler.config import DEBUG, config
|
|
9
|
+
|
|
10
|
+
AUDIT = config.ui.audit
|
|
11
|
+
|
|
12
|
+
AUDIT_KW_TARGET = [
|
|
13
|
+
"sid",
|
|
14
|
+
"sha256",
|
|
15
|
+
"copy_sid",
|
|
16
|
+
"filter",
|
|
17
|
+
"query",
|
|
18
|
+
"username",
|
|
19
|
+
"group",
|
|
20
|
+
"rev",
|
|
21
|
+
"wq_id",
|
|
22
|
+
"index",
|
|
23
|
+
"cache_key",
|
|
24
|
+
"alert_key",
|
|
25
|
+
"alert_id",
|
|
26
|
+
"url",
|
|
27
|
+
"q",
|
|
28
|
+
"fq",
|
|
29
|
+
"file_hash",
|
|
30
|
+
"heuristic_id",
|
|
31
|
+
"error_key",
|
|
32
|
+
"mac",
|
|
33
|
+
"vm_type",
|
|
34
|
+
"vm_name",
|
|
35
|
+
"config_name",
|
|
36
|
+
"servicename",
|
|
37
|
+
"vm",
|
|
38
|
+
"transition",
|
|
39
|
+
"data",
|
|
40
|
+
"id",
|
|
41
|
+
"comment_id",
|
|
42
|
+
"label_set",
|
|
43
|
+
"tool_name",
|
|
44
|
+
"operation_id",
|
|
45
|
+
"category",
|
|
46
|
+
"label",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
AUDIT_LOG = logging.getLogger("howler.api.audit")
|
|
50
|
+
AUDIT_LOG.propagate = False
|
|
51
|
+
|
|
52
|
+
if AUDIT:
|
|
53
|
+
AUDIT_LOG.setLevel(logging.DEBUG)
|
|
54
|
+
|
|
55
|
+
if not os.path.exists(config.logging.log_directory):
|
|
56
|
+
os.makedirs(config.logging.log_directory)
|
|
57
|
+
|
|
58
|
+
fh = logging.FileHandler(os.path.join(config.logging.log_directory, "hwl_audit.log"))
|
|
59
|
+
fh.setLevel(logging.DEBUG)
|
|
60
|
+
fh.setFormatter(
|
|
61
|
+
logging.Formatter(
|
|
62
|
+
HWL_LOG_FORMAT if DEBUG else HWL_AUDIT_FORMAT,
|
|
63
|
+
HWL_DATE_FORMAT if DEBUG else HWL_ISO_DATE_FORMAT,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
AUDIT_LOG.addHandler(fh)
|
|
67
|
+
|
|
68
|
+
ch = logging.StreamHandler(sys.stdout)
|
|
69
|
+
ch.setLevel(logging.INFO)
|
|
70
|
+
ch.setFormatter(
|
|
71
|
+
logging.Formatter(
|
|
72
|
+
HWL_LOG_FORMAT if DEBUG else HWL_AUDIT_FORMAT,
|
|
73
|
+
HWL_DATE_FORMAT if DEBUG else HWL_ISO_DATE_FORMAT,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
AUDIT_LOG.addHandler(ch)
|
|
77
|
+
|
|
78
|
+
#########################
|
|
79
|
+
# End of prepare logger #
|
|
80
|
+
#########################
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def audit(args, kwargs, logged_in_uname, user, func, impersonator=None):
|
|
84
|
+
"""Log audit information for a given function executed by a given user."""
|
|
85
|
+
try:
|
|
86
|
+
json_blob = request.json
|
|
87
|
+
if not isinstance(json_blob, dict):
|
|
88
|
+
json_blob = {}
|
|
89
|
+
except Exception:
|
|
90
|
+
json_blob = {}
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
req_args = ["%s='%s'" % (k, v) for k, v in request.args.items() if k in AUDIT_KW_TARGET]
|
|
94
|
+
except RuntimeError:
|
|
95
|
+
req_args = []
|
|
96
|
+
|
|
97
|
+
params_list = (
|
|
98
|
+
list(args)
|
|
99
|
+
+ ["%s='%s'" % (k, v) for k, v in kwargs.items() if k in AUDIT_KW_TARGET]
|
|
100
|
+
+ req_args
|
|
101
|
+
+ ["%s='%s'" % (k, v) for k, v in json_blob.items() if k in AUDIT_KW_TARGET]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if impersonator:
|
|
105
|
+
audit_user = f"{impersonator} on behalf of {logged_in_uname}"
|
|
106
|
+
else:
|
|
107
|
+
audit_user = logged_in_uname
|
|
108
|
+
if DEBUG:
|
|
109
|
+
# In debug mode, you'll get an output like:
|
|
110
|
+
# 23/03/20 14:26:56 DEBUG howler.api.audit | goose - search(index='...', query='...')
|
|
111
|
+
AUDIT_LOG.debug(
|
|
112
|
+
"%s - %s(%s)",
|
|
113
|
+
audit_user,
|
|
114
|
+
func.__name__,
|
|
115
|
+
", ".join(params_list),
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# In prod, you'll get an output like:
|
|
119
|
+
# {
|
|
120
|
+
# "date": "2023-03-20T18:33:27-0400",
|
|
121
|
+
# "type": "audit",
|
|
122
|
+
# "app_name": "howler",
|
|
123
|
+
# "api": "howler.api.audit",
|
|
124
|
+
# "severity": "INFO",
|
|
125
|
+
# "user": "goose",
|
|
126
|
+
# "function": "search(index='hit', query='howler.escalation:alert AND howler.status:open')",
|
|
127
|
+
# "method": "POST",
|
|
128
|
+
# "path": "/api/v1/search/hit/"
|
|
129
|
+
# }
|
|
130
|
+
AUDIT_LOG.info(
|
|
131
|
+
"",
|
|
132
|
+
extra={
|
|
133
|
+
"user": audit_user,
|
|
134
|
+
"function": f"{func.__name__}({', '.join(params_list)})",
|
|
135
|
+
"method": request.method,
|
|
136
|
+
"path": request.path,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
APP_NAME = os.environ.get("APP_NAME", "howler")
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from howler.common.net import get_hostname
|
|
8
|
+
|
|
9
|
+
hostname = get_hostname()
|
|
10
|
+
except Exception:
|
|
11
|
+
hostname = "unknownhost"
|
|
12
|
+
|
|
13
|
+
HWL_SYSLOG_FORMAT = f"HWL %(levelname)8s {hostname} %(process)5d %(name)40s | %(message)s"
|
|
14
|
+
HWL_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s | %(message)s"
|
|
15
|
+
HWL_DATE_FORMAT = "%y/%m/%d %H:%M:%S"
|
|
16
|
+
HWL_JSON_FORMAT = (
|
|
17
|
+
f"{{"
|
|
18
|
+
f'"@timestamp": "%(asctime)s", '
|
|
19
|
+
f'"event": {{ "module": "{APP_NAME}", "dataset": "%(name)s" }}, '
|
|
20
|
+
f'"host": {{ "hostname": "{hostname}" }}, '
|
|
21
|
+
f'"log": {{ "level": "%(levelname)s", "logger": "%(name)s" }}, '
|
|
22
|
+
f'"process": {{ "pid": "%(process)d" }}, '
|
|
23
|
+
f'"message": %(message)s}}'
|
|
24
|
+
)
|
|
25
|
+
HWL_ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
|
26
|
+
HWL_AUDIT_FORMAT = json.dumps(
|
|
27
|
+
{
|
|
28
|
+
"date": "%(asctime)s",
|
|
29
|
+
"type": "audit",
|
|
30
|
+
"app_name": APP_NAME,
|
|
31
|
+
"api": "howler.api.audit",
|
|
32
|
+
"severity": "%(levelname)s",
|
|
33
|
+
"user": "%(user)s",
|
|
34
|
+
"function": "%(function)s",
|
|
35
|
+
"method": "%(method)s",
|
|
36
|
+
"path": "%(path)s",
|
|
37
|
+
}
|
|
38
|
+
).replace('"msg"', "%(message)s")
|
howler/common/net.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import uuid
|
|
3
|
+
from ipaddress import IPv4Network, ip_address
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from howler.common.net_static import TLDS_ALPHA_BY_DOMAIN
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_valid_port(value: Union[int, str, float]) -> bool:
|
|
10
|
+
"Check if a port is valid"
|
|
11
|
+
try:
|
|
12
|
+
if 1 <= int(value) <= 65535:
|
|
13
|
+
return True
|
|
14
|
+
except ValueError:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_valid_domain(domain: str) -> bool:
|
|
21
|
+
"Check if a domain is valid"
|
|
22
|
+
if "@" in domain:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
if "." in domain:
|
|
26
|
+
tld = domain.split(".")[-1]
|
|
27
|
+
return tld.upper() in TLDS_ALPHA_BY_DOMAIN
|
|
28
|
+
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_valid_ip(ip: str) -> bool:
|
|
33
|
+
"Check if an ip is valid"
|
|
34
|
+
parts = ip.split(".")
|
|
35
|
+
if len(parts) == 4:
|
|
36
|
+
for p in parts:
|
|
37
|
+
try:
|
|
38
|
+
if not (0 <= int(p) <= 255):
|
|
39
|
+
return False
|
|
40
|
+
except ValueError:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
if int(parts[0]) == 0:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
if int(parts[3]) == 0:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_ip_in_network(ip: str, network: IPv4Network) -> bool:
|
|
55
|
+
"Check if an ip is in a given network"
|
|
56
|
+
if not is_valid_ip(ip):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
return ip_address(ip) in network
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_valid_email(email: str) -> bool:
|
|
63
|
+
"Check if an email is valid"
|
|
64
|
+
parts = email.split("@")
|
|
65
|
+
if len(parts) == 2:
|
|
66
|
+
if is_valid_domain(parts[1]):
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_hostname() -> str:
|
|
73
|
+
"Get the hostname of the computer howler is running on"
|
|
74
|
+
return socket.gethostname()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_mac_address() -> str:
|
|
78
|
+
"Get the mac address of the computer howler is running on"
|
|
79
|
+
return "".join(["{0:02x}".format((uuid.getnode() >> i) & 0xFF) for i in range(0, 8 * 6, 8)][::-1]).upper()
|