From 89bf2dfefe85831b1fe9e971f47dc16d32fed260 Mon Sep 17 00:00:00 2001 From: "mzamir.ctr" Date: Mon, 25 May 2026 12:37:45 +0300 Subject: [PATCH] [feature/VM-19509] Add MITRE Heatmap Export API method Adds mitre_heatmap() to the existing TenableOne attack_path Export API, mirroring the attack_paths()/attack_techniques() pattern. - New schemas: MitreHeatmapExportRequest, MitreHeatmapFilter, MitreMatrix - New method: ExportAPI.mitre_heatmap() -> POST /api/v1/export/mitre-heatmap - 4 new tests covering minimal, filter, csv-with-columns, ICS matrix - Status/download reuse existing ExportAPI.status()/download() (shared route) --- tenable/tenableone/attack_path/export/api.py | 48 ++++++++ .../tenableone/attack_path/export/schema.py | 48 ++++++++ .../attack_path/export/test_export_api.py | 111 ++++++++++++++++++ 3 files changed, 207 insertions(+) diff --git a/tenable/tenableone/attack_path/export/api.py b/tenable/tenableone/attack_path/export/api.py index 017b6bb27..b62f99c1b 100644 --- a/tenable/tenableone/attack_path/export/api.py +++ b/tenable/tenableone/attack_path/export/api.py @@ -26,6 +26,8 @@ ExportRequestStatus, ExportSortParams, FileFormat, + MitreHeatmapExportRequest, + MitreHeatmapFilter, ) @@ -139,6 +141,52 @@ def attack_techniques( ) return ExportRequestId(**response) + def mitre_heatmap( + self, + file_format: FileFormat, + filter: Optional[MitreHeatmapFilter] = None, + columns: Optional[List[str]] = None, + file_name: Optional[str] = None, + ) -> ExportRequestId: + """ + Export MITRE ATT&CK heatmap + + Args: + file_format (FileFormat): + The output file format. CSV emits a flat technique table; JSON + emits a Navigator-compatible layer document. + filter (MitreHeatmapFilter, optional): + Filter criteria for the heatmap (platform, query, severities, + matrix, show_all_techniques). + columns (list[str], optional): + Column names to include in the export. + file_name (str, optional): + Custom file name for the export. + + Returns: + ExportRequestId: + The export request ID. + + Examples: + >>> export = tenable_one.attack_path.export.mitre_heatmap( + ... file_format=FileFormat.JSON, + ... filter=MitreHeatmapFilter(matrix=MitreMatrix.ENTERPRISE), + ... ) + >>> print(export.export_id) + + """ + payload = MitreHeatmapExportRequest( + file_format=file_format, + filter=filter, + columns=columns, + file_name=file_name, + ).model_dump(mode='json', exclude_none=True) + + response = self._post( + 'api/v1/export/mitre-heatmap', json=payload + ) + return ExportRequestId(**response) + def status(self, export_id: str) -> ExportRequestStatus: """ Get export status diff --git a/tenable/tenableone/attack_path/export/schema.py b/tenable/tenableone/attack_path/export/schema.py index ae27f4624..1544cf80b 100644 --- a/tenable/tenableone/attack_path/export/schema.py +++ b/tenable/tenableone/attack_path/export/schema.py @@ -41,6 +41,13 @@ class AttackPathColumnKey(str, Enum): ASSET_IDS = 'asset_ids' +class MitreMatrix(str, Enum): + """MITRE ATT&CK matrix type.""" + + ENTERPRISE = 'enterprise' + ICS = 'ics' + + class AttackTechniqueColumnKey(str, Enum): """Column keys available for attack technique exports.""" @@ -148,6 +155,47 @@ class AttackTechniqueExportRequest(BaseModel): ) +class MitreHeatmapFilter(BaseModel): + """Filter for MITRE heatmap exports.""" + + platform: Optional[str] = Field( + None, description='Filter by platform (e.g. Windows, Linux, macOS)' + ) + query: Optional[str] = Field( + None, description='Search query to filter techniques by name' + ) + show_all_techniques: Optional[bool] = Field( + None, + description='When false, only show techniques with active findings', + ) + severities: Optional[List[str]] = Field( + None, description='Filter by severity levels' + ) + matrix: Optional[MitreMatrix] = Field( + None, description='MITRE matrix type (enterprise or ics)' + ) + + +class MitreHeatmapExportRequest(BaseModel): + """Request model for MITRE heatmap exports.""" + + file_format: FileFormat = Field(..., description='The output file format') + filter: Optional[MitreHeatmapFilter] = Field( + None, description='Filter criteria for the heatmap' + ) + columns: Optional[List[str]] = Field( + None, description='Columns to include in the export' + ) + file_name: Optional[str] = Field( + None, + max_length=100, + description=( + 'Optional custom file name for the export. ' + 'Defaults to Tenable_APA_MITRE_YYYY-MM-DD' + ), + ) + + class ExportRequestId(BaseModel): """Export request ID model.""" diff --git a/tests/tenableone/attack_path/export/test_export_api.py b/tests/tenableone/attack_path/export/test_export_api.py index 8010bb902..28eee5a3b 100644 --- a/tests/tenableone/attack_path/export/test_export_api.py +++ b/tests/tenableone/attack_path/export/test_export_api.py @@ -19,6 +19,8 @@ ExportStatus, ExportSortParams, FileFormat, + MitreHeatmapFilter, + MitreMatrix, SortDirection, ) @@ -415,6 +417,115 @@ def test_attack_techniques_all_columns(tenable_one_api, export_request_id_respon assert result.export_id == "export-ap-12345" +# --------------------------------------------------------------------------- +# mitre_heatmap() tests +# --------------------------------------------------------------------------- + +@responses.activate +def test_mitre_heatmap_minimal(tenable_one_api, export_request_id_response): + """Test mitre_heatmap with only required parameters.""" + expected_body = { + "file_format": "JSON", + } + + responses.add( + responses.POST, + "https://cloud.tenable.com/api/v1/export/mitre-heatmap", + json=export_request_id_response, + match=[responses.matchers.json_params_matcher(expected_body)], + ) + + result = tenable_one_api.attack_path.export.mitre_heatmap( + file_format=FileFormat.JSON, + ) + + assert isinstance(result, ExportRequestId) + assert result.export_id == "export-ap-12345" + + +@responses.activate +def test_mitre_heatmap_with_filter(tenable_one_api, export_request_id_response): + """Test mitre_heatmap with a populated filter.""" + expected_body = { + "file_format": "JSON", + "filter": { + "platform": "Windows", + "matrix": "enterprise", + "show_all_techniques": False, + "severities": ["high", "critical"], + }, + } + + responses.add( + responses.POST, + "https://cloud.tenable.com/api/v1/export/mitre-heatmap", + json=export_request_id_response, + match=[responses.matchers.json_params_matcher(expected_body)], + ) + + result = tenable_one_api.attack_path.export.mitre_heatmap( + file_format=FileFormat.JSON, + filter=MitreHeatmapFilter( + platform="Windows", + matrix=MitreMatrix.ENTERPRISE, + show_all_techniques=False, + severities=["high", "critical"], + ), + ) + + assert result.export_id == "export-ap-12345" + + +@responses.activate +def test_mitre_heatmap_csv_with_columns_and_file_name( + tenable_one_api, export_request_id_response +): + """Test mitre_heatmap with CSV format, custom columns, and file name.""" + expected_body = { + "file_format": "CSV", + "columns": ["mitre_id", "technique_name", "priority"], + "file_name": "mitre_export_2026_06", + } + + responses.add( + responses.POST, + "https://cloud.tenable.com/api/v1/export/mitre-heatmap", + json=export_request_id_response, + match=[responses.matchers.json_params_matcher(expected_body)], + ) + + result = tenable_one_api.attack_path.export.mitre_heatmap( + file_format=FileFormat.CSV, + columns=["mitre_id", "technique_name", "priority"], + file_name="mitre_export_2026_06", + ) + + assert result.export_id == "export-ap-12345" + + +@responses.activate +def test_mitre_heatmap_ics_matrix(tenable_one_api, export_request_id_response): + """Test mitre_heatmap with the ICS matrix.""" + expected_body = { + "file_format": "JSON", + "filter": {"matrix": "ics"}, + } + + responses.add( + responses.POST, + "https://cloud.tenable.com/api/v1/export/mitre-heatmap", + json=export_request_id_response, + match=[responses.matchers.json_params_matcher(expected_body)], + ) + + result = tenable_one_api.attack_path.export.mitre_heatmap( + file_format=FileFormat.JSON, + filter=MitreHeatmapFilter(matrix=MitreMatrix.ICS), + ) + + assert result.export_id == "export-ap-12345" + + # --------------------------------------------------------------------------- # status() tests # ---------------------------------------------------------------------------