Tenable.Tools.psm1

# Private Function Example - Replace With Your Function
function Add-PrivateFunction {

  [CmdletBinding()]

  Param (
    # Your parameters go here...
  )

  # Your function code goes here...
  Write-Output "Your private function ran!"

}

function Get-TioAsset {
  <#
  .SYNOPSIS
    Get Asset information
  .DESCRIPTION
    This function returns information about one or more Tenable.io Assets
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ById',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Id (UUID) of Asset for which to retrieve details')]
    [Alias("Id")]
    [string] $Uuid,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByHostname',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Hostname for which to retrieve details')]
    [string] $Hostname,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByIpv4',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'IPv4 for which to retrieve details')]
    [string] $IPv4
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "assets")
  }

  Process {
    if ($PSBoundParameters.ContainsKey('Uuid')) {
      # We're looking up a specific Id
      $Uri.Path = [io.path]::combine($Uri.Path, $Uuid)
    }

    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $Assets = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys

    if ($PSBoundParameters.ContainsKey('Hostname')) {
      Write-Verbose ('Checking Assets by Hostname: ' + $Hostname)
      # We're looking up a specific Hostname
      foreach ($Asset in $Assets.assets) {
        Write-Verbose (' Checking: ' + $Folder.name)
        if ($asset.hostname -eq $Hostname) {
          Write-Output $Asset
        }
      }
    } elseif ($PSBoundParameters.ContainsKey('IPv4')) {
      Write-Verbose ('Checking Folders by Type: ' + $IPv4)
      # We're looking up a specific Type
      foreach ($Asset in $Assets.assets) {
        Write-Verbose (' Checking: ' + $Asset.ipv4)
        if ($Asset.ipv4 -eq $Ipv4) {
          Write-Output $Asset
        }
      }
    } else {
      if ($Assets.assets) {
        Write-Output $Assets.assets
      } else {
        Write-Output $Assets
      }
    }
  }

  End {

  }
}

function Get-TioExportAsset {
  <#
  .SYNOPSIS
    Exports all assets that match the request criteria.
  .DESCRIPTION
    This function returns information about one or more Tenable.io Assets
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Filter
    Specifies filters for exported assets. To return all assets, omit the filters object. If
    your request specifies multiple filters, the system combines the filters using the AND search operator.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ByTag')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "POST",

    [Parameter(Mandatory=$false,
      HelpMessage = 'Results per chunk')]
    [int64] $ChunkSize = 1000,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ByFilter',
      HelpMessage = 'Filter condition')]
    [string] $Filter,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ByTag',
      HelpMessage = 'Tag Category Filter condition')]
    [string] $TagCategory,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ByTag',
      HelpMessage = 'Tag Value Filter condition')]
    [string] $TagValue,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ByUuid',
      HelpMessage = 'Tag Value Filter condition')]
    [string] $Uuid
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $RetryInterval = 30

    $Uri.Path = [io.path]::combine($Uri.Path, "assets/export")

    if (!$PSBoundParameters.ContainsKey('Uuid')) {
      # Starting a new search
      $Body = @{}
      $Body.Add('chunk_size',$ChunkSize)

      if ($PSBoundParameters.ContainsKey('TagCategory')) {
        $Body.Add('filters',@{})
        $Body.filters.add(('tag.' + $TagCategory),$TagValue)
      } elseif ($PSBoundParameters.ContainsKey('Filter')) {
        $Body.Add('filters',$Filter)
      }
    }

  }

  Process {

    if (!$PSBoundParameters.ContainsKey('Uuid')) {
      # Initiate the Asset Export
      Write-Verbose "$Me : Uri : $($Uri.Uri)"
      $AssetExport = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys -Method $Method -Body $Body

      $Uuid = $AssetExport.export_uuid
    }

    Write-Verbose ($Me + ': Asset Export ID: ' + $Uuid)
    # Start by checking the status
    $ExportStatus = Get-TioExportAssetStatus -ApiKeys $ApiKeys -Uuid $Uuid

    if ($ExportStatus.Error) {
      Write-Error ("$Me : Exception: $($ExportStatus.Code) : $($ExportStatus.Note)")
    }

    Write-Verbose ($Me + ': Asset Export Status: ' +$ExportStatus.status)

    # Wait until the export is finished
    while ($ExportStatus.status -ne 'FINISHED') {
      Start-Sleep -Seconds $RetryInterval

      $ExportStatus = Get-TioExportAssetStatus -ApiKeys $ApiKeys -Uuid $Uuid

      # Check for failures
      if ($ExportStatus.status -eq 'CANCELLED' -or $ExportStatus.status -eq 'ERROR') {
        Write-Error 'Asset Export Failed'
        exit 1
      }

      Write-Verbose ($Me + ': Asset Export Status: ' + $ExportStatus.status)
    }

    # We should have our results available for download now

    $Assets = @()

    foreach ($Chunk in $ExportStatus.chunks_available) {
      $Assets += Get-TioExportAssetChunk -ApiKeys $ApiKeys -Uuid $Uuid -Chunk $Chunk
    }

    Write-Output $Assets

  }

  End {

  }
}

