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.
- {psengine-2.7.0 → psengine-2.8.0}/PKG-INFO +2 -2
- {psengine-2.7.0 → psengine-2.8.0}/README.md +1 -1
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/markdown.py +4 -1
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/note.py +2 -2
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/note_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/asi_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/classic_alert_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/collective_insights.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/endpoints.py +10 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/fusion_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/helpers/helpers.py +9 -0
- psengine-2.8.0/psengine/links/__init__.py +38 -0
- psengine-2.8.0/psengine/links/errors.py +26 -0
- psengine-2.8.0/psengine/links/links.py +152 -0
- psengine-2.8.0/psengine/links/links_mgr.py +214 -0
- psengine-2.8.0/psengine/links/models.py +94 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/auto_sigma_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/auto_yara_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/malware_intel_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/risk_history_mgr.py +5 -6
- {psengine-2.7.0 → psengine-2.8.0}/pyproject.toml +1 -1
- {psengine-2.7.0 → psengine-2.8.0}/psengine/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/_sdk_id.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/helpers.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/analyst_notes/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/asi.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/client.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/asi/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/base_http_client.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/classic_alert.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/helpers.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/markdown/markdown.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/classic_alerts/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/insight.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/collective_insights/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/common_models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/config/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/config/config.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/config/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/detection_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/detection_rule.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/helpers.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/detection/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/lookup.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/lookup_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/lookup.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/models/soar.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/soar.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/enrich/soar_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/entity_list.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_lists/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/entity_match.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/entity_match_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/entity_match/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/fusion/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/helpers/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/identity.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/identity_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/common_models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/detections.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/incident_report.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/identity/models/lookup.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/logger/rf_logger.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/helpers.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/malware_intel.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/malware_intel/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/markdown/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/markdown/markdown.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/markdown/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/helpers.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/mappings.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_malware_report.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/common_models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_malware_report.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/pa_category.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/playbook_alert_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/playbook_alerts/playbook_alerts.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/py.typed +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/rf_client.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risk_history/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/risklists/risklist_mgr.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/base_stix_entity.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/complex_entity.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/constants.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/enriched_indicator.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/helpers.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/rf_bundle.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/simple_entity.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/stix2/util.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/__init__.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/errors.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/models.py +0 -0
- {psengine-2.7.0 → psengine-2.8.0}/psengine/threat_maps/threat_map.py +0 -0
- {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.
|
|
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 `
|
|
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__(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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__(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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__(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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__(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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__(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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']]
|