psengine 2.0.4__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.
- psengine/__init__.py +22 -0
- psengine/_sdk_id.py +16 -0
- psengine/_version.py +14 -0
- psengine/analyst_notes/__init__.py +32 -0
- psengine/analyst_notes/constants.py +15 -0
- psengine/analyst_notes/errors.py +42 -0
- psengine/analyst_notes/helpers.py +90 -0
- psengine/analyst_notes/models.py +219 -0
- psengine/analyst_notes/note.py +149 -0
- psengine/analyst_notes/note_mgr.py +400 -0
- psengine/base_http_client.py +285 -0
- psengine/classic_alerts/__init__.py +24 -0
- psengine/classic_alerts/classic_alert.py +275 -0
- psengine/classic_alerts/classic_alert_mgr.py +507 -0
- psengine/classic_alerts/constants.py +31 -0
- psengine/classic_alerts/errors.py +38 -0
- psengine/classic_alerts/helpers.py +87 -0
- psengine/classic_alerts/markdown/__init__.py +13 -0
- psengine/classic_alerts/markdown/markdown.py +359 -0
- psengine/classic_alerts/models.py +141 -0
- psengine/collective_insights/__init__.py +29 -0
- psengine/collective_insights/collective_insights.py +164 -0
- psengine/collective_insights/constants.py +44 -0
- psengine/collective_insights/errors.py +18 -0
- psengine/collective_insights/insight.py +89 -0
- psengine/collective_insights/models.py +81 -0
- psengine/common_models.py +89 -0
- psengine/config/__init__.py +15 -0
- psengine/config/config.py +284 -0
- psengine/config/errors.py +18 -0
- psengine/constants.py +63 -0
- psengine/detection/__init__.py +17 -0
- psengine/detection/detection_mgr.py +135 -0
- psengine/detection/detection_rule.py +85 -0
- psengine/detection/errors.py +26 -0
- psengine/detection/helpers.py +56 -0
- psengine/detection/models.py +47 -0
- psengine/endpoints.py +98 -0
- psengine/enrich/__init__.py +28 -0
- psengine/enrich/constants.py +73 -0
- psengine/enrich/errors.py +26 -0
- psengine/enrich/lookup.py +299 -0
- psengine/enrich/lookup_mgr.py +341 -0
- psengine/enrich/models/__init__.py +13 -0
- psengine/enrich/models/base_enriched_entity.py +43 -0
- psengine/enrich/models/lookup.py +271 -0
- psengine/enrich/models/soar.py +138 -0
- psengine/enrich/soar.py +89 -0
- psengine/enrich/soar_mgr.py +176 -0
- psengine/entity_lists/__init__.py +16 -0
- psengine/entity_lists/constants.py +19 -0
- psengine/entity_lists/entity_list.py +435 -0
- psengine/entity_lists/entity_list_mgr.py +185 -0
- psengine/entity_lists/errors.py +26 -0
- psengine/entity_lists/models.py +87 -0
- psengine/entity_match/__init__.py +16 -0
- psengine/entity_match/entity_match.py +90 -0
- psengine/entity_match/entity_match_mgr.py +235 -0
- psengine/entity_match/errors.py +18 -0
- psengine/entity_match/models.py +22 -0
- psengine/errors.py +41 -0
- psengine/helpers/__init__.py +23 -0
- psengine/helpers/helpers.py +471 -0
- psengine/logger/__init__.py +15 -0
- psengine/logger/constants.py +39 -0
- psengine/logger/errors.py +18 -0
- psengine/logger/rf_logger.py +148 -0
- psengine/markdown/__init__.py +21 -0
- psengine/markdown/markdown.py +169 -0
- psengine/markdown/models.py +22 -0
- psengine/playbook_alerts/__init__.py +34 -0
- psengine/playbook_alerts/constants.py +35 -0
- psengine/playbook_alerts/errors.py +35 -0
- psengine/playbook_alerts/helpers.py +80 -0
- psengine/playbook_alerts/mappings.py +44 -0
- psengine/playbook_alerts/markdown/__init__.py +13 -0
- psengine/playbook_alerts/markdown/markdown.py +98 -0
- psengine/playbook_alerts/markdown/markdown_code_repo.py +64 -0
- psengine/playbook_alerts/markdown/markdown_domain_abuse.py +118 -0
- psengine/playbook_alerts/markdown/markdown_identity_exposure.py +158 -0
- psengine/playbook_alerts/models/__init__.py +36 -0
- psengine/playbook_alerts/models/common_models.py +18 -0
- psengine/playbook_alerts/models/panel_log.py +329 -0
- psengine/playbook_alerts/models/panel_status.py +70 -0
- psengine/playbook_alerts/models/pba_code_repo_leak.py +52 -0
- psengine/playbook_alerts/models/pba_cyber_vulnerability.py +53 -0
- psengine/playbook_alerts/models/pba_domain_abuse.py +139 -0
- psengine/playbook_alerts/models/pba_identity_exposures.py +93 -0
- psengine/playbook_alerts/models/pba_third_party_risk.py +103 -0
- psengine/playbook_alerts/models/search_endpoint.py +68 -0
- psengine/playbook_alerts/pa_category.py +37 -0
- psengine/playbook_alerts/playbook_alert_mgr.py +593 -0
- psengine/playbook_alerts/playbook_alerts.py +393 -0
- psengine/rf_client.py +430 -0
- psengine/risklists/__init__.py +17 -0
- psengine/risklists/constants.py +15 -0
- psengine/risklists/errors.py +20 -0
- psengine/risklists/models.py +65 -0
- psengine/risklists/risklist_mgr.py +156 -0
- psengine/stix2/__init__.py +21 -0
- psengine/stix2/base_stix_entity.py +62 -0
- psengine/stix2/complex_entity.py +372 -0
- psengine/stix2/constants.py +81 -0
- psengine/stix2/enriched_indicator.py +261 -0
- psengine/stix2/errors.py +22 -0
- psengine/stix2/helpers.py +68 -0
- psengine/stix2/rf_bundle.py +240 -0
- psengine/stix2/simple_entity.py +145 -0
- psengine/stix2/util.py +53 -0
- psengine-2.0.4.dist-info/METADATA +189 -0
- psengine-2.0.4.dist-info/RECORD +115 -0
- psengine-2.0.4.dist-info/WHEEL +5 -0
- psengine-2.0.4.dist-info/entry_points.txt +2 -0
- psengine-2.0.4.dist-info/licenses/LICENSE +21 -0
- psengine-2.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
import csv
|
|
14
|
+
import functools
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from inspect import getmodule, isclass, signature
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Callable, Union
|
|
26
|
+
|
|
27
|
+
from dateutil.parser import parse as date_parse
|
|
28
|
+
from requests.exceptions import (
|
|
29
|
+
ConnectionError,
|
|
30
|
+
ConnectTimeout,
|
|
31
|
+
HTTPError,
|
|
32
|
+
JSONDecodeError,
|
|
33
|
+
ReadTimeout,
|
|
34
|
+
SSLError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from ..common_models import RFBaseModel
|
|
38
|
+
from ..constants import ROOT_DIR
|
|
39
|
+
from ..errors import ReadFileError, RecordedFutureError, WriteFileError
|
|
40
|
+
|
|
41
|
+
LOG = logging.getLogger('psengine.helpers')
|
|
42
|
+
VALID_TIME_REGEX = r'^(-?)([1-9]?[0-9]+[dDhH])$'
|
|
43
|
+
IDS = ['ip:', 'idn:', 'url:', 'hash:', 'id:']
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def connection_exceptions(
|
|
47
|
+
ignore_status_code: list[int], exception_to_raise: RecordedFutureError, on_ignore_return=None
|
|
48
|
+
):
|
|
49
|
+
"""Decorator for handling HTTP related errors.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
ignore_status_code (List[int]): list of status codes to be ignored - dont raise exception
|
|
53
|
+
exception_to_raise (Exception): exception to raise in case of error. It should be based on
|
|
54
|
+
the function that is decorated
|
|
55
|
+
on_ignore_return (Any): whatever it is needed to be returned if the ignore_status happens.
|
|
56
|
+
Defaults to None.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
exception_to_raise
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Any: whatever the function decorated returns
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def wrapper(func):
|
|
67
|
+
@functools.wraps(func)
|
|
68
|
+
def wrapped(*args, **kwargs):
|
|
69
|
+
self = args[0]
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
return func(*args, **kwargs)
|
|
73
|
+
except HTTPError as err:
|
|
74
|
+
if err.response is not None and err.response.status_code in ignore_status_code:
|
|
75
|
+
msg = (
|
|
76
|
+
f"Requested data by {func.__name__} wasn't found or you cannot view it. "
|
|
77
|
+
f'Error: {err}'
|
|
78
|
+
)
|
|
79
|
+
self.log.info(msg)
|
|
80
|
+
return on_ignore_return
|
|
81
|
+
|
|
82
|
+
self.log.error(f'HTTPError in {func.__name__}. Error: {err}')
|
|
83
|
+
raise exception_to_raise(message=str(err)) from err
|
|
84
|
+
|
|
85
|
+
except (ConnectTimeout, ConnectionError, ReadTimeout) as err:
|
|
86
|
+
self.log.error(f'Connection error in {func.__name__}. Error: {err}')
|
|
87
|
+
raise exception_to_raise(message=str(err)) from err
|
|
88
|
+
|
|
89
|
+
except (OSError, SSLError) as err:
|
|
90
|
+
self.log.error(
|
|
91
|
+
f'Possible error with custom certificate {err} when calling {func.__name__}'
|
|
92
|
+
)
|
|
93
|
+
raise exception_to_raise(message=str(err)) from err
|
|
94
|
+
|
|
95
|
+
except (JSONDecodeError, KeyError) as err:
|
|
96
|
+
self.log.error(f'Incorrect data returned by {func.__name__}. Error: {err}')
|
|
97
|
+
raise exception_to_raise(message=str(err)) from err
|
|
98
|
+
|
|
99
|
+
return wrapped
|
|
100
|
+
|
|
101
|
+
return wrapper
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def dump_models(models) -> list:
|
|
105
|
+
"""Return a list of model dumped as json."""
|
|
106
|
+
return (
|
|
107
|
+
[json.dumps(model.json()) for model in models]
|
|
108
|
+
if isinstance(models, list)
|
|
109
|
+
else [json.dumps(models.json())]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def debug_call(func):
|
|
114
|
+
"""Print debug logs for public methods."""
|
|
115
|
+
original_func = func
|
|
116
|
+
while hasattr(original_func, '__wrapped__'):
|
|
117
|
+
original_func = original_func.__wrapped__
|
|
118
|
+
func_module = getmodule(original_func)
|
|
119
|
+
|
|
120
|
+
sig = signature(func)
|
|
121
|
+
param_names = list(sig.parameters.keys())
|
|
122
|
+
|
|
123
|
+
@functools.wraps(func)
|
|
124
|
+
def wrapper(*args, **kwargs):
|
|
125
|
+
args_to_print = args
|
|
126
|
+
|
|
127
|
+
logger = logging.getLogger(func_module.__name__)
|
|
128
|
+
|
|
129
|
+
if param_names and param_names[0] in ('self', 'cls') and args:
|
|
130
|
+
args_to_print = args[1:]
|
|
131
|
+
|
|
132
|
+
def format_arg(x):
|
|
133
|
+
return str(x)[:50] if isclass(x) and issubclass(x, RFBaseModel) else str(x)[:200]
|
|
134
|
+
|
|
135
|
+
kwargs_to_print = {k: v for k, v in kwargs.items() if k != 'headers'}
|
|
136
|
+
args_str = ', '.join(format_arg(x) for x in args_to_print)
|
|
137
|
+
kwargs_str = ', '.join(f'{k}={str(v)!r}' for k, v in kwargs_to_print.items())
|
|
138
|
+
if args_str or kwargs_str:
|
|
139
|
+
sep = ', ' if args_str and kwargs_str else ''
|
|
140
|
+
msg = f'Called {func.__qualname__}({args_str}{sep}{kwargs_str})'
|
|
141
|
+
else:
|
|
142
|
+
msg = f'Called {func.__qualname__}()'
|
|
143
|
+
|
|
144
|
+
logger.debug(msg)
|
|
145
|
+
ret_val = func(*args, **kwargs)
|
|
146
|
+
|
|
147
|
+
msg = f'{func.__qualname__} ended with return value {str(ret_val)[:50]!r}'
|
|
148
|
+
logger.debug(msg)
|
|
149
|
+
|
|
150
|
+
return ret_val
|
|
151
|
+
|
|
152
|
+
return wrapper
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TimeHelpers:
|
|
156
|
+
"""Helpers for time related functions."""
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def rel_time_to_date(relative_time) -> str:
|
|
160
|
+
"""Convert a relative time to a date. Minutes not supported.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
.. code-block::
|
|
164
|
+
|
|
165
|
+
1h - > Return -1h from NOW
|
|
166
|
+
1d - > Return -1d from NOW.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
relative_time (str): 7d, 3h, etc..
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: if the relative time is invalid
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: time delta, for example: ``2022-08-08T13:11``
|
|
176
|
+
"""
|
|
177
|
+
logger = logging.getLogger(__name__)
|
|
178
|
+
match = re.match(VALID_TIME_REGEX, relative_time)
|
|
179
|
+
if match is None:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"Invalid relative time '{relative_time}'. Accepted format: [-|][integer][h|d]",
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
relative_time = match.groups()[-1]
|
|
185
|
+
time_now = datetime.utcnow()
|
|
186
|
+
digit = int(re.findall(r'^\d+', relative_time)[0])
|
|
187
|
+
if relative_time.endswith('d'):
|
|
188
|
+
subtracted = (time_now - timedelta(days=digit)).strftime('%Y-%m-%dT%H:%M')
|
|
189
|
+
else:
|
|
190
|
+
subtracted = (time_now - timedelta(hours=digit)).strftime('%Y-%m-%dT%H:%M')
|
|
191
|
+
logger.debug(f'UTC Time now: {time_now}')
|
|
192
|
+
logger.debug(f'Relative time -{relative_time} to date: {subtracted}')
|
|
193
|
+
|
|
194
|
+
return subtracted
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def is_rel_time_valid(rel_time) -> bool:
|
|
198
|
+
"""Helper function to determine if relative time expression is valid.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
rel_time (str): relative time
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
bool: True if valid, False otherwise
|
|
205
|
+
"""
|
|
206
|
+
if rel_time is None:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
return bool(re.match(VALID_TIME_REGEX, rel_time))
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def is_valid_time_range(range_: str) -> bool:
|
|
213
|
+
"""Verifies if an ISO 8601 compliant time range was specified.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
|
|
217
|
+
.. code-block::
|
|
218
|
+
|
|
219
|
+
[2017-07-30,2017-07-31]
|
|
220
|
+
(2017-07-30,2017-07-31)
|
|
221
|
+
[2017-07-30,2017-07-31)
|
|
222
|
+
[2017-07-30,)
|
|
223
|
+
[,2017-07-31)
|
|
224
|
+
https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
range_ (str): time range
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
bool: True if valid, False otherwise
|
|
231
|
+
"""
|
|
232
|
+
if range_ is None:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
match = re.match(r'^(\[|\()(.*)?,\s*(.*)?(\]|\))$', range_)
|
|
236
|
+
if match is None:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
start_time, end_time = match.groups()[1], match.groups()[2]
|
|
240
|
+
try:
|
|
241
|
+
if start_time != '' and not TimeHelpers.is_rel_time_valid(start_time):
|
|
242
|
+
date_parse(start_time)
|
|
243
|
+
if end_time != '' and not TimeHelpers.is_rel_time_valid(end_time):
|
|
244
|
+
date_parse(end_time)
|
|
245
|
+
except ValueError:
|
|
246
|
+
return False
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class FormattingHelpers:
|
|
251
|
+
"""Helpers for formatting related functions."""
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def cleanup_ai_insights(ai_insights: str) -> str:
|
|
255
|
+
"""Clean up RF AI Insights to avoid markdown rendering issues.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
ai_insights (str): ai insights
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
str: cleaned up ai insights
|
|
262
|
+
"""
|
|
263
|
+
return ai_insights.replace('\n', ' ').replace('1. ', '1.')
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def cleanup_rf_id(entity: str) -> str:
|
|
267
|
+
"""Remove the Recorded Future id prefix from an entity."""
|
|
268
|
+
for id_ in IDS:
|
|
269
|
+
if id_ in entity:
|
|
270
|
+
return entity.replace(id_, '')
|
|
271
|
+
return entity
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class OSHelpers:
|
|
275
|
+
"""Helpers for OS related functions."""
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def os_platform():
|
|
279
|
+
"""Get the OS platform information, for example: ``macOS-13.0-x86_64-i386-64bit``.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
str: OS platform info, if unavailable return None
|
|
283
|
+
"""
|
|
284
|
+
return platform.platform(aliased=True, terse=False) or None
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def mkdir(path: Union[str, Path]) -> Path:
|
|
288
|
+
"""Safely create a directory.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
path (str or Path): path to directory
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
ValueError: if path is not a string or is empty
|
|
295
|
+
WriteFileError: if directory is not writeable
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Path: path to directory created
|
|
299
|
+
"""
|
|
300
|
+
if path == '':
|
|
301
|
+
raise ValueError('path cannot be empty')
|
|
302
|
+
|
|
303
|
+
path = Path(path)
|
|
304
|
+
LOG.debug(f'Creating directory: {path.as_posix()}')
|
|
305
|
+
if not path.is_absolute():
|
|
306
|
+
path = Path(sys.path[0]) / path
|
|
307
|
+
if path.is_dir() and os.access(path, os.W_OK):
|
|
308
|
+
return path
|
|
309
|
+
try:
|
|
310
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
except PermissionError as err:
|
|
312
|
+
raise WriteFileError(f'Directory {path} is not writeable') from err
|
|
313
|
+
# In case it already exists, check if it is writeable
|
|
314
|
+
if not os.access(path, os.W_OK):
|
|
315
|
+
raise WriteFileError(f'Directory {path} is not writeable')
|
|
316
|
+
return path
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class FileHelpers:
|
|
320
|
+
"""Helpers for file related functions."""
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def read_csv(
|
|
324
|
+
csv_file: Union[str, Path], as_dict: bool = False, single_column: bool = False
|
|
325
|
+
) -> list:
|
|
326
|
+
"""Reads all rows from a CSV.
|
|
327
|
+
|
|
328
|
+
It is the client's responsibility to ensure column headers are handled appropriately.
|
|
329
|
+
|
|
330
|
+
Using ``as_dict`` will reader the CSV with ``csv.DictReader``, which treats the first row
|
|
331
|
+
as column headers. For example with CSV
|
|
332
|
+
|
|
333
|
+
.. code-block::
|
|
334
|
+
|
|
335
|
+
Name,ID,Level
|
|
336
|
+
Patrick,321,4
|
|
337
|
+
Ernest,123,8
|
|
338
|
+
|
|
339
|
+
``as_dict=True`` will return a list of dictionaries keyed by header names
|
|
340
|
+
|
|
341
|
+
.. code-block::
|
|
342
|
+
|
|
343
|
+
[{'Name': 'Patrick', 'ID': '321', 'Level': '4'},
|
|
344
|
+
{'Name': 'Ernest', 'ID': '123', 'Level': '8'}]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
``single_column=True`` will return a list of only the first column of the CSV
|
|
348
|
+
as strings (note that ``as_dict=False``, ``single_column=False`` returns a list of lists)::
|
|
349
|
+
|
|
350
|
+
['Name', 'Patrick', 'Ernest']
|
|
351
|
+
|
|
352
|
+
``as_dict=False`` and ``single_column=False`` will return a list of lists::
|
|
353
|
+
|
|
354
|
+
[['Name', 'ID', 'Level'], ['Patrick', '321', '4'], ['Ernest', '123', '8']]
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
csv_file (str or Path): path to CSV file
|
|
358
|
+
as_dict (bool, optional): return entities as a dict. Defaults to False.
|
|
359
|
+
single_column (bool, optional): return only entities (not lists) from first column
|
|
360
|
+
cannot be used with ``as_dict``. Defaults to False.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
ValueError: If both ``as_dict`` and ``single_column`` are True
|
|
364
|
+
ReadFileError: If file is not found or has restricted access
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
list of rows from CSV
|
|
368
|
+
"""
|
|
369
|
+
if as_dict and single_column:
|
|
370
|
+
raise ValueError('Cannot use as_dict and single_column together')
|
|
371
|
+
|
|
372
|
+
csv_file = Path(csv_file)
|
|
373
|
+
if not csv_file.is_absolute():
|
|
374
|
+
LOG.debug(
|
|
375
|
+
f'{csv_file} is not an absolute path. Attempting to find it in {ROOT_DIR}',
|
|
376
|
+
)
|
|
377
|
+
file_path = Path(ROOT_DIR) / csv_file
|
|
378
|
+
else:
|
|
379
|
+
file_path = csv_file
|
|
380
|
+
try:
|
|
381
|
+
with file_path.open() as file_obj:
|
|
382
|
+
if as_dict:
|
|
383
|
+
reader = csv.DictReader(file_obj)
|
|
384
|
+
return list(reader)
|
|
385
|
+
|
|
386
|
+
reader = csv.reader(file_obj)
|
|
387
|
+
if single_column:
|
|
388
|
+
return [row[0] for row in reader]
|
|
389
|
+
|
|
390
|
+
return list(reader)
|
|
391
|
+
except OSError as oe:
|
|
392
|
+
raise ReadFileError(f'Error reading entity file: {str(oe)}') from oe
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def write_file(to_write: str, output_directory: Union[str, Path], fname: str) -> Path:
|
|
396
|
+
"""Write bytes to file.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
to_write (bytes): bytes to write to file
|
|
400
|
+
output_directory (str or Path): path to directory to write file
|
|
401
|
+
fname (str): name of file to write
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Path: path to file written
|
|
405
|
+
"""
|
|
406
|
+
LOG.info(f'Writing file: {fname}')
|
|
407
|
+
output_directory = Path(output_directory)
|
|
408
|
+
try:
|
|
409
|
+
if not output_directory.is_absolute():
|
|
410
|
+
output_directory = OSHelpers.mkdir(output_directory)
|
|
411
|
+
full_path = output_directory / fname
|
|
412
|
+
with full_path.open('wb') as f:
|
|
413
|
+
f.write(to_write)
|
|
414
|
+
except OSError as err:
|
|
415
|
+
raise WriteFileError(f"Error writing file '{err.filename}': {str(err)}") from err
|
|
416
|
+
|
|
417
|
+
LOG.info(f'File written to: {full_path.as_posix()}')
|
|
418
|
+
return full_path
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class MultiThreadingHelper:
|
|
422
|
+
"""Multithreading class."""
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def multithread_it(max_workers: int, func: Callable, *, iterator, **kwargs) -> list:
|
|
426
|
+
"""Multithreading helper for I/O Operations.
|
|
427
|
+
|
|
428
|
+
The class can be used in the following way. Given a single thread code like:
|
|
429
|
+
|
|
430
|
+
.. code-block:: python
|
|
431
|
+
:linenos:
|
|
432
|
+
|
|
433
|
+
def _lookup_alert(self, alert_id, index, total_num_of_alerts):
|
|
434
|
+
...
|
|
435
|
+
|
|
436
|
+
def all_alerts(self, alerts):
|
|
437
|
+
res = []
|
|
438
|
+
for index, alert_id in enumerate(alert_ids_to_fetch):
|
|
439
|
+
res.append(self._lookup_alert(alert_id, index, len(alert_ids_to_fetch)))
|
|
440
|
+
|
|
441
|
+
It can be rewritten like:
|
|
442
|
+
|
|
443
|
+
.. code-block:: python
|
|
444
|
+
:linenos:
|
|
445
|
+
|
|
446
|
+
def _lookup_alert(self, alert_id, index, total_num_of_alerts):
|
|
447
|
+
...
|
|
448
|
+
|
|
449
|
+
def all_alerts(self, alerts):
|
|
450
|
+
results = MultiThreadingHelper.multithread_it(
|
|
451
|
+
self.max_workers,
|
|
452
|
+
self._lookup_alert,
|
|
453
|
+
iterator=alert_ids_to_fetch,
|
|
454
|
+
total_num_of_alerts=len(alert_ids_to_fetch)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
max_workers (int): Number of threads to use.
|
|
459
|
+
func (Callable): Function to be executed in parallel.
|
|
460
|
+
iterator (iterator): The list of elements to be dispatched to the threads.
|
|
461
|
+
Example: This can be the list of alerts to download, the IOCs, etc.
|
|
462
|
+
kwargs: Any other argument that the function needs for execution.
|
|
463
|
+
Example: The Alert type, or the IOC type.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
List of objects returned by the calling function.
|
|
467
|
+
"""
|
|
468
|
+
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
469
|
+
futures = [pool.submit(func, element, **kwargs) for element in iterator]
|
|
470
|
+
|
|
471
|
+
return [f.result() for f in futures]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from .errors import LoggingError
|
|
15
|
+
from .rf_logger import RFLogger
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
LOGGER_NAME = 'psengine'
|
|
18
|
+
|
|
19
|
+
DEFAULT_PSENGINE_OUTPUT = Path('logs') / 'psengine_recfut.log'
|
|
20
|
+
DEFAULT_ROOT_OUTPUT = Path('logs') / 'root_recfut.log'
|
|
21
|
+
|
|
22
|
+
MAX_BYTES = 20480 * 1024
|
|
23
|
+
BACKUP_COUNT = 5
|
|
24
|
+
LOGGER_LEVEL = ['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
25
|
+
LOGGER_LEVEL_INT = [
|
|
26
|
+
logging.NOTSET,
|
|
27
|
+
logging.DEBUG,
|
|
28
|
+
logging.INFO,
|
|
29
|
+
logging.WARNING,
|
|
30
|
+
logging.ERROR,
|
|
31
|
+
logging.CRITICAL,
|
|
32
|
+
]
|
|
33
|
+
FILE_FORMAT = (
|
|
34
|
+
'%(asctime)s,%(msecs)03d [%(threadName)s] %(levelname)s [%(module)s] '
|
|
35
|
+
'%(funcName)s:%(lineno)d - %(message)s'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
CONSOLE_FORMAT = '%(asctime)s,%(msecs)03d %(levelname)s [%(module)s] - %(message)s'
|
|
39
|
+
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from ..errors import RecordedFutureError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoggingError(RecordedFutureError):
|
|
18
|
+
"""Error raised when RFLogger can not be initialized."""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import logging.config
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..errors import WriteFileError
|
|
20
|
+
from ..helpers.helpers import OSHelpers
|
|
21
|
+
from .constants import (
|
|
22
|
+
BACKUP_COUNT,
|
|
23
|
+
CONSOLE_FORMAT,
|
|
24
|
+
DATE_FORMAT,
|
|
25
|
+
DEFAULT_PSENGINE_OUTPUT,
|
|
26
|
+
DEFAULT_ROOT_OUTPUT,
|
|
27
|
+
FILE_FORMAT,
|
|
28
|
+
LOGGER_LEVEL,
|
|
29
|
+
LOGGER_LEVEL_INT,
|
|
30
|
+
LOGGER_NAME,
|
|
31
|
+
MAX_BYTES,
|
|
32
|
+
)
|
|
33
|
+
from .errors import LoggingError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RFLogger:
|
|
37
|
+
"""Sets up logging and gives access to its functions."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
output: str = DEFAULT_PSENGINE_OUTPUT,
|
|
42
|
+
root_output: str = DEFAULT_ROOT_OUTPUT,
|
|
43
|
+
level=logging.INFO,
|
|
44
|
+
propagate: bool = True,
|
|
45
|
+
to_file=True,
|
|
46
|
+
to_console=True,
|
|
47
|
+
console_is_root=True,
|
|
48
|
+
):
|
|
49
|
+
if to_file is False and to_console is False:
|
|
50
|
+
raise ValueError('At least one of to_file or to_console must be set to True')
|
|
51
|
+
|
|
52
|
+
if not isinstance(level, (str, int)):
|
|
53
|
+
raise TypeError('level must be a string or int')
|
|
54
|
+
if isinstance(level, str):
|
|
55
|
+
level = level.upper()
|
|
56
|
+
if level not in LOGGER_LEVEL:
|
|
57
|
+
raise ValueError(f'level must be one of: {", ".join(LOGGER_LEVEL)}')
|
|
58
|
+
if isinstance(level, int) and level not in LOGGER_LEVEL_INT:
|
|
59
|
+
raise ValueError(f'level must be one of: {", ".join(LOGGER_LEVEL)}')
|
|
60
|
+
|
|
61
|
+
# Setup logging handlers
|
|
62
|
+
self.logger = logging.getLogger(LOGGER_NAME)
|
|
63
|
+
root_logger = logging.getLogger()
|
|
64
|
+
|
|
65
|
+
if to_file:
|
|
66
|
+
psengine_file_handler = self._create_file_handler(output)
|
|
67
|
+
root_file_handler = self._create_file_handler(root_output)
|
|
68
|
+
self.logger.addHandler(psengine_file_handler)
|
|
69
|
+
root_logger.addHandler(root_file_handler)
|
|
70
|
+
|
|
71
|
+
if to_console:
|
|
72
|
+
console_handler = self._create_console_handler()
|
|
73
|
+
if console_is_root:
|
|
74
|
+
root_logger.addHandler(console_handler)
|
|
75
|
+
else:
|
|
76
|
+
self.logger.addHandler(console_handler)
|
|
77
|
+
|
|
78
|
+
# If false then logging messages are not passed to the handlers of ancestor loggers
|
|
79
|
+
self.logger.propagate = propagate
|
|
80
|
+
|
|
81
|
+
logging.captureWarnings(True)
|
|
82
|
+
|
|
83
|
+
sys.excepthook = self._log_uncaught_exception
|
|
84
|
+
|
|
85
|
+
self.logger.setLevel(level=level)
|
|
86
|
+
self.logger.debug('Logger initialized')
|
|
87
|
+
|
|
88
|
+
def _create_file_handler(self, output):
|
|
89
|
+
log_filename = self._setup_output(output)
|
|
90
|
+
|
|
91
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
92
|
+
log_filename,
|
|
93
|
+
maxBytes=MAX_BYTES,
|
|
94
|
+
backupCount=BACKUP_COUNT,
|
|
95
|
+
)
|
|
96
|
+
formatter_file = logging.Formatter(fmt=FILE_FORMAT, datefmt=DATE_FORMAT)
|
|
97
|
+
file_handler.setFormatter(formatter_file)
|
|
98
|
+
|
|
99
|
+
return file_handler
|
|
100
|
+
|
|
101
|
+
def _create_console_handler(self):
|
|
102
|
+
console_handler = logging.StreamHandler()
|
|
103
|
+
formatter_console = logging.Formatter(fmt=CONSOLE_FORMAT, datefmt=DATE_FORMAT)
|
|
104
|
+
console_handler.setFormatter(formatter_console)
|
|
105
|
+
|
|
106
|
+
return console_handler
|
|
107
|
+
|
|
108
|
+
def _setup_output(self, output):
|
|
109
|
+
"""Confirms path is valid, returns cwd path and log cfg fullpath.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
output (str): logging output path
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
LoggingError: Raised when logging path does not exist
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
str: cwd path
|
|
119
|
+
"""
|
|
120
|
+
output = Path(output)
|
|
121
|
+
if output.is_absolute():
|
|
122
|
+
dir_name = output.parent
|
|
123
|
+
full_path = output
|
|
124
|
+
else:
|
|
125
|
+
main_dir = sys.path[0]
|
|
126
|
+
full_path = Path(main_dir) / output
|
|
127
|
+
dir_name = full_path.parent
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
OSHelpers.mkdir(dir_name)
|
|
131
|
+
except (WriteFileError, ValueError) as err:
|
|
132
|
+
raise LoggingError(f'Unable to create logging directory. Cause: {err}') from err
|
|
133
|
+
|
|
134
|
+
return full_path.as_posix()
|
|
135
|
+
|
|
136
|
+
def _log_uncaught_exception(self, exc_type, exc_value, exc_traceback) -> None:
|
|
137
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
138
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
self.logger.critical(
|
|
142
|
+
'An unexpected error has occurred:\n========================\n',
|
|
143
|
+
exc_info=(exc_type, exc_value, exc_traceback),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def get_logger(self) -> logging.Logger:
|
|
147
|
+
"""Returns self.logger object."""
|
|
148
|
+
return self.logger
|