function Get-TioExportAssetChunk {
  <#
  .SYNOPSIS
    Download exported asset chunk by ID.
  .DESCRIPTION
    Download exported asset chunk by ID. Chunks are available for download for up to 24 hours after they have been created.
    Tenable.io returns a 404 message for expired chunks.
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Uuid
    The UUID of the export request.
  .PARAMETER Chunk
    The ID of the Asset Chunk you want to download
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ById')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ById',
      HelpMessage = 'Asset Export UUID')]
    [string] $Uuid,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ById',
      HelpMessage = 'Asset Chunk Number')]
    [string] $Chunk
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "assets/export", $uuid, "chunks", $Chunk)

  }

  Process {
    # Initiate the Asset Export
    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $ExportChunk = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys -Method $Method -Body $Filter

    Write-Output $ExportChunk

  }

  End {

  }
}

function Get-TioExportAssetStatus {
  <#
  .SYNOPSIS
    Cancel an in-progress asset export
  .DESCRIPTION
    This function returns information about one or more Tenable.io Assets
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Filter
    Specifies filters for exported assets. To return all assets, omit the filters object. If
    your request specifies multiple filters, the system combines the filters using the AND search operator.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ById',
      HelpMessage = 'Filter condition')]
    [string] $Uuid
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "assets/export", $uuid, "status")

  }

  Process {
    # Get an updated asset export status
    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $ExportStatus = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys -Method $Method -Body $Filter

    if ($ExportStatus.exports) {
      Write-Output $ExportStatus.exports
    } else {
      Write-Output $ExportStatus
    }

  }

  End {

  }
}

function Stop-TioExportAsset {
  <#
  .SYNOPSIS
    Cancel an in-progress asset export
  .DESCRIPTION
    This function returns information about one or more Tenable.io Assets
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Filter
    Specifies filters for exported assets. To return all assets, omit the filters object. If
    your request specifies multiple filters, the system combines the filters using the AND search operator.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll',SupportsShouldProcess)]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "POST",

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ById',
      HelpMessage = 'Filter condition')]
    [string] $Uuid
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "assets/export", $uuid, "cancel")

  }

  Process {
    # Initiate the Asset Export
    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    if ($PSCmdlet.ShouldProcess($Uri.Uri, "Cancel Asset Export")) {
      $ExportStatus = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys -Method $Method -Body $Filter
    }

    Write-Output $ExportStatus

  }

  End {

  }
}

function Get-TioFolder {
  <#
  .SYNOPSIS
    Lists both Tenable-provided folders and the current user's custom folders.
  .DESCRIPTION
    This function returns information about one or more Tenable.io Folders
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ByName',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Name of folder for which to retrieve details')]
    [string] $Name,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ByType',
      HelpMessage = 'Type of folder for which to retrieve details')]
    [ValidateSet('main', 'trash', 'custom')]
    [string] $Type
    )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "folders")

  }

  Process {
    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $Folders = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys

    Write-Debug ($Folders | ConvertTo-Json -depth 10)

    if ($PSBoundParameters.ContainsKey('Name')) {
      Write-Verbose ('Checking Folders by Name: ' + $Name)
      # We're looking up a specific Name
      foreach ($Folder in $Folders.folders) {
        Write-Verbose (' Checking: ' + $Folder.name)
        if ($Folder.name -eq $Name) {
          Write-Output $Folder
        }
      }
    } elseif ($PSBoundParameters.ContainsKey('Type')) {
      Write-Verbose ('Checking Folders by Type: ' + $Type)
      # We're looking up a specific Type
      foreach ($Folder in $Folders.folders) {
        Write-Verbose (' Checking: ' + $Folder.type)
        if ($Folder.type -eq $Type) {
          Write-Output $Folder
        }
      }
    } else {
      if ($Folders.folders) {
        Write-Output $Folders.folders
      } else {
        Write-Output $Folders
      }
    }


  }

  End {

  }
}


