diff --git a/tenb2jira/tenable/generators.py b/tenb2jira/tenable/generators.py index e82a93b..cb1a146 100644 --- a/tenb2jira/tenable/generators.py +++ b/tenb2jira/tenable/generators.py @@ -1,14 +1,15 @@ -import typing +from typing import TYPE_CHECKING, Generator, Optional, Any import arrow from restfly.utils import dict_flatten, dict_merge import uuid -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from tenable.io.exports.iterator import ExportsIterator from tenable.sc.analysis import AnalysisResultsIterator -def tvm_asset_cleanup(*assets_iters: 'ExportsIterator') -> dict: +def tvm_asset_cleanup(*assets_iters: 'ExportsIterator' + ) -> Generator[Any, Any, Any]: """ A simple wrapper to coalesce the multiple terminated asset states within TVM. @@ -26,8 +27,9 @@ def tvm_asset_cleanup(*assets_iters: 'ExportsIterator') -> dict: def tvm_merged_data(assets_iter: 'ExportsIterator', vulns_iter: 'ExportsIterator', - asset_fields: list[str] = None - ) -> dict: + asset_fields: Optional[list[str]] = None, + close_accepted: bool = True, + ) -> Generator[Any, Any, Any]: """ Merges the asset and vulnerability finding data together into a single object and adds in a computed finding id based on the following attributes: @@ -86,11 +88,19 @@ def spf(value: str) -> str: )) f['integration_pid_updated'] = pid + # If accepted risks shoudl be flagged as closed, then we will replace + # the state field with "fixed" if the risk was indeed accepted. + sevmod = f.get('severity_modification_type') + if close_accepted and sevmod == 'ACCEPTED': + f['state'] = 'FIXED' + # Return the augmented finding to the caller. yield f -def tsc_merged_data(*vuln_iters: 'AnalysisResultsIterator') -> dict: +def tsc_merged_data(*vuln_iters: 'AnalysisResultsIterator', + close_accepted: bool = True, + ) -> Generator[Any, Any, Any]: """ Flattens and extends the vulnerability results returned from the Security Center analysis API. The following fields are added to the @@ -124,7 +134,9 @@ def tsc_merged_data(*vuln_iters: 'AnalysisResultsIterator') -> dict: # If the hasBeenMitigated flag was flipped, then the finding isn't # open, but is reopened. We want to confer state accurately so we # will check that here. - if f['hasBeenMitigated'] == '1' and state == 'open': + if close_accepted and f['acceptRisk'] == '1': + f['integration_state'] = 'fixed' + elif f['hasBeenMitigated'] == '1' and state == 'open': f['integration_state'] = 'reopened' else: f['integration_state'] = state diff --git a/tenb2jira/tenable/tenable.py b/tenb2jira/tenable/tenable.py index 790ddda..da0b54b 100644 --- a/tenb2jira/tenable/tenable.py +++ b/tenb2jira/tenable/tenable.py @@ -1,4 +1,4 @@ -from typing import Generator +from typing import Generator, Any import arrow from tenable.io import TenableIO from tenable.sc import TenableSC @@ -14,6 +14,7 @@ class Tenable: platform: str timestamp: int age: int + close_accepted: bool severity: list[str] chunk_size: int = 1000 page_size: int = 1000 @@ -29,6 +30,9 @@ def __init__(self, config: dict): self.chunk_size = self.config['tenable']['tvm_chunk_size'] self.page_size = self.config['tenable']['tsc_page_size'] self.query_id = self.config['tenable'].get('tsc_query_id') + self.close_accepted = self.config['tenable'].get('fix_accepted_risks', + True + ) if not self.timestamp: self.timestamp = int(arrow.now() @@ -53,7 +57,7 @@ def __init__(self, config: dict): ) def get_generator(self) -> Generator: - self.last_run = arrow.now().timestamp() + self.last_run = int(arrow.now().timestamp()) if self.platform == 'tvm': assets = self.tvm.exports.assets(updated_at=self.timestamp, chunk_size=self.chunk_size @@ -62,9 +66,12 @@ def get_generator(self) -> Generator: severity=self.severity, state=['open', 'reopened', 'fixed'], include_unlicensed=True, - num_assets=self.chunk_size + num_assets=self.chunk_size, ) - return tvm_merged_data(assets, vulns) + return tvm_merged_data(assets, + vulns, + close_accepted=self.close_accepted, + ) if self.platform == 'tsc': sevmap = { 'info': '0', @@ -87,9 +94,12 @@ def get_generator(self) -> Generator: query_id=self.query_id, limit=self.page_size ) - return tsc_merged_data(cumulative, patched) + return tsc_merged_data(cumulative, + patched, + close_accepted=self.close_accepted, + ) - def get_asset_cleanup(self) -> Generator: + def get_asset_cleanup(self) -> (Generator[Any, Any, Any] | list): if self.platform == 'tvm': dassets = self.tvm.exports.assets(deleted_at=self.timestamp, chunk_size=self.chunk_size @@ -98,7 +108,5 @@ def get_asset_cleanup(self) -> Generator: chunk_size=self.chunk_size ) return tvm_asset_cleanup(dassets, tassets) - if self.platform == 'tsc': + else: return [] - - diff --git a/tenb2jira/version.py b/tenb2jira/version.py index 3a5e5af..cd4fe7b 100644 --- a/tenb2jira/version.py +++ b/tenb2jira/version.py @@ -1 +1 @@ -version = '2.0.5' +version = '2.0.6' diff --git a/tests/tenable/test_generators.py b/tests/tenable/test_generators.py index 8822046..60825bf 100644 --- a/tests/tenable/test_generators.py +++ b/tests/tenable/test_generators.py @@ -158,6 +158,31 @@ def test_tvm_merged_data(tvm_assets, tvm_finding): assert finding['asset.test'] == 'value' +@responses.activate +def test_tvm_merged_data_accepted(tvm_assets, tvm_finding): + pmoddate = arrow.get('2020-04-27T00:00:00Z') + test_uuid = UUID('dd13a88d-2fbf-3d2a-930f-38fdc850f86d') + responses.get('https://cloud.tenable.com/assets/export/0/status', + json={'status': 'FINISHED', 'available_chunks': []}) + tvm = TenableIO(access_key='None', secret_key='None') + asset_iter = ExportsIterator(tvm) + asset_iter.uuid = 0 + asset_iter.type = 'assets' + asset_iter.page = tvm_assets + tvm_finding['severity_modification_type'] = 'ACCEPTED' + finding_iter = ExportsIterator(tvm) + finding_iter.page = [tvm_finding for _ in range(100)] + tvm_generator = tvm_merged_data(assets_iter=asset_iter, + vulns_iter=finding_iter, + close_accepted=True, + ) + finding = next(tvm_generator) + assert finding['state'] == 'FIXED' + assert finding['asset.uuid'] == '7f68f334-17ba-4ba0-b057-b77ddd783e60' + assert finding['integration_finding_id'] == test_uuid + assert finding['integration_pid_updated'] == pmoddate + + def test_tsc_merged_data(tsc_finding): test_uuid = UUID('d90cdab5-b745-3e7e-9268-aa0f445ed924') fuuid = UUID('bd371510-001f-3c13-86f4-20883ef0cd09') @@ -184,3 +209,13 @@ def test_tsc_merged_data(tsc_finding): tsc_generator = tsc_merged_data(findings) finding = next(tsc_generator) assert finding['integration_state'] == 'fixed' + + tsc_finding['acceptRisk'] = '1' + findings = AnalysisResultsIterator(None) + findings.page = [tsc_finding for _ in range(100)] + findings._query = {'sourceType': 'cumulative'} + tsc_generator = tsc_merged_data(findings) + finding = next(tsc_generator) + assert finding['integration_state'] == 'fixed' + + diff --git a/tmpl_v1_conversion_config.toml b/tmpl_v1_conversion_config.toml index 5a856e2..f1f4ca2 100644 --- a/tmpl_v1_conversion_config.toml +++ b/tmpl_v1_conversion_config.toml @@ -57,6 +57,9 @@ tsc_page_size = 1000 # Management. You may need to adjust this number based on memory restrictions. tvm_chunk_size = 1000 +# Should accepted risks be treated as closed findings? +fix_accepted_risks = true + # The names of the platforms to be relayed to Jira for the "Tenable Platform" # field. Generally it's best to leave these values alone. platforms.tvm = "Tenable Vulnerability Management" diff --git a/tmpl_v2_new_config.toml b/tmpl_v2_new_config.toml index 02232f9..d35dcb9 100644 --- a/tmpl_v2_new_config.toml +++ b/tmpl_v2_new_config.toml @@ -57,6 +57,9 @@ tsc_page_size = 1000 # Management. You may need to adjust this number based on memory restrictions. tvm_chunk_size = 1000 +# Should accepted risks be treated as closed findings? +fix_accepted_risks = true + # The names of the platforms to be relayed to Jira for the "Tenable Platform" # field. Generally it's best to leave these values alone. platforms.tvm = "Tenable Vulnerability Management"