SSLLabsScanPS.psm1

#Region './Private/Initialisation.ps1' 0
$script:apiProperties = @(
    @{
        ApiName  = 'Info'
        TypeName = 'SSLLabsScan.Info'
    }
    @{
        ApiName  = 'Analyze'
        TypeName = 'SSLLabsScan.HostData'
    }
    @{
        ApiName  = 'GetEndpointData'
        TypeName = 'SSLLabsScan.EndPointData'
    }
)

$script:baseEndpoint = 'https://api.ssllabs.com/api/v2/'

$script:resourceDirectoryName = $ExecutionContext.SessionState.Module.ModuleBase + '\Resources'
#EndRegion './Private/Initialisation.ps1' 18
#Region './Private/Invoke-SSLLabsScanApi.ps1' 0
Function Invoke-SSLLabsScanApi
{
    <#
    .SYNOPSIS
        Invoke the SSLLabs Scan API

    .DESCRIPTION
        This function invokes the SSLLabs Scan API

    .PARAMETER ApiName
        Specifies the name of the API to invoke

    .PARAMETER QueryParameters
        Specifies the query parameters for the API

    .INPUTS
        None

    .OUTPUTS
        System.Management.Automation.PSCustomObject

    .EXAMPLE
        Invoke-SSLLabsScanApi -ApiName 'info'

        Invokes the SSLLabs Scan Info API.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param(
        [Parameter(
            Mandatory,
            Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ApiName,

        [Parameter()]
        [System.String[]]
        $QueryParameters
    )

    $endpoint = $script:baseEndpoint + $ApiName

    if ($QueryParameters)
    {
        $queryString = '?' + ($QueryParameters -join '&')
        $Uri = $endpoint + $queryString
    }
    else
    {
        $uri = $endpoint
    }

    Write-Debug -Message "Invoking Web Request with URI $Uri"

    # Disable Write-Progress for Invoke-WebRequest to improve performance
    $ProgressPreference = 'SilentlyContinue'

    #$result = Invoke-WebRequest -Uri $Uri
    $result = Invoke-RestMethod -Uri $Uri

    $typeName = ($script:apiProperties | Where-Object -Property 'ApiName' -eq $ApiName).TypeName
    $result.PSTypeNames.Insert(0, $typeName)

    return $result
}
#EndRegion './Private/Invoke-SSLLabsScanApi.ps1' 66
#Region './Private/New-ErrorRecord.ps1' 0
function New-ErrorRecord
{
    <#
    .SYNOPSIS
        Creates an error record from an exception object.

    .DESCRIPTION
        This function creates an error record from an exception object.

    .PARAMETER Exception
        Specifies the exception object to include in the error record.

    .INPUTS
        None

    .OUTPUTS
        System.Management.Automation.ErrorRecord

    .EXAMPLE
        New-ErrorRecord -Exception $exception

        Creates an error record containing the specified exception.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Non state changing')]
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory,
            Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.Exception]
        $Exception
    )

    $exceptionTypeName = $Exception.GetType().FullName

    if ($exceptionTypeName -eq 'System.Net.WebException' -or
        $exceptionTypeName -eq 'Microsoft.PowerShell.Commands.HttpResponseException')
    {
        $fullyQualifiedErrorId = ("$($errorRecord.Exception.Response.StatusCode.value__)-" +
            $errorRecord.Exception.Response.StatusCode)
    }
    else
    {
        $fullyQualifiedErrorId = 'UnknownException'
    }

    $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
        $Exception,
        $fullyQualifiedErrorId,
        [System.Management.Automation.ErrorCategory]::InvalidOperation,
        $Null
    )

    return $ErrorRecord
}
#EndRegion './Private/New-ErrorRecord.ps1' 57
#Region './Public/ConvertTo-SSLLabsScanHtml.ps1' 0
function ConvertTo-SSLLabsScanHtml
{
    <#
    .SYNOPSIS
        Converts an SSL Labs Scan to an HTML report

    .DESCRIPTION
        This function converts an SSL Labs Scan to an HTML report

    .PARAMETER EndPointData
        Specifies the endpoint data to use for the report.

    .PARAMETER Path
        Specifies the output path for the report. If not specified, this defaults to the user's 'Documents' folder,
        with a file name of <hostName>-SSLLabsScanReport-<yyyyMMdd-HHmmss>.html'.

    .INPUTS
        None

    .OUTPUTS
        None

    .EXAMPLE
        ConvertTo-SSLLabsScanHtml -EndPointData $endpointData

        Converts an SSL Labs Scan to an HTML report
#>

    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject[]]
        $EndPointData,

        [Parameter()]
        [System.String]
        $Path
    )

    if ((-not $EndPointData[0].host) -or (-not $EndPointData[0].details.hostStartTime))
    {
        Write-Error 'Invalid EndPointData.' -ErrorAction Stop
    }

    $hostName = $EndPointData[0].host
    $scanDate = $EndPointData[0].details.hostStartTime

    $htmlBody = [System.String]::Empty

    if (-not $PSBoundParameters.ContainsKey('Path'))
    {
        $fileName = "$hostName-SSLLabsScanReport-$($scanDate.ToString('yyyyMMdd-HHmmss')).html"
        $Path = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath $fileName
    }

    $inlineStyleSheet = @(
        '<style>'
        (Get-Content -Path "$script:resourceDirectoryName\default.css")
        '</style>'
    )

    $header = "SSLLabs Scan for $hostName"

    $preContent = "<h2>$header</h2>"

    foreach ($endpoint in $EndpointData)
    {
        Write-Verbose -Message "Converting scan for endpoint $($endpoint.ipAddress)"

        $protocols = @()
        foreach ($protocol in $endpoint.details.protocols)
        {
            $protocols += "$($protocol.name) v$($protocol.version)"
        }

        $cipherSuites = ($endpoint.details.suites.list.name | Out-String).Trim() -replace ('\r\n', '<br/>')

        $openSslCcsStatusMapping = @{
            -1 = 'Test Failed'
            0  = 'Unknown'
            1  = 'Not Vulnerable'
            2  = 'Possible Vulnerable, but not Exploitable'
            3  = 'Vulnerable and Exploitable'
        }

        $openSslCcsStatus = $openSslCcsStatusMapping[$endpoint.details.openSslCcs]

        $openSslLuckyMinus20StatusMapping = @{
            -1 = 'Test Failed'
            0  = 'Unknown'
            1  = 'Not Vulnerable'
            2  = 'Vulnerable and Insecure'
        }

        $openSslLuckyMinus20Status = $openSslLuckyMinus20StatusMapping[$endpoint.details.openSSLLuckyMinus20]

        $poodleTlsStatusMapping = @{
            -3 = 'Timeout'
            -2 = 'TLS Not Supported'
            -1 = 'Test Failed'
            0  = 'Unknown'
            1  = 'Not Vulnerable'
            2  = 'Vulnerable'
        }

        $poodleTlsStatus = $poodleTlsStatusMapping[$endpoint.details.poodleTls]

        $reportData = [PSCustomObject][Ordered]@{
            'Server Name'                   = $endpoint.serverName
            'Grade'                         = $endpoint.grade
            'Grade Ignoring Trust'          = $endpoint.gradeTrustIgnored
            'Has Warnings'                  = $endpoint.hasWarnings
            'Is Exceptional'                = $endpoint.isExceptional
            'Certificate Subject'           = $endpoint.details.cert.subject
            'Supported Protocols'           = $protocols -join ', '
            'Supported Cipher Suites'       = $cipherSuites
            'BEAST Vulnerable'              = $endpoint.details.vulnBeast
            'Heartbleed Vulnerable'         = $endpoint.details.Heartbleed
            'Poodle Vulnerable'             = $endpoint.details.poodle
            'PoodleTLS Status'              = $poodleTlsStatus
            'FREAK Vulnerable'              = $endpoint.details.freak
            'Drown Vulnerable'              = $endpoint.details.drownVulnerable
            'OpenSSL CCS Status'            = $openSslCcsStatus
            'OpenSSL Lucky Minus 20 Status' = $openSslLuckyMinus20Status
        }

        $endpointPreContent = @(
            '<h3>'
            "IP Address: $($endpoint.ipAddress)"
            '</h3>'
        )

        $htmlBody += $reportData | ConvertTo-Html -As List -PreContent $endpointPreContent -Fragment
    }

    $htmlReport = ConvertTo-Html -Head $inlineStyleSheet -PreContent $preContent -PostContent $htmlBody

    [System.Net.WebUtility]::HtmlDecode($htmlReport) | Out-File -FilePath $Path -Encoding ascii
}
#EndRegion './Public/ConvertTo-SSLLabsScanHtml.ps1' 139
#Region './Public/Get-SSLLabsScanEndpointData.ps1' 0
function Get-SSLLabsScanEndpointData
{
    <#
    .SYNOPSIS
        Gets the endpoint data for a scan

    .DESCRIPTION
        This function gets the endpoint data for a scan

    .PARAMETER HostName
        Specifies the hostname of the scan.

    .PARAMETER IPAddress
        Specifies the ip address of the host in the scan.

    .PARAMETER HostData
        Specifies the HostData object containing details of the scan

    .PARAMETER FromCache
        Specifies whether to retrieve the scan from the cache.

    .INPUTS
        None

    .OUTPUTS
        SSLLabsScan.EndPointData

    .EXAMPLE
        Get-SSLLabsScanEndpointData -HostName www.bbc.co.uk -IPAddress 1.1.1.1

        Gets the endpoint data for a scan on the bbc website for the specified IP address.
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType( { ($script:ApiPropertes | Where-Object -Property ApiName -eq 'getEndpointData').TypeName })]
    param(
        [Parameter(
            Mandatory,
            ParameterSetName = 'Default')]
        [System.String]
        $HostName,

        [Parameter(
            Mandatory,
            ParameterSetName = 'Default')]
        [System.String[]]
        $IPAddress,

        [Parameter(
            Mandatory,
            ParameterSetName = 'HostData')]
        [PSCustomObject]
        $HostData,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $FromCache
    )

    $apiName = 'getEndpointData'

    if ($PSCmdlet.ParameterSetName -eq 'HostData')
    {
        $IPAddress = $HostData.endpoints.ipaddress
        $HostName = $HostData.host
    }

    $baseQueryParams = @()

    $baseQueryParams += "host=$HostName"

    if ($PSBoundParameters.ContainsKey('FromCache'))
    {
        $baseQueryParams += 'fromCache=on'
    }

    foreach ($ip in $IPAddress)
    {
        $queryParams = $baseQueryParams + "s=$ip"
        Write-Verbose "Getting SSL Labs Scan endpoint data on host $HostName, IP address $ip"

        try
        {
            $result = Invoke-SSLLabsScanApi -ApiName $apiName -QueryParameters $queryParams -Verbose:$false
        }
        catch
        {
            $errorRecord = New-ErrorRecord -Exception $_.Exception
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        $result | Add-Member -Name 'host' -Value $HostName -MemberType NoteProperty
        $result.details.hostStartTime = ([System.DateTimeOffset]::FromUnixTimeMilliSeconds($result.details.hostStartTime)).UtcDateTime

        return $result
    }
}
#EndRegion './Public/Get-SSLLabsScanEndpointData.ps1' 96
#Region './Public/Get-SSLLabsScanInfo.ps1' 0
function Get-SSLLabsScanInfo
{
    <#
    .SYNOPSIS
        Gets details of the SSLLabs Scan API

    .DESCRIPTION
        This function gets details of the SSLLabs Scan API

    .INPUTS
        None

    .OUTPUTS
        SSLLabsScan.Info

    .EXAMPLE
        Get-SSLLabsScanInfo

        Gets the SSLLabs Scan API info.
#>

    [CmdletBinding()]
    [OutputType( { ($script:ApiPropertes | Where-Object -Property ApiName -eq 'info').TypeName })]
    param()

    $apiName = 'info'

    Write-Verbose 'Getting SSL Labs Scan API Info'

    try
    {
        $result = Invoke-SSLLabsScanApi -ApiName $apiName
    }
    catch
    {
        $errorRecord = New-ErrorRecord -Exception $_.Exception
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    $result
}
#EndRegion './Public/Get-SSLLabsScanInfo.ps1' 40
#Region './Public/Invoke-SSLLabsScanAssessment.ps1' 0
function Invoke-SSLLabsScanAssessment
{
    <#
    .SYNOPSIS
        Invokes an SSL Labs scan assessment of a website

    .DESCRIPTION
        This function invokes an SSL Labs scan assessment of a website

    .PARAMETER HostName
        Specifies the hostname for the scan

    .PARAMETER Publish
        Specifies whether to publish the scan results

    .PARAMETER StartNew
        Specifies whether to start a new scan

    .PARAMETER FromCache
        Specifies whether to retrieve a scan from the cache

    .PARAMETER MaxAge
        Specifies the maximum age in hours of a scan to retrieve from the cache

    .PARAMETER All
        If specified with a value of 'on', full information on individual endpoints will be returned.
        If specified with a value of 'done', full information on individual endpoints will only be returned if the
        assessment is complete.

    .PARAMETER IgnoreMismatch
        Specifies whether to ignore mismatches between the server certificate and the assessment hostname.

    .PARAMETER PollingInterval
        Specifies the polling interval in seconds between scan status checks.

    .INPUTS
        None

    .OUTPUTS
        SSLLabsScan.HostData

    .EXAMPLE
        Invoke-SSLLabsScanAssessment -HostName 'www.bbc.co.uk' -StartNew

        Invokes a new SSL Labs scan assessment of a the bbc web site
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType( { ($script:ApiPropertes | Where-Object -Property ApiName -eq 'Analyze').TypeName })]
    param(
        [Parameter(Mandatory)]
        [System.String]
        $HostName,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Publish,

        [Parameter(ParameterSetName = 'StartNew')]
        [System.Management.Automation.SwitchParameter]
        $StartNew,

        [Parameter(ParameterSetName = 'FromCache')]
        [System.Management.Automation.SwitchParameter]
        $FromCache,

        [Parameter(ParameterSetName = 'FromCache')]
        [System.Int32]
        $MaxAge,

        [Parameter()]
        [ValidateSet('On', 'Done')]
        [System.String]
        $All,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $IgnoreMismatch,

        [Parameter()]
        [System.Int32]
        $PollingInterval = 10

    )

    $apiName = 'analyze'

    $queryParams = @()

    $queryParams += "host=$HostName"

    if ($PSBoundParameters.ContainsKey('Publish'))
    {
        $queryParams += 'publish=on'
    }

    if ($PSBoundParameters.ContainsKey('FromCache'))
    {
        $queryParams += 'fromCache=on'
    }

    if ($PSBoundParameters.ContainsKey('MaxAge'))
    {
        $queryParams += "maxAge=$MaxAge"
    }

    if ($PSBoundParameters.ContainsKey('All'))
    {
        $queryParams += "all=$All"
    }

    if ($PSBoundParameters.ContainsKey('IgnoreMismatch'))
    {
        $queryParams += 'ignoreMismatch=on'
    }

    $initialQueryParams = $queryParams

    # Only add the 'startNew' parameter on the initial query
    if ($PSBoundParameters.ContainsKey('StartNew'))
    {
        $initialQueryParams += 'startNew=on'
    }

    Write-Verbose "Invoking SSL Labs Scan API Analysis on host $HostName"

    $progressActivityMessage = "Checking SSL Labs Scan API Analysis on host $HostName"

    try
    {
        $result = Invoke-SSLLabsScanApi -ApiName $apiName -QueryParameters $initialQueryParams -Verbose:$false
    }
    catch
    {
        $errorRecord = New-ErrorRecord -Exception $_.Exception
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    $retryCount = 0

    while ($result.status -ne 'READY' -and $result.status -ne 'ERROR')
    {
        $retryCount++

        Write-Progress -Activity $progressActivityMessage -Status "Status: $($result.status), $retryCount"

        Start-Sleep -Seconds $PollingInterval

        try
        {
            $result = Invoke-SSLLabsScanApi -ApiName $apiName -QueryParameters $queryParams -Verbose:$false
        }
        catch
        {
            $errorRecord = New-ErrorRecord -Exception $_.Exception
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    # Convert Unix time fields to PowerShell DateTime objects
    $result.startTime = ([System.DateTimeOffset]::FromUnixTimeMilliSeconds($result.startTime)).UtcDateTime
    $result.testTime = ([System.DateTimeOffset]::FromUnixTimeMilliSeconds($result.testTime)).UtcDateTime

    foreach ($endpoint in $result.endpoints)
    {
        $endpoint | Add-Member -Name 'host' -Value $HostName -MemberType NoteProperty

        if ($endpoint.details.hostStartTime)
        {
            $endpoint.details.hostStartTime = ([System.DateTimeOffset]::FromUnixTimeMilliSeconds($endpoint.details.hostStartTime)).UtcDateTime
        }
    }

    Write-Progress -Activity $progressActivityMessage -Completed

    return $result
}
#EndRegion './Public/Invoke-SSLLabsScanAssessment.ps1' 176