function Invoke-TioApiRequest {
  <#
  .SYNOPSIS
    Invoke the Tenable.io API
  .DESCRIPTION
    This function is intended to be called by other functions for specific resources/interactions
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .PARAMETER Depth
    How deep are we going?
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding()]

  param(
    [Parameter(Mandatory=$true,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri,

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$false,
      HelpMessage = 'PsCustomObject containing data that will be sent as the Json Body')]
    [PsCustomObject] $Body,

    [Parameter(Mandatory=$false,
      HelpMessage = 'How deep are we?')]
    [int] $Depth = 0
    )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    if (($Method -eq 'GET') -and $Body) {
      throw "Cannot specify Request Body for Method GET."
    }

    $Header = @{}
    $ApiKey = "accessKey={0}; secretKey={1}" -f $ApiKeys.AccessKey.GetNetworkCredential().Password, $ApiKeys.SecretKey.GetNetworkCredential().Password
    $Header.Add('X-ApiKeys', ($ApiKey))
    $Header.Add('Content-Type', 'application/json')
    $Header.Add('Accept', 'application/json')

  }

  Process {
    # Setup Error Object structure
    $ErrorObject = [PSCustomObject]@{
      Code                  =   $null
      Error                 =   $false
      Type                  =   $null
      Note                  =   $null
      Raw                   =   $_
    }

    $Results = $null

    # Enforce TLSv1.2
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12

    # Make the API Call
    if ($Body) {
      # Make the API Call, using the supplied Body. Contents of $Body are the responsibility of the calling code.
      Write-Verbose "$Me : Body supplied"
      Write-Debug ("$Me : Body : " + ($Body | ConvertTo-Json -Depth 10 -Compress))

      try {
        $Results = Invoke-RestMethod -Method $Method -Uri $Uri.Uri -Headers $Header -Body ($Body|ConvertTo-Json -Depth 10) -ResponseHeadersVariable ResponseHeaders
      }
      catch {
        $Exception = $_.Exception
        Write-Verbose "$Me : Exception : $($Exception.Response.StatusCode.value__) : $($Exception.Message)"
        $ErrorObject.Error = $true
        $ErrorObject.Code = $Exception.Response.StatusCode.value__
        $ErrorObject.Note = $Exception.Message
        $ErrorObject.Raw = $Exception
        Write-Debug ($ErrorObject | ConvertTo-Json -Depth 10)

        return $ErrorObject
      }
      Write-Debug ($ResponseHeaders | ConvertTo-Json -Depth 5)
      Write-Debug ($Results | ConvertTo-Json -Depth 10)
    } else {
      # Make the API Call without a body. This is for GET requests, where details of what we want to get is in the URI
      Write-Verbose "$Me : No Body supplied"
      try {
        $Results = Invoke-RestMethod -Method $Method -Uri $Uri.Uri -Headers $Header -ResponseHeadersVariable ResponseHeaders
      }
      catch {
        $Exception = $_.Exception
        Write-Verbose "$Me : Exception : $($Exception.StatusCode)"
        $ErrorObject.Error = $true
        $ErrorObject.Code = $Exception.Response.StatusCode.value__
        $ErrorObject.Note = $Exception.Message
        $ErrorObject.Raw = $Exception
        # Write-Debug ($ErrorObject | ConvertTo-Json -Depth 2)

        Throw "$Me : Encountered error getting response. $($ErrorObject.Code) : $($ErrorObject.Note) from: $RelLink"

        return $ErrorObject
      }
    }
    Write-Verbose ($ResponseHeaders | ConvertTo-Json -Depth 5)

    Write-Output $Results
  }

  End {
    Write-Verbose "Returning from Depth: $Depth"
    return
  }
}

