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,507 @@
|
|
|
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 itertools import chain
|
|
16
|
+
from typing import Optional, Union
|
|
17
|
+
|
|
18
|
+
from pydantic import Field, validate_call
|
|
19
|
+
|
|
20
|
+
from ..constants import DEFAULT_LIMIT
|
|
21
|
+
from ..endpoints import (
|
|
22
|
+
EP_CLASSIC_ALERTS_HITS,
|
|
23
|
+
EP_CLASSIC_ALERTS_ID,
|
|
24
|
+
EP_CLASSIC_ALERTS_IMAGE,
|
|
25
|
+
EP_CLASSIC_ALERTS_RULES,
|
|
26
|
+
EP_CLASSIC_ALERTS_SEARCH,
|
|
27
|
+
EP_CLASSIC_ALERTS_UPDATE,
|
|
28
|
+
)
|
|
29
|
+
from ..helpers import MultiThreadingHelper, debug_call
|
|
30
|
+
from ..helpers.helpers import connection_exceptions
|
|
31
|
+
from ..rf_client import RFClient
|
|
32
|
+
from .classic_alert import AlertRuleOut, ClassicAlert, ClassicAlertHit
|
|
33
|
+
from .constants import ALERTS_PER_PAGE, ALL_CA_FIELDS, REQUIRED_CA_FIELDS
|
|
34
|
+
from .errors import (
|
|
35
|
+
AlertFetchError,
|
|
36
|
+
AlertImageFetchError,
|
|
37
|
+
AlertSearchError,
|
|
38
|
+
AlertUpdateError,
|
|
39
|
+
NoRulesFoundError,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ClassicAlertMgr:
|
|
44
|
+
"""Alert Manager for Classic Alert (v3) API."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, rf_token: str = None):
|
|
47
|
+
"""Initializes the ClassicAlertMgr object.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
rf_token (str, optional): Recorded Future API token. Defaults to None
|
|
51
|
+
"""
|
|
52
|
+
self.log = logging.getLogger(__name__)
|
|
53
|
+
self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
|
|
54
|
+
|
|
55
|
+
@debug_call
|
|
56
|
+
@validate_call
|
|
57
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=AlertSearchError)
|
|
58
|
+
def search(
|
|
59
|
+
self,
|
|
60
|
+
triggered: Optional[str] = None,
|
|
61
|
+
status: Optional[str] = None,
|
|
62
|
+
rule_id: Union[str, list[str], None] = None,
|
|
63
|
+
freetext: Optional[str] = None,
|
|
64
|
+
tagged_text: Optional[bool] = None,
|
|
65
|
+
order_by: Optional[str] = None,
|
|
66
|
+
direction: Optional[str] = None,
|
|
67
|
+
fields: Optional[list[str]] = REQUIRED_CA_FIELDS,
|
|
68
|
+
max_results: Optional[int] = Field(ge=1, le=1000, default=DEFAULT_LIMIT),
|
|
69
|
+
max_workers: Optional[int] = Field(ge=0, le=50, default=0),
|
|
70
|
+
alerts_per_page: Optional[int] = Field(ge=1, le=1000, default=ALERTS_PER_PAGE),
|
|
71
|
+
) -> list[ClassicAlert]:
|
|
72
|
+
"""Search for triggered alerts.
|
|
73
|
+
|
|
74
|
+
Does pagination requests on batches of ``alerts_per_page`` up to ``max_results``.
|
|
75
|
+
|
|
76
|
+
Note, paginating for a high number of items per page, might lead to timeout errors from the
|
|
77
|
+
API.
|
|
78
|
+
|
|
79
|
+
The fields 'id', 'log', 'title', 'rule' are always retrieved.
|
|
80
|
+
|
|
81
|
+
Endpoint:
|
|
82
|
+
``v3/alerts/``
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
triggered (str): filter on triggered time. Format: -1d or [2017-07-30,2017-07-31]
|
|
86
|
+
status (str): filter on status, such as: 'New', 'Resolved', 'Pending', 'Dismissed'
|
|
87
|
+
rule_id (str, list[str]): filter by a specific Alert Rule ID
|
|
88
|
+
freetext (str): filter by a freetext search
|
|
89
|
+
max_results (int): maximum number of records to return. Default 10, Maximum 1000
|
|
90
|
+
tagged_text (bool): entities in the alert title and message body will be marked up
|
|
91
|
+
order_by (str): sort by a specific field, such as: 'triggered'
|
|
92
|
+
direction (str): sort direction, such as: 'asc' or 'desc'
|
|
93
|
+
fields (List[str]): By default the search will fetch only these fields:
|
|
94
|
+
'id', 'log', 'title', 'rule'. If a user specifies a list of fields, the search
|
|
95
|
+
will use the specified fields + the default fields.
|
|
96
|
+
max_workers (int, Optional): Number of workers to use for concurrent fetches. Only
|
|
97
|
+
applied when more ``rule_id`` are provided.
|
|
98
|
+
alerts_per_page (int, Optional): Number of items to retrieve in for each page.
|
|
99
|
+
Defaults to 50.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
103
|
+
AlertSearchError: if connection error occurs.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List[ClassicAlert]: List of ClassicAlert models
|
|
107
|
+
"""
|
|
108
|
+
rule_id = None if rule_id == [] else rule_id
|
|
109
|
+
params = {
|
|
110
|
+
'triggered': triggered,
|
|
111
|
+
'status': status,
|
|
112
|
+
'freetext': freetext,
|
|
113
|
+
'tagged_text': tagged_text,
|
|
114
|
+
'order_by': order_by,
|
|
115
|
+
'direction': direction,
|
|
116
|
+
'fields': fields,
|
|
117
|
+
'max_results': DEFAULT_LIMIT if max_results is None else max_results,
|
|
118
|
+
'alerts_per_page': alerts_per_page,
|
|
119
|
+
}
|
|
120
|
+
if isinstance(rule_id, list) and max_workers:
|
|
121
|
+
return list(
|
|
122
|
+
chain.from_iterable(
|
|
123
|
+
MultiThreadingHelper.multithread_it(
|
|
124
|
+
max_workers, self._search, iterator=rule_id, **params
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
elif isinstance(rule_id, list):
|
|
130
|
+
return list(chain.from_iterable(self._search(rule, **params) for rule in rule_id))
|
|
131
|
+
|
|
132
|
+
elif isinstance(rule_id, str):
|
|
133
|
+
return self._search(rule_id, **params)
|
|
134
|
+
else:
|
|
135
|
+
return self._search(**params)
|
|
136
|
+
|
|
137
|
+
@debug_call
|
|
138
|
+
@validate_call
|
|
139
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=AlertFetchError)
|
|
140
|
+
def fetch(
|
|
141
|
+
self,
|
|
142
|
+
id_: str,
|
|
143
|
+
fields: Optional[list[str]] = ALL_CA_FIELDS,
|
|
144
|
+
tagged_text: Optional[bool] = None,
|
|
145
|
+
) -> ClassicAlert:
|
|
146
|
+
"""Fetch a specific alert.
|
|
147
|
+
|
|
148
|
+
The alert can be saved on file as shown below:
|
|
149
|
+
|
|
150
|
+
.. code-block:: python
|
|
151
|
+
:linenos:
|
|
152
|
+
|
|
153
|
+
from pathlib import Path
|
|
154
|
+
from json import dumps
|
|
155
|
+
from psengine.classic_alerts import ClassicAlertMgr
|
|
156
|
+
|
|
157
|
+
mgr = ClassicAlertMgr()
|
|
158
|
+
alert = mgr.fetch('zVEe6k')
|
|
159
|
+
OUTPUT_DIR = Path('your' / 'path')
|
|
160
|
+
OUTPUT_DIR.mkdir(exists_ok=True)
|
|
161
|
+
(OUTPUT_DIR / f'{alert.id_}.json').write_text(dumps(alert.json(), indent=2))
|
|
162
|
+
|
|
163
|
+
Endpoint:
|
|
164
|
+
``v3/alerts/{id_}``
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
id_ (str): alertID that should be fetched
|
|
168
|
+
fields (List[str]): by default, all fields are returned; but if only a subset of
|
|
169
|
+
the alert details needed, this parameter can be used to limit which sections
|
|
170
|
+
of the alert details are returned in the API response. If a user specifies a list
|
|
171
|
+
of fields, the fetch will use the specified fields + the default fields required
|
|
172
|
+
by the ADT ['id', 'log', 'title', 'rule'].
|
|
173
|
+
tagged_text (bool): entities in the alert title and message body will be marked up
|
|
174
|
+
with Recorded Future entity IDs
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
178
|
+
AlertFetchError: if a fetch of the alert via API function fails.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ClassicAlert: ClassicAlert model
|
|
182
|
+
"""
|
|
183
|
+
params = {}
|
|
184
|
+
params['fields'] = set((fields or []) + REQUIRED_CA_FIELDS)
|
|
185
|
+
params['fields'] = ','.join(params['fields'])
|
|
186
|
+
|
|
187
|
+
if tagged_text:
|
|
188
|
+
params['taggedText'] = tagged_text
|
|
189
|
+
|
|
190
|
+
self.log.info(f'Fetching alert: {id_}')
|
|
191
|
+
response = self.rf_client.request(
|
|
192
|
+
'get', url=EP_CLASSIC_ALERTS_ID.format(id_), params=params
|
|
193
|
+
).json()
|
|
194
|
+
return ClassicAlert.model_validate(response.get('data'))
|
|
195
|
+
|
|
196
|
+
@debug_call
|
|
197
|
+
@validate_call
|
|
198
|
+
def fetch_bulk(
|
|
199
|
+
self,
|
|
200
|
+
ids: list[str],
|
|
201
|
+
fields: Optional[list[str]] = ALL_CA_FIELDS,
|
|
202
|
+
tagged_text: Optional[bool] = None,
|
|
203
|
+
max_workers: Optional[int] = 0,
|
|
204
|
+
) -> list[ClassicAlert]:
|
|
205
|
+
"""Fetch multiple alerts.
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
Each alert can be saved on file as shown below:
|
|
209
|
+
|
|
210
|
+
.. code-block:: python
|
|
211
|
+
:linenos:
|
|
212
|
+
|
|
213
|
+
from json import dumps
|
|
214
|
+
from pathlib import Path
|
|
215
|
+
from ..helpers import dump_models
|
|
216
|
+
from psengine.classic_alerts import ClassicAlertMgr
|
|
217
|
+
|
|
218
|
+
mgr = ClassicAlertMgr()
|
|
219
|
+
alerts = mgr.fetch_bulk(ids=['zVEe6k', 'zVHPXX'])
|
|
220
|
+
OUTPUT_DIR = Path('your/path')
|
|
221
|
+
OUTPUT_DIR.mkdir(exists_ok=True)
|
|
222
|
+
for i, alert in enumerate(alerts):
|
|
223
|
+
(OUTPUT_DIR / f'filename_{i}.json').write_text(dumps(alert.json(), indent=2))
|
|
224
|
+
|
|
225
|
+
Alternatively all alerts can be saved on a single file:
|
|
226
|
+
|
|
227
|
+
.. code-block:: python
|
|
228
|
+
:linenos:
|
|
229
|
+
|
|
230
|
+
from json import dump
|
|
231
|
+
from pathlib import Path
|
|
232
|
+
from psengine.classic_alerts import ClassicAlertMgr
|
|
233
|
+
from ..helpers import dump_models
|
|
234
|
+
|
|
235
|
+
mgr = ClassicAlertMgr()
|
|
236
|
+
OUTPUT_FILE = Path('your/path/file')
|
|
237
|
+
alerts = mgr.fetch_bulk(ids=['zVEe6k', 'zVHPXX'])
|
|
238
|
+
with OUTPUT_FILE.open('w') as f:
|
|
239
|
+
dump([alert.json() for alert in alerts], f, indent=2)
|
|
240
|
+
|
|
241
|
+
Endpoint:
|
|
242
|
+
``v3/alerts/{id_}``
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
ids (List[str]): alert IDs that should be fetched
|
|
246
|
+
fields (List[str]): by default, all fields are returned; but if only a subset of
|
|
247
|
+
the alert details needed, this parameter can be used to limit which sections
|
|
248
|
+
of the alert details are returned in the API response
|
|
249
|
+
tagged_text (bool): entities in the alert title and message body will be marked up
|
|
250
|
+
with Recorded Future entity IDs
|
|
251
|
+
max_workers (int, optional): number of workers to multithread requests.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
255
|
+
AlertFetchError: if a fetch of the alert via API function fails.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List[ClassicAlert]: List of ClassicAlert model
|
|
259
|
+
"""
|
|
260
|
+
self.log.info(f'Fetching alerts: {ids}')
|
|
261
|
+
results = []
|
|
262
|
+
if max_workers:
|
|
263
|
+
results = MultiThreadingHelper.multithread_it(
|
|
264
|
+
max_workers,
|
|
265
|
+
self.fetch,
|
|
266
|
+
iterator=ids,
|
|
267
|
+
fields=fields,
|
|
268
|
+
tagged_text=tagged_text,
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
results = [self.fetch(id_, fields, tagged_text) for id_ in ids]
|
|
272
|
+
|
|
273
|
+
return results
|
|
274
|
+
|
|
275
|
+
@debug_call
|
|
276
|
+
@validate_call
|
|
277
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=AlertFetchError)
|
|
278
|
+
def fetch_hits(
|
|
279
|
+
self, ids: Union[str, list[str]], tagged_text: Optional[bool] = None
|
|
280
|
+
) -> list[ClassicAlertHit]:
|
|
281
|
+
"""Fetch only a list of all the data that caused the alert to trigger (hits).
|
|
282
|
+
|
|
283
|
+
Endpoint:
|
|
284
|
+
``v3/alerts/hits``
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
ids (Union[str, List[str]]): one or more alert ids to fetch
|
|
288
|
+
tagged_text (bool): entities in the alert title and message body will be marked up
|
|
289
|
+
with Recorded Future entity IDs
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
293
|
+
AlertFetchError: if a fetch of the alert hit via API function fails.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List[ClassicAlertHit]: List of ClassicAlertHit models
|
|
297
|
+
"""
|
|
298
|
+
data = {}
|
|
299
|
+
|
|
300
|
+
if isinstance(ids, list):
|
|
301
|
+
ids = ','.join(ids)
|
|
302
|
+
|
|
303
|
+
data['ids'] = ids
|
|
304
|
+
|
|
305
|
+
if tagged_text:
|
|
306
|
+
data['taggedText'] = tagged_text
|
|
307
|
+
|
|
308
|
+
self.log.info(f'Fetching hits for alerts: {ids}')
|
|
309
|
+
response = self.rf_client.request('get', url=EP_CLASSIC_ALERTS_HITS, params=data).json()
|
|
310
|
+
return [ClassicAlertHit.model_validate(hit) for hit in response.get('data', [])]
|
|
311
|
+
|
|
312
|
+
@debug_call
|
|
313
|
+
@validate_call
|
|
314
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=AlertImageFetchError)
|
|
315
|
+
def fetch_image(self, id_: str) -> bytes:
|
|
316
|
+
"""Fetch an image.
|
|
317
|
+
|
|
318
|
+
Endpoint:
|
|
319
|
+
``v3/alerts/image``
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
id_ (str): image id to fetch, for example: img:d4620c6a-c789-48aa-b652-b47e0d06d91a
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
326
|
+
AlertImageFetchError: if a fetch of the alert image via API function fails.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
bytes: image content
|
|
330
|
+
"""
|
|
331
|
+
self.log.info(f'Fetching image: {id_}')
|
|
332
|
+
response = self.rf_client.request('get', url=EP_CLASSIC_ALERTS_IMAGE, params={'id': id_})
|
|
333
|
+
return response.content
|
|
334
|
+
|
|
335
|
+
@debug_call
|
|
336
|
+
@validate_call
|
|
337
|
+
def fetch_all_images(self, alert: ClassicAlert) -> None:
|
|
338
|
+
"""Fetch all images from an alert and stores them in the alert object under ``@images``.
|
|
339
|
+
|
|
340
|
+
Endpoint:
|
|
341
|
+
``v3/alerts/image``
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
alert (ClassicAlert): alert to fetch images from
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
348
|
+
"""
|
|
349
|
+
for hit in alert.hits:
|
|
350
|
+
for entity in hit.entities:
|
|
351
|
+
if entity.type_ == 'Image':
|
|
352
|
+
alert.store_image(entity.id_, self.fetch_image(entity.id_))
|
|
353
|
+
|
|
354
|
+
@debug_call
|
|
355
|
+
@validate_call
|
|
356
|
+
def fetch_rules(
|
|
357
|
+
self,
|
|
358
|
+
freetext: Union[str, list[str], None] = None,
|
|
359
|
+
max_results: int = Field(default=DEFAULT_LIMIT, ge=1, le=1000),
|
|
360
|
+
) -> list[AlertRuleOut]:
|
|
361
|
+
"""Search for alerting rules.
|
|
362
|
+
|
|
363
|
+
Endpoint:
|
|
364
|
+
``v2/alert/rules``
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
freetext (Union[str, list[str]], optional): filter by a freetext search, can be a
|
|
368
|
+
a freetext string or a list of strings. Default None (will return all rules)
|
|
369
|
+
max_results (int): maximum number of rules to return. Default 10, maximum 1000
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
ValidationError if any supplied parameter is of incorrect type or value
|
|
373
|
+
NoRulesFoundError: if rule has not been found.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
List[AlertRule]: List of AlertRule models
|
|
377
|
+
"""
|
|
378
|
+
if not freetext:
|
|
379
|
+
return self._fetch_rules(max_results=max_results)
|
|
380
|
+
|
|
381
|
+
if isinstance(freetext, str):
|
|
382
|
+
return self._fetch_rules(freetext, max_results)
|
|
383
|
+
|
|
384
|
+
rules = []
|
|
385
|
+
for text in freetext:
|
|
386
|
+
rules += self._fetch_rules(text, max_results - len(rules))
|
|
387
|
+
return rules
|
|
388
|
+
|
|
389
|
+
@debug_call
|
|
390
|
+
@validate_call
|
|
391
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=AlertUpdateError)
|
|
392
|
+
def update(self, updates: list[dict]):
|
|
393
|
+
"""Updates one or more alerts. It's possible to update assignee, ``statusInPortal`` and a
|
|
394
|
+
note tied to the triggered alert.
|
|
395
|
+
|
|
396
|
+
Example:
|
|
397
|
+
updates argument:
|
|
398
|
+
|
|
399
|
+
.. code-block:: python
|
|
400
|
+
|
|
401
|
+
[
|
|
402
|
+
{
|
|
403
|
+
"id": "string",
|
|
404
|
+
"assignee": "string",
|
|
405
|
+
"status": "unassigned",
|
|
406
|
+
"note": "string",
|
|
407
|
+
"statusInPortal": "New"
|
|
408
|
+
}
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
Endpoint:
|
|
412
|
+
``v2/alert/update``
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
updates (List[dict]): list of updates to perform
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
JSON response
|
|
419
|
+
"""
|
|
420
|
+
self.log.info(f'Updating alerts: {updates}')
|
|
421
|
+
response = self.rf_client.request('post', url=EP_CLASSIC_ALERTS_UPDATE, data=updates).json()
|
|
422
|
+
return response
|
|
423
|
+
|
|
424
|
+
@debug_call
|
|
425
|
+
@validate_call
|
|
426
|
+
def update_status(self, ids: Union[str, list[str]], status: str):
|
|
427
|
+
"""Update the status of one or several alerts.
|
|
428
|
+
|
|
429
|
+
Endpoint:
|
|
430
|
+
``v2/alert/update``
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
ids (Union[str, List[str]]): one or more alert ids
|
|
434
|
+
status (str): status to update to
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
438
|
+
AlertUpdateError: if connection error occurs.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
JSON response
|
|
442
|
+
"""
|
|
443
|
+
ids = ids if isinstance(ids, list) else ids.split(',')
|
|
444
|
+
payload = [{'id': alert_id, 'statusInPortal': status} for alert_id in ids]
|
|
445
|
+
return self.update(payload)
|
|
446
|
+
|
|
447
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=NoRulesFoundError)
|
|
448
|
+
def _fetch_rules(
|
|
449
|
+
self,
|
|
450
|
+
freetext: Optional[str] = None,
|
|
451
|
+
max_results: Optional[int] = Field(default=DEFAULT_LIMIT, ge=1, le=1000),
|
|
452
|
+
) -> list[AlertRuleOut]:
|
|
453
|
+
data = {}
|
|
454
|
+
|
|
455
|
+
if freetext:
|
|
456
|
+
data['freetext'] = freetext
|
|
457
|
+
|
|
458
|
+
data['limit'] = max_results or DEFAULT_LIMIT
|
|
459
|
+
|
|
460
|
+
self.log.info(f'Fetching alert rules. Params: {data}')
|
|
461
|
+
response = self.rf_client.request('get', url=EP_CLASSIC_ALERTS_RULES, params=data).json()
|
|
462
|
+
|
|
463
|
+
return [
|
|
464
|
+
AlertRuleOut.model_validate(rule)
|
|
465
|
+
for rule in response.get('data', {}).get('results', [])
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
def _search(
|
|
469
|
+
self,
|
|
470
|
+
rule_id: Optional[str] = None,
|
|
471
|
+
*,
|
|
472
|
+
triggered,
|
|
473
|
+
status,
|
|
474
|
+
freetext,
|
|
475
|
+
tagged_text,
|
|
476
|
+
order_by,
|
|
477
|
+
direction,
|
|
478
|
+
fields,
|
|
479
|
+
max_results,
|
|
480
|
+
alerts_per_page,
|
|
481
|
+
**kwargs, # noqa: ARG002
|
|
482
|
+
) -> list[ClassicAlert]:
|
|
483
|
+
"""rule_id is not a list anymore. We always receive a string. Kwargs is discarded."""
|
|
484
|
+
params = {
|
|
485
|
+
'triggered': triggered,
|
|
486
|
+
'statusInPortal': status,
|
|
487
|
+
'alertRule': rule_id,
|
|
488
|
+
'freetext': freetext,
|
|
489
|
+
'taggedText': tagged_text,
|
|
490
|
+
'orderBy': order_by,
|
|
491
|
+
'direction': direction,
|
|
492
|
+
'fields': ','.join(set(fields + REQUIRED_CA_FIELDS)),
|
|
493
|
+
'limit': min(max_results, alerts_per_page),
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
params = {k: v for k, v in params.items() if v}
|
|
497
|
+
|
|
498
|
+
self.log.info(f'Searching for classic alerts. Params: {params}')
|
|
499
|
+
search_results = self.rf_client.request_paged(
|
|
500
|
+
method='get',
|
|
501
|
+
url=EP_CLASSIC_ALERTS_SEARCH,
|
|
502
|
+
params=params,
|
|
503
|
+
offset_key='from',
|
|
504
|
+
results_path='data',
|
|
505
|
+
max_results=max_results,
|
|
506
|
+
)
|
|
507
|
+
return [ClassicAlert.model_validate(alert) for alert in search_results]
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
REQUIRED_CA_FIELDS = ['id', 'log', 'title', 'rule']
|
|
15
|
+
|
|
16
|
+
ALL_CA_FIELDS = [
|
|
17
|
+
'ai_insights',
|
|
18
|
+
'enriched_entities',
|
|
19
|
+
'hits',
|
|
20
|
+
'owner_organisation_details',
|
|
21
|
+
'review',
|
|
22
|
+
'triggered_by',
|
|
23
|
+
'type',
|
|
24
|
+
'url',
|
|
25
|
+
] + REQUIRED_CA_FIELDS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DEFAULT_CA_OUTPUT_DIR = 'alerts'
|
|
29
|
+
ALERTS_PER_PAGE = 50
|
|
30
|
+
|
|
31
|
+
MARKDOWN_ENTITY_TYPES_TO_DEFANG = ['InternetDomainName', 'URL', 'IpAddress']
|
|
@@ -0,0 +1,38 @@
|
|
|
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 NoRulesFoundError(RecordedFutureError):
|
|
18
|
+
"""Raised when there were no rules returned or an exception occurred during the request."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AlertFetchError(RecordedFutureError):
|
|
22
|
+
"""Raised when there were no alerts returned or an exception occurred during the request."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AlertImageFetchError(RecordedFutureError):
|
|
26
|
+
"""Raised when there were no images returned or an exception occurred during the request."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AlertUpdateError(RecordedFutureError):
|
|
30
|
+
"""Error raised when the update request encounters an exception."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AlertSearchError(RecordedFutureError):
|
|
34
|
+
"""Error raised when there was an error during the search request."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AlertMarkdownError(RecordedFutureError):
|
|
38
|
+
"""Error raised when there was an error during markdown conversion."""
|
|
@@ -0,0 +1,87 @@
|
|
|
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 Union
|
|
17
|
+
|
|
18
|
+
from pydantic import validate_call
|
|
19
|
+
|
|
20
|
+
from ..errors import WriteFileError
|
|
21
|
+
from ..helpers import OSHelpers
|
|
22
|
+
from .classic_alert import ClassicAlert
|
|
23
|
+
from .constants import DEFAULT_CA_OUTPUT_DIR
|
|
24
|
+
|
|
25
|
+
LOG = logging.getLogger('psengine.classic_alerts.helpers')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@validate_call
|
|
29
|
+
def save_image(
|
|
30
|
+
image_bytes: bytes, file_name: str, output_directory: Union[str, Path] = DEFAULT_CA_OUTPUT_DIR
|
|
31
|
+
) -> Path:
|
|
32
|
+
"""Save an image to disk as a png file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
image_bytes (bytes): The image to save.
|
|
36
|
+
output_directory (str): The directory to save the image to.
|
|
37
|
+
file_name (Union[str, Path]): The file to save the image as. Without a file extension.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
41
|
+
WriteFileError: if the write operation fails.
|
|
42
|
+
WriteFileError: if the path provided is not a directory or it cannot be created.
|
|
43
|
+
WriteFileError: if the write operations fail.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Path: The path to the file written
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
LOG.info(f"Saving image '{file_name}' to disk")
|
|
50
|
+
dir_path = OSHelpers.mkdir(output_directory)
|
|
51
|
+
image_filepath = Path(dir_path) / f'{file_name}.png'
|
|
52
|
+
with Path.open(image_filepath, 'wb') as file:
|
|
53
|
+
file.write(image_bytes)
|
|
54
|
+
except OSError as err:
|
|
55
|
+
raise WriteFileError(
|
|
56
|
+
f'Failed to save classic alert image to disk. Cause: {err.args}',
|
|
57
|
+
) from err
|
|
58
|
+
|
|
59
|
+
return image_filepath
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@validate_call
|
|
63
|
+
def save_images(
|
|
64
|
+
alert: ClassicAlert, output_directory: Union[str, Path] = DEFAULT_CA_OUTPUT_DIR
|
|
65
|
+
) -> dict:
|
|
66
|
+
"""Save all images from a ``ClassicAlert`` to disk.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
alert (ClassicAlert): The alert to save images from.
|
|
70
|
+
output_directory (Union[str, Path], optional): The directory to save the image to.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValidationError if any supplied parameter is of incorrect type.
|
|
74
|
+
WriteFileError: if the write operation fails.
|
|
75
|
+
WriteFileError: if the path provided is not a directory or it cannot be created.
|
|
76
|
+
WriteFileError: if the write operations fail.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict: A dictionary of image file paths with the image ID as the key.
|
|
80
|
+
"""
|
|
81
|
+
image_file_paths = {}
|
|
82
|
+
for id_, bytes_ in alert.images.items():
|
|
83
|
+
image_file_paths[id_] = save_image(
|
|
84
|
+
image_bytes=bytes_, output_directory=output_directory, file_name=id_
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return image_file_paths
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
|