psengine 2.8.4__tar.gz → 2.9.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.8.4 → psengine-2.9.0}/PKG-INFO +3 -2
- {psengine-2.8.4 → psengine-2.9.0}/README.md +1 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/note_mgr.py +5 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/config/config.py +33 -13
- {psengine-2.8.4 → psengine-2.9.0}/psengine/constants.py +2 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/endpoints.py +27 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/models/soar.py +0 -1
- {psengine-2.8.4 → psengine-2.9.0}/psengine/helpers/helpers.py +38 -5
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/malware_intel.py +1 -1
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/models.py +2 -2
- psengine-2.9.0/psengine/sandbox/__init__.py +29 -0
- psengine-2.9.0/psengine/sandbox/client.py +225 -0
- psengine-2.9.0/psengine/sandbox/constants.py +25 -0
- psengine-2.9.0/psengine/sandbox/errors.py +103 -0
- psengine-2.9.0/psengine/sandbox/models/__init__.py +13 -0
- psengine-2.9.0/psengine/sandbox/models/analysis.py +70 -0
- psengine-2.9.0/psengine/sandbox/models/behavioral_report.py +212 -0
- psengine-2.9.0/psengine/sandbox/models/overview_report.py +185 -0
- psengine-2.9.0/psengine/sandbox/models/static_report.py +144 -0
- psengine-2.9.0/psengine/sandbox/sandbox.py +493 -0
- psengine-2.9.0/psengine/sandbox/sandbox_mgr.py +1462 -0
- psengine-2.9.0/pyproject.toml +121 -0
- psengine-2.8.4/pyproject.toml → psengine-2.9.0/pyproject.toml.orig +5 -5
- psengine-2.8.4/psengine/py.typed +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/_sdk_id.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/helpers.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/markdown.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/analyst_notes/note.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/asi.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/asi_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/client.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/asi/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/base_http_client.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/classic_alert.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/classic_alert_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/helpers.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/markdown/markdown.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/classic_alerts/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/collective_insights/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/collective_insights/collective_insights.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/collective_insights/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/collective_insights/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/collective_insights/insight.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/collective_insights/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/common_models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/config/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/config/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/detection/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/detection/detection_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/detection/detection_rule.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/detection/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/detection/helpers.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/detection/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/lookup.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/lookup_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/models/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/models/lookup.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/soar.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/enrich/soar_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_lists/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_lists/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_lists/entity_list.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_lists/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_lists/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_match/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_match/entity_match.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_match/entity_match_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_match/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/entity_match/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/fusion/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/fusion/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/fusion/fusion_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/fusion/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/helpers/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/identity.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/identity_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/models/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/models/common_models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/models/detections.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/models/incident_report.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/identity/models/lookup.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/links/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/links/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/links/links.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/links/links_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/links/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/logger/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/logger/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/logger/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/logger/rf_logger.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/auto_sigma_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/auto_yara_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/helpers.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/malware_intel/malware_intel_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/markdown/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/markdown/markdown.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/markdown/markdown_strings.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/markdown/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/helpers.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/mappings.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_malware_report.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/common_models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_malware_report.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/pa_category.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/playbook_alert_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/playbook_alerts/playbook_alerts.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/rf_client.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risk_history/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risk_history/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risk_history/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risk_history/risk_history_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risklists/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risklists/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risklists/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risklists/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/risklists/risklist_mgr.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/base_stix_entity.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/complex_entity.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/constants.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/enriched_indicator.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/helpers.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/rf_bundle.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/simple_entity.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/stix2/util.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/threat_maps/__init__.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/threat_maps/errors.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/threat_maps/models.py +0 -0
- {psengine-2.8.4 → psengine-2.9.0}/psengine/threat_maps/threat_map.py +0 -0
- {psengine-2.8.4 → psengine-2.9.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.9.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
|
|
@@ -23,7 +23,7 @@ Requires-Dist: idna>=3.15
|
|
|
23
23
|
Requires-Dist: jsonpath-ng>=1.5.3,<=1.8.0
|
|
24
24
|
Requires-Dist: stix2~=3.0.2
|
|
25
25
|
Requires-Dist: python-dateutil>=2.7.0
|
|
26
|
-
Requires-Dist: more-itertools>=9.0.0,<=11.0
|
|
26
|
+
Requires-Dist: more-itertools>=9.0.0,<=11.1.0
|
|
27
27
|
Requires-Dist: pydantic>=2.7,<3.0.0
|
|
28
28
|
Requires-Dist: pydantic-settings[toml]>=2.14.2,<2.15.0
|
|
29
29
|
Requires-Python: >=3.10, <3.15
|
|
@@ -76,6 +76,7 @@ It can easily interact with the following Recorded Future datasets:
|
|
|
76
76
|
- On demand IOC enrichment
|
|
77
77
|
- Risklists
|
|
78
78
|
- Risk History
|
|
79
|
+
- Sandbox Detonation of files and URLs
|
|
79
80
|
- STIX conversion
|
|
80
81
|
|
|
81
82
|
|
|
@@ -92,6 +92,11 @@ class AnalystNoteMgr:
|
|
|
92
92
|
|
|
93
93
|
`max_results` is the maximum number of references, not notes.
|
|
94
94
|
|
|
95
|
+
Note:
|
|
96
|
+
Search returns a reduced `AnalystNote` projection. Fields such as
|
|
97
|
+
`diamond_model`, `events`, and `labels` are not populated and must be
|
|
98
|
+
retrieved per note via `lookup()`.
|
|
99
|
+
|
|
95
100
|
Endpoint:
|
|
96
101
|
`/analystnote/search`
|
|
97
102
|
|
|
@@ -18,7 +18,7 @@ from copy import deepcopy
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Annotated
|
|
20
20
|
|
|
21
|
-
from pydantic import Field, Secret, field_validator, validate_call
|
|
21
|
+
from pydantic import Field, Secret, ValidationInfo, field_validator, validate_call
|
|
22
22
|
from pydantic_settings import (
|
|
23
23
|
BaseSettings,
|
|
24
24
|
DotEnvSettingsSource,
|
|
@@ -32,6 +32,7 @@ from typing_extensions import Doc
|
|
|
32
32
|
|
|
33
33
|
from ..constants import (
|
|
34
34
|
ASI_TOKEN_ENV_VAR,
|
|
35
|
+
ASI_TOKEN_VALIDATION_REGEX,
|
|
35
36
|
BACKOFF_FACTOR,
|
|
36
37
|
POOL_MAX_SIZE,
|
|
37
38
|
REQUEST_TIMEOUT,
|
|
@@ -39,6 +40,8 @@ from ..constants import (
|
|
|
39
40
|
RF_TOKEN_ENV_VAR,
|
|
40
41
|
RF_TOKEN_VALIDATION_REGEX,
|
|
41
42
|
ROOT_DIR,
|
|
43
|
+
SANDBOX_TOKEN_ENV_VAR,
|
|
44
|
+
SANDBOX_TOKEN_VALIDATION_REGEX,
|
|
42
45
|
SSL_VERIFY,
|
|
43
46
|
STATUS_FORCELIST,
|
|
44
47
|
)
|
|
@@ -48,6 +51,20 @@ from .errors import ConfigFileError
|
|
|
48
51
|
PLAT_REGEX = r'^([A-Z]|[a-z])+(\/\d+)?((\.\d+)*?)$'
|
|
49
52
|
APP_ID_REGEX = r'^\S+\/\d+((\.\d+)*?)$'
|
|
50
53
|
|
|
54
|
+
TOKEN_CONFIG = {
|
|
55
|
+
'rf_token': (RF_TOKEN_ENV_VAR, RF_TOKEN_VALIDATION_REGEX, 'Recorded Future'),
|
|
56
|
+
'asi_token': (
|
|
57
|
+
ASI_TOKEN_ENV_VAR,
|
|
58
|
+
ASI_TOKEN_VALIDATION_REGEX,
|
|
59
|
+
'Recorded Future Attack Surface Intelligence',
|
|
60
|
+
),
|
|
61
|
+
'sandbox_token': (
|
|
62
|
+
SANDBOX_TOKEN_ENV_VAR,
|
|
63
|
+
SANDBOX_TOKEN_VALIDATION_REGEX,
|
|
64
|
+
'Recorded Future Sandbox',
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
|
|
52
69
|
class RFToken(Secret[str]):
|
|
53
70
|
"""Recorded Future token mask."""
|
|
@@ -93,6 +110,7 @@ class ConfigModel(BaseSettings):
|
|
|
93
110
|
app_id: str | None = Field(default=None, pattern=APP_ID_REGEX, examples=['get-alerts/1.0.0'])
|
|
94
111
|
rf_token: RFToken | None = Field(default=os.environ.get(RF_TOKEN_ENV_VAR, ''))
|
|
95
112
|
asi_token: RFToken | None = Field(default=os.environ.get(ASI_TOKEN_ENV_VAR, ''))
|
|
113
|
+
sandbox_token: RFToken | None = Field(default=os.environ.get(SANDBOX_TOKEN_ENV_VAR, ''))
|
|
96
114
|
http_proxy: str | None = None
|
|
97
115
|
https_proxy: str | None = None
|
|
98
116
|
client_ssl_verify: bool | None = SSL_VERIFY
|
|
@@ -159,28 +177,28 @@ class ConfigModel(BaseSettings):
|
|
|
159
177
|
raise ValueError('The config file extension must be .toml or .json or .env')
|
|
160
178
|
return tuple(sources)
|
|
161
179
|
|
|
162
|
-
@field_validator('rf_token', mode='before')
|
|
180
|
+
@field_validator('rf_token', 'asi_token', 'sandbox_token', mode='before')
|
|
163
181
|
@classmethod
|
|
164
182
|
def validate_token(
|
|
165
183
|
cls,
|
|
166
|
-
|
|
184
|
+
token: Annotated[str, Doc('API token value.')],
|
|
185
|
+
info: ValidationInfo,
|
|
167
186
|
) -> Annotated[str, Doc('Validated token.')]:
|
|
168
|
-
"""Validate a Recorded Future token.
|
|
187
|
+
"""Validate a Recorded Future API token (`rf_token`, `asi_token`, or `sandbox_token`).
|
|
169
188
|
|
|
170
189
|
Raises:
|
|
171
|
-
ValueError: When the token
|
|
190
|
+
ValueError: When the token does not match the expected format for its field.
|
|
172
191
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
192
|
+
env_var, regex, label = TOKEN_CONFIG[info.field_name]
|
|
193
|
+
token = token or os.environ.get(env_var)
|
|
194
|
+
if not token:
|
|
175
195
|
# Edge case: when RF_RF_TOKEN env var is set, it is used and RF_TOKEN is ignored
|
|
176
196
|
# So we check if RF_TOKEN is set and validate it
|
|
177
197
|
return ''
|
|
178
|
-
if not re.
|
|
179
|
-
raise ValueError(
|
|
180
|
-
f'Invalid Recorded Future API token.must match regex {RF_TOKEN_VALIDATION_REGEX}'
|
|
181
|
-
)
|
|
198
|
+
if not re.fullmatch(regex, token):
|
|
199
|
+
raise ValueError(f'Invalid {label} API token. Must match regex {regex}')
|
|
182
200
|
|
|
183
|
-
return
|
|
201
|
+
return token
|
|
184
202
|
|
|
185
203
|
@validate_call
|
|
186
204
|
def save_config(
|
|
@@ -198,7 +216,7 @@ class ConfigModel(BaseSettings):
|
|
|
198
216
|
"""
|
|
199
217
|
directory = Path(directory)
|
|
200
218
|
log = logging.getLogger(__name__)
|
|
201
|
-
data = self.model_dump_json(exclude=['rf_token', 'asi_token'], indent=4)
|
|
219
|
+
data = self.model_dump_json(exclude=['rf_token', 'asi_token', 'sandbox_token'], indent=4)
|
|
202
220
|
OSHelpers.mkdir(directory)
|
|
203
221
|
config_path = directory / file
|
|
204
222
|
log.info(f'Saving config in {config_path.as_posix()}')
|
|
@@ -245,6 +263,8 @@ class Config:
|
|
|
245
263
|
platform_id (str): Name & version of the tool this integrates with, example: ES/8.0.0.
|
|
246
264
|
app_id (str): Name & version of the integration itself, example: get-alerts/1.0.0.
|
|
247
265
|
rf_token (str): Recorded Future API token.
|
|
266
|
+
asi_token (str): Recorded Future Attack Surface Intelligence API token.
|
|
267
|
+
sandbox_token (str): Recorded Future Sandbox API token.
|
|
248
268
|
http_proxy (str): HTTP proxy.
|
|
249
269
|
https_proxy (str): HTTPS proxy.
|
|
250
270
|
client_ssl_verify (bool): SSL verification. Default is True.
|
|
@@ -26,6 +26,8 @@ RF_TOKEN_ENV_VAR = 'RF_TOKEN' # noqa: S105
|
|
|
26
26
|
RF_TOKEN_VALIDATION_REGEX = r'^[a-z0-9]{32}$' # noqa: S105
|
|
27
27
|
ASI_TOKEN_ENV_VAR = 'RF_ASI_TOKEN' # noqa: S105
|
|
28
28
|
ASI_TOKEN_VALIDATION_REGEX = r'^[a-zA-Z0-9-_]{32}$' # noqa: S105
|
|
29
|
+
SANDBOX_TOKEN_ENV_VAR = 'RF_SANDBOX_TOKEN' # noqa: S105
|
|
30
|
+
SANDBOX_TOKEN_VALIDATION_REGEX = r'^[a-f0-9]{40}$' # noqa: S105
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
#####################
|
|
@@ -158,6 +158,33 @@ EP_ASI_ASSETS_SEARCH = f'{EP_ASI_ASSETS}/_search'
|
|
|
158
158
|
EP_ASI_EXPOSURES = f'{EP_ASI_PROJECTS}/{{}}/exposures'
|
|
159
159
|
EP_ASI_EXPOSURES_BY_SIGNATURE = f'{EP_ASI_EXPOSURES}/{{}}'
|
|
160
160
|
|
|
161
|
+
###############################################################################
|
|
162
|
+
# Recorded Future Sandbox API Base URLs and Endpoints
|
|
163
|
+
###############################################################################
|
|
164
|
+
SANDBOX_API_VERSION = 'v0'
|
|
165
|
+
|
|
166
|
+
# Sandbox base URLs by region
|
|
167
|
+
SANDBOX_BASE_URLS = {
|
|
168
|
+
'eu': 'https://sandbox.recordedfuture.com/api/' + SANDBOX_API_VERSION,
|
|
169
|
+
'usa': 'https://us-sandbox.recordedfuture.com/api/' + SANDBOX_API_VERSION,
|
|
170
|
+
'apj': 'https://apj-sandbox.recordedfuture.com/api/' + SANDBOX_API_VERSION,
|
|
171
|
+
'public': 'https://tria.ge/api/' + SANDBOX_API_VERSION,
|
|
172
|
+
'private': 'https://private.tria.ge/api/' + SANDBOX_API_VERSION,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Sandbox Sample Endpoints (using {base_url} as placeholder)
|
|
176
|
+
EP_SANDBOX_SAMPLES = '{base_url}/samples'
|
|
177
|
+
EP_SANDBOX_SAMPLES_ID = '{base_url}/samples/{sample_id}'
|
|
178
|
+
EP_SANDBOX_SAMPLES_DOWNLOAD = '{base_url}/samples/{sample_id}/sample'
|
|
179
|
+
EP_SANDBOX_SAMPLES_SUMMARY = '{base_url}/samples/{sample_id}/summary'
|
|
180
|
+
EP_SANDBOX_SAMPLES_STATIC_REPORT = '{base_url}/samples/{sample_id}/reports/static'
|
|
181
|
+
EP_SANDBOX_SAMPLES_OVERVIEW = '{base_url}/samples/{sample_id}/overview.json'
|
|
182
|
+
EP_SANDBOX_SAMPLES_BEHAVIORAL = '{base_url}/samples/{sample_id}/{task_id}/report_triage.json'
|
|
183
|
+
EP_SANDBOX_SAMPLES_PROFILE = '{base_url}/samples/{sample_id}/profile'
|
|
184
|
+
EP_SANDBOX_SEARCH = '{base_url}/search'
|
|
185
|
+
EP_SANDBOX_PROFILES = '{base_url}/profiles'
|
|
186
|
+
EP_SANDBOX_PROFILES_ID = '{base_url}/profiles/{profile_id}'
|
|
187
|
+
|
|
161
188
|
################################################################################
|
|
162
189
|
# Threat Map API Endpoints
|
|
163
190
|
################################################################################
|
|
@@ -486,13 +486,46 @@ class Validators:
|
|
|
486
486
|
@staticmethod
|
|
487
487
|
def convert_str_to_list(
|
|
488
488
|
value: Annotated[str | list | None, Doc('String or list to convert.')],
|
|
489
|
-
) -> Annotated[list | None, Doc('Converted list
|
|
490
|
-
"""Convert value from str to list
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
489
|
+
) -> Annotated[list | None, Doc('Converted list, stripped and cleaned.')]:
|
|
490
|
+
"""Convert value from str to list, strip strings, drop None/empty strings.
|
|
491
|
+
|
|
492
|
+
Non-string items in a list are passed through unchanged so enum-typed
|
|
493
|
+
fields (e.g. `list[DomainTypes]`) keep their values.
|
|
494
|
+
"""
|
|
495
|
+
if value is None:
|
|
496
|
+
return None
|
|
497
|
+
if isinstance(value, str):
|
|
498
|
+
value = value.strip()
|
|
499
|
+
return [value] if value else []
|
|
500
|
+
if isinstance(value, list):
|
|
501
|
+
result = []
|
|
502
|
+
for v in value:
|
|
503
|
+
if v is None:
|
|
504
|
+
continue
|
|
505
|
+
if isinstance(v, str):
|
|
506
|
+
stripped = v.strip()
|
|
507
|
+
if not stripped:
|
|
508
|
+
continue
|
|
509
|
+
result.append(stripped)
|
|
510
|
+
else:
|
|
511
|
+
result.append(v)
|
|
512
|
+
return result
|
|
494
513
|
return value
|
|
495
514
|
|
|
515
|
+
@staticmethod
|
|
516
|
+
def none_to_empty_list(
|
|
517
|
+
value: Annotated[list | None, Doc('A list or `null`/empty value from the API.')],
|
|
518
|
+
) -> Annotated[list, Doc('The value, or [] when it is None/empty.')]:
|
|
519
|
+
"""Coerce a `null`/empty API value to a list so the field is always iterable."""
|
|
520
|
+
return value or []
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def empty_str_to_none(
|
|
524
|
+
value: Annotated[str | None, Doc('A value that may be an empty string.')],
|
|
525
|
+
) -> Annotated[str | None, Doc('None if the value is an empty string, else the value.')]:
|
|
526
|
+
"""Normalise an empty string (`''`) to None; pass everything else through."""
|
|
527
|
+
return None if value == '' else value
|
|
528
|
+
|
|
496
529
|
@staticmethod
|
|
497
530
|
def convert_relative_time(
|
|
498
531
|
input_time: Annotated[str, Doc("Relative time string, e.g., '7d', '3h'.")],
|
|
@@ -138,7 +138,7 @@ class AutoSigmaJobOut(RFBaseModel):
|
|
|
138
138
|
end_date: str | None = None
|
|
139
139
|
n_matched_hashes: int | None = None
|
|
140
140
|
family_counts: dict[str, int] = {}
|
|
141
|
-
patterns: list[SigmaPatternItem] =
|
|
141
|
+
patterns: list[SigmaPatternItem] = []
|
|
142
142
|
|
|
143
143
|
def __str__(self):
|
|
144
144
|
created = self.created.strftime(TIMESTAMP_STR) if self.created else 'N/A'
|
|
@@ -319,13 +319,13 @@ class YaraJob(RFBaseModel):
|
|
|
319
319
|
class SigmaPatternStats(RFBaseModel):
|
|
320
320
|
n_hashes: int | float | None = None
|
|
321
321
|
overlap: int | float | None = None
|
|
322
|
-
family_counts: dict[str, int | float] =
|
|
322
|
+
family_counts: dict[str, int | float] = {}
|
|
323
323
|
|
|
324
324
|
|
|
325
325
|
class SigmaPatternItem(RFBaseModel):
|
|
326
326
|
image: str | None = None
|
|
327
327
|
cmd_pattern: str | None = None
|
|
328
|
-
matched_cmds: list[str] =
|
|
328
|
+
matched_cmds: list[str] = []
|
|
329
329
|
stats: SigmaPatternStats | None = None
|
|
330
330
|
|
|
331
331
|
|
|
@@ -0,0 +1,29 @@
|
|
|
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 .sandbox import (
|
|
15
|
+
BehavioralReport,
|
|
16
|
+
BehavioralReportFailure,
|
|
17
|
+
BehavioralReportsResult,
|
|
18
|
+
OverviewReport,
|
|
19
|
+
Profile,
|
|
20
|
+
ProfileDeleteOut,
|
|
21
|
+
ProfileOptions,
|
|
22
|
+
ProfileUpdateOut,
|
|
23
|
+
Sample,
|
|
24
|
+
SampleDeleteOut,
|
|
25
|
+
SampleProfileOut,
|
|
26
|
+
SampleTasks,
|
|
27
|
+
StaticAnalysisReport,
|
|
28
|
+
)
|
|
29
|
+
from .sandbox_mgr import SandboxChoice, SandboxMgr, SandboxSubset
|
|
@@ -0,0 +1,225 @@
|
|
|
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 re
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
from pydantic import Field, validate_call
|
|
18
|
+
from requests.exceptions import JSONDecodeError
|
|
19
|
+
from requests.models import Response
|
|
20
|
+
from typing_extensions import Doc
|
|
21
|
+
|
|
22
|
+
from ..base_http_client import BaseHTTPClient
|
|
23
|
+
from ..constants import SANDBOX_TOKEN_VALIDATION_REGEX
|
|
24
|
+
from ..helpers import debug_call
|
|
25
|
+
from .constants import DEFAULT_PAGE_LIMIT, MAXIMUM_SAMPLES, SAMPLES_PER_PAGE
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@validate_call
|
|
29
|
+
def is_api_token_format_valid(
|
|
30
|
+
token: Annotated[str, Doc('A Recorded Future API token.')],
|
|
31
|
+
) -> Annotated[bool, Doc('True if the token format is valid, False otherwise.')]:
|
|
32
|
+
"""Check if the token format is valid.
|
|
33
|
+
|
|
34
|
+
The function performs a simple regex check but does not validate the token against the API.
|
|
35
|
+
"""
|
|
36
|
+
return re.fullmatch(SANDBOX_TOKEN_VALIDATION_REGEX, token) is not None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SandboxClient(BaseHTTPClient):
|
|
40
|
+
"""Recorded Future Sandbox API client."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
api_token: Annotated[
|
|
45
|
+
str | None,
|
|
46
|
+
Doc('The Sandbox API token. Defaults to RF_SANDBOX_TOKEN environment variable.'),
|
|
47
|
+
] = None,
|
|
48
|
+
http_proxy: Annotated[str, Doc('An HTTP proxy URL.')] = None,
|
|
49
|
+
https_proxy: Annotated[str, Doc('An HTTPS proxy URL.')] = None,
|
|
50
|
+
verify: Annotated[
|
|
51
|
+
str | bool,
|
|
52
|
+
Doc('An SSL verification flag or path to CA bundle.'),
|
|
53
|
+
] = None,
|
|
54
|
+
auth: Annotated[tuple[str, str], Doc('Basic Auth credentials.')] = None,
|
|
55
|
+
cert: Annotated[str | tuple[str, str] | None, Doc('Client certificates.')] = None,
|
|
56
|
+
timeout: Annotated[int, Doc('A request timeout. Defaults to 120.')] = None,
|
|
57
|
+
retries: Annotated[int, Doc('A number of retries. Defaults to 5.')] = None,
|
|
58
|
+
backoff_factor: Annotated[int, Doc('A backoff factor. Defaults to 1.')] = None,
|
|
59
|
+
status_forcelist: Annotated[
|
|
60
|
+
list, Doc('A list of status codes to force a retry. Defaults to [502, 503, 504].')
|
|
61
|
+
] = None,
|
|
62
|
+
pool_max_size: Annotated[
|
|
63
|
+
int, Doc('The maximum number of connections in the pool. Defaults to 120.')
|
|
64
|
+
] = None,
|
|
65
|
+
):
|
|
66
|
+
"""Recorded Future HTTP API client."""
|
|
67
|
+
super().__init__(
|
|
68
|
+
http_proxy=http_proxy,
|
|
69
|
+
https_proxy=https_proxy,
|
|
70
|
+
verify=verify,
|
|
71
|
+
auth=auth,
|
|
72
|
+
cert=cert,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
retries=retries,
|
|
75
|
+
backoff_factor=backoff_factor,
|
|
76
|
+
status_forcelist=status_forcelist,
|
|
77
|
+
pool_max_size=pool_max_size,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self._api_token = api_token or self.config.sandbox_token.get_secret_value()
|
|
81
|
+
if not self._api_token:
|
|
82
|
+
raise ValueError('Missing Recorded Future Sandbox API token.')
|
|
83
|
+
if not is_api_token_format_valid(self._api_token):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f'Invalid Sandbox API token: must match regex {SANDBOX_TOKEN_VALIDATION_REGEX}'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@debug_call
|
|
89
|
+
@validate_call
|
|
90
|
+
def request(
|
|
91
|
+
self,
|
|
92
|
+
method: Annotated[
|
|
93
|
+
str, Doc('An HTTP method, one of GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH.')
|
|
94
|
+
],
|
|
95
|
+
url: Annotated[str, Doc('A URL to make the request to.')],
|
|
96
|
+
data: Annotated[dict | list[dict] | bytes | None, Doc('A request body.')] = None,
|
|
97
|
+
*,
|
|
98
|
+
params: Annotated[dict | None, Doc('HTTP query parameters.')] = None,
|
|
99
|
+
headers: Annotated[
|
|
100
|
+
dict | None,
|
|
101
|
+
Doc('If specified, it overrides default headers and does not set the token.'),
|
|
102
|
+
] = None,
|
|
103
|
+
content_type_header: Annotated[
|
|
104
|
+
str | None, Doc('Content-Type header value.')
|
|
105
|
+
] = 'application/json',
|
|
106
|
+
**kwargs,
|
|
107
|
+
) -> Annotated[Response, Doc('A requests.Response object.')]:
|
|
108
|
+
"""Perform an HTTP request.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValidationError: If method is not one of GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH.
|
|
112
|
+
"""
|
|
113
|
+
headers = headers or self._prepare_headers(content_type_header)
|
|
114
|
+
|
|
115
|
+
return self.call(
|
|
116
|
+
method=method,
|
|
117
|
+
url=url,
|
|
118
|
+
headers=headers,
|
|
119
|
+
data=data,
|
|
120
|
+
params=params,
|
|
121
|
+
**kwargs,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@debug_call
|
|
125
|
+
@validate_call
|
|
126
|
+
def request_paged(
|
|
127
|
+
self,
|
|
128
|
+
method: Annotated[
|
|
129
|
+
str, Doc('An HTTP method, one of GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH.')
|
|
130
|
+
],
|
|
131
|
+
url: Annotated[str, Doc('A URL to make the request to.')],
|
|
132
|
+
data: Annotated[dict | list[dict] | bytes | None, Doc('A request body.')] = None,
|
|
133
|
+
*,
|
|
134
|
+
params: Annotated[dict | None, Doc('HTTP query parameters.')] = None,
|
|
135
|
+
headers: Annotated[
|
|
136
|
+
dict | None,
|
|
137
|
+
Doc('If specified, it overrides default headers and does not set the token.'),
|
|
138
|
+
] = None,
|
|
139
|
+
content_type_header: Annotated[
|
|
140
|
+
str | None, Doc('Content-Type header value.')
|
|
141
|
+
] = 'application/json',
|
|
142
|
+
max_results: int = DEFAULT_PAGE_LIMIT,
|
|
143
|
+
results_per_page: Annotated[
|
|
144
|
+
int | None, Doc('The number of samples per page for pagination.')
|
|
145
|
+
] = Field(ge=1, le=MAXIMUM_SAMPLES, default=SAMPLES_PER_PAGE),
|
|
146
|
+
**kwargs,
|
|
147
|
+
) -> Annotated[list, Doc('Result rows accumulated across all fetched pages.')]:
|
|
148
|
+
"""Perform a paged HTTP request, following `next` offsets until `max_results` is reached.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValidationError: If method is not one of GET, PUT, POST, DELETE, HEAD, OPTIONS, PATCH.
|
|
152
|
+
"""
|
|
153
|
+
headers = headers or self._prepare_headers(content_type_header)
|
|
154
|
+
# Copy so our `limit`/`offset` bookkeeping never mutates the caller's dict.
|
|
155
|
+
params = dict(params or {})
|
|
156
|
+
all_results: list = []
|
|
157
|
+
|
|
158
|
+
# Trim `limit` to remaining headroom so the last page doesn't overfetch.
|
|
159
|
+
params['limit'] = min(results_per_page, max_results - len(all_results))
|
|
160
|
+
response = self.request(
|
|
161
|
+
method=method,
|
|
162
|
+
url=url,
|
|
163
|
+
headers=headers,
|
|
164
|
+
data=data,
|
|
165
|
+
params=params,
|
|
166
|
+
**kwargs,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
json_response = response.json()
|
|
171
|
+
except JSONDecodeError:
|
|
172
|
+
self.log.debug(f'Paged request does not contain valid JSON:\n{response.text}')
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
page = json_response['data'] or []
|
|
177
|
+
except KeyError:
|
|
178
|
+
self.log.debug(f'Paged request does not contain `data` JSON key:\n{response.text}')
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
all_results.extend(page)
|
|
182
|
+
offset = json_response.get('next')
|
|
183
|
+
prev_offset = None
|
|
184
|
+
|
|
185
|
+
while offset and offset != prev_offset and len(all_results) < max_results:
|
|
186
|
+
params['offset'] = offset
|
|
187
|
+
params['limit'] = min(results_per_page, max_results - len(all_results))
|
|
188
|
+
response = self.request(
|
|
189
|
+
method=method,
|
|
190
|
+
url=url,
|
|
191
|
+
headers=headers,
|
|
192
|
+
data=data,
|
|
193
|
+
params=params,
|
|
194
|
+
**kwargs,
|
|
195
|
+
)
|
|
196
|
+
json_response = response.json()
|
|
197
|
+
page = json_response.get('data', [])
|
|
198
|
+
|
|
199
|
+
if not page:
|
|
200
|
+
# The last page is always empty
|
|
201
|
+
self.log.debug('Paged request returned an empty `data` page; stopping.')
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
all_results.extend(page)
|
|
205
|
+
prev_offset = offset
|
|
206
|
+
offset = json_response.get('next')
|
|
207
|
+
|
|
208
|
+
return all_results[:max_results]
|
|
209
|
+
|
|
210
|
+
def _prepare_headers(self, content_type_header: str | None = 'application/json'):
|
|
211
|
+
user_agent = self._get_user_agent_header()
|
|
212
|
+
headers = {
|
|
213
|
+
'User-Agent': user_agent,
|
|
214
|
+
'accept': 'application/json',
|
|
215
|
+
}
|
|
216
|
+
# `content_type_header=None` omits Content-Type so `requests` can set the
|
|
217
|
+
# multipart/form-data boundary itself (used by multipart file uploads).
|
|
218
|
+
if content_type_header is not None:
|
|
219
|
+
headers['Content-Type'] = content_type_header
|
|
220
|
+
if self._api_token:
|
|
221
|
+
headers['Authorization'] = f'Bearer {self._api_token}'
|
|
222
|
+
else:
|
|
223
|
+
# In theory should never happen, but just in case
|
|
224
|
+
self.log.warning('Request being made with no Sandbox API key set')
|
|
225
|
+
return headers
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
DEFAULT_PAGE_LIMIT = 10
|
|
15
|
+
SAMPLES_PER_PAGE = 50
|
|
16
|
+
MAXIMUM_SAMPLES = 200
|
|
17
|
+
|
|
18
|
+
STATIC_REPORT_WAIT_INTERVAL_SECONDS = 20
|
|
19
|
+
STATIC_REPORT_WAIT_DEFAULT_TIMEOUT_SECONDS = 600
|
|
20
|
+
|
|
21
|
+
BEHAVIORAL_REPORT_WAIT_INTERVAL_SECONDS = 20
|
|
22
|
+
BEHAVIORAL_REPORT_WAIT_DEFAULT_TIMEOUT_SECONDS = 1800
|
|
23
|
+
|
|
24
|
+
OVERVIEW_REPORT_WAIT_INTERVAL_SECONDS = 20
|
|
25
|
+
OVERVIEW_REPORT_WAIT_DEFAULT_TIMEOUT_SECONDS = 1800
|