NexusIQ.psm1

"Import Microsoft.PowerShell.Commands.WebRequestMethod" | Out-Null

filter Invoke-NexusIQAPI
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Path,
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = "Get",
        # Use the api or rest extension
        [ValidateSet("api","rest","ui")]
        [string]$RequestType = "api",
        [Hashtable]$Parameters,
        $Body,
        [string]$ContentType,
        # Optionally where to output the result
        [string]$OutFile
    )
    $Settings = Get-NexusIQSettings
    $StringBuilder = [System.Text.StringBuilder]::new("$($Settings.BaseUrl)$RequestType")
    if ($RequestType -eq "api") { $StringBuilder.Append("/$($Settings.APIVersion.ToString())") | Out-Null }
    $StringBuilder.Append("/$Path") | Out-Null

    if ($Parameters)
    {
        $Separator = "?"
        $Parameters.Keys | ForEach-Object {
            $StringBuilder.Append(("{0}{1}={2}" -f $Separator,$_,[System.Web.HttpUtility]::UrlEncode($Parameters."$_".ToString()))) | Out-Null
            $Separator = "&"
        }
    }
    $Uri = $StringBuilder.ToString()
    Write-Verbose "Invoking Url $Uri"

    $Splat = @{
        Uri=$Uri
        Method=$Method
    }
    if ($PSVersionTable.PSVersion.Major -ge 6)
    {
        $Splat.Add("Authentication","Basic")
        $Splat.Add("Credential",$Settings.Credential)
    }
    else
    {
        $Pair = "$($Settings.Username):$($Settings.Credential.GetNetworkCredential().Password)"
        $EncodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Pair))
        $Headers = @{ Authorization = "Basic $EncodedCreds" }
        $Splat.Add("Headers",$Headers)
    }

    if ($ContentType) { $Splat.Add("ContentType",$ContentType) }
    # Either output to a file or save the response to the "Response" variable
    if ($PSBoundParameters.ContainsKey("Outfile")) { ($Splat.Add("OutFile",$OutFile) ) }
    else  { $Splat.Add("OutVariable","Response") }
    if ($Body) { $Splat.Add("Body",$Body) }

    Invoke-RestMethod @Splat | Out-Null
    if ($Response)
    {
        Write-Verbose "Unravel the response so it outputs each item to the pipeline instead of all at once"
        for ([UInt16]$i = 0; $i -lt $Response.Count; $i++) { $Response[$i] }
    }
}

class NexusIQSettings
{
    static [String]$SaveDir = "$env:APPDATA$([System.IO.Path]::DirectorySeparatorChar)NexusIQ"
    static [String]$SavePath = "$([NexusIQSettings]::SaveDir)$([System.IO.Path]::DirectorySeparatorChar)Auth.xml"

    # Parameters
    [String]$BaseUrl
    [PSCredential]$Credential
    [NexusIQAPIVersion]$APIVersion

    NexusIQSettings([PSCredential]$Credential,[uri]$BaseUrl,[NexusIQAPIVersion]$APIVersion)
    {
        $this.BaseUrl = $BaseUrl
        $this.Credential = $Credential
        $this.APIVersion = $APIVersion
    }
}

enum NexusIQAPIVersion
{
    v1
    v2
}

<#
.SYNOPSIS
    Retrieves all the applications in NexusIQ
#>

filter Find-NexusIQApplication
{
    [CmdletBinding()]
    param (
        # Name of the application to search for by wildcard.
        [SupportsWildcards()]
        [string]$Name
    )
    Write-Verbose "Listing all applications..."
    $Applications = Invoke-NexusIQAPI -Path "applications" | Select-Object -ExpandProperty applications
    if ($Name) { $Applications | Where-Object -Property name -Like $Name }
    else { $Applications }
}

<#
.SYNOPSIS
    Retrieves the Application in Nexus IQ based on its "Public ID"
.EXAMPLE
    Get-NexusIQApplication -ApplicationId App1Id
.EXAMPLE
    Get-NexusIQApplication -Name App1Name
.EXAMPLE
    Get-NexusIQApplication -Name App1*
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/application-rest-api---v2
#>

