"""
WAS
===
The following methods allow for interaction into the Tenable Vulnerability Management
:devportal:`WAS <was>` API endpoints.
Methods available on ``tio.was``:
.. rst-class:: hide-signature
.. autoclass:: WasAPI
:members:
"""
from typing import Any, Dict, Tuple
from tenable.io.base import TIOEndpoint, TIOIterator
from tenable.io.was.iterator import WasIterator
from tenable.utils import scrub
[docs]
class WasAPI(TIOEndpoint):
"""
This class contains methods related to WAS.
"""
[docs]
def export(self, **kwargs) -> WasIterator:
"""
Export Web Application Scan Results based on filters applied.
Args:
single_filter (tuple):
A single filter to apply to the scan configuration search. This is a
tuple with three elements - field, operator, and value in that order.
and_filter (list): An array of filters that must all be satisfied. This
is a list of tuples with three elements - field, operator, and value
in that order.
or_filter (list): An array of filters where at least one must be
satisfied. This is a list of tuples with three elements -
field, operator, and value in that order.
Returns:
WasIterator
Examples:
Passing AND filter to the API
>>> was_iterator = tio.was.export(
... and_filter=[
... ("scans_started_at", "gte", "2023/03/24"),
... ("scans_status", "contains", ["completed"])
... ]
... )
...
... for finding in was_iterator:
... print(finding)
"""
# Get scan configuration iterator.
scan_config = self._search_scan_configurations(**kwargs)
# Iterate through the scan configs and collect the parent scan IDs and the
# finalized_at param. This finalized_at property belonging to the parent will
# be passed down to its children's findings.
parent_scan_ids_with_finalized_at = [
_parent_id_with_finalized_at(sc) for sc in scan_config if sc
]
# initialize parent_scan_ids_with_finalized_at if it's empty.
if not parent_scan_ids_with_finalized_at:
parent_scan_ids_with_finalized_at = []
self._log.debug(
f'We have {len(parent_scan_ids_with_finalized_at)} parent scan ID(s) to process.'
)
# Fetch the target scans info for all the above parent scan IDs, and flatten it.
# We need to flatten because, each parent ID will have multiple target scans.
self._log.debug(
f'Fetching Target scan IDs for {len(parent_scan_ids_with_finalized_at)} parent scan ID(s)'
)
target_scans = [
scan
for p in parent_scan_ids_with_finalized_at
for scan in self._get_target_scan_ids_for_parent(p)
]
# Iterate through the target scans info and collect the target scan IDs.
target_scan_ids_with_parent_finalized_at = [
_target_id_with_parent_finalized_at(ts) for ts in target_scans if ts
]
self._log.debug(
f'We have {len(target_scan_ids_with_parent_finalized_at)} target scan(s) to process.'
)
return WasIterator(
api=self._api.was, target_scan_ids=target_scan_ids_with_parent_finalized_at
)
[docs]
def download_scan_report(self, scan_uuid: str) -> Dict:
"""
Downloads the individual target scan results.
Args:
scan_uuid (str):
UUID of the scan whose report to download.
"""
return self._api.get(
path=f'was/v2/scans/{scrub(scan_uuid)}/report',
headers={'Content-Type': 'application/json'},
).json()
def _search_scan_configurations(self, **kwargs) -> TIOIterator:
"""
Returns a list of web application scan configurations based on the provided filter parameters.
"""
payload = dict()
# Either single_filter should be passed alone. Or, any or all of these [and_filter, or_filter] can be passed.
if 'single_filter' in kwargs and (
('and_filter' in kwargs) or ('or_filter' in kwargs)
):
raise AttributeError(
'single_filter cannot be passed alongside and_filter or or_filter.'
)
if 'single_filter' in kwargs:
payload = _tuple_to_filter(kwargs['single_filter'])
if 'and_filter' in kwargs:
payload['AND'] = [_tuple_to_filter(t) for t in kwargs['and_filter']]
if 'or_filter' in kwargs:
payload['OR'] = [_tuple_to_filter(t) for t in kwargs['or_filter']]
self._log.debug(
f'Fetching the scan configuration information with filters: {payload} ...'
)
return TIOIterator(
self._api,
_limit=self._check('limit', 200, int),
_offset=self._check('offset', 0, int),
_query=dict(),
_path='was/v2/configs/search',
_method='POST',
_payload=payload,
_resource='items',
)
def _get_target_scan_ids_for_parent(self, parent: dict) -> list[dict[str, Any]]:
"""
Returns the vulns by target scans of the given parent scan ID.
"""
# This method does not have an iterator and is not public as the API it invokes has not been publicly documented.
# However, the API is in use in the Tenable Vulnerability Management UI.
parent_scan_id = parent['parent_scan_id']
parent_finalized_at = parent['parent_finalized_at']
offset = 0
limit = 200
# initialize the flattened responses list
flattened_list = []
while True:
# Fetch the page
response = self._api.post(
path=f'was/v2/scans/{scrub(parent_scan_id)}/vulnerabilities/by-targets/search?limit={limit}&offset={offset}'
).json()
# Collect the items; flatten; and write to the flattened list (extend).
items_in_response = response['items']
items = [
{'items': item, 'parent_finalized_at': parent_finalized_at}
for item in items_in_response
]
flattened_list.extend(items)
# Increment the page number by limit
offset += limit
if not items_in_response:
self._log.debug(
'Stopping the iteration as we encountered an empty response from the API.'
)
break
self._log.debug(
f'Parent ID: {parent_scan_id} has {len(flattened_list)} target ID(s).'
)
return flattened_list
def _tuple_to_filter(t: Tuple[str, str, Any]) -> Dict:
"""
Accepts a tuple with three elements, and returns a filter object.
"""
return {'field': t[0], 'operator': t[1], 'value': t[2]}
def _parent_id_with_finalized_at(scan_config: dict):
return {
'parent_scan_id': scan_config['last_scan']['scan_id'],
'parent_finalized_at': scan_config['last_scan']['finalized_at'],
}
def _target_id_with_parent_finalized_at(target_scan: dict):
return {
'target_scan_id': target_scan['items']['scan']['scan_id'],
'parent_finalized_at': target_scan['parent_finalized_at'],
}