Source code for tenable.io.was.api

"""
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'], }