filter Get-NexusIQApplication
{
    [CmdletBinding(DefaultParameterSetName="Id")]
    param (
        # This is the application ID for the application. In the IQ Server GUI this is represented by the "Application" field. It must be unique., i.e. publicId
        [Parameter(Mandatory,ParameterSetName="Id",ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("ApplicationId")]
        [string[]]$PublicId,

        # This is the name of the application. In the IQ Server GUI this corresponds to the "Application Name" field. It must be unique.
        [Parameter(Mandatory,ParameterSetName="Name",Position=0)]
        [SupportsWildcards()]
        [string[]]$Name
    )
    if ($PublicId)
    {
        foreach ($AppId in $PublicId)
        {
            Invoke-NexusIQAPI -Path "applications" -Parameters @{ publicId=$AppId } | Select-Object -ExpandProperty applications -OutVariable Result
            if (-not $Result) { Write-Error "No application with ID $AppId" }
        }
    }
    elseif ($Name)
    {
        $AllApplications = Find-NexusIQApplication
        foreach ($AppName in $Name)
        {
            $AllApplications | Where-Object -Property name -Like $AppName | Select-Object -ExpandProperty publicId | ForEach-Object -Process {
                Write-Verbose "Found app with name $AppName and id $_"
                Invoke-NexusIQAPI -Path "applications" -Parameters @{ publicId=$_ } | Select-Object -ExpandProperty applications
            }
        }
    }
}

<#
.SYNOPSIS
    Retrieves the Application in Nexus IQ based on its "Public ID"
.EXAMPLE
    New-NexusIQApplication -ApplicationId AppId1 -Name "My Special App" -OrganizationName MyOrg
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/application-rest-api---v2
#>

filter New-NexusIQApplication
{
    [CmdletBinding()]
    param (
        # This is the application ID for the application. In the IQ Server GUI this is represented by the "Application" field. It must be unique., i.e. publicId
        [Parameter(Mandatory)]
        [Alias("ApplicationId")]
        [string]$PublicId,

        # This is the name of the application. In the IQ Server GUI this corresponds to the "Application Name" field. It must be unique.
        [Parameter(Mandatory)]
        [string]$Name,

        # Name of the organization to add this application to
        [Parameter(Mandatory)]
        [string]$OrganizationName
    )
    $Organization = Get-NexusIQOrganization -Name $OrganizationName
    Invoke-NexusIQAPI -Path "applications" -Method Post -Body (
        [PSCustomObject]@{
            publicId       = $PublicId
            name           = $Name
            organizationId = $Organization.id
        } | ConvertTo-Json
    ) -ContentType "application/json"
}

<#
.SYNOPSIS
    Removes an application
.EXAMPLE
    Remove-NexusIQApplication -ApplicationId AppId1
.EXAMPLE
    Get-NexusIQApplication -ApplicationId AppId1 | Remove-NexusIQApplication
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/application-rest-api---v2
#>

filter Remove-NexusIQApplication
{
    [CmdletBinding()]
    param (
        # This is the application ID for the application. In the IQ Server GUI this is represented by the "Application" field. It must be unique., i.e. publicId
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias("ApplicationId")]
        [string]$PublicId
    )
    # Halt if the application wasn't found
    $Application = Get-NexusIQApplication -PublicId $PublicId -ErrorAction Stop
    Invoke-NexusIQAPI -Path "applications/$($Application.id)" -Method Delete
}

<#
.SYNOPSIS
    Set an Application's properties in Nexus IQ based on its "Public ID"
.EXAMPLE
    Get-NexusIQApplication -PublicId App1Id | Set-NexusIQApplication -PublicId App2Id -Name "This is my renamed app"
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/application-rest-api---v2
#>

filter Set-NexusIQApplication
{
    [CmdletBinding()]
    param (
        # This is the application ID for the application. In the IQ Server GUI this is represented by the "Application" field. It must be unique., NOT the public Id
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [string]$Id,

        # The new Public Id to set the application to
        [string]$PublicId,

        # The new name
        [string]$Name
    )
    $IQApp = Invoke-NexusIQAPI -Path "applications/$Id"
    if ($PublicId) { $IQApp.publicId = $PublicId }
    if ($Name) { $IQApp.name = $Name }
    Invoke-NexusIQAPI -Path "applications/$Id" -Body ($IQApp | ConvertTo-Json -Depth 99) -Method Put -ContentType "application/json"
}

<#
.SYNOPSIS
    Retrieves the saved profile information of the current user, including BaseUrl and the userCode and passCode they are using. No parameters required.
#>

