Source code for tenable.base.platform
'''
Base Platform
=============
The APIPlatform class is the base class that all platform packages will inherit
from. Throughout pyTenable v1, packages will be transitioning to using this
base class over the original APISession class.
.. autoclass:: APIPlatform
:members:
:inherited-members:
'''
import os
import warnings
from restfly import APISession as Base
from tenable.errors import AuthenticationWarning
from tenable.utils import url_validator
from tenable.version import version
[docs]class APIPlatform(Base):
'''
Base class for all API Platform packages. This class handles all of the
base connection logic.
Args:
adaptor (Object, optional):
A Requests Session adaptor to bind to the session object.
backoff (float, optional):
If a 429 response is returned, how much do we want to backoff
if the response didn't send a Retry-After header. If left
unspecified, the default is 1 second.
box (bool, optional):
Should responses be passed through Box? If left unspecified, the
default is ``True``.
box_attrs (dict, optional):
Any additional attributes to pass to the Box constructor for this
session? For a list of attributes that can be sent, please refer
to the
`Box documentation <https://github.com/cdgriffith/Box/wiki>`_
for more information.
build (str, optional):
The build number to put into the User-Agent string.
product (str, optional):
The product name to put into the User-Agent string.
proxies (dict, optional):
A dictionary detailing what proxy should be used for what
transport protocol. This value will be passed to the session
object after it has been either attached or created. For
details on the structure of this dictionary, consult the
:requests:`proxies <user/advanced/#proxies>` section of the
Requests documentation.
retries (int, optional):
The number of retries to make before failing a request. The
default is 5.
session (requests.Session, optional):
Provide a pre-built session instead of creating a requests
session at instantiation.
squash_camel (bool, optional):
Should the responses have CamelCase responses be squashed into
snake_case? If left unspecified, the default value is ``False``.
Note that this will only work when Box is enabled.
ssl_verify (bool, optional):
If SSL Verification needs to be disabled (for example when using
a self-signed certificate), then this parameter should be set to
``False`` to disable verification and mask the Certificate
warnings.
url (str, optional):
The base URL that the paths will be appended onto.
vendor (str, optional):
The vendor name to put into the User-Agent string.
'''
_lib_name = 'pyTenable'
_lib_version = version
_backoff = 1
_retries = 5
_env_base = ''
_auth = {}
_auth_mech = None
def __init__(self, **kwargs):
# if the constructed URL isn't valid, then we will throw a TypeError
# to inform the caller that something isn't right here.
self._url = kwargs.get('url',
os.environ.get(f'{self._env_base}_URL',
self._url
)
)
if not url_validator(self._url):
raise TypeError(f'{self._url} is not a valid URL')
# CamelCase squashing is an optional parameter thanks to Box. if the
# user has requested it, then we should add the appropriate parameter
# to the box_attrs.
if kwargs.get('squash_camel'):
box_attrs = kwargs.get('box_attrs', {})
box_attrs['camel_killer_box'] = bool(kwargs.pop('squash_camel'))
kwargs['box_attrs'] = box_attrs
# Call the RESTfly constructor
super().__init__(**kwargs)
def _session_auth(self, username, password):
'''
Default Session auth behavior
'''
self.post('session', json={
'username': username,
'password': password
})
self._auth_mech = 'user'
def _key_auth(self, access_key, secret_key):
'''
Default API Key Auth Behavior
'''
self._session.headers.update({
'X-APIKeys': f'accessKey={access_key}; secretKey={secret_key}'
})
self._auth_mech = 'keys'
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.')
def _deauthenticate(self, # noqa PLW0221
method: str = 'DELETE',
path: str = 'session'
):
'''
This method handles de-authentication. This is only necessary for
session-based authentication.
'''
if self._auth_mech == 'user':
self._req(method, path)
self._auth = {}
self._auth_mech = None