tenable / pyTenable

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

Release 1.5.0 has caused issues with working code from 1.4.22 when updating credentials with SSH key using sc.credentials.edit #808

Closed samwmarsh closed 2 months ago

samwmarsh commented 2 months ago

Describe the bug We've noticed one of our pipelines for editing a credential begin to fail. This works if we pin to 1.4.22 so must have been introduced in 1.5.0.

To Reproduce

import os
from tenable.sc import TenableSC

SC_USERNAME='non_admin'
SC_PASSWORD=os.getenv('PASSWORD')
priv_key=open('id_rsa')
pub_key=open('id_rsa-cert')
sc = TenableSC('nessusmgr')
sc.login(SC_USERNAME,SC_PASSWORD)

CRED_ID=10000001
CRED_USER='scan_user'
CRED_PASSWORD=os.getenv('SCAN_PASSWORD')

sc.credentials.edit(cred_id,private_key=priv_key,public_key=pub_key,privilege_escalation='sudo',escalation_password=CRED_PASSWORD,usernameCRED_USER)

This returns a 400,

a bytes-like object is required, not 'str'

It's possible to get this to not return a 400 error by adding ,'rb' inside the open() for both key objects, however this wasn't the case prior to the 1.5.0 release.

Expected behavior The credential should update with the new key pair and escalation password.

SteveMcGrath commented 2 months ago

It's not "broken", just enforcing a BytesIO object instead of a StringIO object to be passed in. Passing files to the API as StringIO objects creates a lot of potential issues depending on how the files are formatted (ASCII vs UTF-8, CR vs CR/LF, etc.). While we aren't enforcing anything specific between BytesIO vs StringIO, the underlying requests and urllib3 libraries started enforcing that a while ago, and was likely surfaced when we addressed a bug in how the files API was handled and refactored it to use the appropriate multi-part encoder:

https://github.com/tenable/pyTenable/blob/master/tenable/sc/files.py

I'd suggest the following changes (below). The example uses a package called typer to wrap the function into a command-line script as well, however isn't necessary.

I would also suggest that to move away from session auth and move toward API Keys for integrations, as token auth for API interactions with Security Center has been deprecated for some time and is only working thanks to some backwards compatibility with the current authentication mechanisms, however is no longer guaranteed.

import os
import typer
from pathlib import path
from tenable.sc import TenableSC

# You should use API Keys, not Username and password.  The SDK will
# automatically use the following env vars:
# TSC_ACCESS_KEY=""
# TSC_SECRET_KEY=""
# TSC_URL="https://nessusmgr"

def update_credentials(public_key: Path,
                       private_key: Path,
                       cred_id: int,
                       username: str,
                       escalation_passwd: str,
                       ) -> Dict:
    """
    Updates the credentials with the specified details.

    Args:
        public_key (Path): The location of the public key file.
        private_key (Path): The location of the private key file.
        cred_id (int): The credential id to update.
        username (str): The username of the credential.
        escalation_passwd (str): The escalation password of the credential.
    """
    tsc = TenableSC()
    with public_key.open('rb') as pubkey, private_key.open('rb') as privkey:
        resp = tsc.credentials.edit(cred_id,
                                    private_key=privkey,
                                    public_key=pubkey,
                                    privilege_escalation='sudo',
                                    escalation_password=escalation_passwd,
                                    username=username
                                    )
    return resp

if __name__ == '__main__':
    typer.run(update_credentials)
samwmarsh commented 2 months ago

Thanks @SteveMcGrath, really good to know, will look to implement this