filter Get-NexusIQSettings
{
    [CmdletBinding()]
    [OutputType([NexusIQSettings])]
    param ()
    if (Test-Path -Path ([NexusIQSettings]::SavePath))
    {
        $XML = Import-Clixml -Path ([NexusIQSettings]::SavePath)
        [NexusIQSettings]::new($XML.Credential,$XML.BaseUrl,$XML.APIVersion)
    }
    else
    {
        throw "Use Connect-NexusIQ to create a login profile"
    }
}

<#
.SYNOPSIS
    Saves the user's Nexus IQ token using a saved PSCredential object stored as a CliXml file with an XML extension.
    Only works on a per-machine, per-user basis.
.PARAMETER Credential
    PSCredential where the username is the UserCode and the password is the PassCode. This will be passed to Nexus IQ when calling the
    API. PowerShell 7+ automatically formats the username and password properly using Base-64 encoding and Basic authentication.
.PARAMETER BaseUrl
    The URL of the Nexus IQ website
.EXAMPLE
    Save-NexusIQLogin -BaseUrl https://nexusiq.mycompany.com
.EXAMPLE
    # Reuse an existing profile's base URL and change the credentials
    $Settings = Get-NexusIQSettings
    $Settings | Connect-NexusIQ -Credential (Get-Credential)
#>

filter Connect-NexusIQ
{
    [CmdletBinding()]
    [Alias("Login-NexusIQ","Save-NexusIQLogin")]
    param (
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [string]$BaseUrl,

        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [PSCredential]$Credential,

        [Parameter(ValueFromPipeline)]
        [NexusIQAPIVersion]$APIVersion = "V2"
    )
    if (-not (Test-Path([NexusIQSettings]::SaveDir))) { New-Item -Type Directory -Path ([NexusIQSettings]::SaveDir) | Out-Null }
    else { Write-Verbose "Profile folder already existed" }

    $Settings = [NexusIQSettings]::new($Credential,$BaseUrl,$APIVersion)
    $Settings | Export-CliXml -Path ([NexusIQSettings]::SavePath) -Encoding 'utf8' -Force
    $Settings
}

<#
.SYNOPSIS
    Removes the user's login profile
.EXAMPLE
    Disconnect-NexusIQ
#>

filter Disconnect-NexusIQ
{
    [CmdletBinding()]
    [Alias("Logout-NexusIQ","Remove-NexusIQLogin")]
    param ()
    if (Test-Path ([NexusIQSettings]::SaveDir))
    {
        Remove-Item [NexusIQSettings]::SaveDir -Recurse
    }
    else
    {
        Write-Warning "The profile did not exist in path $([NexusIQSettings]::SaveDir)"
    }
}

<#
.SYNOPSIS
    Verifies the user can log into the system. No parameters required.
#>

filter Test-NexusIQLogin
{
    [CmdletBinding()]
    [OutputType([bool])]
    param ()
    try
    {
        if (Invoke-NexusIQAPI -Path "userTokens/currentUser/hasToken" -ErrorAction Stop) { $true }
    }
    catch { $false }
}

<#
.SYNOPSIS
    Retrieves an Organization in Nexus IQ
.EXAMPLE
    Get-NexusIQOrganization -Name MyOrg
.EXAMPLE
    "MyOrg1" | Get-NexusIQOrganization
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/organizations-rest-api---v2
#>

filter Get-NexusIQOrganization
{
    [CmdletBinding(DefaultParameterSetName="Id")]
    param (
        # Name of the organization
        [Parameter(ParameterSetName="Name",ValueFromPipeline,Position=0)]
        [string[]]$Name,

        # Id of the organization
        [Parameter(ParameterSetName="Id",ValueFromPipelineByPropertyName)]
        [string[]]$Id
    )
    if ($Name)
    {
        foreach ($OrgName in $Name)
        {
            Invoke-NexusIQAPI -Path "organizations" -Parameters @{ organizationName=$OrgName } |
            Select-Object -ExpandProperty organizations
        }
    }
    elseif ($Id)
    {
        foreach ($OrgId in $Id)
        {
            Invoke-NexusIQAPI -Path "organizations/$OrgId"
        }
    }
    else
    {
        (Invoke-NexusIQAPI -Path "organizations").organizations
    }
}

<#
.SYNOPSIS
    Retrieves polices and their associated organizations or applications. Basically makes it easier for the user to look up policies by
    doing all the heavy lifting under the hood.
.EXAMPLE
    Get-NexusIQPolicy -Type Organization -Name MyOrg
    # Retrieves all the policies of the specified organization
