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 RestMethod with URI $Uri"

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

    try
    {
        $result = Invoke-RestMethod -Uri $Uri
    }
    catch
    {
        throw $_
    }

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

    return $result
}
#EndRegion './Private/Invoke-SSLLabsScanApi.ps1' 72
#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

        Converts an SSL Labs Scan to an HTML report
#>

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

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

    $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' 134
#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"

        $result = Invoke-SSLLabsScanApi -ApiName $apiName -QueryParameters $queryParams -Verbose:$false

        $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' 88
#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'

    $result = Invoke-SSLLabsScanApi -ApiName $apiName

    $result
}
#EndRegion './Public/Get-SSLLabsScanInfo.ps1' 32
#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"

    $result = Invoke-SSLLabsScanApi -ApiName $apiName -QueryParameters $initialQueryParams -Verbose:$false

    $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

        $result = Invoke-SSLLabsScanApi -ApiName $apiName -QueryParameters $queryParams -Verbose:$false
    }

    # 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
        $endpoint.details.hostStartTime = ([System.DateTimeOffset]::FromUnixTimeMilliSeconds($endpoint.details.hostStartTime)).UtcDateTime
    }

    Write-Progress -Activity $progressActivityMessage -Completed

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