psengine 2.6.0__tar.gz → 2.7.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 (158) hide show
  1. {psengine-2.6.0 → psengine-2.7.0}/PKG-INFO +4 -4
  2. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/models.py +1 -1
  3. {psengine-2.6.0 → psengine-2.7.0}/psengine/endpoints.py +10 -0
  4. {psengine-2.6.0 → psengine-2.7.0}/psengine/helpers/helpers.py +3 -3
  5. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/complex_entity.py +1 -1
  6. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/enriched_indicator.py +1 -1
  7. psengine-2.7.0/psengine/threat_maps/__init__.py +30 -0
  8. psengine-2.7.0/psengine/threat_maps/errors.py +34 -0
  9. psengine-2.7.0/psengine/threat_maps/models.py +56 -0
  10. psengine-2.7.0/psengine/threat_maps/threat_map.py +134 -0
  11. psengine-2.7.0/psengine/threat_maps/threat_map_mgr.py +173 -0
  12. {psengine-2.6.0 → psengine-2.7.0}/pyproject.toml +12 -11
  13. {psengine-2.6.0 → psengine-2.7.0}/README.md +0 -0
  14. {psengine-2.6.0 → psengine-2.7.0}/psengine/__init__.py +0 -0
  15. {psengine-2.6.0 → psengine-2.7.0}/psengine/_sdk_id.py +0 -0
  16. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/__init__.py +0 -0
  17. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/constants.py +0 -0
  18. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/errors.py +0 -0
  19. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/helpers.py +0 -0
  20. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/markdown.py +0 -0
  21. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/models.py +0 -0
  22. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/note.py +0 -0
  23. {psengine-2.6.0 → psengine-2.7.0}/psengine/analyst_notes/note_mgr.py +0 -0
  24. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/__init__.py +0 -0
  25. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/asi.py +0 -0
  26. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/asi_mgr.py +0 -0
  27. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/client.py +0 -0
  28. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/constants.py +0 -0
  29. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/errors.py +0 -0
  30. {psengine-2.6.0 → psengine-2.7.0}/psengine/asi/models.py +0 -0
  31. {psengine-2.6.0 → psengine-2.7.0}/psengine/base_http_client.py +0 -0
  32. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/__init__.py +0 -0
  33. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/classic_alert.py +0 -0
  34. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/classic_alert_mgr.py +0 -0
  35. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/constants.py +0 -0
  36. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/errors.py +0 -0
  37. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/helpers.py +0 -0
  38. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
  39. {psengine-2.6.0 → psengine-2.7.0}/psengine/classic_alerts/markdown/markdown.py +0 -0
  40. {psengine-2.6.0 → psengine-2.7.0}/psengine/collective_insights/__init__.py +0 -0
  41. {psengine-2.6.0 → psengine-2.7.0}/psengine/collective_insights/collective_insights.py +0 -0
  42. {psengine-2.6.0 → psengine-2.7.0}/psengine/collective_insights/constants.py +0 -0
  43. {psengine-2.6.0 → psengine-2.7.0}/psengine/collective_insights/errors.py +0 -0
  44. {psengine-2.6.0 → psengine-2.7.0}/psengine/collective_insights/insight.py +0 -0
  45. {psengine-2.6.0 → psengine-2.7.0}/psengine/collective_insights/models.py +0 -0
  46. {psengine-2.6.0 → psengine-2.7.0}/psengine/common_models.py +0 -0
  47. {psengine-2.6.0 → psengine-2.7.0}/psengine/config/__init__.py +0 -0
  48. {psengine-2.6.0 → psengine-2.7.0}/psengine/config/config.py +0 -0
  49. {psengine-2.6.0 → psengine-2.7.0}/psengine/config/errors.py +0 -0
  50. {psengine-2.6.0 → psengine-2.7.0}/psengine/constants.py +0 -0
  51. {psengine-2.6.0 → psengine-2.7.0}/psengine/detection/__init__.py +0 -0
  52. {psengine-2.6.0 → psengine-2.7.0}/psengine/detection/detection_mgr.py +0 -0
  53. {psengine-2.6.0 → psengine-2.7.0}/psengine/detection/detection_rule.py +0 -0
  54. {psengine-2.6.0 → psengine-2.7.0}/psengine/detection/errors.py +0 -0
  55. {psengine-2.6.0 → psengine-2.7.0}/psengine/detection/helpers.py +0 -0
  56. {psengine-2.6.0 → psengine-2.7.0}/psengine/detection/models.py +0 -0
  57. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/__init__.py +0 -0
  58. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/constants.py +0 -0
  59. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/errors.py +0 -0
  60. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/lookup.py +0 -0
  61. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/lookup_mgr.py +0 -0
  62. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/models/__init__.py +0 -0
  63. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
  64. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/models/lookup.py +0 -0
  65. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/models/soar.py +0 -0
  66. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/soar.py +0 -0
  67. {psengine-2.6.0 → psengine-2.7.0}/psengine/enrich/soar_mgr.py +0 -0
  68. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_lists/__init__.py +0 -0
  69. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_lists/constants.py +0 -0
  70. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_lists/entity_list.py +0 -0
  71. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
  72. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_lists/errors.py +0 -0
  73. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_lists/models.py +0 -0
  74. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_match/__init__.py +0 -0
  75. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_match/entity_match.py +0 -0
  76. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_match/entity_match_mgr.py +0 -0
  77. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_match/errors.py +0 -0
  78. {psengine-2.6.0 → psengine-2.7.0}/psengine/entity_match/models.py +0 -0
  79. {psengine-2.6.0 → psengine-2.7.0}/psengine/errors.py +0 -0
  80. {psengine-2.6.0 → psengine-2.7.0}/psengine/fusion/__init__.py +0 -0
  81. {psengine-2.6.0 → psengine-2.7.0}/psengine/fusion/errors.py +0 -0
  82. {psengine-2.6.0 → psengine-2.7.0}/psengine/fusion/fusion_mgr.py +0 -0
  83. {psengine-2.6.0 → psengine-2.7.0}/psengine/fusion/models.py +0 -0
  84. {psengine-2.6.0 → psengine-2.7.0}/psengine/helpers/__init__.py +0 -0
  85. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/__init__.py +0 -0
  86. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/constants.py +0 -0
  87. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/errors.py +0 -0
  88. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/identity.py +0 -0
  89. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/identity_mgr.py +0 -0
  90. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/models/__init__.py +0 -0
  91. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/models/common_models.py +0 -0
  92. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/models/detections.py +0 -0
  93. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/models/incident_report.py +0 -0
  94. {psengine-2.6.0 → psengine-2.7.0}/psengine/identity/models/lookup.py +0 -0
  95. {psengine-2.6.0 → psengine-2.7.0}/psengine/logger/__init__.py +0 -0
  96. {psengine-2.6.0 → psengine-2.7.0}/psengine/logger/constants.py +0 -0
  97. {psengine-2.6.0 → psengine-2.7.0}/psengine/logger/errors.py +0 -0
  98. {psengine-2.6.0 → psengine-2.7.0}/psengine/logger/rf_logger.py +0 -0
  99. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/__init__.py +0 -0
  100. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/auto_sigma_mgr.py +0 -0
  101. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/auto_yara_mgr.py +0 -0
  102. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/constants.py +0 -0
  103. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/errors.py +0 -0
  104. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/helpers.py +0 -0
  105. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/malware_intel.py +0 -0
  106. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/malware_intel_mgr.py +0 -0
  107. {psengine-2.6.0 → psengine-2.7.0}/psengine/malware_intel/models.py +0 -0
  108. {psengine-2.6.0 → psengine-2.7.0}/psengine/markdown/__init__.py +0 -0
  109. {psengine-2.6.0 → psengine-2.7.0}/psengine/markdown/markdown.py +0 -0
  110. {psengine-2.6.0 → psengine-2.7.0}/psengine/markdown/models.py +0 -0
  111. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/__init__.py +0 -0
  112. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/constants.py +0 -0
  113. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/errors.py +0 -0
  114. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/helpers.py +0 -0
  115. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/mappings.py +0 -0
  116. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/__init__.py +0 -0
  117. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown.py +0 -0
  118. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +0 -0
  119. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
  120. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
  121. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +0 -0
  122. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
  123. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_malware_report.py +0 -0
  124. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
  125. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/__init__.py +0 -0
  126. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/common_models.py +0 -0
  127. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
  128. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
  129. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
  130. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
  131. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
  132. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
  133. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
  134. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_malware_report.py +0 -0
  135. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
  136. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
  137. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/pa_category.py +0 -0
  138. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/playbook_alert_mgr.py +0 -0
  139. {psengine-2.6.0 → psengine-2.7.0}/psengine/playbook_alerts/playbook_alerts.py +0 -0
  140. {psengine-2.6.0 → psengine-2.7.0}/psengine/py.typed +0 -0
  141. {psengine-2.6.0 → psengine-2.7.0}/psengine/rf_client.py +0 -0
  142. {psengine-2.6.0 → psengine-2.7.0}/psengine/risk_history/__init__.py +0 -0
  143. {psengine-2.6.0 → psengine-2.7.0}/psengine/risk_history/errors.py +0 -0
  144. {psengine-2.6.0 → psengine-2.7.0}/psengine/risk_history/models.py +0 -0
  145. {psengine-2.6.0 → psengine-2.7.0}/psengine/risk_history/risk_history_mgr.py +0 -0
  146. {psengine-2.6.0 → psengine-2.7.0}/psengine/risklists/__init__.py +0 -0
  147. {psengine-2.6.0 → psengine-2.7.0}/psengine/risklists/constants.py +0 -0
  148. {psengine-2.6.0 → psengine-2.7.0}/psengine/risklists/errors.py +0 -0
  149. {psengine-2.6.0 → psengine-2.7.0}/psengine/risklists/models.py +0 -0
  150. {psengine-2.6.0 → psengine-2.7.0}/psengine/risklists/risklist_mgr.py +0 -0
  151. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/__init__.py +0 -0
  152. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/base_stix_entity.py +0 -0
  153. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/constants.py +0 -0
  154. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/errors.py +0 -0
  155. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/helpers.py +0 -0
  156. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/rf_bundle.py +0 -0
  157. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/simple_entity.py +0 -0
  158. {psengine-2.6.0 → psengine-2.7.0}/psengine/stix2/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psengine
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: psengine is a simple, yet elegant, library for rapid development of integrations with Recorded Future.
5
5
  Keywords: API,Recorded Future,Cyber Security Engineering,Threat Intelligence
