tenable / pyTenable

Python Library for interfacing into Tenable's platform APIs
https://pytenable.readthedocs.io
MIT License
345 stars 172 forks source link

Unable to download scans with api access key and secret key; returns a 403. #628

Closed ingenium21 closed 1 year ago

ingenium21 commented 1 year ago

Describe the bug Having trouble downloading scans. My script does show that I am authenticating correctly, because I can retrieve a list of the scans, but when I call my function to download them, it fails with a 403 error and the error message: "Scan Result # failed". Not much else is given.

To Reproduce

relevant functions:

def create_export_folder(scan_name):
    if not os.path.exists("./Scans"):
        os.mkdir("./Scans")
    if not os.path.exists(f"./Scans/{scan_name}"):
        os.mkdir(f"./Scans/{scan_name}")
    if not os.path.exists("./tmp"):
        os.mkdir("./tmp")

def epoch_to_datetime(epoch_string):
    dt = datetime.fromtimestamp(int(epoch_string))
    dt = dt.strftime('%Y%m%d')
    return dt

def tenable_login(access, secret, instance):
    sc = TenableSC(instance)
    sc.access_key=access
    sc.secret_key=secret
    sc.login(access_key=access, secret_key=secret)
    return sc

def tenable_logout(sc):
    sc.logout()

def export_scans(sc, scans):
    scans.reverse()
    for scan in scans:
        if "TEDPW" in scan['name']:
            continue
        scan_date = (sc.scan_instances.details(scan['id'], fields=['finishTime']))['finishTime']
        if check_scan_age(scan_date):
            scan_date = epoch_to_datetime(scan_date)
            filename = f"{scan['name']}_{scan_date}.zip"
            create_export_folder(scan['name'])
            with open(f"Scans/{scan['name']}/{filename}", 'wb') as file_object:
                try:
                    sc.scan_instances.export_scan(scan['id'], fobj=file_object)
                except:
                    print(f"could not download {filename}")
            #Extract the zip file
            unzip_files(f"Scans/{scan['name']}/",filename)
        else:
            continue

def check_scan_age(scan_date, daysAgo=7):
    """Checks to only download the most recent scan that was done at the most 7 days ago. """
    now = datetime.now()
    check_date = now - timedelta(days=daysAgo)
    #convert to epoch
    scan_date = datetime.fromtimestamp(int(scan_date))
    if scan_date > check_date:
        return True
    else:
        return False

def unzip_files(filepath, filename):
    """will unzip a file you give it"""
    with ZipFile(f"{filepath}{filename}", "r") as zip_ref:
        zip_ref.extractall(filepath)

main function:

if __name__ == '__main__':
    access = os.getenv("ACCESS")
    secret = os.getenv("SECRET")
    instance = os.getenv("INSTANCE")
    sc = tenable_login(access, secret, instance)
    scans = sc.scan_instances.list()
    scans = scans['usable']
    export_scans(sc, scans)

Steps to reproduce the behavior:

  1. Go to 'main function'
  2. Call 'export_scans() function'
  3. See error

Expected behavior download the scans into zip files. unzip the files.

Screenshots If applicable, add screenshots to help explain your problem. image

System Information (please complete the following information):

Additional context Add any other context about the problem here.

ingenium21 commented 1 year ago