.EXAMPLE
    Get-NexusIQPolicy -Type Application -Name MyApp1
    # Retrieves the policies of the specified application
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/policy-violation-rest-api---v2#PolicyViolationRESTAPIv2-Step1-GetthePolicyIDs
#>

filter Get-NexusIQPolicy
{
    [CmdletBinding()]
    param (
        # Whether to retrieve an application's policies or an organization's
        [ValidateSet("Organization","Application")]
        [string]$Type,

        # Name of the organization to query for policies
        [string[]]$Name,

        # Name of the application to query for policies
        [Parameter(ParameterSetName="Application Name")]
        [string[]]$ApplicationName
    )
    switch ($Type)
    {
        "Organization" {
            $Organizations = Get-NexusIQOrganization -Name $Name
            $Name | Where-Object { $_ -notin $Organizations.name } | ForEach-Object {
                Write-Error "The organization '$_' was not found" -ErrorAction Stop
            }
            Get-NexusIQPolicyId | Where-Object -Property ownerType -EQ "ORGANIZATION" | Where-Object -Property ownerId -In $Organizations.id
            continue
        }
        "Application" {
            $Applications = Get-NexusIQApplication -Name $Name
            $Name | Where-Object { $_ -NotIn $Applications.name }  | ForEach-Object {
                Write-Error "The application '$_' was not found" -ErrorAction Stop
            }
            Get-NexusIQPolicyId | Where-Object -Property ownerType -EQ "APPLICATION" | Where-Object -Property ownerId -In $Applications.id
            continue
        }
        default {
            # Just retrieve all of them
            Get-NexusIQPolicyId
        }
    }
}

<#
.SYNOPSIS
    Retrieves the policy IDs used to retrieve policy violations
.EXAMPLE
    $PolicyInfo = Get-NexusIQPolicyId
    Get-NexusIQPolicyViolation -PolicyId $PolicyInfo[0].id
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/policy-violation-rest-api---v2#PolicyViolationRESTAPIv2-Step1-GetthePolicyIDs
#>

filter Get-NexusIQPolicyId
{
    [CmdletBinding()]
    param ()
    (Invoke-NexusIQAPI -Path "policies").policies
}

<#
.SYNOPSIS
    The Policy Violation REST APIs allow you to access and extract policy violations gathered during the evaluation of applications.
    In most cases the desire for getting to this data is to integrate into other tools your company may have.
    For example you may have a specific dashboard or reporting application that should have this data.
.EXAMPLE
    $PolicyInfo = Get-NexusIQPolicyId | Where-Object -Property name -EQ "Security-High"
    Get-NexusIQPolicyViolation -PolicyId $PolicyInfo.id
.EXAMPLE
    Get-NexusIQPolicyId | Where-Object -Property threatLevel -gt 5 | Get-NexusIQPolicyViolation
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/policy-violation-rest-api---v2#PolicyViolationRESTAPIv2-Step2-GetthePolicyViolations
#>

filter Get-NexusIQPolicyViolation
{
    [CmdletBinding()]
    param (
        # Id of the policy to find the violations for
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("PolicyId")]
        [guid[]]$Id
    )
    foreach ($PolicyId in $Id)
    {
        Invoke-NexusIQAPI -Path "policyViolations" -Parameters @{ p=$PolicyId }
    }
}

<#
.SYNOPSIS
    Retrieves reports for all stages of an application
#>

filter Find-NexusIQReport
{
    [CmdletBinding()]
    param (
        # The unique ID of the application
        [Parameter(Mandatory)]
        [Alias("ApplicationId")]
        [string]$PublicId
    )
    Write-Verbose "Finding application's internal Id with public ID $PublicId"
    $InternalId = Get-NexusIQApplication @PSBoundParameters | Select-Object -ExpandProperty id
    Invoke-NexusIQAPI -Path "reports/applications/$InternalId"
}

<#
.SYNOPSIS
    Retrieves reports for the specified stage of an application
.EXAMPLE
    Get-NexusIQReport -ApplicationId MyApp1 -Stage stage-release
.EXAMPLE
    Get-NexusIQApplication -ApplicationId MyApp1 | Get-NexusIQReport -Stage stage-release
.NOTES
    https://help.sonatype.com/iqserver/automating/rest-apis/application-rest-api---v2#ApplicationRESTAPIv2-Step1-GettheOrganizationID
#>