6
6
  Author: Moise Medici, Patrick Kinsella, Ernest Bartosevic
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Requires-Dist: typing-extensions>=4.8.0
20
20
  Requires-Dist: requests>=2.27.1
21
- Requires-Dist: jsonpath-ng>=1.5.3,<=1.6.1
21
+ Requires-Dist: jsonpath-ng>=1.5.3,<=1.8.0
22
22
  Requires-Dist: stix2~=3.0.1
23
23
  Requires-Dist: python-dateutil>=2.7.0
24
- Requires-Dist: more-itertools>=9.0.0,<=10.2.0
24
+ Requires-Dist: more-itertools>=9.0.0,<=11.0.2
25
25
  Requires-Dist: pydantic>=2.7,<3.0.0
26
- Requires-Dist: pydantic-settings[toml]>=2.12.2,<2.13.1
26
+ Requires-Dist: pydantic-settings[toml]>=2.12.2,<2.14.1
27
27
  Requires-Dist: markdown-strings==3.4.0
28
28
  Requires-Python: >=3.10, <3.15
29
29
  Project-URL: Homepage, https://recordedfuture-professionalservices.github.io/psengine/latest/
@@ -94,7 +94,7 @@ class AlertCounts(RFBaseModel):
94
94
 
95
95
  class NotificationSettings(RFBaseModel):