function New-TioCredential {
  <#
  .SYNOPSIS
    Build a new Tenable.io credential object
  .DESCRIPTION
    This function is intended to create a PSObject containing 2 PSCredential Objects containing the AccessKey and SecretKey
  .PARAMETER AccessKey
    String containing the Access Key. If not specified, user will be prompted.
  .PARAMETER SecretKey
    String containing the Secret Key. If not specified, user will be prompted.
  .OUTPUTS
    PSCustomObject containing the AccessKey and SecretKey PSCredential Object, containing the keys supplied.
  #>

  [CmdletBinding(SupportsShouldProcess=$true)]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Tenable.IO Access Key')]
        [string]  $AccessKey,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Tenable.IO Secret Key')]
    [string]  $SecretKey
    )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

  }

  Process {

    $Credential = @{}

    if ($AccessKey) {
      Write-Verbose 'Access Key Supplied'
      $AccessKeyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'AccessKey', ($AccessKey | ConvertTo-SecureString -AsPlainText -Force)
    } else {
      $AccessKeyCredential = (Get-Credential -UserName 'AccessKey' -Message 'Tenable.IO Access Key')
    }

    $Credential.Add('AccessKey', $AccessKeyCredential)

    if ($SecretKey) {
      Write-Verbose 'Secret Key Supplied'
      $SecretKeyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'SecretKey', ($SecretKey | ConvertTo-SecureString -AsPlainText -Force)
    } else {
      $SecretKeyCredential = (Get-Credential -UserName 'SecretKey' -Message 'Tenable.IO Secret Key')
    }

    $Credential.Add('SecretKey', $SecretKeyCredential)

    if ($PSCmdlet.ShouldProcess("Tenable.IO Credential", "Generate a new Tenable.IO Credential Object")) {
      Write-Output $Credential
    }
  }

  End {
    return
  }
}

function Get-TioMsspAccount {
  <#
  .SYNOPSIS
    Get Account information
  .DESCRIPTION
    This function returns information about one or more Tenable.io Accounts
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ById',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Id (UUID) of Account for which to retrieve details')]
    [Alias("Id")]
    [string] $Uuid,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByContainerName',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Container Name of account for which to retrieve details')]
    [string] $Container,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByCustomName',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Custom name of account for which to retrieve details')]
    [string] $Name,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByCustomName',
      HelpMessage = 'Require an exact match')]
    [switch] $Exact
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "mssp/accounts")
  }

  Process {
    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $Accounts = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys

    if ($PSBoundParameters.ContainsKey('Uuid')) {
      Write-Verbose ('Checking Accounts by Uuid: ' + $Uuid)
      # We're looking up a specific Uuid
      foreach ($Account in $Accounts.accounts) {
        Write-Verbose (' Checking: ' + $Account.uuid)
        if ($Account.uuid -eq $Uuid) {
          Write-Output $Account
        }
      }
    } elseif ($PSBoundParameters.ContainsKey('Container')) {
      Write-Verbose ('Checking Accounts by Container: ' + $Container)
      # We're looking up by Container Name
      foreach ($Account in $Accounts.Accounts) {
        Write-Verbose (' Checking: ' + $Account.container_name)
        if ($Account.container_name -eq $Container) {
          Write-Output $Account
        }
      }
    } elseif ($PSBoundParameters.ContainsKey('Name')) {
      Write-Verbose ('Checking Accounts by Custom Name: ' + $Name)
      # We're looking up by custom name
      foreach ($Account in $Accounts.Accounts) {
        if ($Exact) {
          Write-Verbose (' Checking (exact): ' + $Account.custom_name)
          if ($Account.custom_name -eq $Name) {
            Write-Output $Account
          }
        } else {
          Write-Verbose (' Checking (match): ' + $Account.custom_name)
          if ($Account.custom_name -match $Name) {
            Write-Output $Account
          }
        }
      }
    } else {
      if ($Accounts.accounts) {
        Write-Output $Accounts.accounts
      } else {
        Write-Output $Accounts
      }
    }
  }

  End {

  }
}

