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.
Files changed (115) hide show
  1. psengine/__init__.py +22 -0
  2. psengine/_sdk_id.py +16 -0
  3. psengine/_version.py +14 -0
  4. psengine/analyst_notes/__init__.py +32 -0
  5. psengine/analyst_notes/constants.py +15 -0
  6. psengine/analyst_notes/errors.py +42 -0
  7. psengine/analyst_notes/helpers.py +90 -0
  8. psengine/analyst_notes/models.py +219 -0
  9. psengine/analyst_notes/note.py +149 -0
  10. psengine/analyst_notes/note_mgr.py +400 -0
  11. psengine/base_http_client.py +285 -0
  12. psengine/classic_alerts/__init__.py +24 -0
  13. psengine/classic_alerts/classic_alert.py +275 -0
  14. psengine/classic_alerts/classic_alert_mgr.py +507 -0
  15. psengine/classic_alerts/constants.py +31 -0
  16. psengine/classic_alerts/errors.py +38 -0
  17. psengine/classic_alerts/helpers.py +87 -0
  18. psengine/classic_alerts/markdown/__init__.py +13 -0
  19. psengine/classic_alerts/markdown/markdown.py +359 -0
  20. psengine/classic_alerts/models.py +141 -0
  21. psengine/collective_insights/__init__.py +29 -0
  22. psengine/collective_insights/collective_insights.py +164 -0
  23. psengine/collective_insights/constants.py +44 -0
  24. psengine/collective_insights/errors.py +18 -0
  25. psengine/collective_insights/insight.py +89 -0
  26. psengine/collective_insights/models.py +81 -0
  27. psengine/common_models.py +89 -0
  28. psengine/config/__init__.py +15 -0
  29. psengine/config/config.py +284 -0
  30. psengine/config/errors.py +18 -0
  31. psengine/constants.py +63 -0
  32. psengine/detection/__init__.py +17 -0
  33. psengine/detection/detection_mgr.py +135 -0
  34. psengine/detection/detection_rule.py +85 -0
  35. psengine/detection/errors.py +26 -0
  36. psengine/detection/helpers.py +56 -0
  37. psengine/detection/models.py +47 -0
  38. psengine/endpoints.py +98 -0
  39. psengine/enrich/__init__.py +28 -0
  40. psengine/enrich/constants.py +73 -0
  41. psengine/enrich/errors.py +26 -0
  42. psengine/enrich/lookup.py +299 -0
  43. psengine/enrich/lookup_mgr.py +341 -0
  44. psengine/enrich/models/__init__.py +13 -0
  45. psengine/enrich/models/base_enriched_entity.py +43 -0
  46. psengine/enrich/models/lookup.py +271 -0
  47. psengine/enrich/models/soar.py +138 -0
  48. psengine/enrich/soar.py +89 -0
  49. psengine/enrich/soar_mgr.py +176 -0
  50. psengine/entity_lists/__init__.py +16 -0
  51. psengine/entity_lists/constants.py +19 -0
  52. psengine/entity_lists/entity_list.py +435 -0
  53. psengine/entity_lists/entity_list_mgr.py +185 -0
  54. psengine/entity_lists/errors.py +26 -0
  55. psengine/entity_lists/models.py +87 -0
  56. psengine/entity_match/__init__.py +16 -0
  57. psengine/entity_match/entity_match.py +90 -0
  58. psengine/entity_match/entity_match_mgr.py +235 -0
  59. psengine/entity_match/errors.py +18 -0
  60. psengine/entity_match/models.py +22 -0
  61. psengine/errors.py +41 -0
  62. psengine/helpers/__init__.py +23 -0
  63. psengine/helpers/helpers.py +471 -0
  64. psengine/logger/__init__.py +15 -0
  65. psengine/logger/constants.py +39 -0
  66. psengine/logger/errors.py +18 -0
  67. psengine/logger/rf_logger.py +148 -0
  68. psengine/markdown/__init__.py +21 -0
  69. psengine/markdown/markdown.py +169 -0
  70. psengine/markdown/models.py +22 -0
  71. psengine/playbook_alerts/__init__.py +34 -0
  72. psengine/playbook_alerts/constants.py +35 -0
  73. psengine/playbook_alerts/errors.py +35 -0
  74. psengine/playbook_alerts/helpers.py +80 -0
  75. psengine/playbook_alerts/mappings.py +44 -0
  76. psengine/playbook_alerts/markdown/__init__.py +13 -0
  77. psengine/playbook_alerts/markdown/markdown.py +98 -0
  78. psengine/playbook_alerts/markdown/markdown_code_repo.py +64 -0
  79. psengine/playbook_alerts/markdown/markdown_domain_abuse.py +118 -0
  80. psengine/playbook_alerts/markdown/markdown_identity_exposure.py +158 -0
  81. psengine/playbook_alerts/models/__init__.py +36 -0
  82. psengine/playbook_alerts/models/common_models.py +18 -0
  83. psengine/playbook_alerts/models/panel_log.py +329 -0
  84. psengine/playbook_alerts/models/panel_status.py +70 -0
  85. psengine/playbook_alerts/models/pba_code_repo_leak.py +52 -0
  86. psengine/playbook_alerts/models/pba_cyber_vulnerability.py +53 -0
  87. psengine/playbook_alerts/models/pba_domain_abuse.py +139 -0
  88. psengine/playbook_alerts/models/pba_identity_exposures.py +93 -0
  89. psengine/playbook_alerts/models/pba_third_party_risk.py +103 -0
  90. psengine/playbook_alerts/models/search_endpoint.py +68 -0
  91. psengine/playbook_alerts/pa_category.py +37 -0
  92. psengine/playbook_alerts/playbook_alert_mgr.py +593 -0
  93. psengine/playbook_alerts/playbook_alerts.py +393 -0
  94. psengine/rf_client.py +430 -0
  95. psengine/risklists/__init__.py +17 -0
  96. psengine/risklists/constants.py +15 -0
  97. psengine/risklists/errors.py +20 -0
  98. psengine/risklists/models.py +65 -0
  99. psengine/risklists/risklist_mgr.py +156 -0
  100. psengine/stix2/__init__.py +21 -0
  101. psengine/stix2/base_stix_entity.py +62 -0
  102. psengine/stix2/complex_entity.py +372 -0
  103. psengine/stix2/constants.py +81 -0
  104. psengine/stix2/enriched_indicator.py +261 -0
  105. psengine/stix2/errors.py +22 -0
  106. psengine/stix2/helpers.py +68 -0
  107. psengine/stix2/rf_bundle.py +240 -0
  108. psengine/stix2/simple_entity.py +145 -0
  109. psengine/stix2/util.py +53 -0
  110. psengine-2.0.4.dist-info/METADATA +189 -0
  111. psengine-2.0.4.dist-info/RECORD +115 -0
  112. psengine-2.0.4.dist-info/WHEEL +5 -0
  113. psengine-2.0.4.dist-info/entry_points.txt +2 -0
  114. psengine-2.0.4.dist-info/licenses/LICENSE +21 -0
  115. 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