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,19 @@
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
+ IS_READY_INCREMENT = 5
15
+
16
+ ADD_OP = 'add'
17
+ REMOVE_OP = 'remove'
18
+ UNCHANGED_NAME = 'unchanged'
19
+ ERROR_NAME = 'error'
@@ -0,0 +1,435 @@
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
+ import time
16
+ from datetime import datetime
17
+ from functools import total_ordering
18
+ from typing import Optional, Union
19
+
20
+ from pydantic import ConfigDict, Field, validate_call
21
+
22
+ from ..common_models import IdNameType, RFBaseModel
23
+ from ..constants import TIMESTAMP_STR
24
+ from ..endpoints import EP_LIST
25
+ from ..entity_match import EntityMatchMgr, MatchApiError
26
+ from ..helpers import debug_call
27
+ from ..helpers.helpers import connection_exceptions
28
+ from ..rf_client import RFClient
29
+ from .constants import ADD_OP, ERROR_NAME, IS_READY_INCREMENT, REMOVE_OP, UNCHANGED_NAME
30
+ from .errors import ListApiError
31
+ from .models import (
32
+ AddEntityRequestModel,
33
+ ListEntityOperationResponse,
34
+ OwnerOrganisationDetails,
35
+ RemoveEntityRequestModel,
36
+ )
37
+
38
+
39
+ class ListInfoOut(RFBaseModel):
40
+ """Validate data received from ``/{listId}/info`` endpoint."""
41
+
42
+ id_: str = Field(alias='id')
43
+ name: str
44
+ type_: str = Field(alias='type')
45
+ created: datetime
46
+ updated: datetime
47
+ owner_organisation_details: OwnerOrganisationDetails = Field(
48
+ default_factory=OwnerOrganisationDetails
49
+ )
50
+ owner_id: str
51
+ owner_name: str
52
+ organisation_id: str
53
+ organisation_name: str
54
+
55
+
56
+ class ListStatusOut(RFBaseModel):
57
+ """Validate data received from ``/{listId}/status`` endpoint."""
58
+
59
+ size: int
60
+ status: str
61
+
62
+
63
+ @total_ordering
64
+ class ListEntity(RFBaseModel):
65
+ """Validate data received from ``/{listId}/entities`` endpoint."""
66
+
67
+ entity: IdNameType
68
+ context: Optional[dict] = None
69
+ status: str
70
+ added: datetime
71
+
72
+ def __hash__(self):
73
+ return hash(self.entity.id_)
74
+
75
+ def __eq__(self, other: 'ListEntity'):
76
+ return self.entity.id_ == other.entity.id_
77
+
78
+ def __gt__(self, other: 'ListEntity'):
79
+ return (self.entity.name, self.added) > (other.entity.name, other.added)
80
+
81
+ def __str__(self):
82
+ return (
83
+ f'{self.entity.type_}: {self.entity.name}, added {self.added.strftime(TIMESTAMP_STR)}'
84
+ )
85
+
86
+
87
+ class EntityList(RFBaseModel):
88
+ """Validate data received from ``/create`` endpoint."""
89
+
90
+ model_config = ConfigDict(arbitrary_types_allowed=True)
91
+ rf_client: RFClient = Field(exclude=True)
92
+ match_mgr: EntityMatchMgr = Field(exclude=True)
93
+ log: logging.Logger = Field(exclude=True, default=logging.getLogger(__name__))
94
+ id_: str = Field(alias='id')
95
+ name: str
96
+ type_: str = Field(alias='type')
97
+ created: datetime
98
+ updated: datetime
99
+ owner_id: str
100
+ owner_name: str
101
+ organisation_id: Optional[str] = None
102
+ organisation_name: Optional[str] = None
103
+ owner_organisation_details: OwnerOrganisationDetails = Field(
104
+ default_factory=OwnerOrganisationDetails
105
+ )
106
+
107
+ def __hash__(self):
108
+ return hash(self.id_)
109
+
110
+ def __eq__(self, other: 'EntityList'):
111
+ return self.id_ == other.id_
112
+
113
+ def __str__(self):
114
+ """String representation of the list.
115
+
116
+ Returns:
117
+ str: list data with standard info + entities
118
+ """
119
+
120
+ def format_date(date):
121
+ return date.strftime(TIMESTAMP_STR)
122
+
123
+ def format_field(name, value):
124
+ return f"{name}: {value or 'None'}"
125
+
126
+ main_fields = [
127
+ format_field('id', self.id_),
128
+ format_field('name', self.name),
129
+ format_field('type', self.type_),
130
+ format_field('created', format_date(self.created)),
131
+ format_field('last updated', format_date(self.updated)),
132
+ format_field('owner id', self.owner_id),
133
+ format_field('owner name', self.owner_name),
134
+ format_field('organisation id', self.organisation_id),
135
+ format_field('organisation name', self.organisation_name),
136
+ ]
137
+
138
+ org_details = self.owner_organisation_details
139
+ org_fields = [
140
+ format_field('owner id', org_details.owner_id),
141
+ format_field('owner name', org_details.owner_name),
142
+ format_field('enterprise id', org_details.enterprise_id),
143
+ format_field('enterprise name', org_details.enterprise_name),
144
+ ]
145
+
146
+ sub_orgs = org_details.organisations
147
+ if sub_orgs:
148
+ sub_org_str = '\n '.join(
149
+ f'organisation id: {org.organisation_id}\n'
150
+ f' organisation name: {org.organisation_name}'
151
+ for org in sub_orgs
152
+ )
153
+ org_fields.append(f'sub-organisations:\n {sub_org_str}')
154
+ else:
155
+ org_fields.append('sub-organisations: None')
156
+
157
+ return (
158
+ '\n'.join(main_fields) + '\nowner organisation details:\n ' + '\n '.join(org_fields)
159
+ )
160
+
161
+ @debug_call
162
+ @validate_call
163
+ def add(
164
+ self, entity: Union[str, tuple[str, str]], context: dict = None
165
+ ) -> ListEntityOperationResponse:
166
+ """Add entity to list.
167
+
168
+ Endpoint:
169
+ ``list/{id}/entity/add``
170
+
171
+ Args:
172
+ entity (str, tuple): ID or (name, type) tuple of entity to add
173
+ context (dict, optional): context object for entity. Defaults to None
174
+
175
+ Raises:
176
+ ListApiError: if connection error occurs.
177
+
178
+ Returns:
179
+ ListEntityOperationResponse: list/{id}/entity/add response
180
+ """
181
+ return self._list_op(entity, ADD_OP, context=context or {})
182
+
183
+ @debug_call
184
+ @validate_call
185
+ def remove(self, entity: Union[str, tuple[str, str]]) -> ListEntityOperationResponse:
186
+ """Remove entity from list.
187
+
188
+ Endpoint:
189
+ ``list/{id}/entity/remove``
190
+
191
+ Args:
192
+ entity (str, tuple): ID or (name, type) tuple of entity to remove
193
+
194
+ Raises:
195
+ ListApiError: if connection error occurs.
196
+
197
+ Returns:
198
+ ListEntityOperationResponse: list/{id}/entity/remove response
199
+ """
200
+ return self._list_op(entity, REMOVE_OP)
201
+
202
+ @debug_call
203
+ @validate_call
204
+ def bulk_add(self, entities: list[Union[str, tuple[str, str]]]) -> dict:
205
+ """Bulk add entities to list.
206
+
207
+ Adds entities 1 at a time due to List API requirement. Logs progress every 10%.
208
+
209
+ Endpoint:
210
+ ``list/{id}/entity/add``
211
+
212
+ Args:
213
+ entities (list[Union[str, tuple[str, str]]]): list of entity string IDs or
214
+ entity (name, type) tuples to add
215
+
216
+ Raises:
217
+ ValidationError if any supplied parameter is of incorrect type.
218
+ ValueError: if wrong operation is supplied
219
+ ListApiError: if connection error occurs
220
+
221
+ Returns:
222
+ dict: results JSON with added, unchanged, error keys containing lists of entities
223
+ """
224
+ result = self._bulk_op(entities, ADD_OP)
225
+ status = self.status()
226
+ while status.status != 'ready':
227
+ self.log.info(f"Awaiting list 'ready' status, current status '{status.status}'")
228
+ status = self.status()
229
+ time.sleep(IS_READY_INCREMENT)
230
+
231
+ return result
232
+
233
+ @debug_call
234
+ @validate_call
235
+ def bulk_remove(self, entities: list[Union[str, tuple[str, str]]]) -> dict:
236
+ """Bulk remove entities from list.
237
+
238
+ Removes entities 1 at a time due to List API requirement. Logs progress every 10%.
239
+
240
+ Endpoint:
241
+ ``list/{id}/entity/remove``
242
+
243
+ Args:
244
+ entities (list): list of entity string IDs or entity (name, type) tuples to remove
245
+
246
+ Raises:
247
+ ValidationError if any supplied parameter is of incorrect type.
248
+ ValueError: if wrong operation is supplied
249
+ ListApiError: if connection error occurs
250
+
251
+ Returns:
252
+ dict: results JSON with removed, unchanged, error keys containing lists of entities
253
+ """
254
+ result = self._bulk_op(entities, REMOVE_OP)
255
+ status = self.status()
256
+ while status.status != 'ready':
257
+ self.log.info(f"Awaiting list 'ready' status, current status '{status.status}'")
258
+ status = self.status()
259
+ time.sleep(IS_READY_INCREMENT)
260
+
261
+ return result
262
+
263
+ @debug_call
264
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
265
+ def entities(self) -> list[ListEntity]:
266
+ """Get entities for list.
267
+
268
+ Endpoint:
269
+ ``list/{id}/entities``
270
+
271
+ Raises:
272
+ ListApiError: if connection error occurs.
273
+
274
+ Returns:
275
+ list[ListEntity]: list/{id}/entities JSON response
276
+ """
277
+ url = EP_LIST + '/' + self.id_ + '/entities'
278
+ response = self.rf_client.request('get', url)
279
+ return [ListEntity.model_validate(entity) for entity in response.json()]
280
+
281
+ @debug_call
282
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
283
+ def text_entries(self) -> list[str]:
284
+ """Get text entries for list.
285
+
286
+ Endpoint:
287
+ ``list/{id}/textEntries``
288
+
289
+ Raises:
290
+ ListApiError: if connection error occurs.
291
+
292
+ Returns:
293
+ list[str]: list/{id}/textEntries JSON response
294
+ """
295
+ url = EP_LIST + '/' + self.id_ + '/textEntries'
296
+ return self.rf_client.request('get', url).json()
297
+
298
+ @debug_call
299
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
300
+ def status(self) -> ListStatusOut:
301
+ """Get status information about list.
302
+
303
+ Endpoint:
304
+ ``list/{id}/status``
305
+
306
+ Raises:
307
+ ListApiError: if connection error occurs
308
+
309
+ Returns:
310
+ ListStatusOut: list/{id}/status response
311
+ """
312
+ self.log.debug(f"Getting list status for '{self.name}'")
313
+ url = EP_LIST + f'/{self.id_}/status'
314
+ response = self.rf_client.request('get', url)
315
+ validated_status = ListStatusOut.model_validate(response.json())
316
+ self.log.debug(
317
+ f"List '{self.name}' status: {validated_status.status}, entities: {validated_status.size}" # noqa: E501
318
+ )
319
+
320
+ return validated_status
321
+
322
+ @debug_call
323
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
324
+ def info(self) -> ListInfoOut:
325
+ """Get info for list.
326
+
327
+ Endpoint:
328
+ ``list/{id}/info``
329
+
330
+ Raises:
331
+ ListApiError: if connection error occurs
332
+
333
+ Returns:
334
+ ListInfoOut: list/{id}/info response
335
+ """
336
+ self.log.debug(f"Getting list status for '{self.name}'")
337
+ url = EP_LIST + f'/{self.id_}/info'
338
+ response = self.rf_client.request('get', url)
339
+ validated_info = ListInfoOut.model_validate(response.json())
340
+
341
+ return validated_info
342
+
343
+ @debug_call
344
+ def _bulk_op(self, entities: list[Union[str, tuple[str, str]]], operation: str) -> dict:
345
+ """Bulk add or remove entities from list.
346
+
347
+ List API requires that entities are added one at a time. Logs progress every 10%
348
+
349
+ Args:
350
+ entities (list): list of entity string IDs or (name, type) tuples to add
351
+ operation (str): the operation to perform on the list. Can be 'added' or 'removed'.
352
+
353
+ Raises:
354
+ ValueError: if wrong operation is supplied
355
+ ListApiError: if connection error occurs
356
+
357
+ Returns:
358
+ dict: results JSON with added, unchanged, error keys containing lists of entities added
359
+ """
360
+ if operation == ADD_OP:
361
+ op_func = self.add
362
+ op_name = 'added'
363
+ elif operation == REMOVE_OP:
364
+ op_func = self.remove
365
+ op_name = 'removed'
366
+ else:
367
+ raise ValueError(f"Operation must be either '{ADD_OP}' or '{REMOVE_OP}'")
368
+ result = {op_name: [], UNCHANGED_NAME: [], ERROR_NAME: []}
369
+ total = len(entities)
370
+ step = 10
371
+ for idx, entity in enumerate(entities):
372
+ try:
373
+ if isinstance(entity, str):
374
+ entity_id = entity
375
+ else: # entity is tuple
376
+ entity_id = self.match_mgr.resolve_entity_id(entity[0], entity_type=entity[1])
377
+ if not entity_id.is_found:
378
+ result[ERROR_NAME].append({'message': entity_id.content, 'id': entity})
379
+ continue
380
+ else:
381
+ entity_id = entity_id.content.id_
382
+ response = op_func(entity)
383
+ if response.result == op_name:
384
+ result[op_name].append(entity_id)
385
+ elif response.result == UNCHANGED_NAME:
386
+ result[UNCHANGED_NAME].append(entity_id)
387
+ except (TypeError, ListApiError, MatchApiError) as err:
388
+ result[ERROR_NAME].append({'message': str(err), 'id': entity})
389
+ if ((idx + 1) / total) * 100 >= step:
390
+ self.log.info(f'{op_name.capitalize()} {step}% of entities')
391
+ step += 10
392
+
393
+ return result
394
+
395
+ @debug_call
396
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
397
+ def _list_op(
398
+ self, entity: Union[str, tuple[str, str]], op_name: str, context: dict = None
399
+ ) -> ListEntityOperationResponse:
400
+ """Add or remove an entity from list.
401
+
402
+ Args:
403
+ entity (str, tuple): ID or (name, type) tuple of entity to add
404
+ op_name (str): operation to perform. Either 'added' or 'removed'
405
+ context (dict, optional): context object for entity. Defaults to {}
406
+
407
+ Raises:
408
+ ListApiError: if connection error occurs
409
+
410
+ Returns:
411
+ ListEntityOperationResponse: list/{id}/entity/[add|remove] response
412
+ """
413
+ if isinstance(entity, str):
414
+ resolved_entity_id = entity
415
+ else:
416
+ resolved_entity = self.match_mgr.resolve_entity_id(entity[0], entity_type=entity[1])
417
+ if not resolved_entity.is_found:
418
+ return ListEntityOperationResponse(result=resolved_entity.content)
419
+ resolved_entity_id = resolved_entity.content.id_
420
+
421
+ url = EP_LIST + f'/{self.id_}/entity/' + op_name
422
+ request_body = {'entity': {'id': resolved_entity_id}}
423
+
424
+ if context:
425
+ request_body['context'] = context
426
+ if op_name == ADD_OP:
427
+ AddEntityRequestModel.model_validate(request_body)
428
+ else:
429
+ RemoveEntityRequestModel.model_validate(request_body)
430
+ response = self.rf_client.request('post', url, data=request_body)
431
+ validated_response = ListEntityOperationResponse.model_validate(response.json())
432
+ if validated_response.result != UNCHANGED_NAME:
433
+ self.log.debug(f'Entity {entity} {validated_response.result} to list {self.id_}')
434
+
435
+ return validated_response
@@ -0,0 +1,185 @@
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 typing import Union
16
+
17
+ from pydantic import validate_call
18
+
19
+ from ..constants import DEFAULT_LIMIT
20
+ from ..endpoints import EP_CREATE_LIST, EP_LIST, EP_SEARCH_LIST
21
+ from ..entity_match import EntityMatchMgr
22
+ from ..helpers import debug_call
23
+ from ..helpers.helpers import connection_exceptions
24
+ from ..rf_client import RFClient
25
+ from .entity_list import EntityList
26
+ from .errors import ListApiError, ListResolutionError
27
+
28
+
29
+ class EntityListMgr:
30
+ """Manages requests for Recorded Future List API."""
31
+
32
+ def __init__(self, rf_token: str = None) -> None:
33
+ """Initialize EntityListMgr object.
34
+
35
+ Args:
36
+ rf_token (str, optional): Recorded Future API token. Defaults to None
37
+ """
38
+ self.log = logging.getLogger(__name__)
39
+ self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
40
+ self.match_mgr = EntityMatchMgr(rf_token=rf_token) if rf_token else EntityMatchMgr()
41
+
42
+ @debug_call
43
+ @validate_call
44
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
45
+ def fetch(self, list_: Union[str, tuple[str, str]]) -> EntityList:
46
+ """Gets a list by its ID. Use this function for list info response.
47
+
48
+ Endpoint:
49
+ ``list/{list_id}/info``
50
+
51
+ Args:
52
+ list_ (str, tuple): list string ID or tuple of (name, type)
53
+
54
+ Raises:
55
+ ValidationError if any supplied parameter is of incorrect type.
56
+ ListResolutionError: when ``list_`` is a tuple and name matches 0 or multiple entities
57
+ ListApiError: if connection error occurs.
58
+
59
+ Returns:
60
+ RFList: RFList object for list ID
61
+ """
62
+ resolved_id = self._resolve_list_id(list_)
63
+ self.log.info(f'Getting list with ID: {resolved_id}')
64
+ url = EP_LIST + f'/{resolved_id}/info'
65
+ response = self.rf_client.request('get', url)
66
+ list_info_data = response.json()
67
+ self.log.debug("Found list ID '{}'".format(list_info_data['id']))
68
+ self.log.debug(' Type: {}'.format(list_info_data['type']))
69
+ self.log.debug(' Created: {}'.format(list_info_data['created']))
70
+ self.log.debug(' Updated: {}'.format(list_info_data['updated']))
71
+
72
+ return EntityList(rf_client=self.rf_client, match_mgr=self.match_mgr, **list_info_data)
73
+
74
+ @debug_call
75
+ @validate_call
76
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
77
+ def create(self, list_name: str, list_type: str = 'entity') -> EntityList:
78
+ """Create list.
79
+
80
+ Endpoint:
81
+ ``list/create``
82
+
83
+ Args:
84
+ list_name (str): list name to use for new list
85
+ list_type (str, optional): list type. Defaults to "entity"
86
+
87
+ Supported list types are available on the support page for List API:
88
+ https://support.recordedfuture.com/hc/en-us/articles/360058691913-List-API
89
+
90
+ Raises:
91
+ ValidationError if any supplied parameter is of incorrect type.
92
+ ListApiError: if connection error occurs.
93
+
94
+ Returns:
95
+ EntityList: EntityList object for new list
96
+ """
97
+ self.log.debug(f"Creating list '{list_name}'")
98
+ request_body = {'name': list_name, 'type': list_type}
99
+ response = self.rf_client.request('post', EP_CREATE_LIST, data=request_body)
100
+ list_create_data = response.json()
101
+ self.log.debug(f"List '{list_name}' created")
102
+ self.log.debug(' ID: {}'.format(list_create_data['id']))
103
+ self.log.debug(' Type: {}'.format(list_create_data['type']))
104
+
105
+ return EntityList(rf_client=self.rf_client, match_mgr=self.match_mgr, **list_create_data)
106
+
107
+ @debug_call
108
+ @validate_call
109
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ListApiError)
110
+ def search(
111
+ self, list_name: str = None, list_type: str = None, max_results: int = DEFAULT_LIMIT
112
+ ) -> list[EntityList]:
113
+ """Search lists.
114
+
115
+ Endpoint:
116
+ ``list/search``
117
+
118
+ Args:
119
+ list_name (str): list name to search
120
+ list_type (str, optional): list type. Defaults to None, ignored when None
121
+ max_results (int, optional): maximum total number of lists to return
122
+
123
+ Raises:
124
+ ValidationError if any supplied parameter is of incorrect type.
125
+ ListApiError: if list API call fails
126
+
127
+ Returns:
128
+ list: EntityList objects from list/search
129
+ """
130
+ request_body = {}
131
+ request_body['limit'] = max_results
132
+ if list_name:
133
+ request_body['name'] = list_name
134
+ if list_type:
135
+ request_body['type'] = list_type
136
+ self.log.info(f'Searching list API with parameters: {request_body}')
137
+ response = self.rf_client.request('post', EP_SEARCH_LIST, data=request_body)
138
+ list_search_data = response.json()
139
+ self.log.info(
140
+ 'Found {} matching {}'.format(
141
+ len(list_search_data), 'lists' if len(list_search_data) != 1 else 'list'
142
+ )
143
+ )
144
+
145
+ return [
146
+ EntityList(rf_client=self.rf_client, match_mgr=self.match_mgr, **list_)
147
+ for list_ in list_search_data
148
+ ]
149
+
150
+ @debug_call
151
+ def _resolve_list_id(self, list_: Union[str, tuple[str, str]]) -> str:
152
+ """Resolves a list name to a list ID.
153
+
154
+ Args:
155
+ list_ (str, tuple): list string ID or (name, type) tuple
156
+
157
+ Raises:
158
+ ListResolutionError: when a list name matches none or multiple entities
159
+
160
+ Returns:
161
+ str: list ID
162
+ """
163
+ if isinstance(list_, str):
164
+ resolved_id = list_
165
+ else:
166
+ list_name, list_type = list_
167
+ self.log.info(f"Resolving ID for list '{list_name}' with type '{list_type}'")
168
+ matches = self.search(list_name, list_type)
169
+ if len(matches) == 0:
170
+ message = f"No match found for string '{list_name}'"
171
+ raise ListResolutionError(message)
172
+ elif len(matches) > 1:
173
+ exact_count = 0
174
+ resolved_id = None
175
+ for match in matches:
176
+ if match.name == list_name:
177
+ resolved_id = match.id_
178
+ exact_count += 1
179
+ if (not resolved_id) or exact_count > 1:
180
+ message = f"Multiple matches found for string '{list_name}'"
181
+ raise ListResolutionError(message)
182
+ else:
183
+ resolved_id = matches[0].id_
184
+
185
+ return resolved_id
@@ -0,0 +1,26 @@
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 ListApiError(RecordedFutureError):
18
+ """Error raised when an exception occurs performing a list API operation."""
19
+
20
+
21
+ class ListStateError(RecordedFutureError):
22
+ """Error raised when list state is invalid."""
23
+
24
+
25
+ class ListResolutionError(RecordedFutureError):
26
+ """Error raised when list resolution fails."""