pyTenable icon indicating copy to clipboard operation
pyTenable copied to clipboard

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

Open ingenium21 opened this issue 3 years ago • 3 comments

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):

  • OS: [e.g. MacOS] : Windows
  • Architecture [e.g. 64bit, 32bit] : 64bit
  • Version [e.g. 2.7.9] : 10
  • Memory [e.g. 4G]: 32GB

Additional context Add any other context about the problem here.

ingenium21 avatar Oct 04 '22 03:10 ingenium21

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 avatar Oct 04 '22 16:10 ingenium21

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

ingenium21 avatar Oct 06 '22 17:10 ingenium21

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.

ingenium21 avatar Oct 06 '22 17:10 ingenium21

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'])

SteveMcGrath avatar Oct 25 '22 14:10 SteveMcGrath

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

inayathulla avatar Nov 08 '22 06:11 inayathulla

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

ingenium21 avatar Nov 08 '22 11:11 ingenium21