filter Get-NexusIQReport
{
    [CmdletBinding()]
    param (
        # The unique ID of the application
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias("ApplicationId")]
        [string]$PublicId,

        # Stage the report was run on
        [Parameter(Mandatory)]
        [ValidateSet("stage-release","source","build","release")]
        [string]$Stage
    )
    Find-NexusIQReport -PublicId $PublicId | Where-Object -Property stage -EQ $Stage
}

<#
.SYNOPSIS
    Downloads a security scan report based on the type specified. It will end up in your default download location.
.EXAMPLE
    $NexusApp = Get-NexusIQApplication -ApplicationId MyAppId -ErrorAction Stop
    $FileName = "$($NexusApp.publicId.Substring(0,5))_$($NexusApp.name).pdf"
    $IQReportPath = "$env:OneDrive\Documents\$FileName" # Set it to the app's display name
    Export-NexusIQReport -ApplicationId MyAppId1 -Stage stage-release -ReportType PDF -OutFile $IQReportPath
.EXAMPLE
    Get-NexusIQApplication -ApplicationId MyAppId | Export-NexusIQReport -Stage stage-release -ReportType PDF -OutFile "$env:TEMP\Report.pdf"
#>

filter Export-NexusIQReport
{
    [CmdletBinding()]
    param (
        # The unique ID of the application
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias("ApplicationId")]
        [string]$PublicId,

        # Stage the report was run on
        [Parameter(Mandatory)]
        [ValidateSet("stage-release","source","build","release")]
        [string]$Stage,

        # Format to output the data in
        [Parameter(Mandatory)]
        [ValidateSet("RAW","PDF")]
        [string]$ReportType,

        # Directory to output the reports to
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path (Split-Path $_) })]
        [string]$OutFile
    )
    $ReportsInfo = Get-NexusIQReport -PublicId $PublicId -Stage $Stage
    if ($ReportsInfo)
    {
        switch ($ReportType)
        {
            "PDF" {
                $BasePath = $ReportsInfo.reportDataUrl -replace "api\/v2\/applications\/","report/" -replace "\/raw","" -replace "\/reports\/","/"
                Invoke-NexusIQAPI -Path "$BasePath/printReport" -RequestType rest -OutFile $OutFile
            }
            "RAW" {
                $Path = $ReportsInfo.reportDataUrl -replace "api\/v2\/",""
                Invoke-NexusIQAPI -Path $Path -RequestType api -OutFile $OutFile
            }
        }
        Get-ItemProperty -Path $OutFile
    }
    else { Write-Warning "No report was found for App Id '$PublicId' in stage '$Stage'. Have you generated a report?" }
}

<#
.SYNOPSIS
    Starts a scan and waits for the scan to complete. Doesn't really work right and the documentation seems incorrect.
.PARAMETER ApplicationId
    The application ID for your application
.PARAMETER TargetDirectory
    Working directory to scan files within
.PARAMETER Target
    This is the path to a specific application archive file, a directory containing such archives or the ID of a Docker image.
    For archives, a number of formats are supported, including jar, war, ear, tar, tar.gz, zip and many others.
    You can specify multiple scan targets ( directories or files) separated by spaces test/dir/*/*.jar test/*/*.ear
.EXAMPLE
    Invoke-NexusIQScan -ApplicationId AppId1 -TargetDirectory "$env:USERPROFILE\MyRepo" -Target "**/*.dll"
.NOTES
    https://help.sonatype.com/iqserver/integrations/nexus-iq-cli
#>

filter Invoke-NexusIQScan
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias("ApplicationId")]
        [string]$PublicId,

        [Parameter(Mandatory)]
        [Alias("Directory")]
        [string]$TargetDirectory,

        [Parameter(Mandatory)]
        [string]$Target,

        [ValidateSet("stage-release","source","build","release")]
        [string]$Stage = "build",

        [ValidateSet("Windows","Linux","Mac","Cross-platform")]
        [string]$Platform = "Windows",

        [string]$Version = "1.161.0-01+630"
    )
    $CliPath = Save-NexusIQCli -Platform $Platform -Version $Version -PassThru
    $Settings = Get-NexusIQSettings
    Get-NexusIQApplication -PublicId $PublicId | Out-Null
    Push-Location
    Set-Location $TargetDirectory
    $ParamFileName = "$([NexusIQSettings]::SaveDir)$([System.IO.Path]::DirectorySeparatorChar)cli-params.txt"
    @"
