Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions tenable/tenableone/attack_path/export/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
ExportRequestStatus,
ExportSortParams,
FileFormat,
MitreHeatmapExportRequest,
MitreHeatmapFilter,
)


Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions tenable/tenableone/attack_path/export/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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."""

Expand Down
111 changes: 111 additions & 0 deletions tests/tenableone/attack_path/export/test_export_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
ExportStatus,
ExportSortParams,
FileFormat,
MitreHeatmapFilter,
MitreMatrix,
SortDirection,
)

Expand Down Expand Up @@ -414,6 +416,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
# ---------------------------------------------------------------------------
Expand Down
Loading