psengine 2.7.0__tar.gz → 2.8.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 (163) hide show
  1. {psengine-2.7.0 → psengine-2.8.0}/PKG-INFO +2 -2
  2. {psengine-2.7.0 → psengine-2.8.0}/README.md +1 -1
  3. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/markdown.py +4 -1
  4. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/note.py +2 -2
  5. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/note_mgr.py +5 -6
  6. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/asi_mgr.py +5 -6
  7. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/classic_alert_mgr.py +5 -6
  8. {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/collective_insights.py +5 -6
  9. {psengine-2.7.0 → psengine-2.8.0}/psengine/endpoints.py +10 -0
  10. {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/fusion_mgr.py +5 -6
  11. {psengine-2.7.0 → psengine-2.8.0}/psengine/helpers/helpers.py +9 -0
  12. psengine-2.8.0/psengine/links/__init__.py +38 -0
  13. psengine-2.8.0/psengine/links/errors.py +26 -0
  14. psengine-2.8.0/psengine/links/links.py +152 -0
  15. psengine-2.8.0/psengine/links/links_mgr.py +214 -0
  16. psengine-2.8.0/psengine/links/models.py +94 -0
  17. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/auto_sigma_mgr.py +5 -6
  18. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/auto_yara_mgr.py +5 -6
  19. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/malware_intel_mgr.py +5 -6
  20. {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/risk_history_mgr.py +5 -6
  21. {psengine-2.7.0 → psengine-2.8.0}/pyproject.toml +1 -1
  22. {psengine-2.7.0 → psengine-2.8.0}/psengine/__init__.py +0 -0
  23. {psengine-2.7.0 → psengine-2.8.0}/psengine/_sdk_id.py +0 -0
  24. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/__init__.py +0 -0
  25. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/constants.py +0 -0
  26. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/errors.py +0 -0
  27. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/helpers.py +0 -0
  28. {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/models.py +0 -0
  29. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/__init__.py +0 -0
  30. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/asi.py +0 -0
  31. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/client.py +0 -0
  32. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/constants.py +0 -0
  33. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/errors.py +0 -0
  34. {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/models.py +0 -0
  35. {psengine-2.7.0 → psengine-2.8.0}/psengine/base_http_client.py +0 -0
  36. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/__init__.py +0 -0
  37. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/classic_alert.py +0 -0
  38. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/constants.py +0 -0
  39. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/errors.py +0 -0
  40. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/helpers.py +0 -0
  41. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
  42. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/markdown/markdown.py +0 -0
  43. {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/models.py +0 -0
  44. {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/__init__.py +0 -0
  45. {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/constants.py +0 -0
  46. {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/errors.py +0 -0
  47. {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/insight.py +0 -0
  48. {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/models.py +0 -0
  49. {psengine-2.7.0 → psengine-2.8.0}/psengine/common_models.py +0 -0
  50. {psengine-2.7.0 → psengine-2.8.0}/psengine/config/__init__.py +0 -0
  51. {psengine-2.7.0 → psengine-2.8.0}/psengine/config/config.py +0 -0
  52. {psengine-2.7.0 → psengine-2.8.0}/psengine/config/errors.py +0 -0
  53. {psengine-2.7.0 → psengine-2.8.0}/psengine/constants.py +0 -0
  54. {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/__init__.py +0 -0
  55. {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/detection_mgr.py +0 -0
  56. {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/detection_rule.py +0 -0
  57. {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/errors.py +0 -0
  58. {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/helpers.py +0 -0
  59. {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/models.py +0 -0
  60. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/__init__.py +0 -0
  61. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/constants.py +0 -0
  62. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/errors.py +0 -0
  63. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/lookup.py +0 -0
  64. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/lookup_mgr.py +0 -0
  65. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/__init__.py +0 -0
  66. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
  67. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/lookup.py +0 -0
  68. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/soar.py +0 -0
  69. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/soar.py +0 -0
  70. {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/soar_mgr.py +0 -0
  71. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/__init__.py +0 -0
  72. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/constants.py +0 -0
  73. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/entity_list.py +0 -0
  74. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
  75. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/errors.py +0 -0
  76. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/models.py +0 -0
  77. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/__init__.py +0 -0
  78. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/entity_match.py +0 -0
  79. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/entity_match_mgr.py +0 -0
  80. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/errors.py +0 -0
  81. {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/models.py +0 -0
  82. {psengine-2.7.0 → psengine-2.8.0}/psengine/errors.py +0 -0
  83. {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/__init__.py +0 -0
  84. {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/errors.py +0 -0
  85. {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/models.py +0 -0
  86. {psengine-2.7.0 → psengine-2.8.0}/psengine/helpers/__init__.py +0 -0
  87. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/__init__.py +0 -0
  88. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/constants.py +0 -0
  89. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/errors.py +0 -0
  90. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/identity.py +0 -0
  91. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/identity_mgr.py +0 -0
  92. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/__init__.py +0 -0
  93. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/common_models.py +0 -0
  94. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/detections.py +0 -0
  95. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/incident_report.py +0 -0
  96. {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/lookup.py +0 -0
  97. {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/__init__.py +0 -0
  98. {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/constants.py +0 -0
  99. {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/errors.py +0 -0
  100. {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/rf_logger.py +0 -0
  101. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/__init__.py +0 -0
  102. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/constants.py +0 -0
  103. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/errors.py +0 -0
  104. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/helpers.py +0 -0
  105. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/malware_intel.py +0 -0
  106. {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/models.py +0 -0
  107. {psengine-2.7.0 → psengine-2.8.0}/psengine/markdown/__init__.py +0 -0
  108. {psengine-2.7.0 → psengine-2.8.0}/psengine/markdown/markdown.py +0 -0
  109. {psengine-2.7.0 → psengine-2.8.0}/psengine/markdown/models.py +0 -0
  110. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/__init__.py +0 -0
  111. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/constants.py +0 -0
  112. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/errors.py +0 -0
  113. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/helpers.py +0 -0
  114. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/mappings.py +0 -0
  115. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/__init__.py +0 -0
  116. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown.py +0 -0
  117. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +0 -0
  118. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
  119. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
  120. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +0 -0
  121. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
  122. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_malware_report.py +0 -0
  123. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
  124. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/__init__.py +0 -0
  125. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/common_models.py +0 -0
  126. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
  127. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
  128. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
  129. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
  130. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
  131. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
  132. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
  133. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_malware_report.py +0 -0
  134. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
  135. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
  136. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/pa_category.py +0 -0
  137. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/playbook_alert_mgr.py +0 -0
  138. {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/playbook_alerts.py +0 -0
  139. {psengine-2.7.0 → psengine-2.8.0}/psengine/py.typed +0 -0
  140. {psengine-2.7.0 → psengine-2.8.0}/psengine/rf_client.py +0 -0
  141. {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/__init__.py +0 -0
  142. {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/errors.py +0 -0
  143. {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/models.py +0 -0
  144. {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/__init__.py +0 -0
  145. {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/constants.py +0 -0
  146. {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/errors.py +0 -0
  147. {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/models.py +0 -0
  148. {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/risklist_mgr.py +0 -0
  149. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/__init__.py +0 -0
  150. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/base_stix_entity.py +0 -0
  151. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/complex_entity.py +0 -0
  152. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/constants.py +0 -0
  153. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/enriched_indicator.py +0 -0
  154. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/errors.py +0 -0
  155. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/helpers.py +0 -0
  156. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/rf_bundle.py +0 -0
  157. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/simple_entity.py +0 -0
  158. {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/util.py +0 -0
  159. {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/__init__.py +0 -0
  160. {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/errors.py +0 -0
  161. {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/models.py +0 -0
  162. {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/threat_map.py +0 -0
  163. {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/threat_map_mgr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psengine
3
- Version: 2.7.0
3
+ Version: 2.8.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
@@ -42,7 +42,7 @@ PSEngine is a simple, yet elegant, library for rapid development of integrations
42
42
 
43
43
  PSEngine allows you to interact with the Recorded Future API extremely easily. There’s no need to manually build the URLs and query parameters, just use the modules dedicated to individual API endpoints.
44
44
 
45
- PSEngine is a Python package solely built and maintained by the Recorded Future Cyber Security Engineering team powering a number of high profile integrations, such as: [Banshee](https://recordedfuture-professionalservices.github.io/banshee); [Recorded Future Alerts for QRadar](https://apps.xforce.ibmcloud.com/extension/b36efdf42b7bf5e3759d036dbcdbf606); Anomali ThreatStream: [Alerts](https://support.recordedfuture.com/hc/en-us/articles/29255683708691-Recorded-Future-Alerts-for-Anomali-ThreatStream) and [Analyst Notes](https://support.recordedfuture.com/hc/en-us/articles/12928414947475-Recorded-Future-Analyst-Notes-for-Anomali-ThreatStream) integrations; [Google SecOps](https://app.recordedfuture.com/portal/integration-center/detail/google-chronicle-nbfi?organization=uhash%3A5cJsHMHeSM&filter_tab=all) and many more.
45
+ PSEngine is a Python package solely built and maintained by the Recorded Future Cyber Security Engineering team powering a number of high profile integrations, such as: [PS Banshee](https://recordedfuture-professionalservices.github.io/ps-banshee/latest/); [Recorded Future Alerts for QRadar](https://apps.xforce.ibmcloud.com/extension/b36efdf42b7bf5e3759d036dbcdbf606); Anomali ThreatStream: [Alerts](https://support.recordedfuture.com/hc/en-us/articles/29255683708691-Recorded-Future-Alerts-for-Anomali-ThreatStream) and [Analyst Notes](https://support.recordedfuture.com/hc/en-us/articles/12928414947475-Recorded-Future-Analyst-Notes-for-Anomali-ThreatStream) integrations; [Google SecOps](https://app.recordedfuture.com/portal/integration-center/detail/google-chronicle-nbfi?organization=uhash%3A5cJsHMHeSM&filter_tab=all) and many more.
46
46
 
47
47
 
48
48
  ## Installation
@@ -10,7 +10,7 @@ PSEngine is a simple, yet elegant, library for rapid development of integrations
10
10
 
11
11
  PSEngine allows you to interact with the Recorded Future API extremely easily. There’s no need to manually build the URLs and query parameters, just use the modules dedicated to individual API endpoints.
12
12
 
13
- PSEngine is a Python package solely built and maintained by the Recorded Future Cyber Security Engineering team powering a number of high profile integrations, such as: [Banshee](https://recordedfuture-professionalservices.github.io/banshee); [Recorded Future Alerts for QRadar](https://apps.xforce.ibmcloud.com/extension/b36efdf42b7bf5e3759d036dbcdbf606); Anomali ThreatStream: [Alerts](https://support.recordedfuture.com/hc/en-us/articles/29255683708691-Recorded-Future-Alerts-for-Anomali-ThreatStream) and [Analyst Notes](https://support.recordedfuture.com/hc/en-us/articles/12928414947475-Recorded-Future-Analyst-Notes-for-Anomali-ThreatStream) integrations; [Google SecOps](https://app.recordedfuture.com/portal/integration-center/detail/google-chronicle-nbfi?organization=uhash%3A5cJsHMHeSM&filter_tab=all) and many more.
13
+ PSEngine is a Python package solely built and maintained by the Recorded Future Cyber Security Engineering team powering a number of high profile integrations, such as: [PS Banshee](https://recordedfuture-professionalservices.github.io/ps-banshee/latest/); [Recorded Future Alerts for QRadar](https://apps.xforce.ibmcloud.com/extension/b36efdf42b7bf5e3759d036dbcdbf606); Anomali ThreatStream: [Alerts](https://support.recordedfuture.com/hc/en-us/articles/29255683708691-Recorded-Future-Alerts-for-Anomali-ThreatStream) and [Analyst Notes](https://support.recordedfuture.com/hc/en-us/articles/12928414947475-Recorded-Future-Analyst-Notes-for-Anomali-ThreatStream) integrations; [Google SecOps](https://app.recordedfuture.com/portal/integration-center/detail/google-chronicle-nbfi?organization=uhash%3A5cJsHMHeSM&filter_tab=all) and many more.
14
14
 
15
15
 
16
16
  ## Installation
@@ -36,9 +36,12 @@ EXTRACTED_KEYS = {
36
36
 
37
37
  def _cleanup_insikt_note_text(note_text: str) -> str:
38
38
  """Clean up insikt note text to avoid markdown rendering issues."""
39
+ note_text = ''.join(
40
+ line if line.lstrip().startswith('|') else re.sub(r'--+', '', line)
41
+ for line in note_text.splitlines(keepends=True)
42
+ )
39
43
  translation = {
40
44
  r'\•': '+ ',
41
- r'--+': '',
42
45
  r'>>+': '',
43
46
  r'<<+': '',
44
47
  r'\*\*': '••',
@@ -80,9 +80,9 @@ class AnalystNote(RFBaseModel):
80
80
  @property
81
81
  def detection_rule_type(self) -> str | None:
82
82
  """Returns the attachment type if present, else None. It checks for specific types like
83
- `sigma rule`, `yara rule`, and `snort rule` in the topics of the note.
83
+ `sigma rule`, `yara rule`, `snort rule` and `suricata rule` in the topics of the note.
84
84
  """
85
- topics_type = ('sigma rule', 'yara rule', 'snort rule')
85
+ topics_type = ('sigma rule', 'yara rule', 'snort rule', 'suricata rule')
86
86
 
87
87
  topics = (
88
88
  {topic.name.lower() for topic in self.attributes.topic if topic.name}
@@ -53,12 +53,11 @@ from .note import (
53
53
  class AnalystNoteMgr:
54
54
  """Manages requests for Recorded Future analyst notes."""
55
55
 
56
- def __init__(self, rf_token: str = None):
57
- """Initializes the `AnalystNoteMgr` object.
58
-
59
- Args:
60
- rf_token (str, optional): Recorded Future API token.
61
- """
56
+ def __init__(
57
+ self,
58
+ rf_token: Annotated[str | None, Doc('Recorded Future API token.')] = None,
59
+ ):
60
+ """Initializes the `AnalystNoteMgr` object."""
62
61
  self.log = logging.getLogger(__name__)
63
62
  self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
64
63
 
@@ -95,12 +95,11 @@ _ASSET_SEARCH_QUERY_MAP: dict[str, tuple[str, str, callable]] = {
95
95
  class AttackSurfaceMgr:
96
96
  """Manages requests for Recorded Future SecurityTrails (ASI) API."""
97
97
 
98
- def __init__(self, api_token: str = None):
99
- """Initializes the `AttackSurfaceMgr` object.
100
-
101
- Args:
102
- api_token (str, optional): ASI API token.
103
- """
98
+ def __init__(
99
+ self,
100
+ api_token: Annotated[str | None, Doc('ASI API token.')] = None,
101
+ ):
102
+ """Initializes the `AttackSurfaceMgr` object."""
104
103
  self.log = logging.getLogger(__name__)
105
104
  self.asi_client = ASIClient(api_token=api_token) if api_token else ASIClient()
106
105
 
@@ -43,12 +43,11 @@ from .errors import (
43
43
  class ClassicAlertMgr:
44
44
  """Alert Manager for Classic Alert (v3) API."""
45
45
 
46
- def __init__(self, rf_token: str = None):
47
- """Initializes the ClassicAlertMgr object.
48
-
49
- Args:
50
- rf_token (str, optional): Recorded Future API token.
51
- """
46
+ def __init__(
47
+ self,
48
+ rf_token: Annotated[str | None, Doc('Recorded Future API token.')] = None,
49
+ ):
50
+ """Initializes the ClassicAlertMgr object."""
52
51
  self.log = logging.getLogger(__name__)
53
52
  self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
54
53
 
@@ -29,12 +29,11 @@ from .insight import Insight, InsightsIn, InsightsOut
29
29
  class CollectiveInsights:
30
30
  """Class for interacting with the Recorded Future Collective Insights API."""
31
31
 
32
- def __init__(self, rf_token: str = None):
33
- """Initializes the CollectiveInsights object.
34
-
35
- Args:
36
- rf_token (str, optional): Recorded Future API token.
37
- """
32
+ def __init__(
33
+ self,
34
+ rf_token: Annotated[str | None, Doc('Recorded Future API token.')] = None,
35
+ ):
36
+ """Initializes the CollectiveInsights object."""
38
37
  self.log = logging.getLogger(__name__)
39
38
  self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
40
39
 
@@ -136,6 +136,16 @@ EP_AUTO_SIGMA_JOB_ID_RETRY = EP_AUTO_SIGMA_JOB_ID + '/retry'
136
136
  EP_RISK_HISTORY_BASE = BASE_URL + '/risk'
137
137
  EP_RISK_HISTORY = EP_RISK_HISTORY_BASE + '/history'
138
138
 
139
+ ###############################################################################
140
+ # Links API Endpoints
141
+ ###############################################################################
142
+ LINKS_BASE_URL = f'{BASE_URL}/links'
143
+ LINKS_METADATA_URL = f'{LINKS_BASE_URL}/metadata'
144
+ EP_LINKS_SEARCH = f'{LINKS_BASE_URL}/search'
145
+ EP_LINKS_METADATA_SECTIONS = f'{LINKS_METADATA_URL}/sections'
146
+ EP_LINKS_METADATA_EVENTS = f'{LINKS_METADATA_URL}/events'
147
+ EP_LINKS_METADATA_ENTITIES = f'{LINKS_METADATA_URL}/entities'
148
+
139
149
  ################################################################################
140
150
  # Attack Surface Intelligence API Endpoints
141
151
  ################################################################################
@@ -36,12 +36,11 @@ from .models import DirectoryListOut, FileDeleteOut, FileGetOut, FileHeadOut, Fi
36
36
  class FusionMgr:
37
37
  """Manages requests for Recorded Future Fusion files."""
38
38
 
39
- def __init__(self, rf_token: str = None):
40
- """Initializes the `FusionMgr` object.
41
-
42
- Args:
43
- rf_token (str, optional): Recorded Future API token.
44
- """
39
+ def __init__(
40
+ self,
41
+ rf_token: Annotated[str | None, Doc('Recorded Future API token.')] = None,
42
+ ):
43
+ """Initializes the `FusionMgr` object."""
45
44
  self.log = logging.getLogger(__name__)
46
45
  self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
47
46
 
@@ -504,6 +504,15 @@ class Validators:
504
504
  else input_time
505
505
  )
506
506
 
507
+ @staticmethod
508
+ def is_rel_time_valid(
509
+ input_time: Annotated[str | None, Doc("Relative time string, e.g., '7d', '3h'.")],
510
+ ):
511
+ """Check that a relative time like `-3d` is valid."""
512
+ if input_time is not None and not TimeHelpers.is_rel_time_valid(input_time):
513
+ raise ValueError(f'Invalid relative time: {input_time}')
514
+ return input_time
515
+
507
516
  @staticmethod
508
517
  def check_uhash_prefix(
509
518
  value: Annotated[str | list, Doc('String or list of strings to check for uhash prefix.')],
@@ -0,0 +1,38 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ from .errors import (
15
+ LinksError,
16
+ LinksMetadataError,
17
+ LinksSearchError,
18
+ )
19
+ from .links import (
20
+ EntityLinks,
21
+ LinkedEntity,
22
+ LinkedIOC,
23
+ LinkedMalware,
24
+ LinkedTA,
25
+ LinkedTTP,
26
+ )
27
+ from .links_mgr import LinksMgr
28
+ from .models import (
29
+ CriticalityAttribute,
30
+ EntityAttribute,
31
+ EntitySearchError,
32
+ GenericAttribute,
33
+ LinkSource,
34
+ MitreNameAttribute,
35
+ RiskAttribute,
36
+ SearchScope,
37
+ ThreatActorAttribute,
38
+ )
@@ -0,0 +1,26 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ from ..errors import RecordedFutureError
15
+
16
+
17
+ class LinksError(RecordedFutureError):
18
+ """Base class for all exceptions raised by the Links module."""
19
+
20
+
21
+ class LinksSearchError(LinksError):
22
+ """Error raised when a Links search request fails."""
23
+
24
+
25
+ class LinksMetadataError(LinksError):
26
+ """Error raised when fetching or validating Links metadata fails."""
@@ -0,0 +1,152 @@
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
+
15
+ from collections import defaultdict
16
+
17
+ from ..common_models import IdNameType, RFBaseModel
18
+ from .models import EntityAttribute, EntitySearchError, LinksFilterObjects, LinksLimitsObjects
19
+
20
+ LINK_IOC_TYPE = [
21
+ 'type:InternetDomainName',
22
+ 'type:CyberVulnerability',
23
+ 'type:IpAddress',
24
+ 'type:Hash',
25
+ 'type:Url',
26
+ ]
27
+
28
+
29
+ class LinkedIOC(IdNameType):
30
+ """Return linked iocs entities."""
31
+
32
+ risk_score: int
33
+ source: str | None = None
34
+
35
+
36
+ class LinkedTTP(IdNameType):
37
+ """Return linked TTPs entities."""
38
+
39
+ display_name: str
40
+ source: str | None = None
41
+
42
+
43
+ class LinkedTA(IdNameType):
44
+ """Return linked threat actors entities."""
45
+
46
+ source: str | None = None
47
+
48
+
49
+ class LinkedMalware(IdNameType):
50
+ """Return linked malware entities."""
51
+
52
+ source: str | None = None
53
+
54
+
55
+ class LinkedEntity(IdNameType):
56
+ """An entity connected to the search target."""
57
+
58
+ source: str | None = None
59
+ section: str | None = None
60
+ attributes: list[EntityAttribute] = []
61
+
62
+
63
+ class EntityLinks(RFBaseModel):
64
+ """The result set for a single entity that was queried."""
65
+
66
+ entity: IdNameType | None = None
67
+ links: list[LinkedEntity] = []
68
+ error: EntitySearchError | None = None
69
+
70
+ def iocs(self) -> list[LinkedIOC]:
71
+ """Return linked indicators of compromise grouped by IOC type."""
72
+ iocs = defaultdict(list)
73
+ for link in self.links:
74
+ if link.type_ in LINK_IOC_TYPE:
75
+ ioc_score = next(
76
+ (attr.value for attr in link.attributes if attr.id_ == 'risk_score'),
77
+ 0,
78
+ )
79
+ iocs[link.type_].append(
80
+ LinkedIOC(
81
+ id=link.id_,
82
+ type=link.type_,
83
+ name=link.name,
84
+ risk_score=ioc_score,
85
+ source=link.source,
86
+ )
87
+ )
88
+
89
+ return iocs
90
+
91
+ def ttps(self) -> list[LinkedTTP]:
92
+ """Return linked MITRE ATT&CK techniques and their display names."""
93
+ ttps = []
94
+ for link in self.links:
95
+ if link.type_ == 'type:MitreAttackIdentifier':
96
+ display_name = next(
97
+ (attr.value for attr in link.attributes if attr.id_ == 'display_name'),
98
+ 'N/A',
99
+ )
100
+ ttps.append(
101
+ LinkedTTP(
102
+ id=link.id_,
103
+ type=link.type_,
104
+ name=link.name,
105
+ display_name=display_name,
106
+ source=link.source,
107
+ )
108
+ )
109
+
110
+ return ttps
111
+
112
+ def threat_actors(self) -> list[LinkedTA]:
113
+ """Return linked organizations marked as threat actors."""
114
+ tas = []
115
+ for link in self.links:
116
+ if link.type_ == 'type:Organization':
117
+ is_threat_actor = next(
118
+ (attr.value for attr in link.attributes if attr.id_ == 'threat_actor'),
119
+ False,
120
+ )
121
+ if is_threat_actor:
122
+ tas.append(
123
+ LinkedTA(
124
+ id=link.id_,
125
+ type=link.type_,
126
+ name=link.name,
127
+ source=link.source,
128
+ )
129
+ )
130
+
131
+ return tas
132
+
133
+ def malwares(self) -> list[LinkedMalware]:
134
+ """Return linked malware entities."""
135
+ return [
136
+ LinkedMalware(
137
+ id=link.id_,
138
+ type=link.type_,
139
+ name=link.name,
140
+ source=link.source,
141
+ )
142
+ for link in self.links
143
+ if link.type_ == 'type:Malware'
144
+ ]
145
+
146
+
147
+ class LinksSearchIn(RFBaseModel):
148
+ """Model for payload sent to POST `/links/search` endpoint."""
149
+
150
+ entities: list[str]
151
+ filters: LinksFilterObjects | None = None
152
+ limits: LinksLimitsObjects | None = None
@@ -0,0 +1,214 @@
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
16
+
17
+ from pydantic import AfterValidator, Field, validate_call
18
+ from typing_extensions import Doc
19
+
20
+ from ..common_models import IdName
21
+ from ..endpoints import (
22
+ EP_LINKS_METADATA_ENTITIES,
23
+ EP_LINKS_METADATA_EVENTS,
24
+ EP_LINKS_METADATA_SECTIONS,
25
+ EP_LINKS_SEARCH,
26
+ )
27
+ from ..helpers import connection_exceptions, debug_call
28
+ from ..helpers.helpers import Validators
29
+ from ..rf_client import RFClient
30
+ from .errors import LinksMetadataError, LinksSearchError
31
+ from .links import (
32
+ EntityLinks,
33
+ LinksSearchIn,
34
+ )
35
+ from .models import (
36
+ FilterTechnical,
37
+ LinksFilterObjects,
38
+ LinksLimitsObjects,
39
+ LinkSource,
40
+ SearchScope,
41
+ )
42
+
43
+
44
+ class LinksMgr:
45
+ """Manager for interacting with the Recorded Future Links API."""
46
+
47
+ def __init__(
48
+ self,
49
+ rf_token: Annotated[str | None, Doc('Recorded Future API token.')] = None,
50
+ ):
51
+ """Initialize the `LinksMgr` object."""
52
+ self.log = logging.getLogger(__name__)
53
+ self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
54
+
55
+ @debug_call
56
+ @validate_call
57
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=LinksMetadataError)
58
+ def list_sections(
59
+ self,
60
+ ) -> Annotated[list[IdName], Doc('List of section objects with id and name.')]:
61
+ """List all sections that can be used to filter a Link search.
62
+
63
+ Sections are the high-level categories the Links API groups results into,
64
+ for example *Actors, Tools & TTPs* or *Indicators & Detection Rules*.
65
+
66
+ Endpoint:
67
+ `/links/metadata/sections`
68
+
69
+ Raises:
70
+ ValidationError: If any supplied parameter is of incorrect type.
71
+ LinksMetadataError: If an API or connection error occurs.
72
+ """
73
+ response = self.rf_client.request(method='GET', url=EP_LINKS_METADATA_SECTIONS)
74
+ return [IdName.model_validate(item) for item in response.json()['data']]
75
+
76
+ @debug_call
77
+ @validate_call
78
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=LinksMetadataError)
79
+ def list_events(
80
+ self,
81
+ ) -> Annotated[list[IdName], Doc('List of event objects with id and name.')]:
82
+ """List all event types that can be used to filter technical Link searches.
83
+
84
+ Event types describe the kind of analytical evidence that produced a
85
+ technical link (for example `TTPAnalysis` or `InfrastructureAnalysis`).
86
+
87
+ Endpoint:
88
+ `/links/metadata/events`
89
+
90
+ Raises:
91
+ ValidationError: If any supplied parameter is of incorrect type.
92
+ LinksMetadataError: If an API or connection error occurs.
93
+ """
94
+ response = self.rf_client.request(method='GET', url=EP_LINKS_METADATA_EVENTS)
95
+ return [IdName.model_validate(item) for item in response.json()['data']]
96
+
97
+ @debug_call
98
+ @validate_call
99
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=LinksMetadataError)
100
+ def list_entity_types(
101
+ self,
102
+ ) -> Annotated[list[IdName], Doc('List of entity-type objects with id and name.')]:
103
+ """List all entity types that can be used to filter a Link search.
104
+
105
+ The returned values are the supported types for connected entities
106
+ (for example `Malware`, `Company`, `IpAddress`).
107
+
108
+ Endpoint:
109
+ `/links/metadata/entities`
110
+
111
+ Raises:
112
+ ValidationError: If any supplied parameter is of incorrect type.
113
+ LinksMetadataError: If an API or connection error occurs.
114
+ """
115
+ response = self.rf_client.request(method='GET', url=EP_LINKS_METADATA_ENTITIES)
116
+ return [IdName.model_validate(item) for item in response.json()['data']]
117
+
118
+ @debug_call
119
+ @validate_call
120
+ @connection_exceptions(ignore_status_code=[], exception_to_raise=LinksSearchError)
121
+ def search(
122
+ self,
123
+ entities: Annotated[
124
+ Annotated[str | list[str], AfterValidator(Validators.convert_str_to_list)],
125
+ Doc('One or more Recorded Future entity IDs to look up links for.'),
126
+ ],
127
+ sections: Annotated[
128
+ str | list[str] | None, Doc('Filter results to these link section IDs.')
129
+ ] = None,
130
+ entity_types: Annotated[
131
+ str | list[str] | None,
132
+ Doc('Restrict linked entities to these entity types (e.g. "type:IpAddress").'),
133
+ ] = None,
134
+ sources: Annotated[
135
+ str | list[LinkSource] | None,
136
+ Doc('Limit to source type(s): "technical", "insikt", or both if argument omitted.'),
137
+ ] = None,
138
+ timeframe: Annotated[
139
+ str | None,
140
+ Doc('Technical-link timeframe (e.g. "-30d", default "-30d", max "-90d").'),
141
+ ] = None,
142
+ events: Annotated[
143
+ str | list[str] | None,
144
+ Doc('Restrict technical links to these event types (e.g. "type:MalwareAnalysis").'),
145
+ ] = None,
146
+ connected_entities: Annotated[
147
+ str | list[str] | None,
148
+ Doc('Only return technical links that connect to these entities.'),
149
+ ] = None,
150
+ search_scope: Annotated[
151
+ SearchScope | None,
152
+ Doc('Result-volume scope: "small", "medium" (default), or "large".'),
153
+ ] = 'medium',
154
+ per_entity_type: Annotated[
155
+ int | None,
156
+ Field(ge=1, le=1_000_000_000),
157
+ Doc('Max linked entities returned per entity type (>= 1 <= 1,000,000,000).'),
158
+ ] = None,
159
+ ) -> Annotated[
160
+ list[EntityLinks],
161
+ Doc('A list of EntityLinks objects'),
162
+ ]:
163
+ """Search for technically validated relationships between threat intelligence
164
+ entities in the Recorded Future Intelligence Cloud — connections established
165
+ through sandbox analysis, infrastructure analysis, network traffic analysis,
166
+ and Insikt Group research.
167
+
168
+ Issues a single batched request: the response contains one
169
+ `EntityLinks` per entity in `entities`, in the same order. If the
170
+ API failed for a specific entity, that result's `error` is populated
171
+ and `links` is empty — the rest of the batch still succeeds.
172
+
173
+ Entities must be supplied as Recorded Future entity IDs; if you only have
174
+ a name, resolve it with `EntityMatchMgr` first.
175
+
176
+ Endpoint:
177
+ `/links/search`
178
+
179
+ If the API failed for a specific entity in the batch, its result looks like:
180
+ ```python
181
+ EntityLinks(
182
+ entity=IdNameType(id_='QCwdoU', name='...', type_='...'),
183
+ links=[],
184
+ error=EntitySearchError(message='...', status_code=404),
185
+ )
186
+ ```
187
+
188
+ Raises:
189
+ ValidationError: If any supplied parameter is of incorrect type.
190
+ LinksSearchError: If an API or connection error occurs at the request level.
191
+ """
192
+ technical_filters = FilterTechnical(
193
+ timeframe=timeframe, events=events, connected_entities=connected_entities
194
+ ).json()
195
+
196
+ filters = LinksFilterObjects(
197
+ sections=sections,
198
+ entity_types=entity_types,
199
+ sources=sources,
200
+ technical=technical_filters or None,
201
+ ).json()
202
+ limits = LinksLimitsObjects(
203
+ search_scope=search_scope, per_entity_type=per_entity_type
204
+ ).json()
205
+ payload = LinksSearchIn(
206
+ entities=entities,
207
+ filters=filters or None,
208
+ limits=limits or None,
209
+ ).json()
210
+
211
+ self.log.info(f'Executing links search for {len(entities)} entities.')
212
+
213
+ response = self.rf_client.request(method='POST', url=EP_LINKS_SEARCH, data=payload)
214
+ return [EntityLinks.model_validate(item) for item in response.json()['data']]