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.
Files changed (137) hide show
  1. {psengine-2.0.6 → psengine-2.1.0}/PKG-INFO +1 -1
  2. {psengine-2.0.6 → psengine-2.1.0}/psengine/_version.py +1 -1
  3. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/markdown/markdown.py +2 -1
  4. {psengine-2.0.6 → psengine-2.1.0}/psengine/common_models.py +11 -2
  5. {psengine-2.0.6 → psengine-2.1.0}/psengine/endpoints.py +13 -0
  6. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/entity_list.py +2 -1
  7. {psengine-2.0.6 → psengine-2.1.0}/psengine/helpers/__init__.py +1 -0
  8. {psengine-2.0.6 → psengine-2.1.0}/psengine/helpers/helpers.py +39 -3
  9. psengine-2.1.0/psengine/identity/__init__.py +31 -0
  10. psengine-2.1.0/psengine/identity/constants.py +15 -0
  11. psengine-2.1.0/psengine/identity/errors.py +34 -0
  12. psengine-2.1.0/psengine/identity/identity.py +396 -0
  13. psengine-2.1.0/psengine/identity/identity_mgr.py +895 -0
  14. psengine-2.1.0/psengine/identity/models/common_models.py +227 -0
  15. psengine-2.1.0/psengine/identity/models/detections.py +54 -0
  16. psengine-2.1.0/psengine/identity/models/incident_report.py +38 -0
  17. psengine-2.1.0/psengine/identity/models/lookup.py +55 -0
  18. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/__init__.py +3 -0
  19. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/constants.py +28 -5
  20. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/errors.py +4 -0
  21. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/helpers.py +5 -3
  22. psengine-2.1.0/psengine/playbook_alerts/markdown/__init__.py +12 -0
  23. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown.py +5 -23
  24. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +1 -1
  25. psengine-2.1.0/psengine/playbook_alerts/markdown/markdown_malware_report.py +66 -0
  26. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_malware_report.py +6 -6
  27. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/pa_category.py +0 -1
  28. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/playbook_alert_mgr.py +190 -195
  29. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/playbook_alerts.py +13 -19
  30. {psengine-2.0.6 → psengine-2.1.0}/psengine/rf_client.py +37 -16
  31. {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/risklist_mgr.py +13 -0
  32. {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/PKG-INFO +1 -1
  33. {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/SOURCES.txt +11 -1
  34. {psengine-2.0.6 → psengine-2.1.0}/pyproject.toml +1 -5
  35. psengine-2.0.6/psengine.egg-info/entry_points.txt +0 -2
  36. {psengine-2.0.6 → psengine-2.1.0}/LICENSE +0 -0
  37. {psengine-2.0.6 → psengine-2.1.0}/README.rst +0 -0
  38. {psengine-2.0.6 → psengine-2.1.0}/psengine/__init__.py +0 -0
  39. {psengine-2.0.6 → psengine-2.1.0}/psengine/_sdk_id.py +0 -0
  40. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/__init__.py +0 -0
  41. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/constants.py +0 -0
  42. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/errors.py +0 -0
  43. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/helpers.py +0 -0
  44. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/markdown.py +0 -0
  45. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/models.py +0 -0
  46. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/note.py +0 -0
  47. {psengine-2.0.6 → psengine-2.1.0}/psengine/analyst_notes/note_mgr.py +0 -0
  48. {psengine-2.0.6 → psengine-2.1.0}/psengine/base_http_client.py +0 -0
  49. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/__init__.py +0 -0
  50. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/classic_alert.py +0 -0
  51. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/classic_alert_mgr.py +0 -0
  52. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/constants.py +0 -0
  53. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/errors.py +0 -0
  54. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/helpers.py +0 -0
  55. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
  56. {psengine-2.0.6 → psengine-2.1.0}/psengine/classic_alerts/models.py +0 -0
  57. {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/__init__.py +0 -0
  58. {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/collective_insights.py +0 -0
  59. {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/constants.py +0 -0
  60. {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/errors.py +0 -0
  61. {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/insight.py +0 -0
  62. {psengine-2.0.6 → psengine-2.1.0}/psengine/collective_insights/models.py +0 -0
  63. {psengine-2.0.6 → psengine-2.1.0}/psengine/config/__init__.py +0 -0
  64. {psengine-2.0.6 → psengine-2.1.0}/psengine/config/config.py +0 -0
  65. {psengine-2.0.6 → psengine-2.1.0}/psengine/config/errors.py +0 -0
  66. {psengine-2.0.6 → psengine-2.1.0}/psengine/constants.py +0 -0
  67. {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/__init__.py +0 -0
  68. {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/detection_mgr.py +0 -0
  69. {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/detection_rule.py +0 -0
  70. {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/errors.py +0 -0
  71. {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/helpers.py +0 -0
  72. {psengine-2.0.6 → psengine-2.1.0}/psengine/detection/models.py +0 -0
  73. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/__init__.py +0 -0
  74. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/constants.py +0 -0
  75. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/errors.py +0 -0
  76. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/lookup.py +0 -0
  77. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/lookup_mgr.py +0 -0
  78. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/__init__.py +0 -0
  79. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
  80. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/lookup.py +0 -0
  81. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/models/soar.py +0 -0
  82. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/soar.py +0 -0
  83. {psengine-2.0.6 → psengine-2.1.0}/psengine/enrich/soar_mgr.py +3 -3
  84. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/__init__.py +0 -0
  85. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/constants.py +0 -0
  86. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
  87. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/errors.py +0 -0
  88. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_lists/models.py +0 -0
  89. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/__init__.py +0 -0
  90. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/entity_match.py +0 -0
  91. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/entity_match_mgr.py +0 -0
  92. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/errors.py +0 -0
  93. {psengine-2.0.6 → psengine-2.1.0}/psengine/entity_match/models.py +0 -0
  94. {psengine-2.0.6 → psengine-2.1.0}/psengine/errors.py +0 -0
  95. {psengine-2.0.6/psengine/playbook_alerts/markdown → psengine-2.1.0/psengine/identity/models}/__init__.py +0 -0
  96. {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/__init__.py +0 -0
  97. {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/constants.py +0 -0
  98. {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/errors.py +0 -0
  99. {psengine-2.0.6 → psengine-2.1.0}/psengine/logger/rf_logger.py +0 -0
  100. {psengine-2.0.6 → psengine-2.1.0}/psengine/markdown/__init__.py +0 -0
  101. {psengine-2.0.6 → psengine-2.1.0}/psengine/markdown/markdown.py +0 -0
  102. {psengine-2.0.6 → psengine-2.1.0}/psengine/markdown/models.py +0 -0
  103. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/mappings.py +0 -0
  104. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
  105. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
  106. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +0 -0
  107. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
  108. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
  109. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/__init__.py +0 -0
  110. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/common_models.py +0 -0
  111. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
  112. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
  113. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
  114. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
  115. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
  116. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
  117. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
  118. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
  119. {psengine-2.0.6 → psengine-2.1.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
  120. {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/__init__.py +0 -0
  121. {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/constants.py +0 -0
  122. {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/errors.py +0 -0
  123. {psengine-2.0.6 → psengine-2.1.0}/psengine/risklists/models.py +0 -0
  124. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/__init__.py +0 -0
  125. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/base_stix_entity.py +0 -0
  126. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/complex_entity.py +0 -0
  127. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/constants.py +0 -0
  128. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/enriched_indicator.py +0 -0
  129. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/errors.py +0 -0
  130. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/helpers.py +0 -0
  131. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/rf_bundle.py +0 -0
  132. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/simple_entity.py +0 -0
  133. {psengine-2.0.6 → psengine-2.1.0}/psengine/stix2/util.py +0 -0
  134. {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/dependency_links.txt +0 -0
  135. {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/requires.txt +0 -0
  136. {psengine-2.0.6 → psengine-2.1.0}/psengine.egg-info/top_level.txt +0 -0
  137. {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.6
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
@@ -11,4 +11,4 @@
11
11
  # accessed from any third party API. #
12
12
  ##############################################################################################
13
13
 
14
- __version__ = '2.0.6'
14
+ __version__ = '2.1.0'
@@ -94,7 +94,8 @@ def _process_hit_fragment(
94
94
  content.append(f'{blockquote(fragment)}\n')
95
95
  else:
96
96
  content.append(
97
- f'_Reference text is missing, check the Recorded Future {link("Portal", str(classic_alert.url.portal))} for more information._\n' # noqa: E501
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}, entities: {validated_status.size}" # noqa: E501
317
+ f"List '{self.name}' status: {validated_status.status}, "
318
+ f'entities: {validated_status.size}'
318
319
  )
319
320
 
320
321
  return validated_status
@@ -17,6 +17,7 @@ from .helpers import (
17
17
  MultiThreadingHelper,
18
18
  OSHelpers,
19
19
  TimeHelpers,
20
+ Validators,
20
21
  connection_exceptions,
21
22
  debug_call,
22
23
  dump_models,
@@ -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