Here is the authentication function for TenableSC in platform.py I don't understand why it's saying it's an authenticated session.

    def _authenticate(self, **kwargs):
        '''
        This method handles authentication for both API Keys and for session
        authentication.
        '''
        # Here we are grafting the authentication functions into the keyword
        # arguments for later usage.  If a function is provided in the keywords
        # under the key names below, we will use those instead.  This should
        # essentially allow for the authentication logic to be overridden with
        # minimal effort.
        kwargs['key_auth_func'] = kwargs.get('key_auth_func',
                                             self._key_auth)
        kwargs['session_auth_func'] = kwargs.get('session_auth_func',
                                                 self._session_auth)

        # Pull the API keys from the keyword arguments passed to the
        # constructor and build the keys tuple.  As API Keys will be
        # injected directly into the session, there is no need to store these.
        keys = kwargs.get('_key_auth_dict', {
            'access_key': kwargs.get('access_key',
                                     os.getenv(f'{self._env_base}_ACCESS_KEY')
                                     ),
            'secret_key': kwargs.get('secret_key',
                                     os.getenv(f'{self._env_base}_SECRET_KEY')
                                     )
        })

        # The session authentication tuple.  We will be storing these as its
        # possible for the session to timeout on the user.  This would require
        # re-authentication.
        self._auth = kwargs.get('_session_auth_dict', {
            'username': kwargs.get('username',
                                   os.getenv(f'{self._env_base}_USERNAME')
                                   ),
            'password': kwargs.get('password',
                                   os.getenv(f'{self._env_base}_PASSWORD')
                                   )
        })

        # Run the desired authentication function.  As API keys are generally
        # preferred over session authentication, we will first check to see
        # that keys have been set, as we prefer stateless auth to stateful.
        if None not in [v for _, v in keys.items()]:
            kwargs['key_auth_func'](**keys)
        elif None not in [v for _, v in self._auth.items()]:
            kwargs['session_auth_func'](**self._auth)
        else:
            warnings.warn('Starting an unauthenticated session',
                          AuthenticationWarning)
            self._log.warning('Starting an unauthenticated session.')
ingenium21 commented 1 year ago

Hi I haven't heard from anyone about this, any chance someone could take a look?

ingenium21 commented 1 year ago

I'm now getting weirder results where it won't download the scans i launched on Tuesday, but it does grab the ones from the 30th.

SteveMcGrath commented 1 year ago

Sounds like there may be partial results or scans that may have failed. Also your unauth session warning is correct as the library have been preferring authenticating at instantiation for some time now and depregating the separate login method (which is only currently being kept intact for backwards compat).

Try something more like this:

#!/usr/bin/env python3
from typing import List, Optional
from tenable.sc import TenableSC
from zipfile import ZipFile
from pathlib import Path
import arrow

def export_scans(sc: TenableSC,
                 downloads: Optional[Path] = None,
                 ignore: List[str] = []) -> None:
    """
    Export scans from Tenable.sc into the specified folder
    """
    tf = {'false': False, 'true': True}
    if not downloads:
        downloads = Path('Scans')
    for scan in sc.scan_instances.list(fields=[
                                                'id',
                                                'name',
                                                'status',
                                                'downloadAvailable',
                                                'finishTime',
                                              ]
                                       )['usable']:
        skip = False
        for item in ignore:
            if item.lower() in scan['name'].lower():
                skip = True
        if skip:
            continue
        scan_date = arrow.get(int(scan['finishTime']))
        download_available = tf[scan['downloadAvailable']]
        if download_available:
            fn = f'{scan["name"]}_{scan_date.format("YYYY-MM-DD")}'
            scan_path = downloads.joinpath(fn)
            zfile = scan_path.joinpath(f'{fn}.zip')
            scan_path.mkdir(exist_ok=True, parents=True)
            with open(zfile, 'wb') as compressed:
                sc.scan_instances.export_scan(scan['id'], fobj=compressed)
                print(f'{scan["id"]} - {scan["name"]} Downloaded')
            with ZipFile(zfile, 'r') as zipfile:
                zipfile.extractall(scan_path)
                print(f'{scan["id"]} - {scan["name"]} Extracted')
        else:
            print(f'{scan["id"]} - {scan["name"]} Download isn\'t available')

if __name__ == '__main__':
    # Use the TSC_ACCESS_KEY, TSC_SECRET_KEY, and TSC_URL envvars
    sc = TenableSC()
    export_scans(sc, Path('Scans'), ['tedpw'])
inayathulla commented 1 year ago

Hi @ingenium21,

We hope you would have get rid of authentication issue with @SteveMcGrath solution, please revert us so we can close the issue.

Thanks pyTenable Team

ingenium21 commented 1 year ago

Hi, yes, you may close this case. Thank you all for your help!