psengine 2.0.4__py3-none-any.whl

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