function Get-TioMsspLogo {
  <#
  .SYNOPSIS
    Get Logo information
  .DESCRIPTION
    This function returns information about one or more Tenable.io Logos
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ById',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Id (UUID) of Logo for which to retrieve details')]
    [Alias("Id")]
    [string] $Uuid,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByContainerId',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Container Id of Logo for which to retrieve details')]
    [string] $ContainerId,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByName',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Custom name of Logo for which to retrieve details')]
    [string] $Name,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByName',
      HelpMessage = 'Require an exact match')]
    [switch] $Exact
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "mssp/logos")
  }

  Process {
    if ($PSBoundParameters.ContainsKey('Uuid')) {
      # We're looking up a specific Id
      $Uri.Path = [io.path]::combine($Uri.Path, $Uuid)
    }

    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $Logos = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys

    if ($PSBoundParameters.ContainsKey('ContainerId')) {
      Write-Verbose ('Checking Logos by ContainerId: ' + $ContainerID)
      # We're looking up by Container Id
      foreach ($Logo in $Logos.Logos) {
        Write-Verbose (' Checking: ' + $Logo.container_uuid)
        if ($Logo.container_uuid -eq $ContainerId) {
          Write-Output $Logo
        }
      }
    } elseif ($PSBoundParameters.ContainsKey('Name')) {
      Write-Verbose ('Checking Logos by Custom Name: ' + $Name)
      # We're looking up by custom name
      foreach ($Logo in $Logos.logos) {
        if ($Exact) {
          Write-Verbose (' Checking (exact): ' + $Logo.name)
          if ($Logo.name -eq $Name) {
            Write-Output $Logo
          }
        } else {
          Write-Verbose (' Checking (match): ' + $Logo.name)
          if ($Logo.name -match $Name) {
            Write-Output $Logo
          }
        }
      }
    } else {
      if ($Logos.logos) {
        Write-Output $Logos.logos
      } else {
        Write-Output $Logos
      }
    }
  }

  End {

  }
}

function Set-TioMsspLogo {
  <#
  .SYNOPSIS
    Get Logo information
  .DESCRIPTION
    This function returns information about one or more Tenable.io Logos
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess)]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "PUT",

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ById',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Id (UUID) of Logo for which to assign on account(s)')]
    [Alias("Id")]
    [string] $Uuid,

    [Parameter(Mandatory=$true,
      ParameterSetName = 'ById',
      ValueFromPipeline = $true,
      ValueFromPipelineByPropertyName = $true,
      HelpMessage = 'Array of Account Ids to assign logo to')]
    [string[]] $Accounts
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "mssp/logos")
  }

  Process {

    Write-Verbose "$Me : Uri : $($Uri.Uri)"

    $Body = @{}
    $Body.Add('logo_uuid',$Uuid)

    if (($Accounts.GetType()).Name -eq 'String') {
      $AccountList = @()
      $AccountList += $Accounts
    } else {
      $AccountList = $Accounts
    }

    $Body.Add('account_uuids',$AccountList)

    if ($PSCmdlet.ShouldProcess("Logo ID $Uuid", "Assign to Accounts")) {
      $Logos = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys -Method $Method -Body $Body
    }

    Write-Output $Logos

  }

  End {

  }
}

function Get-TioTagCategory {
  <#
  .SYNOPSIS
    Get Tag Category information
  .DESCRIPTION
    This function returns information about one or more Tenable.io Tag Categories
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ById',
      HelpMessage = 'Id (UUID) of Category for which to retrieve details')]
    [string] $Uuid,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByFilter',
      HelpMessage = 'Filter condition')]
    [string] $Filter,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByName',
      HelpMessage = 'Get details of Asset Tag Categry by name')]
    [string] $Name
    )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "tags/categories")

    # Uri Parameter based processing
    $UriQuery = [System.Web.HttpUtility]::ParseQueryString([string]$Uri.Query)

    if ($PSBoundParameters.ContainsKey('Filter')) {
      $UriQuery.Add('f',$Filter)
    } elseif ($PSBoundParameters.ContainsKey('Name')) {
      $NameFilter = 'name:eq:{0}' -f $Name
      $UriQuery.Add('f',$NameFilter)
    }

    # Add the parameters to the URI object
    $Uri.Query = $UriQuery.ToString()
  }

  Process {
    if ($PSBoundParameters.ContainsKey('Uuid')) {
      # We're looking up a specific Id
      $Uri.Path = [io.path]::combine($Uri.Path, $Uuid)
    }

    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $Category = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys

    if ($Category.categories) {
      Write-Output $Category.categories
    } else {
      Write-Output $Category
    }
  }

  End {

  }
}