96
96
  email_subscribers: list[IdName]
97
- mobile_subsribers: list[IdName] | None = None
97
+ mobile_subscribers: list[IdName] | None = None
98
98
 
99
99
 
100
100
  class Evidence(RFBaseModel):
@@ -147,3 +147,13 @@ EP_ASI_ASSET_EXPOSURES = f'{EP_ASI_ASSETS}/{{}}/exposures'
147
147
  EP_ASI_ASSETS_SEARCH = f'{EP_ASI_ASSETS}/_search'
148
148
  EP_ASI_EXPOSURES = f'{EP_ASI_PROJECTS}/{{}}/exposures'
149
149
  EP_ASI_EXPOSURES_BY_SIGNATURE = f'{EP_ASI_EXPOSURES}/{{}}'
150
+
151
+ ################################################################################
152
+ # Threat Map API Endpoints
153
+ ################################################################################
154
+ EP_THREAT_MAPS_BASE = BASE_URL + '/threat'
155
+ EP_THREAT_MAPS_LIST = EP_THREAT_MAPS_BASE + '/maps'
156
+ EP_THREAT_MAP = EP_THREAT_MAPS_BASE + '/map/{}'
157
+ EP_THREAT_MAP_ORG = EP_THREAT_MAPS_BASE + '/map/{}/{}'
158
+ EP_ACTOR_SEARCH = EP_THREAT_MAPS_BASE + '/actor/search'
159
+ EP_CATEGORIES = EP_THREAT_MAPS_BASE + '/{}/categories'
@@ -325,10 +325,10 @@ class OSHelpers:
325
325
  try:
