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
psengine/rf_client.py ADDED
@@ -0,0 +1,430 @@
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 re
15
+ from json.decoder import JSONDecodeError
16
+ from typing import Union
17
+
18
+ import jsonpath_ng
19
+ from jsonpath_ng.exceptions import JsonPathParserError
20
+ from pydantic import validate_call
21
+ from requests.models import Response
22
+
23
+ from .base_http_client import BaseHTTPClient
24
+ from .constants import RF_TOKEN_VALIDATION_REGEX
25
+ from .helpers import debug_call
26
+
27
+
28
+ @validate_call
29
+ def is_api_token_format_valid(token: str):
30
+ """Checks if the token format is valid. The function does a
31
+ simple regex check, but does not validate the token against the API.
32
+
33
+ Args:
34
+ token(str): Recorded Future API token
35
+
36
+ Returns:
37
+ bool: True if token format is valid, False otherwise
38
+ """
39
+ return re.match(RF_TOKEN_VALIDATION_REGEX, token) is not None
40
+
41
+
42
+ class RFClient(BaseHTTPClient):
43
+ """Recorded Future HTTP API client."""
44
+
45
+ def __init__(
46
+ self,
47
+ api_token: Union[str, None] = None,
48
+ http_proxy=None,
49
+ https_proxy=None,
50
+ verify: Union[str, bool] = None,
51
+ auth: tuple[str, str] = None,
52
+ cert: Union[str, tuple[str, str], None] = None,
53
+ timeout: int = None,
54
+ retries: int = None,
55
+ backoff_factor: int = None,
56
+ status_forcelist: list = None,
57
+ pool_max_size: int = None,
58
+ ):
59
+ """Recorded Future HTTP API client.
60
+
61
+ Args:
62
+ api_token (str, optional): RF API token. Defaults to RF_TOKEN env variable.
63
+ http_proxy (str, optional): HTTP Proxy URL. Defaults to None.
64
+ https_proxy (str, optional): HTTPS Proxy URL. Defaults to None.
65
+ verify (Union[str, bool], optional): SSL verification flag or path to CA bundle.
66
+ Defaults to True.
67
+ auth (Tuple[str, str], optional): Basic Auth credentials. Defaults to None.
68
+ cert (Union[str, Tuple[str, str], None], optional): Client certificates.
69
+ Defaults to None.
70
+ timeout (int, optional): Request timeout. Defaults to 120.
71
+ retries (int, optional): Number of retries. Defaults to 5.
72
+ backoff_factor (int, optional): Backoff factor. Defaults to 1.
73
+ status_forcelist (int, optional): List of status codes to force a retry.
74
+ Defaults to [502, 503, 504].
75
+ pool_max_size (int, optional): Maximum number of connections in the pool.
76
+ Defaults to 120.
77
+ """
78
+ super().__init__(
79
+ http_proxy=http_proxy,
80
+ https_proxy=https_proxy,
81
+ verify=verify,
82
+ auth=auth,
83
+ cert=cert,
84
+ timeout=timeout,
85
+ retries=retries,
86
+ backoff_factor=backoff_factor,
87
+ status_forcelist=status_forcelist,
88
+ pool_max_size=pool_max_size,
89
+ )
90
+
91
+ self._api_token = api_token or self.config.rf_token.get_secret_value()
92
+ if not self._api_token:
93
+ raise ValueError('Missing Recorded Future API token.')
94
+ if not is_api_token_format_valid(self._api_token):
95
+ raise ValueError(
96
+ f'Invalid Recorded Future API token, must match regex {RF_TOKEN_VALIDATION_REGEX}'
97
+ )
98
+
99
+ @debug_call
100
+ @validate_call
101
+ def request(
102
+ self,
103
+ method: str,
104
+ url: str,
105
+ data: Union[dict, list[dict], None] = None,
106
+ *,
107
+ params: Union[dict, None] = None,
108
+ headers: Union[dict, None] = None,
109
+ **kwargs,
110
+ ) -> Response:
111
+ """Perform an HTTP request.
112
+
113
+ Args:
114
+ method (str): HTTP Method, one of GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH
115
+ url (str): URL to make the request to
116
+ data (dict, optional): Request body. Defaults to None.
117
+ params (dict, optional): HTTP query parameters. Defaults to None.
118
+ headers (dict, optional): If specified it will override default headers and wont set
119
+ the token.
120
+ **kwargs: Additional keyword arguments, passed to the requests library
121
+
122
+ Raises:
123
+ ValueError: if method is neither of GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH
124
+
125
+ Returns:
126
+ requests.Response: requests.Response object
127
+ """
128
+ headers = headers or self._prepare_headers()
129
+
130
+ return self.call(
131
+ method=method,
132
+ url=url,
133
+ headers=headers,
134
+ data=data,
135
+ params=params,
136
+ **kwargs,
137
+ )
138
+
139
+ def _request_paged_get(
140
+ self,
141
+ all_results,
142
+ params,
143
+ max_results,
144
+ offset_key,
145
+ method,
146
+ url,
147
+ headers,
148
+ data,
149
+ results_expr,
150
+ json_response,
151
+ **kwargs,
152
+ ):
153
+ if (
154
+ 'counts' not in json_response
155
+ or 'total' not in json_response['counts']
156
+ or 'returned' not in json_response['counts']
157
+ ):
158
+ return json_response
159
+
160
+ seen = json_response['counts']['returned']
161
+ if json_response['counts']['total'] > max_results:
162
+ total = max_results
163
+ else:
164
+ total = json_response['counts']['total']
165
+
166
+ while seen < total:
167
+ if not params:
168
+ params = {}
169
+ params[offset_key] = seen
170
+ params['limit'] = min(json_response['counts']['returned'], max_results - seen)
171
+ response = self.request(
172
+ method=method,
173
+ url=url,
174
+ headers=headers,
175
+ data=data,
176
+ params=params,
177
+ **kwargs,
178
+ )
179
+ json_response = response.json()
180
+ all_results += self._get_matches(results_expr, json_response)
181
+ seen += json_response['counts']['returned']
182
+ return all_results
183
+
184
+ def _request_paged_post(
185
+ self,
186
+ data,
187
+ offset_key,
188
+ method,
189
+ url,
190
+ headers,
191
+ params,
192
+ results_expr,
193
+ max_results,
194
+ json_response,
195
+ all_results,
196
+ **kwargs,
197
+ ):
198
+ if 'next_offset' in json_response:
199
+ while 'next_offset' in json_response:
200
+ data[offset_key] = json_response['next_offset']
201
+ json_response = self.request(
202
+ method=method,
203
+ url=url,
204
+ headers=headers,
205
+ data=data,
206
+ params=params,
207
+ **kwargs,
208
+ ).json()
209
+ all_results += self._get_matches(results_expr, json_response)
210
+ if len(all_results) >= max_results:
211
+ all_results = all_results[:max_results]
212
+ break
213
+ else:
214
+ seen = json_response['counts']['returned']
215
+ if json_response['counts']['total'] > max_results:
216
+ total = max_results
217
+ else:
218
+ total = json_response['counts']['total']
219
+
220
+ while seen < total:
221
+ data[offset_key] = seen
222
+ json_response = self.request(
223
+ method=method,
224
+ url=url,
225
+ headers=headers,
226
+ data=data,
227
+ params=params,
228
+ **kwargs,
229
+ ).json()
230
+ all_results += self._get_matches(results_expr, json_response)
231
+ seen += json_response['counts']['returned']
232
+ return all_results
233
+
234
+ def request_paged(
235
+ self,
236
+ method: str,
237
+ url: str,
238
+ max_results: int = 1000,
239
+ data: Union[dict, list[dict], None] = None,
240
+ *,
241
+ params: Union[dict, None] = None,
242
+ headers: Union[dict, None] = None,
243
+ results_path: str = 'data',
244
+ offset_key: str = 'offset',
245
+ **kwargs,
246
+ ) -> list[dict]:
247
+ """Perform a paged HTTP request.
248
+
249
+ Please note that some RF APIs can not paginate through more than 1000 results and will
250
+ result in an error (HTTP 400) if ``max_results`` is set to a higher value. While APIs such
251
+ as Identity can paginate through more than 1000 results.
252
+
253
+ .. code-block:: python
254
+ :linenos:
255
+
256
+ >>> response = rfc.request_paged(
257
+ method='post',
258
+ url='https://api.recordedfuture.com/identity/credentials/search',
259
+ max_results=1565,
260
+ data={
261
+ 'domains': ['norsegods.online'],
262
+ 'filter': {'first_downloaded_gte': '2024-01-01T23:40:47.034Z'},
263
+ 'limit': 100,
264
+ },
265
+ results_path='identities',
266
+ offset_key='offset',
267
+ )
268
+
269
+ >>> response = rfc.request_paged(
270
+ method='get',
271
+ url='https://api.recordedfuture.com/v2/ip/search',
272
+ params={'limit': 100, 'fields': 'entity', 'riskRule': 'dnsAbuse'},
273
+ results_path='data.results',
274
+ offset_key='from',
275
+ )
276
+
277
+ Args:
278
+ method (str): HTTP method: GET or POST
279
+ url (str): URL to make the request to
280
+ max_results (int, optional): Maximum number of results to return. Defaults to 1000.
281
+ data (dict, optional): Request body. Defaults to None.
282
+ params (dict, optional): HTTP query parameters. Defaults to None.
283
+ headers (dict, optional): If specified it will override default headers and wont set
284
+ the token.
285
+ results_path (str, optional): Path to extract paged results from. Defaults to 'data'.
286
+ offset_key (str, optional): Key to use for paging. Defaults to 'offset'.
287
+ **kwargs: Additional keyword arguments, passed to the requests library
288
+
289
+ Raises:
290
+ ValueError: if method is not GET or POST
291
+ ValueError: If results_path is invalid
292
+ KeyError: If no results are found in the API response
293
+
294
+ Returns:
295
+ list[dict]: List of dict containing the results
296
+ """
297
+ try:
298
+ results_expr = jsonpath_ng.parse(results_path)
299
+ except JsonPathParserError as err:
300
+ raise ValueError(f'Invalid results_path: {results_path}') from err
301
+ root_key = self._get_root_key(results_expr)
302
+
303
+ # Make the first request
304
+ response = self.request(
305
+ method=method,
306
+ url=url,
307
+ headers=headers,
308
+ data=data,
309
+ params=params,
310
+ **kwargs,
311
+ )
312
+
313
+ all_results = []
314
+ try:
315
+ # Try to parse the response as JSON
316
+ json_response = response.json()
317
+ except JSONDecodeError:
318
+ self.log.debug(f'Paged request does not contain valid JSON:\n{response.text}')
319
+ raise
320
+
321
+ # Check if the root key is in the response
322
+ if root_key not in json_response:
323
+ raise KeyError(results_path)
324
+
325
+ if len(json_response[root_key]) == 0:
326
+ return all_results
327
+
328
+ # Get the initial results from the first response and add them to the list
329
+ all_results += self._get_matches(results_expr, json_response)
330
+
331
+ if method.lower() == 'get':
332
+ return self._request_paged_get(
333
+ url=url,
334
+ headers=headers,
335
+ data=data,
336
+ method=method,
337
+ params=params,
338
+ max_results=max_results,
339
+ results_expr=results_expr,
340
+ offset_key=offset_key,
341
+ json_response=json_response,
342
+ all_results=all_results,
343
+ **kwargs,
344
+ )
345
+
346
+ if method.lower() == 'post':
347
+ return self._request_paged_post(
348
+ url=url,
349
+ method=method,
350
+ headers=headers,
351
+ data=data,
352
+ params=params,
353
+ max_results=max_results,
354
+ results_expr=results_expr,
355
+ offset_key=offset_key,
356
+ json_response=json_response,
357
+ all_results=all_results,
358
+ **kwargs,
359
+ )
360
+
361
+ raise ValueError('Invalid method for paged request. Must be GET or POST')
362
+
363
+ @debug_call
364
+ @validate_call
365
+ def is_authorized(self, method: str, url: str, **kwargs) -> bool:
366
+ """Check if the request is authorized to a given Recorded Future API endpoint.
367
+
368
+ Args:
369
+ method (str): HTTP method
370
+ url (str): URL to perform the check against
371
+ **kwargs: Additional keyword arguments, passed to the requests library
372
+
373
+ Returns:
374
+ bool: True if authorized, False otherwise
375
+ """
376
+ try:
377
+ response = self.request(method, url, **kwargs)
378
+ return response.status_code == 200
379
+ except Exception as err: # noqa: BLE001:
380
+ self.log.error(f'Error during validation: {err}')
381
+ return False
382
+
383
+ def _prepare_headers(self):
384
+ user_agent = self._get_user_agent_header()
385
+ headers = {
386
+ 'User-Agent': user_agent,
387
+ 'Content-Type': 'application/json',
388
+ 'accept': 'application/json',
389
+ }
390
+ if self._api_token:
391
+ headers['X-RFToken'] = self._api_token
392
+ else:
393
+ # In theory should never happen, but just in case
394
+ self.log.warning('Request being made with no Recorded Future API key set')
395
+
396
+ return headers
397
+
398
+ def _get_root_key(self, path: jsonpath_ng.jsonpath.Child) -> str:
399
+ try:
400
+ return self._get_root_key(path.left)
401
+ except AttributeError:
402
+ return str(path)
403
+
404
+ def _get_matches(
405
+ self, results_expr: jsonpath_ng.jsonpath.Fields, results: Union[list, dict]
406
+ ) -> list:
407
+ """Get matches from results.
408
+
409
+ Args:
410
+ results_expr (jsonpath_ng): jsonpath_ng object
411
+ results (dict): results
412
+
413
+ Raises:
414
+ KeyError: if no results are found
415
+
416
+ Returns:
417
+ list: list of matches
418
+ """
419
+ matches = results_expr.find(results)
420
+ results = []
421
+ if not len(matches):
422
+ self.log.warning(f'No results found for path: {str(results_expr)}')
423
+ raise KeyError(str(results_expr))
424
+
425
+ for match in matches:
426
+ if isinstance(match.value, list):
427
+ results += match.value
428
+ else:
429
+ results.append(match.value)
430
+ return results
@@ -0,0 +1,17 @@
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 .constants import DEFAULT_RISKLIST_FORMAT
15
+ from .errors import RiskListNotAvailableError
16
+ from .models import DefaultRiskList
17
+ from .risklist_mgr import RisklistMgr
@@ -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
+
15
+ DEFAULT_RISKLIST_FORMAT = 'csv/splunk'
@@ -0,0 +1,20 @@
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 RiskListNotAvailableError(RecordedFutureError):
18
+ """Error raised when a risklist cannot be obtained from the API
19
+ due to HTTP, transport errors.
20
+ """
@@ -0,0 +1,65 @@
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 json
15
+ from datetime import datetime
16
+ from typing import Optional
17
+
18
+ from pydantic import Field, field_validator
19
+
20
+ from ..common_models import RFBaseModel
21
+
22
+
23
+ class EvidenceDetail(RFBaseModel):
24
+ name: str = Field(alias='Name')
25
+ evidence_string: str = Field(alias='EvidenceString')
26
+ criticality_label: str = Field(alias='CriticalityLabel')
27
+ mitigation_string: str = Field(alias='MitigationString')
28
+ sightings_count: float = Field(alias='SightingsCount')
29
+ criticality: int = Field(alias='Criticality')
30
+ rule: str = Field(alias='Rule')
31
+ source_count: Optional[int] = Field(alias='SourceCount', default=None)
32
+ sources: list[str] = Field(alias='Sources')
33
+ timestamp: datetime = Field(alias='Timestamp')
34
+
35
+ def __str__(self):
36
+ return f'Evidence Details: {self.name}, {self.timestamp}'
37
+
38
+ def __repr__(self):
39
+ return f'Evidence Details: {self.name}, {self.timestamp}'
40
+
41
+
42
+ class DefaultRiskList(RFBaseModel):
43
+ ioc: str = Field(validation_alias='Name')
44
+ algorithm: Optional[str] = Field(validation_alias='Algorithm', default=None)
45
+ risk_score: int = Field(validation_alias='Risk')
46
+ risk_string: str = Field(validation_alias='RiskString')
47
+ evidence_details: list[EvidenceDetail] = Field(validation_alias='EvidenceDetails')
48
+
49
+ @field_validator('evidence_details', mode='before')
50
+ @classmethod
51
+ def evidence_to_dict(cls, v):
52
+ """Dump the EvidenceDetails block to dict, if possible."""
53
+ if isinstance(v, str):
54
+ try:
55
+ return json.loads(v)['EvidenceDetails']
56
+ except (json.JSONDecodeError, KeyError) as err:
57
+ raise ValueError(
58
+ 'Evidence details cannot be converted to json or key not found'
59
+ ) from err
60
+
61
+ def __str__(self):
62
+ return f'{self.ioc}: {self.risk_score} {self.evidence_details}'
63
+
64
+ def __repr__(self):
65
+ return f'{self.ioc}: {self.risk_score} {self.evidence_details}'