function Get-TioTagValue {
  <#
  .SYNOPSIS
    Get Tag Category information
  .DESCRIPTION
    This function returns information about one or more Tenable.io Tag Categories
  .PARAMETER Uri
    Base API URL for the API Call
  .PARAMETER ApiKeys
    PSObject containing PSCredential Objects with AccessKey and SecretKey.
    Must contain PSCredential Objects named AccessKey and SecretKey with the respective keys stored in the Password property
  .PARAMETER Method
    Valid HTTP Method to use: GET (Default), POST, DELETE, PUT
  .PARAMETER Body
    PSCustomObject containing data to be sent as HTTP Request Body in JSON format.
  .OUTPUTS
    PSCustomObject containing results if successful. May be $null if no data is returned
    ErrorObject containing details of error if one is encountered.
  #>

  [CmdletBinding(DefaultParameterSetName='ListAll')]

  param(
    [Parameter(Mandatory=$false,
      HelpMessage = 'Full URI to requested resource, including URI parameters')]
    [ValidateScript({
      $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
      if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') {
        [System.UriBuilder]$_
      }
    })]
        [System.UriBuilder]  $Uri = 'https://cloud.tenable.com',

    [Parameter(Mandatory=$true,
      HelpMessage = 'PSObject containing PSCredential Objects with AccessKey and SecretKey')]
    [PSObject]  $ApiKeys,

    [Parameter(Mandatory=$false,
      HelpMessage = 'Method to use when making the request. Defaults to GET')]
    [ValidateSet("Post","Get","Put","Delete")]
    [string] $Method = "GET",

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ById',
      HelpMessage = 'Id (UUID) of Tag Category Value for which to retrieve details')]
    [string] $Uuid,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByFilter',
      HelpMessage = 'Filter condition')]
    [string] $Filter,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByValue',
      HelpMessage = 'Get details of Asset Tag by Value')]
    [string] $Value,

    [Parameter(Mandatory=$false,
      ParameterSetName = 'ByCategoryName',
      HelpMessage = 'Get details of Asset Tag Vaues by Categry name')]
    [string] $CategoryName
    )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me

    $Uri.Path = [io.path]::combine($Uri.Path, "tags/values")

    # Uri Parameter based processing
    $UriQuery = [System.Web.HttpUtility]::ParseQueryString([string]$Uri.Query)

    if ($PSBoundParameters.ContainsKey('Filter')) {
      $UriQuery.Add('f',$Filter)
    } elseif ($PSBoundParameters.ContainsKey('Value')) {
      $TagFilter = 'value:eq:{0}' -f $Value
      $UriQuery.Add('f',$TagFilter)
    } elseif ($PSBoundParameters.ContainsKey('CategoryName')) {
      $TagFilter = 'category_name:eq:{0}' -f $CategoryName
      $UriQuery.Add('f',$TagFilter)
    }

    # Add the parameters to the URI object
    $Uri.Query = $UriQuery.ToString()
  }

  Process {
    if ($PSBoundParameters.ContainsKey('Uuid')) {
      # We're looking up a specific Id
      $Uri.Path = [io.path]::combine($Uri.Path, $Uuid)
    }

    Write-Verbose "$Me : Uri : $($Uri.Uri)"
    $Category = Invoke-TioApiRequest -Uri $Uri -ApiKeys $ApiKeys

    if ($Category.values) {
      Write-Output $Category.values
    } else {
      Write-Output $Category
    }
  }

  End {

  }
}

Export-ModuleMember -Function Get-TioAsset, Get-TioExportAsset, Get-TioExportAssetChunk, Get-TioExportAssetStatus, Stop-TioExportAsset, Get-TioFolder, Invoke-TioApiRequest, New-TioCredential, Get-TioMsspAccount, Get-TioMsspLogo, Set-TioMsspLogo, Get-TioTagCategory, Get-TioTagValue