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
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}'
|