--application-id
$PublicId
--server-url
$($Settings.BaseUrl)
--stage
$Stage
$Target
"@
.TrimStart() | Out-File -FilePath $ParamFileName

    if ($Platform -eq "Cross-Platform")
    {
        $Java = "$env:ProgramFiles$([System.IO.Path]::DirectorySeparatorChar)Microsoft$([System.IO.Path]::DirectorySeparatorChar)jdk-11.0.12.7-hotspot$([System.IO.Path]::DirectorySeparatorChar)bin$([System.IO.Path]::DirectorySeparatorChar)java.exe"
        if (-not (Test-Path $Java)) { $Java = "java" }
        . "$Java" -jar "$CliPath"--authentication "$($Settings.Credential.Username)`:$($Settings.Credential.GetNetworkCredential().Password)" @$ParamFileName
    }
    else
    {
        . "$CliPath" --authentication "$($Settings.Credential.Username)`:$($Settings.Credential.GetNetworkCredential().Password)" @$ParamFileName
    }

    Remove-Item $ParamFileName
    Pop-Location
}

filter Save-NexusIQCli
{
    [CmdletBinding()]
    param (
        [ValidateSet("Windows","Linux","Mac","Cross-Platform")]
        [string]$Platform = "Windows",

        [ValidateNotNullOrEmpty()]
        [string]$Version = "1.161.0-01+630",

        # Tells the function to output the full path to the CLI tool
        [switch]$PassThru
    )
    $CliName = @{
        Windows = "nexus-iq-cli.exe"
        Linux   = "nexus-iq-cli"
        Mac     = "nexus-iq-cli"
        "Cross-Platform" = "nexus-iq-cli-$Version.jar" -replace "\+.*\.jar",".jar"
    }
    $CliPath = "$([NexusIQSettings]::SaveDir)$([System.IO.Path]::DirectorySeparatorChar)$($CliName.Item($Platform))"
    if (-not (Test-Path $CliPath))
    {
        Write-Verbose "CLI tool wasn't found '$CliPath'. Downloading..."
        $Links = @{
            Windows          = "https://download.sonatype.com/clm/scanner/nexus-iq-cli-$Version-windows.zip"
            Linux            = "https://download.sonatype.com/clm/scanner/nexus-iq-cli-$Version-unix.zip"
            Mac              = "https://download.sonatype.com/clm/scanner/nexus-iq-cli-$Version-mac.pkg"
            "Cross-Platform" = "https://download.sonatype.com/clm/scanner/latest.jar"
        }
        $Url = $Links.Item($Platform)
        $OrigSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol
        if ($PSVersionTable.PSEdition -ne "Core" -and "Tls12" -notin ([Net.ServicePointManager]::SecurityProtocol))
        {
            [Net.ServicePointManager]::SecurityProtocol=@(([Net.ServicePointManager]::SecurityProtocol),[Net.SecurityProtocolType]::Tls12)
        }
        $WebClient = New-Object Net.WebClient # Way faster than Invoke-WebRequest
        $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy
        if ($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Url)))
        {
            $WebClient.Proxy = New-Object Net.WebProxy($DefaultProxy.GetProxy($Url).OriginalString, $true)
            $WebClient.Proxy.UseDefaultCredentials = $true
        }
        $ArchivePath = "$env:TEMP$([System.IO.Path]::DirectorySeparatorChar)$([System.IO.Path]::GetFileName($Links.Item($Platform)))"
        Write-Verbose "Downloading package using URL '$Url'"
        $WebClient.DownloadFile($Url, $ArchivePath)
        switch ([System.IO.Path]::GetExtension($ArchivePath))
        {
            ".zip" {
                [System.IO.Compression.ZipFile]::ExtractToDirectory($ArchivePath,(Split-Path $CliPath)) # Expand-Archive $ArchivePath -Destination "$([NexusIQSettings]::SaveDir)"
            }
            ".pkg" {
                pkgutil --expand-full "$ArchivePath" "$(Split-Path $CliPath)"
            }
            ".jar" {
                Move-Item $ArchivePath -Destination $CliPath
            }
        }
        if ($PSVersionTable.PSVersion -lt 6) { [Net.ServicePointManager]::SecurityProtocol = $OrigSecurityProtocol }
    }
    else { Write-Verbose "The executable '$CliPath' was already downloaded" }
    if ($Platform -ne "Cross-Platform")
    {
        if (-not (Test-Path $CliPath)) { Write-Error "Something went wrong and the cli wasn't found"}
        else { Remove-Item $ArchivePath }
    }

    if ($PassThru) { $CliPath }
}