326
326
  path.mkdir(parents=True, exist_ok=True)
327
327
  except PermissionError as err:
328
- raise WriteFileError(f'Directory {path} is not writeable') from err
329
- # In case it already exists, check if it is writeable
328
+ raise WriteFileError(f'Directory {path} is not writable') from err
329
+ # In case it already exists, check if it is writable
330
330
  if not os.access(path, os.W_OK):
331
- raise WriteFileError(f'Directory {path} is not writeable')
331
+ raise WriteFileError(f'Directory {path} is not writable')
332
332
  return path
333
333
 
334
334
 
@@ -225,7 +225,7 @@ class IndicatorEntity(BaseStixEntity):
225
225
  """
226
226
  if not create_indicator and not create_obs:
227
227
  raise STIX2TransformError(
228
- 'Inidcator must create at least one of "Observable" or "Indicator"',
228
+ 'Indicator must create at least one of "Observable" or "Indicator"',
229
229
  )
230
230
 
231
231
  type_ = CONVERTED_TYPES.get(type_, type_)
@@ -126,7 +126,7 @@ class EnrichedIndicator(IndicatorEntity):
126
126
  """Creates relationship between object and indicator/observabe.
127
127
 
128
128
  Raises:
129
- STIX2TransformError: Generic transofmr error
129
+ STIX2TransformError: Generic transform error
130
130
  """
131
131
  if isinstance(obj, IndicatorEntity):
132
132
  sources = []
@@ -0,0 +1,30 @@
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 (
15
+ ThreatActorSearchError,
16
+ ThreatMapCategoriesError,
17
+ ThreatMapFetchError,
18
+ ThreatMapInfoError,
19
+ ThreatMapsError,
20
+ )
21
+ from .threat_map import (
22
+ EntityCategory,
23
+ ThreatActorAttributes,
24
+ ThreatActorProfile,
25
+ ThreatMap,
26
+ ThreatMapEntity,
27
+ ThreatMapFetchIn,
28
+ ThreatMapInfo,
29
+ )
30
+ from .threat_map_mgr import ThreatMapMgr
@@ -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 ThreatMapsError(RecordedFutureError):
18
+ """Error raised when there was an issue with the threat maps API."""
19
+
20
+
21
+ class ThreatMapFetchError(ThreatMapsError):
22
+ """Error raised when there was an issue fetching a threat map."""
23
+
24
+
25
+ class ThreatMapInfoError(ThreatMapsError):
26
+ """Error raised when there was an error fetching available threat maps."""
27
+
28
+
29
+ class ThreatMapCategoriesError(ThreatMapsError):
30
+ """Error raised when there was an error searching threat categories."""
31
+
32
+
33
+ class ThreatActorSearchError(ThreatMapsError):
34
+ """Error raised when there was an error searching threat actors."""
@@ -0,0 +1,56 @@
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 enum import Enum
16
+
17
+ from ..common_models import IdName, RFBaseModel
18
+
19
+
20
+ class ThreatMapAxis(Enum):
21
+ opportunity = 'opportunity'
22
+ intent = 'intent'
23
+
24
+
25
+ class ThreatMapType(Enum):
26
+ actors = 'actors'
27
+ malware = 'malware'
28
+
29
+ @property
30
+ def category_slug(self) -> str:
31
+ """Return the URL slug used by the categories endpoint.
32
+
33
+ The map endpoint uses `actors`/`malware` (this enum's value), but the
34
+ categories endpoint uses the singular `actor`/`malware` — see api.md.
35
+ """
36
+ return 'actor' if self is ThreatMapType.actors else 'malware'
37
+
38
+
39
+ class LogEntry(RFBaseModel):
40
+ watchlist: IdName | None = None
41
+ entity: IdName
42
+ severity: int
43
+ axis: str
44
+ date: datetime
45
+
46
+
47
+ class EntityAttributes(RFBaseModel):
48
+ name: str
49
+ alias: list[str] = []
50
+
51
+
52
+ class ThreatActorAttributes(RFBaseModel):
53
+ name: str
54
+ common_names: list[str] = []
55
+ alias: list[str] = []
56
+ categories: list[IdName] = []
@@ -0,0 +1,134 @@
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
17
+
18
+ from pydantic import BeforeValidator, Field
19
+
20
+ from ..common_models import IdName, RFBaseModel
21
+ from ..helpers.helpers import Validators
22
+ from .models import EntityAttributes, LogEntry, ThreatActorAttributes
23
+
24
+
25
+ @total_ordering
26
+ class ThreatMapEntity(RFBaseModel):
27
+ """Model to validate data received from the `threat/map/{type}` endpoint.
28
+
29
+ This class supports string representation, equality comparison, and hashing of `ThreatMapEntity`
30
+ instances.
31
+
32
+ Hashing:
33
+ Defines uniqueness of a `ThreatMapEntity` object by the entity ID.
34
+
35
+ Equality:
36
+ Validates equality between two `ThreatMapEntity` objects based on the entity ID.
37
+
38
+ Greater-than Comparison:
39
+ Defines a greater-than comparison between two `ThreatMapEntity` instances based on
40
+ `opportunity`, `intent` and `prevalence`. Lastly on `id_`
41
+
42
+ String Representation:
43
+ Returns a string representation of the `ThreatMapEntity` instance including the
44
+ entity match name, ID, opportunity, and intent or prevalence depending on category.
45
+
46
+ ```python
47
+ >>> print(entity)
48
+ Entity Name: BlueDelta, ID: L37nw-, Opportunity: 65, Intent: 65'
49
+ ```
50
+ Ordering:
51
+ The ordering of `ThreatMapEntity` instances is determined primarily by the `opportunity`
52
+ score followed by the `intent` and `prevalence`.
53
+ If two instances have the same scores, the `id_` is used as a last criterion.
54
+ """
55
+
56
+ id_: str = Field(alias='id')
57
+ name: str
58
+ alias: list[str]
59
+ categories: list[IdName]
60
+ intent: int | None = None
61
+ prevalence: int | None = None
62
+ opportunity: int
63
+ log_entries: list[LogEntry]
64
+
65
+ def __hash__(self):
66
+ return hash(self.id_)
67
+
68
+ def __eq__(self, other: 'ThreatMapEntity'):
69
+ return self.id_ == other.id_
70
+
71
+ def __gt__(self, other: 'ThreatMapEntity'):
72
+ return (self.opportunity, self.intent or 0, self.prevalence or 0, self.id_) > (
73
+ other.opportunity or 0,
74
+ other.intent or 0,
75
+ other.prevalence or 0,
76
+ other.id_,
77
+ )
78
+
79
+ def __str__(self):
80
+ key = 'intent' if self.intent is not None else 'prevalence'
81
+ score = getattr(self, key)
82
+ return (
83
+ f'Entity Name: {self.name}, ID: {self.id_}, '
84
+ f'Opportunity: {self.opportunity}, {key.capitalize()}: {score}'
85
+ )
86
+
87
+
88
+ class ThreatMap(RFBaseModel):
89
+ """Model for payload received by POST `/threat/map/{type}` endpoint."""
90
+
91
+ threat_map: list[ThreatMapEntity]
92
+ date: datetime = Field(description='Threat map generation timestamp')
93
+
94
+ def __str__(self):
95
+ return '\n'.join(str(entity) for entity in sorted(self.threat_map))
96
+
97
+
98
+ class ThreatMapInfo(RFBaseModel):
99
+ """Model for payload received by GET `/threat/maps` endpoint."""
100
+
101
+ name: str
102
+ type_: str = Field(alias='type')
103
+ organization: IdName
104
+ url: str
105
+
106
+
107
+ class EntityCategory(RFBaseModel):
108
+ """Model for payload received by GET `threat/{type}/categories` endpoint."""
109
+
110
+ id_: str = Field(alias='id')
111
+ type_: str = Field(alias='type')
112
+ attributes: EntityAttributes
113
+
114
+
115
+ class ThreatActorProfile(RFBaseModel):
116
+ """Model for payload received by POST `threat/actor/search` endpoint."""
117
+
118
+ id_: str = Field(alias='id')
119
+ type_: str = Field(alias='type')
120
+ attributes: ThreatActorAttributes
121
+
122
+ def __str__(self):
123
+ attr = self.attributes
124
+ common = f', Common Names: {", ".join(attr.common_names)}' if attr.common_names else ''
125
+ return f'ID: {self.id_} Name: {attr.name}' + common
126
+
127
+
128
+ class ThreatMapFetchIn(RFBaseModel):
129
+ """Model to validate `threat/map/{org}/{type}` endpoint payload sent."""
130
+
131
+ malware: Annotated[list[str] | None, BeforeValidator(Validators.convert_str_to_list)] = None
132
+ actors: Annotated[list[str] | None, BeforeValidator(Validators.convert_str_to_list)] = None
133
+ categories: Annotated[list[str] | None, BeforeValidator(Validators.convert_str_to_list)] = None
134
+ watchlists: Annotated[list[str] | None, BeforeValidator(Validators.convert_str_to_list)] = None
@@ -0,0 +1,173 @@
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 Annotated, Literal
16
+
17
+ from pydantic import Field, validate_call
18
+ from typing_extensions import Doc
19
+
20
+ from ..constants import DEFAULT_LIMIT
21
+ from ..endpoints import (
22
+ EP_ACTOR_SEARCH,
23
+ EP_CATEGORIES,
24
+ EP_THREAT_MAP,
25
+ EP_THREAT_MAP_ORG,
26
+ EP_THREAT_MAPS_LIST,
27
+ )
28
+ from ..helpers import debug_call
29
+ from ..helpers.helpers import connection_exceptions
30
+ from ..rf_client import RFClient
31
+ from .errors import (
32
+ ThreatActorSearchError,
33
+ ThreatMapCategoriesError,
34
+ ThreatMapFetchError,
35
+ ThreatMapInfoError,
36
+ )
37
+ from .models import ThreatMapType
38
+ from .threat_map import (
39
+ EntityCategory,
40
+ ThreatActorProfile,
41
+ ThreatMap,
42
+ ThreatMapFetchIn,
43
+ ThreatMapInfo,
44
+ )
45
+
46
+ MAP_TYPE = Literal['actors', 'malware']
47
+
48
+
49
+ class ThreatMapMgr:
50
+ """Manages requests for Recorded Future Threat Maps API."""
51
+
52
+ def __init__(
53
+ self,
54
+ rf_token: Annotated[str | None, Doc('Recorded Future API token.')] = None,
55
+ ):
56
+ """Initialize the `ThreatMapMgr` object."""
57
+ self.log = logging.getLogger(__name__)
58
+ self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
59
+
60
+ @debug_call
61
+ @validate_call
62
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ThreatMapInfoError)
63
+ def fetch_available_maps(
64
+ self,
65
+ ) -> Annotated[list[ThreatMapInfo], Doc('A list of available threat maps.')]:
66
+ """Fetch available threat maps for the organization.
67
+
68
+ Endpoint:
69
+ `threat/maps`
70
+
71
+ Raises:
72
+ ValidationError: If any supplied parameter is of incorrect type.
73
+ ThreatMapInfoError: If connection error occurs.
74
+ """
75
+ maps_response = self.rf_client.request(method='get', url=EP_THREAT_MAPS_LIST).json()['data']
76
+ return [ThreatMapInfo.model_validate(entry) for entry in maps_response]
77
+
78
+ @debug_call
79
+ @validate_call
80
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ThreatMapCategoriesError)
81
+ def fetch_entity_categories(
82
+ self,
83
+ map_type: Annotated[MAP_TYPE, Doc('Type of threat map.')],
84
+ ) -> Annotated[list[EntityCategory], Doc('A list of threat map taxonomy categories.')]:
85
+ """Fetch the entity category taxonomy used to filter threat maps.
86
+
87
+ Endpoint:
88
+ `threat/{type}/categories`
89
+
90
+ Raises:
91
+ ValidationError: If any supplied parameter is of incorrect type.
92
+ ThreatMapCategoriesError: If connection error occurs.
93
+ """
94
+ map_type = ThreatMapType(map_type)
95
+ url = EP_CATEGORIES.format(map_type.category_slug)
96
+ cat_response = self.rf_client.request(method='get', url=url).json()['data']
97
+ return [EntityCategory.model_validate(ent) for ent in cat_response]
98
+
99
+ @debug_call
100
+ @validate_call
101
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ThreatActorSearchError)
102
+ def search_threat_actor(
103
+ self,
104
+ name: Annotated[
105
+ str | None, Doc('Free text search of threat actor names, common names, or aliases.')
106
+ ] = None,
107
+ max_results: Annotated[
108
+ int | None, Doc('Limit the total number of results returned.')
109
+ ] = DEFAULT_LIMIT,
110
+ actors_per_page: Annotated[
111
+ int | None, Doc('The number of threat actors per page for pagination.')
112
+ ] = Field(ge=1, le=10_000, default=DEFAULT_LIMIT),
113
+ ) -> Annotated[
114
+ list[ThreatActorProfile], Doc('A list of threat actors matching the search criteria.')
115
+ ]:
116
+ """Search Recorded Future's threat actor database by name, alias, or classification.
117
+
118
+ Endpoint:
119
+ `threat/actor/search`
120
+
121
+ Raises:
122
+ ValidationError: If any supplied parameter is of incorrect type.
123
+ ThreatActorSearchError: If connection error occurs.
124
+ """
125
+ data = {
126
+ 'name': name,
127
+ 'limit': min(max_results or DEFAULT_LIMIT, actors_per_page or DEFAULT_LIMIT),
128
+ }
129
+ search_response = self.rf_client.request_paged(
130
+ method='post',
131
+ url=EP_ACTOR_SEARCH,
132
+ data=data,
133
+ results_path='data',
134
+ offset_key='offset',
135
+ max_results=max_results or DEFAULT_LIMIT,
136
+ )
137
+ return [ThreatActorProfile.model_validate(ta) for ta in search_response]
138
+
139
+ @debug_call
140
+ @validate_call
141
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=ThreatMapFetchError)
142
+ def fetch_map(
143
+ self,
144
+ map_type: Annotated[MAP_TYPE, Doc('Type of threat map.')],
145
+ org_id: Annotated[str | None, Doc('Organization ID.')] = None,
146
+ malware: Annotated[str | list[str] | None, Doc('Filter by malware entity ID(s).')] = None,
147
+ actors: Annotated[str | list[str] | None, Doc('Filter by threat actor ID(s).')] = None,
148
+ categories: Annotated[str | list[str] | None, Doc('Filter by category ID(s).')] = None,
149
+ watchlists: Annotated[str | list[str] | None, Doc('Filter by watch list ID(s).')] = None,
150
+ ) -> Annotated[ThreatMap, Doc('Threat map with entities matching filter criteria.')]:
151
+ """Fetch a threat map with optional entity, category, and watchlist filters.
152
+
153
+ Endpoint:
154
+ `threat/map/{type}` or `threat/map/{org_id}/{type}`
155
+
156
+ Raises:
157
+ ValidationError: If any supplied parameter is of incorrect type.
158
+ ThreatMapFetchError: If connection error occurs.
159
+ """
160
+ body = {'categories': categories, 'watchlists': watchlists}
161
+ map_type = ThreatMapType(map_type).value
162
+ if map_type is ThreatMapType.actors:
163
+ body['actors'] = actors
164
+ else:
165
+ body['malware'] = malware
166
+
167
+ url = (
168
+ EP_THREAT_MAP_ORG.format(org_id, map_type) if org_id else EP_THREAT_MAP.format(map_type)
169
+ )
170
+
171
+ data = ThreatMapFetchIn.model_validate(body).json()
172
+ map_response = self.rf_client.request(method='post', url=url, data=data).json()['data']
173
+ return ThreatMap.model_validate(map_response)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "psengine"
3
- version = "2.6.0"
3
+ version = "2.7.0"
4
4
  readme = "README.md"
5
5
  license = "MIT"
6
6
  requires-python = ">=3.10, <3.15"
@@ -26,15 +26,15 @@ classifiers=[
26
26
  "Programming Language :: Python :: 3.14"
27
27
  ]
28
28
 
29
- dependencies = [
30
- "typing_extensions >= 4.8.0",
29
+ dependencies = [
30
+ "typing_extensions >= 4.8.0",
31
31
  "requests>=2.27.1",
32
- "jsonpath_ng>=1.5.3, <=1.6.1",
32
+ "jsonpath_ng>=1.5.3, <=1.8.0",
33
33
  "stix2~=3.0.1",
34
34
  "python-dateutil>=2.7.0",
35
- "more-itertools>=9.0.0, <=10.2.0",
35
+ "more-itertools>=9.0.0, <=11.0.2",
36
36
  "pydantic>=2.7, <3.0.0",
37
- "pydantic-settings[toml]>=2.12.2,<2.13.1",
37
+ "pydantic-settings[toml]>=2.12.2,<2.14.1",
38
38
  "markdown-strings==3.4.0"
39
39
  ]
40
40
 
@@ -51,24 +51,24 @@ dev = [
51
51
  "pytest-mock==3.15.1",
52
52
  "pytest-md==0.2.0",
53
53
  "pytest-random-order==1.2.0",
54
- "pytest-httpdbg==0.10.1",
54
+ "pytest-httpdbg==0.10.2",
55
55
  "ruff~=0.15.8",
56
56
  "mimesis>=19.1.0",
57
57
  "freezegun>=1.5.5",
58
58
  ]
59
59
 
60
60
  docs = [
61
- "mike~=2.1.4",
61
+ "mike>=2.1.4,<2.3.0",
62
62
  "mkdocs~=1.6.1",
63
- "mkdocs-material~=9.6.18",
63
+ "mkdocs-material>=9.6.18,<9.8.0",
64
64
  "mkdocstrings[python]>=0.18",
65
- "griffe-typingdoc~=0.2.8",
65
+ "griffe-typingdoc>=0.2.8,<0.4.0",
66
66
  "mkdocs-codeinclude-plugin~=0.2.1",
67
67
  "markdown-include~=0.8.1",
68
68
  "mkdocs-exclude~=1.0.2",
69
69
 
70
70
  # Tools for examples
71
- "rich==14.3.3",
71
+ "rich==15.0.0",
72
72
  "logging-tree==1.10",
73
73
  ]
74
74
 
@@ -78,6 +78,7 @@ build-backend = "uv_build"
78
78
 
79
79
  [tool.uv]
80
80
  default-groups = "all"
81
+ exclude-newer = "7 days"
81
82
 
82
83
  [tool.uv.build-backend]
83
84
  module-root = ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes