psengine 2.0.6__tar.gz → 2.1.0__tar.gz
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-2.0.6 → psengine-2.1.0}/PKG-INFO +1 -1
- {psengine-2.0.6 → psengine-2.1.0}/psengine/_version.py +1 -1
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/markdown/markdown.py +2 -1
- {psengine-2.0.6 → psengine-2.1.0}/psengine/common_models.py +11 -2
- {psengine-2.0.6 → psengine-2.1.0}/psengine/endpoints.py +13 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/entity_list.py +2 -1
- {psengine-2.0.6 → psengine-2.1.0}/psengine/helpers/__init__.py +1 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/helpers/helpers.py +39 -3
- psengine-2.1.0/psengine/identity/__init__.py +31 -0
- psengine-2.1.0/psengine/identity/constants.py +15 -0
- psengine-2.1.0/psengine/identity/errors.py +34 -0
- psengine-2.1.0/psengine/identity/identity.py +396 -0
- psengine-2.1.0/psengine/identity/identity_mgr.py +895 -0
- psengine-2.1.0/psengine/identity/models/common_models.py +227 -0
- psengine-2.1.0/psengine/identity/models/detections.py +54 -0
- psengine-2.1.0/psengine/identity/models/incident_report.py +38 -0
- psengine-2.1.0/psengine/identity/models/lookup.py +55 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/__init__.py +3 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/constants.py +28 -5
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/errors.py +4 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/helpers.py +5 -3
- psengine-2.1.0/psengine/playbook_alerts/markdown/__init__.py +12 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown.py +5 -23
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +1 -1
- psengine-2.1.0/psengine/playbook_alerts/markdown/markdown_malware_report.py +66 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_malware_report.py +6 -6
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/pa_category.py +0 -1
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/playbook_alert_mgr.py +190 -195
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/playbook_alerts.py +13 -19
- {psengine-2.0.6 → psengine-2.1.0}/psengine/rf_client.py +37 -16
- {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/risklist_mgr.py +13 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/PKG-INFO +1 -1
- {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/SOURCES.txt +11 -1
- {psengine-2.0.6 → psengine-2.1.0}/pyproject.toml +1 -5
- psengine-2.0.6/psengine.egg-info/entry_points.txt +0 -2
- {psengine-2.0.6 → psengine-2.1.0}/LICENSE +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/README.rst +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/_sdk_id.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/helpers.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/markdown.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/note.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/note_mgr.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/base_http_client.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/classic_alert.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/classic_alert_mgr.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/helpers.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/collective_insights.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/insight.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/config/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/config/config.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/config/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/detection_mgr.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/detection_rule.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/helpers.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/lookup.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/lookup_mgr.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/lookup.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/soar.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/soar.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/soar_mgr.py +3 -3
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/entity_match.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/entity_match_mgr.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/errors.py +0 -0
- {psengine-2.0.6/psengine/playbook_alerts/markdown → psengine-2.1.0/psengine/identity/models}/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/rf_logger.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/markdown/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/markdown/markdown.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/markdown/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/mappings.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/common_models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/models.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/__init__.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/base_stix_entity.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/complex_entity.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/constants.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/enriched_indicator.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/errors.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/helpers.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/rf_bundle.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/simple_entity.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/util.py +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/dependency_links.txt +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/requires.txt +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/top_level.txt +0 -0
- {psengine-2.0.6 → psengine-2.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: psengine
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: psengine is a simple, yet elegant, library for rapid development of integrations with Recorded Future.
|
|
5
5
|
Author-email: Moise Medici <moise.medici@recordedfuture.com>, Patrick Kinsella <patrick.kinsella@recordedfuture.com>, Ernest Bartosevic <ernest.bartosevic@recordedfuture.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -94,7 +94,8 @@ def _process_hit_fragment(
|
|
|
94
94
|
content.append(f'{blockquote(fragment)}\n')
|
|
95
95
|
else:
|
|
96
96
|
content.append(
|
|
97
|
-
|
|
97
|
+
'_Reference text is missing, check the Recorded Future '
|
|
98
|
+
f'{link("Portal", str(classic_alert.url.portal))} for more information._\n'
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
if include_triggered_by:
|
|
@@ -15,7 +15,7 @@ import os
|
|
|
15
15
|
from enum import Enum
|
|
16
16
|
from typing import Optional
|
|
17
17
|
|
|
18
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, Secret
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class RFBaseModel(BaseModel):
|
|
@@ -50,7 +50,6 @@ class RFBaseModel(BaseModel):
|
|
|
50
50
|
else kwargs['exclude_unset']
|
|
51
51
|
)
|
|
52
52
|
kwargs['exclude_unset'] = exclude_unset
|
|
53
|
-
|
|
54
53
|
return self.model_dump(mode='json', by_alias=by_alias, exclude_none=exclude_none, **kwargs)
|
|
55
54
|
|
|
56
55
|
|
|
@@ -87,3 +86,13 @@ class DetectionRuleType(Enum):
|
|
|
87
86
|
sigma = 'sigma'
|
|
88
87
|
yara = 'yara'
|
|
89
88
|
snort = 'snort'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ClearTextPassword(Secret[str]):
|
|
92
|
+
"""Model to hide passwords while logging.
|
|
93
|
+
|
|
94
|
+
To view the clear text password do ``value.get_secret_value()``
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def _display(self) -> str:
|
|
98
|
+
return self.get_secret_value()[:4] + '********'
|
|
@@ -98,3 +98,16 @@ EP_ANALYST_NOTE_PREVIEW = EP_ANALYST_NOTE + 'preview'
|
|
|
98
98
|
EP_ANALYST_NOTE_PUBLISH = EP_ANALYST_NOTE + 'publish'
|
|
99
99
|
EP_ANALYST_NOTE_DELETE = EP_ANALYST_NOTE + 'delete/{}'
|
|
100
100
|
EP_ANALYST_NOTE_ATTACHMENT = EP_ANALYST_NOTE + 'attachment/{}'
|
|
101
|
+
|
|
102
|
+
###############################################################################
|
|
103
|
+
# Identity API Endpoints
|
|
104
|
+
###############################################################################
|
|
105
|
+
EP_IDENTITY = BASE_URL + '/identity/'
|
|
106
|
+
EP_IDENTITY_DETECTIONS = EP_IDENTITY + 'detections'
|
|
107
|
+
EP_IDENTITY_INCIDENT_REPORT = EP_IDENTITY + 'incident/report'
|
|
108
|
+
EP_IDENTITY_HOSTNAME_LOOKUP = EP_IDENTITY + 'hostname/lookup'
|
|
109
|
+
EP_IDENTITY_PASSWORD_LOOKUP = EP_IDENTITY + 'password/lookup'
|
|
110
|
+
EP_IDENTITY_IP_LOOKUP = EP_IDENTITY + 'ip/lookup'
|
|
111
|
+
EP_IDENTITY_CREDENTIALS_SEARCH = EP_IDENTITY + 'credentials/search'
|
|
112
|
+
EP_IDENTITY_CREDENTIALS_LOOKUP = EP_IDENTITY + 'credentials/lookup'
|
|
113
|
+
EP_IDENTITY_DUMP_SEARCH = EP_IDENTITY + 'metadata/dump/search'
|
|
@@ -314,7 +314,8 @@ class EntityList(RFBaseModel):
|
|
|
314
314
|
response = self.rf_client.request('get', url)
|
|
315
315
|
validated_status = ListStatusOut.model_validate(response.json())
|
|
316
316
|
self.log.debug(
|
|
317
|
-
f"List '{self.name}' status: {validated_status.status},
|
|
317
|
+
f"List '{self.name}' status: {validated_status.status}, "
|
|
318
|
+
f'entities: {validated_status.size}'
|
|
318
319
|
)
|
|
319
320
|
|
|
320
321
|
return validated_status
|
|
@@ -156,7 +156,7 @@ class TimeHelpers:
|
|
|
156
156
|
"""Helpers for time related functions."""
|
|
157
157
|
|
|
158
158
|
@staticmethod
|
|
159
|
-
def rel_time_to_date(relative_time) -> str:
|
|
159
|
+
def rel_time_to_date(relative_time: str) -> str:
|
|
160
160
|
"""Convert a relative time to a date. Minutes not supported.
|
|
161
161
|
|
|
162
162
|
Example:
|
|
@@ -193,7 +193,7 @@ class TimeHelpers:
|
|
|
193
193
|
return subtracted
|
|
194
194
|
|
|
195
195
|
@staticmethod
|
|
196
|
-
def is_rel_time_valid(rel_time) -> bool:
|
|
196
|
+
def is_rel_time_valid(rel_time: str) -> bool:
|
|
197
197
|
"""Helper function to determine if relative time expression is valid.
|
|
198
198
|
|
|
199
199
|
Args:
|
|
@@ -202,7 +202,7 @@ class TimeHelpers:
|
|
|
202
202
|
Returns:
|
|
203
203
|
bool: True if valid, False otherwise
|
|
204
204
|
"""
|
|
205
|
-
if rel_time is None:
|
|
205
|
+
if rel_time is None or not isinstance(rel_time, str):
|
|
206
206
|
return False
|
|
207
207
|
|
|
208
208
|
return bool(re.match(VALID_TIME_REGEX, rel_time))
|
|
@@ -468,3 +468,39 @@ class MultiThreadingHelper:
|
|
|
468
468
|
futures = [pool.submit(func, element, **kwargs) for element in iterator]
|
|
469
469
|
|
|
470
470
|
return [f.result() for f in futures]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class Validators:
|
|
474
|
+
"""Common validators for pydantic models."""
|
|
475
|
+
|
|
476
|
+
@staticmethod
|
|
477
|
+
def convert_str_to_list(value: Union[str, list]) -> list:
|
|
478
|
+
"""Convert value from str to list and remove None values."""
|
|
479
|
+
value = value if isinstance(value, list) else [value]
|
|
480
|
+
return [v for v in value if v is not None]
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def convert_relative_time(input_time: str) -> str:
|
|
484
|
+
"""Covert relative time to datetime string if possible."""
|
|
485
|
+
return (
|
|
486
|
+
TimeHelpers.rel_time_to_date(input_time)
|
|
487
|
+
if TimeHelpers.is_rel_time_valid(input_time)
|
|
488
|
+
else input_time
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
@staticmethod
|
|
492
|
+
def check_uhash_prefix(value: Union[str, list]) -> Union[str, list]:
|
|
493
|
+
"""Validates that the field contains fields starting with uhash and add it otherwise."""
|
|
494
|
+
uhash = 'uhash:'
|
|
495
|
+
if isinstance(value, str):
|
|
496
|
+
return f'{uhash}{value}' if not value.startswith(uhash) else value
|
|
497
|
+
|
|
498
|
+
if isinstance(value, list):
|
|
499
|
+
new_values = []
|
|
500
|
+
for h in value:
|
|
501
|
+
if h:
|
|
502
|
+
complete_value = f'{uhash}{h}' if not h.startswith(uhash) else h
|
|
503
|
+
new_values.append(complete_value)
|
|
504
|
+
return new_values
|
|
505
|
+
|
|
506
|
+
return value
|
|
@@ -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
|
+
from .errors import IdentityError
|
|
15
|
+
from .identity import (
|
|
16
|
+
Credential,
|
|
17
|
+
CredentialSearch,
|
|
18
|
+
CredentialsLookupIn,
|
|
19
|
+
CredentialsSearchIn,
|
|
20
|
+
Detection,
|
|
21
|
+
Detections,
|
|
22
|
+
DetectionsIn,
|
|
23
|
+
DumpSearchIn,
|
|
24
|
+
HostnameLookupIn,
|
|
25
|
+
IncidentReportIn,
|
|
26
|
+
IncidentReportOut,
|
|
27
|
+
IPLookupIn,
|
|
28
|
+
LeakedIdentity,
|
|
29
|
+
PasswordLookup,
|
|
30
|
+
)
|
|
31
|
+
from .identity_mgr import IdentityMgr
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
DETECTIONS_PER_PAGE = 20
|
|
15
|
+
MAXIMUM_IDENTITIES = 500
|
|
@@ -0,0 +1,34 @@
|
|
|
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 IdentityError(RecordedFutureError):
|
|
18
|
+
"""Error raised when there is an error with the Identity API."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DetectionsFetchError(IdentityError):
|
|
22
|
+
"""Error raised when there is an issue searching for detections."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IncidentReportFetchError(IdentityError):
|
|
26
|
+
"""Error raised when there is an issue fetching an incident report."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IdentityLookupError(IdentityError):
|
|
30
|
+
"""Error raised when there is an issue looking up identities."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IdentitySearchError(IdentityError):
|
|
34
|
+
"""Error raised when there is an issue searching identities."""
|
|
@@ -0,0 +1,396 @@
|
|
|
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 datetime import datetime
|
|
15
|
+
from functools import total_ordering
|
|
16
|
+
from typing import Annotated, Optional
|
|
17
|
+
|
|
18
|
+
from pydantic import AfterValidator, BeforeValidator, Field, field_validator
|
|
19
|
+
|
|
20
|
+
from ..common_models import IdName, RFBaseModel
|
|
21
|
+
from ..constants import TIMESTAMP_STR
|
|
22
|
+
from ..helpers import Validators
|
|
23
|
+
from .models.common_models import (
|
|
24
|
+
BaseIdentityIn,
|
|
25
|
+
Cookie,
|
|
26
|
+
DomainTypes,
|
|
27
|
+
DumpSearchOut,
|
|
28
|
+
FilterIn,
|
|
29
|
+
IdentityOrgIn,
|
|
30
|
+
PasswordHash,
|
|
31
|
+
)
|
|
32
|
+
from .models.detections import AuthorizationService, DetectionsCreated, DetectionsFilterIn, Password
|
|
33
|
+
from .models.incident_report import IncidentReportCredentials, IncidentReportDetails
|
|
34
|
+
from .models.lookup import IdentityDetails, IPRange, SecretDetails
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@total_ordering
|
|
38
|
+
class Detection(RFBaseModel):
|
|
39
|
+
"""Model to validate output of the ``/identity/detections`` endpoint.
|
|
40
|
+
|
|
41
|
+
Methods:
|
|
42
|
+
__hash__:
|
|
43
|
+
Returns hash value based on Detection ``id_`` and created time.
|
|
44
|
+
|
|
45
|
+
__eq__:
|
|
46
|
+
Checks equality between two Detection instances based on ``id_`` and created time.
|
|
47
|
+
|
|
48
|
+
__gt__:
|
|
49
|
+
Defines a greater-than comparison between two Detection instances based on
|
|
50
|
+
created timestamp and ``id_``.
|
|
51
|
+
|
|
52
|
+
__str__:
|
|
53
|
+
Returns a string representation of the Detection instance with:
|
|
54
|
+
``id_``, created timestamp, type, and novel.
|
|
55
|
+
|
|
56
|
+
.. code-block:: python
|
|
57
|
+
|
|
58
|
+
>>> print(detection)
|
|
59
|
+
ID: detection123, Created: 2025-01-01 05:00:30AM, Type: Workforce, Novel: True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Total Ordering:
|
|
63
|
+
The ordering of Detection instances is determined primarily by the created timestamp
|
|
64
|
+
of the detection. If two instances have the same created timestamp, their
|
|
65
|
+
``id_`` is used as a secondary criterion for ordering.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
id_: str = Field(alias='id')
|
|
69
|
+
organization_id: Annotated[
|
|
70
|
+
Optional[list[str]], BeforeValidator(Validators.check_uhash_prefix)
|
|
71
|
+
] = None
|
|
72
|
+
novel: bool
|
|
73
|
+
type_: str = Field(alias='type')
|
|
74
|
+
subject: str
|
|
75
|
+
password: Password
|
|
76
|
+
authorization_service: Optional[AuthorizationService] = None
|
|
77
|
+
cookies: list[Cookie]
|
|
78
|
+
malware_family: Optional[IdName] = None
|
|
79
|
+
dump: DumpSearchOut
|
|
80
|
+
created: datetime
|
|
81
|
+
|
|
82
|
+
def __hash__(self):
|
|
83
|
+
return hash((self.id_, self.created))
|
|
84
|
+
|
|
85
|
+
def __eq__(self, other: 'Detection'):
|
|
86
|
+
return (self.id_, self.created) == (other.id_, other.created)
|
|
87
|
+
|
|
88
|
+
def __gt__(self, other: 'Detection'):
|
|
89
|
+
return (self.created, self.id_) > (other.created, other.id_)
|
|
90
|
+
|
|
91
|
+
def __str__(self):
|
|
92
|
+
return (
|
|
93
|
+
f'Detection ID: {self.id_}, '
|
|
94
|
+
f'Created: {self.created.strftime(TIMESTAMP_STR)}, '
|
|
95
|
+
f'Type: {self.type_}, '
|
|
96
|
+
f'Novel: {self.novel}'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@total_ordering
|
|
101
|
+
class CredentialSearch(RFBaseModel):
|
|
102
|
+
"""Model to validate output of the ``/identity/credentials/search`` endpoint.
|
|
103
|
+
|
|
104
|
+
Methods:
|
|
105
|
+
__hash__:
|
|
106
|
+
Returns hash value based on CredentialSearch ``login`` and ``domain``.
|
|
107
|
+
|
|
108
|
+
__eq__:
|
|
109
|
+
Checks equality between two CredentialSearch based on ``login`` and ``domain``.
|
|
110
|
+
|
|
111
|
+
__gt__:
|
|
112
|
+
Defines a greater-than comparison between two CredentialSearch instances based on
|
|
113
|
+
``login`` and ``domain``.
|
|
114
|
+
|
|
115
|
+
__str__:
|
|
116
|
+
Returns a string representation of the CredentialSearch instance with:
|
|
117
|
+
``login`` and ``domain``.
|
|
118
|
+
|
|
119
|
+
.. code-block:: python
|
|
120
|
+
|
|
121
|
+
>>> print(credential)
|
|
122
|
+
Login: example Domain: norsegods.online
|
|
123
|
+
|
|
124
|
+
Total Ordering:
|
|
125
|
+
The ordering of CredentialSearch instances is determined primarily by the ``login`` of
|
|
126
|
+
the detection. If two instances have the same ``login, their ``domain`` is used as a
|
|
127
|
+
secondary criterion for ordering.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
login: str
|
|
132
|
+
login_sha1: Optional[str] = None # This is used only by CredentialLookupIn.subject_login
|
|
133
|
+
domain: str
|
|
134
|
+
|
|
135
|
+
def __hash__(self):
|
|
136
|
+
return hash((self.login, self.domain))
|
|
137
|
+
|
|
138
|
+
def __eq__(self, other: 'CredentialSearch'):
|
|
139
|
+
return (self.login, self.domain) == (other.login, other.domain)
|
|
140
|
+
|
|
141
|
+
def __gt__(self, other: 'CredentialSearch'):
|
|
142
|
+
return (self.login, self.domain) > (other.login, other.domain)
|
|
143
|
+
|
|
144
|
+
def __str__(self):
|
|
145
|
+
return f'Login: {self.login}, Domain: {self.domain}'
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Detections(RFBaseModel):
|
|
149
|
+
"""Model for payload received by POST ``/identity/detections`` endpoint."""
|
|
150
|
+
|
|
151
|
+
total: int
|
|
152
|
+
detections: list[Detection]
|
|
153
|
+
|
|
154
|
+
def __str__(self):
|
|
155
|
+
data = '\n'.join(str(d) for d in self.detections)
|
|
156
|
+
return f'[{data}]'
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@total_ordering
|
|
160
|
+
class PasswordLookup(RFBaseModel):
|
|
161
|
+
"""Model to validate output of the ``/identity/credentials/lookup`` endpoint.
|
|
162
|
+
|
|
163
|
+
Methods:
|
|
164
|
+
__hash__:
|
|
165
|
+
Returns hash value based on ``password.hash_`` (or ``hash_prefix``) and ``algorithm``.
|
|
166
|
+
|
|
167
|
+
__eq__:
|
|
168
|
+
Checks equality between two PasswordLookup instances based on ``password.hash_`` (or
|
|
169
|
+
``hash_prefix``) and ``algorithm``.
|
|
170
|
+
|
|
171
|
+
__gt__:
|
|
172
|
+
Defines a greater-than comparison between two PasswordLookup instances based on
|
|
173
|
+
``password.hash_`` (or ``hash_prefix``) and ``algorithm``.
|
|
174
|
+
|
|
175
|
+
__str__:
|
|
176
|
+
Returns a string representation of the PasswordLookup instance with:
|
|
177
|
+
``password.hash_`` (or ``hash_prefix``), ``algorithm``, and ``exposure_status``.
|
|
178
|
+
|
|
179
|
+
.. code-block:: python
|
|
180
|
+
|
|
181
|
+
>>> print(lookup)
|
|
182
|
+
Hash: abc123 Algorithm: sha1 Exposure Status: Common
|
|
183
|
+
|
|
184
|
+
Total Ordering:
|
|
185
|
+
The ordering of PasswordLookup instances is determined primarily by the
|
|
186
|
+
``password.hash_`` (or ``hash_prefix``). If two instances have the same hash, their
|
|
187
|
+
``algorithm`` is used as a secondary criterion for ordering.
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
password: PasswordHash
|
|
192
|
+
exposure_status: str
|
|
193
|
+
|
|
194
|
+
def __hash__(self):
|
|
195
|
+
return hash(
|
|
196
|
+
((self.password.hash_ or self.password.hash_prefix), self.password.algorithm.value)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def __eq__(self, other: 'PasswordLookup'):
|
|
200
|
+
return (
|
|
201
|
+
(self.password.hash_ or self.password.hash_prefix),
|
|
202
|
+
self.password.algorithm.value,
|
|
203
|
+
) == ((other.password.hash_ or other.password.hash_prefix), other.password.algorithm.value)
|
|
204
|
+
|
|
205
|
+
def __gt__(self, other: 'PasswordLookup'):
|
|
206
|
+
return (
|
|
207
|
+
(self.password.hash_ or self.password.hash_prefix),
|
|
208
|
+
self.password.algorithm.value,
|
|
209
|
+
) > ((other.password.hash_ or other.password.hash_prefix), other.password.algorithm.value)
|
|
210
|
+
|
|
211
|
+
def __str__(self):
|
|
212
|
+
return (
|
|
213
|
+
f'Hash: {(self.password.hash_ or self.password.hash_prefix)}, '
|
|
214
|
+
f'Algorithm: {self.password.algorithm.value}, '
|
|
215
|
+
f'Exposure Status: {self.exposure_status}'
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Credential(RFBaseModel):
|
|
220
|
+
"""Detection model to validate output of the ``/identity/credentials/search`` endpoint.
|
|
221
|
+
|
|
222
|
+
Methods:
|
|
223
|
+
__hash__:
|
|
224
|
+
Returns hash value based on ``subject``, ``first_downloaded``, the exposed secret's
|
|
225
|
+
``hashes``, and the ``authorization_service`` URL (if present).
|
|
226
|
+
|
|
227
|
+
__eq__:
|
|
228
|
+
Checks equality between two Credential instances based on ``subject``,
|
|
229
|
+
``first_downloaded``, the exposed secret's ``hashes``, and the
|
|
230
|
+
``authorization_service`` URL.
|
|
231
|
+
|
|
232
|
+
__gt__:
|
|
233
|
+
Defines a greater-than comparison between two Credential instances based on
|
|
234
|
+
``subject``, ``first_downloaded``, the exposed secret's ``hashes``, and the
|
|
235
|
+
``authorization_service`` URL.
|
|
236
|
+
|
|
237
|
+
__str__:
|
|
238
|
+
Returns a string representation of the Credential instance with:
|
|
239
|
+
``subject``, ``first_downloaded``, exposed secret ``hashes``, and
|
|
240
|
+
``authorization_service``.
|
|
241
|
+
|
|
242
|
+
.. code-block:: python
|
|
243
|
+
|
|
244
|
+
>>> print(credential)
|
|
245
|
+
Subject: admin@example.com, First Downloaded: 2024-03-01T12:00:00,
|
|
246
|
+
Hashes: [abc123, def456], Authorization Service: login.service.com
|
|
247
|
+
|
|
248
|
+
Total Ordering:
|
|
249
|
+
The ordering of Credential instances is determined primarily by the ``subject`` and
|
|
250
|
+
``first_downloaded`` timestamp. If those are equal, the ``hashes`` and then the
|
|
251
|
+
``authorization_service`` URL are used as secondary criteria.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
subject: str
|
|
255
|
+
dumps: list[DumpSearchOut]
|
|
256
|
+
first_downloaded: datetime
|
|
257
|
+
latest_downloaded: datetime
|
|
258
|
+
exposed_secret: SecretDetails
|
|
259
|
+
compromise: Optional[dict[str, datetime]] = None
|
|
260
|
+
malware_family: Optional[IdName] = None
|
|
261
|
+
authorization_service: Optional[AuthorizationService] = None
|
|
262
|
+
cookies: Optional[list[Cookie]] = None
|
|
263
|
+
|
|
264
|
+
def __hash__(self):
|
|
265
|
+
hashes = ', '.join(sorted(a.hash_ or a.hash_prefix for a in self.exposed_secret.hashes))
|
|
266
|
+
auth = self.authorization_service.url if self.authorization_service else ''
|
|
267
|
+
return hash((self.subject, self.first_downloaded, hashes, auth))
|
|
268
|
+
|
|
269
|
+
def __eq__(self, other: 'Credential'):
|
|
270
|
+
hashes_self = sorted(a.hash_ or a.hash_prefix for a in self.exposed_secret.hashes)
|
|
271
|
+
hashes_other = sorted(a.hash_ or a.hash_prefix for a in other.exposed_secret.hashes)
|
|
272
|
+
auth_self = self.authorization_service.url if self.authorization_service else ''
|
|
273
|
+
auth_other = other.authorization_service.url if other.authorization_service else ''
|
|
274
|
+
return (
|
|
275
|
+
self.subject == other.subject
|
|
276
|
+
and self.first_downloaded == other.first_downloaded
|
|
277
|
+
and hashes_self == hashes_other
|
|
278
|
+
and auth_self == auth_other
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def __gt__(self, other: 'Credential'):
|
|
282
|
+
hashes_self = ', '.join(
|
|
283
|
+
sorted(a.hash_ or a.hash_prefix for a in self.exposed_secret.hashes)
|
|
284
|
+
)
|
|
285
|
+
hashes_other = ', '.join(
|
|
286
|
+
sorted(a.hash_ or a.hash_prefix for a in other.exposed_secret.hashes)
|
|
287
|
+
)
|
|
288
|
+
auth_self = self.authorization_service.url if self.authorization_service else ''
|
|
289
|
+
auth_other = other.authorization_service.url if other.authorization_service else ''
|
|
290
|
+
return (self.subject, self.first_downloaded, hashes_self, auth_self) > (
|
|
291
|
+
other.subject,
|
|
292
|
+
other.first_downloaded,
|
|
293
|
+
hashes_other,
|
|
294
|
+
auth_other,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def __str__(self):
|
|
298
|
+
hashes = ', '.join(sorted(a.hash_ or a.hash_prefix for a in self.exposed_secret.hashes))
|
|
299
|
+
auth = self.authorization_service.url if self.authorization_service else 'None'
|
|
300
|
+
return (
|
|
301
|
+
f'Subject: {self.subject}, '
|
|
302
|
+
f'First Downloaded: {self.first_downloaded.strftime(TIMESTAMP_STR)}, '
|
|
303
|
+
f'Hashes: [{hashes}], '
|
|
304
|
+
f'Authorization Service: {auth}'
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class LeakedIdentity(RFBaseModel):
|
|
309
|
+
"""Model to validate output of several endpoints.
|
|
310
|
+
|
|
311
|
+
``/identity/ip/lookup``, ``/identity/credentials/lookup``, ``/identity/hostname/lookup``
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
identity: IdentityDetails
|
|
315
|
+
count: int
|
|
316
|
+
credentials: list[Credential]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class DetectionsIn(RFBaseModel):
|
|
320
|
+
"""Model for payload sent to POST ``/identity/detections`` endpoint."""
|
|
321
|
+
|
|
322
|
+
organization_id: Annotated[
|
|
323
|
+
Optional[list[str]],
|
|
324
|
+
BeforeValidator(Validators.convert_str_to_list),
|
|
325
|
+
AfterValidator(Validators.check_uhash_prefix),
|
|
326
|
+
] = []
|
|
327
|
+
include_enterprise_level: Optional[bool] = None
|
|
328
|
+
filter: Optional[DetectionsFilterIn] = Field(default_factory=DetectionsFilterIn)
|
|
329
|
+
limit: int
|
|
330
|
+
offset: Optional[str] = None
|
|
331
|
+
created: Optional[DetectionsCreated] = Field(default_factory=DetectionsCreated)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class IncidentReportIn(IdentityOrgIn):
|
|
335
|
+
"""Model for payload sent to POST ``/identity/detections`` endpoint."""
|
|
336
|
+
|
|
337
|
+
source: str
|
|
338
|
+
include_details: bool
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class IncidentReportOut(RFBaseModel):
|
|
342
|
+
"""Model for payload received by POST ``/identity/incident/report`` endpoint."""
|
|
343
|
+
|
|
344
|
+
details: Optional[IncidentReportDetails] = None
|
|
345
|
+
credentials: list[IncidentReportCredentials]
|
|
346
|
+
|
|
347
|
+
@field_validator('details', mode='before')
|
|
348
|
+
@classmethod
|
|
349
|
+
def transform_details(cls, value):
|
|
350
|
+
"""Paged request returns lists only so transforming by extracting the first one."""
|
|
351
|
+
if isinstance(value, list) and len(value):
|
|
352
|
+
return value.pop(0)
|
|
353
|
+
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class HostnameLookupIn(BaseIdentityIn):
|
|
358
|
+
"""Model for payload sent to POST ``/identity/incident/report` endpoint."""
|
|
359
|
+
|
|
360
|
+
hostname: str
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class IPLookupIn(BaseIdentityIn):
|
|
364
|
+
"""Model for payload sent to POST ``/identity/ip/lookup`` endpoint."""
|
|
365
|
+
|
|
366
|
+
ip: Optional[str] = None
|
|
367
|
+
range_: Optional[IPRange] = Field(alias='range', default=None)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class CredentialsLookupIn(BaseIdentityIn):
|
|
371
|
+
"""Model for payload sent to POST ``/identity/credentials/lookup`` endpoint."""
|
|
372
|
+
|
|
373
|
+
subjects: Annotated[Optional[list[str]], BeforeValidator(Validators.convert_str_to_list)] = None
|
|
374
|
+
subjects_sha1: Annotated[
|
|
375
|
+
Optional[list[str]], BeforeValidator(Validators.convert_str_to_list)
|
|
376
|
+
] = None
|
|
377
|
+
subjects_login: Optional[list[CredentialSearch]] = None
|
|
378
|
+
filter: Optional[FilterIn] = None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class CredentialsSearchIn(BaseIdentityIn):
|
|
382
|
+
"""Model for payload sent to POST ``/identity/credentials/search`` endpoint."""
|
|
383
|
+
|
|
384
|
+
domains: Annotated[list[str], BeforeValidator(Validators.convert_str_to_list)]
|
|
385
|
+
domain_types: Annotated[
|
|
386
|
+
Optional[list[DomainTypes]], BeforeValidator(Validators.convert_str_to_list)
|
|
387
|
+
] = None
|
|
388
|
+
|
|
389
|
+
filter: Optional[FilterIn] = None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class DumpSearchIn(RFBaseModel):
|
|
393
|
+
"""Model for payload sent to POST ``/identity/metadata/dump/search`` endpoint."""
|
|
394
|
+
|
|
395
|
+
names: Annotated[list[str], BeforeValidator(Validators.convert_str_to_list)]
|
|
396
|
+
limit: Optional[int] = None
|