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,593 @@
|
|
|
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
|
+
from typing import Optional, Union
|
|
17
|
+
|
|
18
|
+
import pydantic
|
|
19
|
+
import requests
|
|
20
|
+
from more_itertools import batched
|
|
21
|
+
from pydantic import validate_call
|
|
22
|
+
|
|
23
|
+
from ..constants import DEFAULT_LIMIT, ROOT_DIR
|
|
24
|
+
from ..endpoints import (
|
|
25
|
+
EP_PLAYBOOK_ALERT,
|
|
26
|
+
EP_PLAYBOOK_ALERT_COMMON,
|
|
27
|
+
EP_PLAYBOOK_ALERT_DOMAIN_ABUSE,
|
|
28
|
+
EP_PLAYBOOK_ALERT_SEARCH,
|
|
29
|
+
)
|
|
30
|
+
from ..helpers import TimeHelpers, connection_exceptions, debug_call
|
|
31
|
+
from ..rf_client import RFClient
|
|
32
|
+
from .constants import PLAYBOOK_ALERT_TYPE, STATUS_PANEL_NAME
|
|
33
|
+
from .errors import (
|
|
34
|
+
PlaybookAlertFetchError,
|
|
35
|
+
PlaybookAlertRetrieveImageError,
|
|
36
|
+
PlaybookAlertSearchError,
|
|
37
|
+
PlaybookAlertUpdateError,
|
|
38
|
+
)
|
|
39
|
+
from .mappings import CATEGORY_ENDPOINTS, CATEGORY_TO_OBJECT_MAP
|
|
40
|
+
from .models import SearchResponse
|
|
41
|
+
from .pa_category import PACategory
|
|
42
|
+
from .playbook_alerts import (
|
|
43
|
+
LookupAlertIn,
|
|
44
|
+
PBA_DomainAbuse,
|
|
45
|
+
PBA_Generic,
|
|
46
|
+
PreviewAlertOut,
|
|
47
|
+
SearchIn,
|
|
48
|
+
UpdateAlertIn,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
BULK_LOOKUP_BATCH_SIZE = 200
|
|
52
|
+
|
|
53
|
+
DEFAULT_ALERTS_OUTPUT_DIR = Path(ROOT_DIR) / 'playbook_alerts'
|
|
54
|
+
PLAYBOOK_ALERTS_OUTPUT_FNAME = 'rf_playbook_alerts_'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PlaybookAlertMgr:
|
|
58
|
+
"""Manages requests for Recorded Future playbook alerts."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, rf_token: str = None):
|
|
61
|
+
"""Initializes the PlaybookAlertMgr object.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
rf_token (str, optional): Recorded Future API token. Defaults to None
|
|
65
|
+
"""
|
|
66
|
+
self.log = logging.getLogger(__name__)
|
|
67
|
+
self.rf_client = RFClient(api_token=rf_token) if rf_token is not None else RFClient()
|
|
68
|
+
|
|
69
|
+
@debug_call
|
|
70
|
+
@validate_call
|
|
71
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=PlaybookAlertFetchError)
|
|
72
|
+
def fetch(
|
|
73
|
+
self,
|
|
74
|
+
alert_id: str,
|
|
75
|
+
category: Optional[str] = None,
|
|
76
|
+
panels: Optional[list[str]] = None,
|
|
77
|
+
fetch_images: Optional[bool] = True,
|
|
78
|
+
) -> PLAYBOOK_ALERT_TYPE:
|
|
79
|
+
"""Fetch an individual Playbook Alert.
|
|
80
|
+
|
|
81
|
+
Endpoints:
|
|
82
|
+
|
|
83
|
+
- ``playbook-alert/{category}``
|
|
84
|
+
- ``playbook-alert/common/{alert_id}``
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
alert_id (str): Alert ID to fetch
|
|
88
|
+
category (Optional[PACategory], optional): Category to fetch. When this is not supplied,
|
|
89
|
+
fetch uses ``playbook-alert/common`` to find
|
|
90
|
+
the alert category. Defaults to None
|
|
91
|
+
panels (Optional[List[str]]): Panels to fetch. The ``status`` panel is always fetched,
|
|
92
|
+
to correctly initialize ADTs. Defaults to None (all)
|
|
93
|
+
fetch_images (Optional[bool]): Fetch images for Domain Abuse alerts. Defaults to True
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValidationError if any parameter is of incorrect type
|
|
97
|
+
PlaybookAlertFetchError: if an API-related error occurs
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
PBA ADT: Any one of the playbook alert ADTs. Unknown alert types return PBA_Generic
|
|
101
|
+
"""
|
|
102
|
+
if category is None:
|
|
103
|
+
category = self._fetch_alert_category(alert_id)
|
|
104
|
+
|
|
105
|
+
category = category.lower()
|
|
106
|
+
if category in CATEGORY_ENDPOINTS:
|
|
107
|
+
endpoint = CATEGORY_ENDPOINTS[category]
|
|
108
|
+
else:
|
|
109
|
+
# Workaround to fetch new PAs that have not been officially supported
|
|
110
|
+
self.log.warning(
|
|
111
|
+
f'Unknown playbook alert category: {category}, for alert: {alert_id}. '
|
|
112
|
+
'Using category as an endpoint for this lookup'
|
|
113
|
+
)
|
|
114
|
+
endpoint = f'{EP_PLAYBOOK_ALERT}/{category}'
|
|
115
|
+
|
|
116
|
+
data = {}
|
|
117
|
+
if panels:
|
|
118
|
+
# We must always fetch status panel for ADT initialization
|
|
119
|
+
panels = panels.append(STATUS_PANEL_NAME) if STATUS_PANEL_NAME not in panels else panels
|
|
120
|
+
data = {'panels': panels}
|
|
121
|
+
request_data = LookupAlertIn.model_validate(data)
|
|
122
|
+
|
|
123
|
+
url = f'{endpoint}/{alert_id}'
|
|
124
|
+
self.log.info(f'Fetching playbook alert: {alert_id}, category: {category}')
|
|
125
|
+
response = self.rf_client.request('post', url=url, data=request_data.json())
|
|
126
|
+
p_alert = self._playbook_alert_factory(category, response.json()['data'])
|
|
127
|
+
|
|
128
|
+
if isinstance(p_alert, PBA_DomainAbuse) and fetch_images:
|
|
129
|
+
self.fetch_images(p_alert)
|
|
130
|
+
|
|
131
|
+
return p_alert
|
|
132
|
+
|
|
133
|
+
@debug_call
|
|
134
|
+
@validate_call
|
|
135
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=PlaybookAlertFetchError)
|
|
136
|
+
def fetch_bulk(
|
|
137
|
+
self,
|
|
138
|
+
alerts: Optional[list[tuple[str, str]]] = None,
|
|
139
|
+
panels: Optional[list] = None,
|
|
140
|
+
fetch_images: Optional[bool] = False,
|
|
141
|
+
filter_from: Optional[int] = 0,
|
|
142
|
+
max_results: Optional[int] = DEFAULT_LIMIT,
|
|
143
|
+
order_by: Optional[str] = None,
|
|
144
|
+
direction: Optional[str] = None,
|
|
145
|
+
entity: Optional[Union[str, list]] = None,
|
|
146
|
+
statuses: Optional[Union[str, list]] = None,
|
|
147
|
+
priority: Optional[Union[str, list]] = None,
|
|
148
|
+
category: Optional[Union[str, list]] = None,
|
|
149
|
+
assignee: Optional[Union[str, list]] = None,
|
|
150
|
+
created_from: Optional[str] = None,
|
|
151
|
+
created_until: Optional[str] = None,
|
|
152
|
+
updated_from: Optional[str] = None,
|
|
153
|
+
updated_until: Optional[str] = None,
|
|
154
|
+
) -> list[PLAYBOOK_ALERT_TYPE]:
|
|
155
|
+
"""Fetch alerts in bulk based on a search query.
|
|
156
|
+
|
|
157
|
+
Endpoints:
|
|
158
|
+
|
|
159
|
+
- ``playbook-alert/search``
|
|
160
|
+
- ``playbook-alert/{category}/{alert_id}``
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
alerts (tuple, optional): Alert (id, category) tuples to fetch. If alerts are supplied,
|
|
164
|
+
other search parameters (query, limit, statuses, category,
|
|
165
|
+
priority, created, updated) are ignored. Defaults to None
|
|
166
|
+
panels (list, optional): Panels to fetch. Always fetches status panel. Defaults to None
|
|
167
|
+
fetch_images (bool, optional): Fetch images for Domain Abuse alerts. Defaults to True
|
|
168
|
+
filter_from: (int, optional): Offset to page from. Defaults to 0
|
|
169
|
+
max_results (int, optional): Maximum total number of alerts to fetch. Defaults to 10
|
|
170
|
+
order_by (str, optional): Order alerts by field [created|updated]
|
|
171
|
+
direction (str, optional): Direction to order alerts [asc|desc]
|
|
172
|
+
entity (list, optional): List of entities to fetch alerts for
|
|
173
|
+
statuses (list, optional): List of statuses to fetch e.g. ['New', 'InProgress']
|
|
174
|
+
priority (list, optional): Priority of alerts to fetch e.g. ['High', 'Informational']
|
|
175
|
+
category (list, optional): List of categories to fetch e.g. ['domain_abuse']
|
|
176
|
+
assignee (list, optional): List of assignee uhashes to fetch alerts for
|
|
177
|
+
created_from (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
178
|
+
created_until (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
179
|
+
updated_from (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
180
|
+
updated_until (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValidationError if any parameter is of incorrect type
|
|
184
|
+
PlaybookAlertFetchError: if connection error occurs
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
list: PlaybookAlert ADTs
|
|
188
|
+
"""
|
|
189
|
+
query_params = locals()
|
|
190
|
+
for param in ['self', 'alerts', 'panels', 'fetch_images']:
|
|
191
|
+
query_params.pop(param)
|
|
192
|
+
if alerts is None:
|
|
193
|
+
search_result = self.search(**query_params)
|
|
194
|
+
alerts = [
|
|
195
|
+
{'id': x.playbook_alert_id, 'category': x.category} for x in search_result.data
|
|
196
|
+
]
|
|
197
|
+
else:
|
|
198
|
+
alerts = [{'id': x[0], 'category': x[1]} for x in alerts]
|
|
199
|
+
|
|
200
|
+
fetched_alerts = []
|
|
201
|
+
errors = 0
|
|
202
|
+
for cat in {x['category'] for x in alerts}:
|
|
203
|
+
in_cat_alerts = filter(lambda x: x['category'] == cat, alerts)
|
|
204
|
+
in_cat_ids = [x['id'] for x in in_cat_alerts]
|
|
205
|
+
try:
|
|
206
|
+
fetched_alerts.extend(self._do_bulk(in_cat_ids, cat, fetch_images, panels or []))
|
|
207
|
+
except PlaybookAlertFetchError as err: # noqa: PERF203
|
|
208
|
+
errors += 1
|
|
209
|
+
self.log.error(err)
|
|
210
|
+
|
|
211
|
+
if errors:
|
|
212
|
+
self.log.error(f'Failed to fetch alerts due to {errors} error(s). See errors above')
|
|
213
|
+
raise PlaybookAlertFetchError('Failed to fetch alerts')
|
|
214
|
+
|
|
215
|
+
return fetched_alerts
|
|
216
|
+
|
|
217
|
+
@debug_call
|
|
218
|
+
@validate_call
|
|
219
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=PlaybookAlertSearchError)
|
|
220
|
+
def search(
|
|
221
|
+
self,
|
|
222
|
+
filter_from: Optional[int] = 0,
|
|
223
|
+
max_results: Optional[int] = DEFAULT_LIMIT,
|
|
224
|
+
order_by: Optional[str] = None,
|
|
225
|
+
direction: Optional[str] = None,
|
|
226
|
+
entity: Optional[Union[str, list]] = None,
|
|
227
|
+
statuses: Optional[Union[str, list]] = None,
|
|
228
|
+
priority: Optional[Union[str, list]] = None,
|
|
229
|
+
category: Optional[Union[str, list]] = None,
|
|
230
|
+
assignee: Optional[Union[str, list]] = None,
|
|
231
|
+
created_from: Optional[str] = None,
|
|
232
|
+
created_until: Optional[str] = None,
|
|
233
|
+
updated_from: Optional[str] = None,
|
|
234
|
+
updated_until: Optional[str] = None,
|
|
235
|
+
) -> SearchResponse:
|
|
236
|
+
"""Search playbook alerts.
|
|
237
|
+
|
|
238
|
+
Endpoints:
|
|
239
|
+
``playbook-alert/search``
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
filter_from: (int, optional): Offset to page from. Defaults to 0
|
|
243
|
+
max_results (int, optional): Maximum total number of alerts to fetch. Defaults to 10
|
|
244
|
+
order_by (str, optional): Order alerts by field [created|updated]
|
|
245
|
+
direction (str, optional): Direction to order alerts [asc|desc]
|
|
246
|
+
entity (list, optional): List of entities to fetch alerts for
|
|
247
|
+
statuses (list, optional): List of statuses to fetch e.g. ['New', 'InProgress']
|
|
248
|
+
priority (list, optional): Priority of alerts to fetch e.g. ['High', 'Informational']
|
|
249
|
+
category (list, optional): List of categories to fetch e.g. ['domain_abuse']
|
|
250
|
+
assignee (list, optional): List of assignee uhashes to fetch alerts for
|
|
251
|
+
created_from (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
252
|
+
created_until (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
253
|
+
updated_from (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
254
|
+
updated_until (str, optional): ISO or relative [h|d], e.g. -3d or 2023-07-21T17:32:28Z
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValidationError if any parameter is of incorrect type
|
|
258
|
+
PlaybookAlertSearchError: if connection error occurs
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
SearchResponse: Search results
|
|
262
|
+
"""
|
|
263
|
+
query_params = locals()
|
|
264
|
+
query_params.pop('self')
|
|
265
|
+
request_body = self._prepare_query(**query_params).json()
|
|
266
|
+
self.log.info(f'Searching for playbook alerts with params {request_body}')
|
|
267
|
+
response = self.rf_client.request('post', EP_PLAYBOOK_ALERT_SEARCH, data=request_body)
|
|
268
|
+
search_results = response.json()
|
|
269
|
+
|
|
270
|
+
status_message = search_results.get('status', {}).get('status_message')
|
|
271
|
+
count_returned = search_results.get('counts', {}).get('returned')
|
|
272
|
+
count_total = search_results.get('counts', {}).get('total')
|
|
273
|
+
self.log.info(
|
|
274
|
+
'Status: {}, returned: {} {}, total: {} {}'.format(
|
|
275
|
+
status_message,
|
|
276
|
+
count_returned,
|
|
277
|
+
'alert' if count_returned == 1 else 'alerts',
|
|
278
|
+
count_total,
|
|
279
|
+
'alert' if count_total == 1 else 'alerts',
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return SearchResponse.model_validate(search_results)
|
|
284
|
+
|
|
285
|
+
@debug_call
|
|
286
|
+
@validate_call
|
|
287
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=PlaybookAlertUpdateError)
|
|
288
|
+
def update(
|
|
289
|
+
self,
|
|
290
|
+
alert: PLAYBOOK_ALERT_TYPE,
|
|
291
|
+
priority: Optional[str] = None,
|
|
292
|
+
status: Optional[str] = None,
|
|
293
|
+
assignee: Optional[str] = None,
|
|
294
|
+
log_entry: Optional[str] = None,
|
|
295
|
+
reopen_strategy: Optional[str] = None,
|
|
296
|
+
) -> requests.Response:
|
|
297
|
+
"""Update a playbook alert.
|
|
298
|
+
|
|
299
|
+
Endpoints:
|
|
300
|
+
``playbook-alert/common/{playbook_alert_id}``
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
alert (BasePlaybookAlert): Playbook alert to update
|
|
304
|
+
priority (str, optional): Alert priority. Defaults to None
|
|
305
|
+
status (str, optional): Alert Status. Defaults to None
|
|
306
|
+
assignee (str, optional): Assignee. Defaults to None
|
|
307
|
+
log_entry (str, optional): Log entry. Defaults to None
|
|
308
|
+
reopen_strategy (str, optional): Reopen strategy. Defaults to None
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
ValidationError if any parameter is of incorrect type
|
|
312
|
+
ValueError: If no update parameters are supplied
|
|
313
|
+
PlaybookAlertUpdateError: If the update request fails
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
requests.Response: API response
|
|
317
|
+
"""
|
|
318
|
+
if (
|
|
319
|
+
priority is None
|
|
320
|
+
and status is None
|
|
321
|
+
and assignee is None
|
|
322
|
+
and log_entry is None
|
|
323
|
+
and reopen_strategy is None
|
|
324
|
+
):
|
|
325
|
+
raise ValueError('No update parameters were supplied')
|
|
326
|
+
|
|
327
|
+
body = {}
|
|
328
|
+
if priority is not None:
|
|
329
|
+
body['priority'] = priority
|
|
330
|
+
|
|
331
|
+
if status is not None:
|
|
332
|
+
body['status'] = status
|
|
333
|
+
|
|
334
|
+
if assignee is not None:
|
|
335
|
+
body['assignee'] = assignee
|
|
336
|
+
|
|
337
|
+
if log_entry is not None:
|
|
338
|
+
body['log_entry'] = log_entry
|
|
339
|
+
|
|
340
|
+
if reopen_strategy is not None:
|
|
341
|
+
body['reopen'] = reopen_strategy
|
|
342
|
+
|
|
343
|
+
alert_id = alert.playbook_alert_id
|
|
344
|
+
validated_payload = UpdateAlertIn.model_validate(body)
|
|
345
|
+
url = f'{EP_PLAYBOOK_ALERT_COMMON}/{alert_id}'
|
|
346
|
+
self.log.info(f'Updating playbook alert: {alert_id}')
|
|
347
|
+
response = self.rf_client.request('put', url=url, data=validated_payload.json())
|
|
348
|
+
|
|
349
|
+
return response
|
|
350
|
+
|
|
351
|
+
@debug_call
|
|
352
|
+
@validate_call
|
|
353
|
+
def _prepare_query(
|
|
354
|
+
self,
|
|
355
|
+
filter_from: Optional[int] = 0,
|
|
356
|
+
max_results: Optional[int] = DEFAULT_LIMIT,
|
|
357
|
+
order_by: Optional[str] = None,
|
|
358
|
+
direction: Optional[str] = None,
|
|
359
|
+
entity: Optional[Union[str, list]] = None,
|
|
360
|
+
statuses: Optional[Union[str, list]] = None,
|
|
361
|
+
priority: Optional[Union[str, list]] = None,
|
|
362
|
+
category: Optional[Union[str, list]] = None,
|
|
363
|
+
assignee: Optional[Union[str, list]] = None,
|
|
364
|
+
created_from: Optional[str] = None,
|
|
365
|
+
created_until: Optional[str] = None,
|
|
366
|
+
updated_from: Optional[str] = None,
|
|
367
|
+
updated_until: Optional[str] = None,
|
|
368
|
+
) -> SearchIn:
|
|
369
|
+
"""Create a query for searching playbook alerts.
|
|
370
|
+
|
|
371
|
+
See search() and fetch_bulk() for parameter descriptions.
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
ValidationError if any parameter is of incorrect type
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
SearchIn: Validated search query
|
|
378
|
+
"""
|
|
379
|
+
params = {key: val for key, val in locals().items() if val and key != 'self'}
|
|
380
|
+
|
|
381
|
+
query = {'created_range': {}, 'updated_range': {}}
|
|
382
|
+
|
|
383
|
+
for arg in params:
|
|
384
|
+
key, value = self._process_arg(arg, params[arg])
|
|
385
|
+
if isinstance(value, dict):
|
|
386
|
+
query[key].update(value)
|
|
387
|
+
else:
|
|
388
|
+
query[key] = value
|
|
389
|
+
|
|
390
|
+
query = {
|
|
391
|
+
key: val
|
|
392
|
+
for key, val in query.items()
|
|
393
|
+
if not ((isinstance(val, (dict, list))) and len(val) == 0)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return SearchIn.model_validate(query)
|
|
397
|
+
|
|
398
|
+
@debug_call
|
|
399
|
+
@validate_call
|
|
400
|
+
@connection_exceptions(
|
|
401
|
+
ignore_status_code=[], exception_to_raise=PlaybookAlertRetrieveImageError
|
|
402
|
+
)
|
|
403
|
+
def fetch_one_image(self, alert_id: str, image_id: str) -> bytes:
|
|
404
|
+
"""Retrieve image from Domain Abuse playbook alert.
|
|
405
|
+
|
|
406
|
+
Endpoints:
|
|
407
|
+
``playbook-alert/domain_abuse/{alert_id}/image/{image_id}``
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
alert_id (str): Alert ID for corresponding image ID
|
|
411
|
+
image_id (str): Image ID to fetch
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
ValidationError if any parameter is of incorrect type
|
|
415
|
+
PlaybookAlertRetrieveImageError: If the image fetch fails
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
bytes: Bytes of the image
|
|
419
|
+
"""
|
|
420
|
+
url = f'{EP_PLAYBOOK_ALERT_DOMAIN_ABUSE}/{alert_id}/image/{image_id}'
|
|
421
|
+
|
|
422
|
+
self.log.info(f'Retrieving image: {image_id} for alert: {alert_id}')
|
|
423
|
+
response = self.rf_client.request('get', url)
|
|
424
|
+
|
|
425
|
+
return response.content
|
|
426
|
+
|
|
427
|
+
@debug_call
|
|
428
|
+
@validate_call
|
|
429
|
+
@connection_exceptions(
|
|
430
|
+
ignore_status_code=[], exception_to_raise=PlaybookAlertRetrieveImageError
|
|
431
|
+
)
|
|
432
|
+
def fetch_images(self, playbook_alert: PBA_DomainAbuse) -> None:
|
|
433
|
+
"""Domain Abuse: Retrieve the associated images, if any available.
|
|
434
|
+
|
|
435
|
+
Endpoint:
|
|
436
|
+
``playbook-alert/domain_abuse/{alert_id}/image/{image_id}``
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
playbook_alert (DomainAbuse): Domain Abuse Playbook Alert ADT
|
|
440
|
+
|
|
441
|
+
Raises:
|
|
442
|
+
ValidationError if any parameter is of incorrect type
|
|
443
|
+
PlaybookAlertRetrieveImageError: if an API error occurs
|
|
444
|
+
"""
|
|
445
|
+
if isinstance(playbook_alert, PBA_DomainAbuse):
|
|
446
|
+
for image_id in playbook_alert.image_ids:
|
|
447
|
+
image_bytes = self.fetch_one_image(playbook_alert.playbook_alert_id, image_id)
|
|
448
|
+
playbook_alert.store_image(image_id, image_bytes)
|
|
449
|
+
else:
|
|
450
|
+
self.log.debug('Image fetching is only supported for Domain Abuse alerts')
|
|
451
|
+
|
|
452
|
+
@debug_call
|
|
453
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=PlaybookAlertFetchError)
|
|
454
|
+
def _fetch_alert_category(self, alert_id: str) -> PACategory:
|
|
455
|
+
"""Fetch the alert category based on the alert ID.
|
|
456
|
+
|
|
457
|
+
Endpoints:
|
|
458
|
+
``playbook-alert/common/{alert_id}``
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
alert_id (str): Alert ID
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
RFPACategory: Alert category
|
|
465
|
+
"""
|
|
466
|
+
endpoint = EP_PLAYBOOK_ALERT_COMMON + '/' + alert_id
|
|
467
|
+
result = self.rf_client.request('get', endpoint)
|
|
468
|
+
validated_alert_info = PreviewAlertOut.model_validate(result.json()['data'])
|
|
469
|
+
|
|
470
|
+
return PACategory(validated_alert_info.category)
|
|
471
|
+
|
|
472
|
+
@debug_call
|
|
473
|
+
def _playbook_alert_factory(
|
|
474
|
+
self,
|
|
475
|
+
category: str,
|
|
476
|
+
raw_alert: dict,
|
|
477
|
+
) -> PLAYBOOK_ALERT_TYPE:
|
|
478
|
+
"""Return correct playbook alert type from raw alert and category.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
category (string): Alert category
|
|
482
|
+
raw_alert (dict): Raw alert payload
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Playbook Alert ADT
|
|
486
|
+
"""
|
|
487
|
+
p_alert = None
|
|
488
|
+
try:
|
|
489
|
+
try:
|
|
490
|
+
p_alert = CATEGORY_TO_OBJECT_MAP[category].model_validate(raw_alert)
|
|
491
|
+
except KeyError:
|
|
492
|
+
# This way when we consume an unmapped(new) PA, we get a base object to work with
|
|
493
|
+
self.log.warning(
|
|
494
|
+
f'Unmapped playbook alert category: {category}. '
|
|
495
|
+
+ 'Will initialize {} as a BasePlaybookAlert'.format(
|
|
496
|
+
raw_alert['playbook_alert_id']
|
|
497
|
+
),
|
|
498
|
+
)
|
|
499
|
+
p_alert = PBA_Generic.model_validate(raw_alert)
|
|
500
|
+
except pydantic.ValidationError as validation_error:
|
|
501
|
+
self.log.error(
|
|
502
|
+
'Error validating playbook alert {}'.format(raw_alert['playbook_alert_id'])
|
|
503
|
+
)
|
|
504
|
+
for error in validation_error.errors():
|
|
505
|
+
self.log.error('{} at location: {}'.format(error['msg'], error['loc']))
|
|
506
|
+
raise
|
|
507
|
+
|
|
508
|
+
return p_alert
|
|
509
|
+
|
|
510
|
+
@validate_call
|
|
511
|
+
@connection_exceptions(
|
|
512
|
+
ignore_status_code=[], exception_to_raise=PlaybookAlertRetrieveImageError
|
|
513
|
+
)
|
|
514
|
+
def _do_bulk(
|
|
515
|
+
self, alert_ids: list, category: str, fetch_image: bool, panels: list
|
|
516
|
+
) -> list[PLAYBOOK_ALERT_TYPE]:
|
|
517
|
+
"""Does bulk fetch (used by bulk() after alert IDs have been sorted by category).
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
alert_ids (list): List of alert IDs to fetch
|
|
521
|
+
category (str): Category of alert to fetch
|
|
522
|
+
fetch_image (bool): Whether to fetch images for Domain Abuse alerts
|
|
523
|
+
panels (list): List of panels to fetch
|
|
524
|
+
|
|
525
|
+
Raises:
|
|
526
|
+
ValidationError if any supplied parameter is of incorrect type
|
|
527
|
+
PlaybookAlertRetrieveImageError: if connection error occurs
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
list: Playbook alert ADTs. Unknown alert types return PBA_Generic
|
|
531
|
+
"""
|
|
532
|
+
category = category.lower()
|
|
533
|
+
if category in CATEGORY_ENDPOINTS:
|
|
534
|
+
endpoint = CATEGORY_ENDPOINTS[category]
|
|
535
|
+
else:
|
|
536
|
+
# Workaround to fetch new PAs that have not been officially supported
|
|
537
|
+
self.log.warning(
|
|
538
|
+
f'Unknown playbook alert category: {category}.'
|
|
539
|
+
'Using category as an endpoint for this lookup',
|
|
540
|
+
)
|
|
541
|
+
endpoint = f'{EP_PLAYBOOK_ALERT}/{category}'
|
|
542
|
+
|
|
543
|
+
data = {}
|
|
544
|
+
if panels:
|
|
545
|
+
# We must always fetch status panel for ADT initialization
|
|
546
|
+
panels = panels.append(STATUS_PANEL_NAME) if STATUS_PANEL_NAME not in panels else panels
|
|
547
|
+
data = {'panels': panels}
|
|
548
|
+
|
|
549
|
+
self.log.info(f'Fetching {len(alert_ids)} {category} alerts')
|
|
550
|
+
|
|
551
|
+
results = []
|
|
552
|
+
for batch in batched(alert_ids, BULK_LOOKUP_BATCH_SIZE):
|
|
553
|
+
data['playbook_alert_ids'] = batch
|
|
554
|
+
response = self.rf_client.request('post', url=endpoint, data=data)
|
|
555
|
+
results += response.json()['data']
|
|
556
|
+
|
|
557
|
+
p_alerts = [self._playbook_alert_factory(category, raw_alert) for raw_alert in results]
|
|
558
|
+
|
|
559
|
+
if category == PACategory.DOMAIN_ABUSE.value and fetch_image:
|
|
560
|
+
for alert in p_alerts:
|
|
561
|
+
self.fetch_images(alert)
|
|
562
|
+
|
|
563
|
+
return p_alerts
|
|
564
|
+
|
|
565
|
+
def _process_arg(
|
|
566
|
+
self,
|
|
567
|
+
attr: str,
|
|
568
|
+
value: Union[int, str, list],
|
|
569
|
+
) -> tuple[str, Union[str, list]]:
|
|
570
|
+
"""Return attribute and value normalized based on type of value.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
attr (str): Attribute to verify
|
|
574
|
+
value (Union[str, list]): Value of attribute
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
tuple (str, Union[str, list]): canonicalized query attributes
|
|
578
|
+
"""
|
|
579
|
+
list_or_str_args = ['entity', 'statuses', 'priority', 'category', 'assignee']
|
|
580
|
+
if attr == 'filter_from':
|
|
581
|
+
return 'from', value
|
|
582
|
+
elif attr in ['created_from', 'created_until', 'updated_from', 'updated_until']:
|
|
583
|
+
range_field = attr.split('_')[0] + '_range'
|
|
584
|
+
query_key = 'from' if attr.endswith('from') else 'until'
|
|
585
|
+
if TimeHelpers.is_rel_time_valid(value):
|
|
586
|
+
return range_field, {query_key: TimeHelpers.rel_time_to_date(value)}
|
|
587
|
+
return range_field, {query_key: value}
|
|
588
|
+
elif attr in list_or_str_args and isinstance(value, str):
|
|
589
|
+
return attr, [value]
|
|
590
|
+
elif attr == 'max_results':
|
|
591
|
+
return 'limit', value
|
|
592
|
+
|
|
593
|
+
return attr, value
|