NetShell.ps1

<#PSScriptInfo
    .VERSION 1.0.0
    .GUID 1abcbef0-1c9d-4aad-a00a-d4b17040b3f5
    .AUTHOR Darren R. Starr
    .COMPANYNAME Conscia Norway AS
    .COPYRIGHT 2016 Conscia Norway AS
    .TAGS netsh
    .LICENSEURI https://opensource.org/licenses/MIT
    .PROJECTURI https://github.com/darrenstarr/PowershellAutomation
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES
    .REQUIREDSCRIPTS
    .EXTERNALSCRIPTDEPENDENCIES
    .RELEASENOTES
        1.0.0
          - Initial release of a NetShell class
#>


<#
    .DESCRIPTION
        Powershell classes to process output of NetShell as powershell objects
#>

Param()

<#
    .SYNOPSIS
        Base class for refering to NetShell binding representations
#>

class NetShellBindingDefinition
{
    <#
        .SYNOPSIS
            The port bound to
    #>

    [int] $Port
}

<#
    .SYNOPSIS
        Representation of the Central Certificate Store binding
 
    .DESCRIPTION
        This class represents the central certificate store binding
        as displayed on the output of netsh http show sslcert. This class
        is not intended to be used directly, but instead is meant to be
        called by parsing functions in the NetShell class
#>

class NetShellBindingCentralCertificateStore : NetShellBindingDefinition
{
    <#
        .SYNOPSIS
            Parses and returns a Central Certificate Store binding object
    #>

    static [NetShellBindingCentralCertificateStore] Parse([string]$input)
    {
        [System.Text.RegularExpressions.MatchCollection] $matches = [RegEx]::Matches($input, '[0-9]+')

        if(
            ($null -eq $matches) -or 
            ($matches.Count -ne 1) -or
            ($matches[0].Success -ne $true) -or
            ($matches[0].Groups.Count -ne 1)
          ) {
            throw 'Invalid input passed to Central Certificate Store binding parser'
        }

        return [NetShellBindingCentralCertificateStore] @{
            Port = [Convert]::ToInt32($matches[0].Groups[0].Value)
        }
    }

    <#
        .SYNOPSIS
            Returns a string representation of the object
    #>

    [string] ToString()
    {
        return ('Central Certificate Store Port :' + $this.Port.ToString())
    }
}

<#
    .SYNOPSIS
        Representation of an IP Address and Port binding
 
    .DESCRIPTION
        This class represents a IP address and port binding
        as displayed on the output of netsh http show sslcert. This class
        is not intended to be used directly, but instead is meant to be
        called by parsing functions in the NetShell class
#>

class NetShellBindingIPAddressPort : NetShellBindingDefinition
{
    <#
        .SYNOPSIS
            The IP address of the binding
    #>

    [System.Net.IPAddress] $IPAddress

    <#
        .SYNOPSIS
            Parses and returns a IP Address and port binding object
    #>

    static [NetShellBindingIPAddressPort] Parse([string]$input)
    {
        # TODO : Make a better regular expression for matching IP Address
        [System.Text.RegularExpressions.MatchCollection] $matches = [RegEx]::Matches($input, '([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\:([0-9]+)')

        if(
            ($null -eq $matches) -or 
            ($matches.Count -ne 1) -or
            ($matches[0].Success -ne $true) -or
            ($matches[0].Groups.Count -ne 3)
          ) {
            throw 'Invalid input passed to IP:Port binding parser'
        }

        return [NetShellBindingIPAddressPort] @{
            IPAddress = [System.Net.IPAddress]::Parse($matches[0].Groups[1].Value)
            Port = [Convert]::ToInt32($matches[0].Groups[2].Value)
        }
    }

    <#
        .SYNOPSIS
            Returns a string representation of the object
    #>

    [string] ToString()
    {
        return ($this.IPAddress.ToString() + ':' + $this.Port.ToString())
    }
}

<#
    .SYNOPSIS
        Representation of a Hostname and Port binding
 
    .DESCRIPTION
        This class represents a Hostname and port binding
        as displayed on the output of netsh http show sslcert. This class
        is not intended to be used directly, but instead is meant to be
        called by parsing functions in the NetShell class
#>

class NetShellBindingHostnamePort : NetShellBindingDefinition
{
    <#
        .SYNOPSIS
            The Hostname of the binding
    #>

    [string] $Hostname

    <#
        .SYNOPSIS
            Parses and returns a Hostname and port binding object
    #>

    static [NetShellBindingHostnamePort] Parse([string]$input)
    {
        # TODO : Make a better regular expression for matching hostname
        [System.Text.RegularExpressions.MatchCollection] $matches = [RegEx]::Matches($input, '([^:]+)\:([0-9]+)')

        if(
            ($null -eq $matches) -or 
            ($matches.Count -ne 1) -or
            ($matches[0].Success -ne $true) -or
            ($matches[0].Groups.Count -ne 3)
          ) {
            throw 'Invalid input passed to Hostname:Port binding parser'
        }

        return [NetShellBindingHostnamePort] @{
            IPAddress = [System.Net.IPAddress]::Parse($matches[0].Groups[1].Value)
            Hostname = $matches[0].Groups[2].Value
        }
    }

    <#
        .SYNOPSIS
            Returns a string representation of the object
    #>

    [string] ToString()
    {
        return ($this.Hostname + ':' + $this.Port.ToString())
    }
}

<#
    .SYNOPSIS
        Representation of record from the output of netsh show sslcert
#>

class NetshellSSLBinding
{
    [NetshellBindingDefinition]$PortBinding = $null
    [string]$CertificateHash
    [GUID]$ApplicationID
    [string]$CertificateStoreName
    [bool]$VerifyClientCertificateRevocation
    [bool]$VerifyRevocationUsingCachedClientCertificateOnly
    [bool]$UsageCheck
    [TimeSpan]$RevocationFreshnessTime
    [TimeSpan]$UrlRetrievalTimeout
    [string[]]$CtlIdentifier
    [string]$CtlStoreName
    [bool]$DsMapperUsage
    [bool]$NegotiateClientCertificate
    [bool]$RejectConnections
}

<#
    .SYNOPSIS
        A class wrapper around netsh from Windows
 
    .DESCRIPTION
        This class implements functions as static members to execute
        and structure the output from netsh from within Windows.
         
        The progress of development of this class is directly in relation
        to the necessity of adding functionality where it is not readily
        available elsewhere in standard Powershell scripts.
 
        This class was written initially because WebManagement and xWebManagement
        modules from the OneGet repository lacked any method of finding
        the thumbprint of an SSL binding not directly connected to and IIS
        website. This made construction of a DSC resource to bind to
        configure a certificate for the Web Management Service of IIS
        impossible.
#>

class NetShell
{
    <#
        .SYNOPSIS
            Returns a the full path of the netsh.exe found in the path
    #>

    hidden static [string] NetshPath() {
        return (Get-Command -Name 'netsh.exe').Source
    }

    <#
        .SYNOPSIS
            Returns a GUID or null given a string input
    #>

    hidden static [Guid] ParseGuid([string]$input)
    {
        if(($null -eq $input) -or ($input -eq '(null)')) {
            return $null
        }
        return [Guid]::Parse($input)
    }

    <#
        .SYNOPSIS
            Returns a string value or null given a string input
    #>

    hidden static [string] ParseString([string]$input)
    {
        if(($null -eq $input) -or ($input -eq '(null)')) {
            return $null
        }
        return $input
    }

    <#
        .SYNOPSIS
            Returns a string list or null given a string input
 
        .NOTES
            As I don't have example data to test against (even
            after a few creative google searches, this function
            is incomplete.
    #>

    hidden static [string[]] ParseStringList([string]$input)
    {
        if(($null -eq $input) -or ($input -eq '(null)')) {
            return $null
        }

        # TODO : Find an example of multiple CTL Identifiers to learn to parse it properly

        return @($input)
    }

    <#
        .SYNOPSIS
            Returns a enabled/disabled boolean or null given a string input
    #>

    hidden static [bool] ParseEnabled([string]$input)
    {
        if(($null -eq $input) -or ($input -eq '(null)')) {
            return $null
        }

        switch ($input)
        {
            'Enabled' { return $true }
            'Disabled' { return $false }
            default { throw 'Invalid value for enabled/disabled field' }
        }

        throw "Powershell ISE shouldn't generate warning for this"
    }

    <#
        .SYNOPSIS
            Returns a timespan or null given a seconds count as a string input
    #>

    hidden static [TimeSpan] ParseSeconds([string]$input)
    {
        if(($null -eq $input) -or ($input -eq '(null)')) {
            return $null
        }

        $value = [Convert]::ToInt32($input)

        return [TimeSpan]::FromSeconds($value)
    }

    <#
        .SYNOPSIS
            Returns a timespan or null given a milliseconds count as a string input
    #>

    hidden static [TimeSpan] ParseMilliseconds([string]$input)
    {
        if(($null -eq $input) -or ($input -eq '(null)')) {
            return $null
        }

        $value = [Convert]::ToInt32($input)

        return [TimeSpan]::FromMilliseconds($value)
    }

    <#
        .SYNOPSIS
            Parses the values from a single netsh http show sslcert entry
 
        .NOTES
            The input of this function is extracted from the netsh http show sslcert
            command. Each line is expected to be formated as 'name ; value' instead of
            a dumb key/value store, this function parses and where convenient verifies
            the content of each field syntactically where the acceptable values are known.
    #>

    hidden static [NetshellSSLBinding] ParseShowSSLCertBlock([string]$input)
    {
        [string[]] $lines = [regex]::Split($input, '\r?\n')

        [NetshellSSLBinding]$result = [NetshellSSLBinding]::new()

        foreach($line in $lines) {
            [string[]] $parts = [regex]::Split($line, '[ \t]+\:[ \t]+')
            if ($parts.Count -ne 2) {
                Write-Warning ('Encountered line with more or less than 3 parts`n' + $line)
            }

            [string]$Name = $parts[0].Trim()
            [string]$Value = $parts[1].Trim() 
            # Write-Debug ('Name = [' + $Name + '], Value = [' + $Value + ']')

            switch($Name)
            {
                'IP:Port' { 
                    $result.PortBinding = [NetShellBindingIPAddressPort]::Parse($Value) 
                }

                'Hostname:Port' {
                    $result.PortBinding = [NetShellBindingHostnamePort]::Parse($Value)
                }

                'Central Certificate Store' { 
                    $result.PortBinding = [NetShellBindingCentralCertificateStore]::Parse($Value) 
                }
                
                'Certificate Hash' { 
                    $result.CertificateHash = [NetShell]::ParseString($Value) 
                }

                'Application ID' {
                    $result.ApplicationID = [NetShell]::ParseGUID($Value) 
                }

                'Certificate Store Name' { 
                    $result.CertificateStoreName = [NetShell]::ParseString($Value) 
                }

                'Verify Client Certificate Revocation' {
                    $result.VerifyClientCertificateRevocation = [NetShell]::ParseEnabled($Value) 
                }

                'Verify Revocation Using Cached Client Certificate Only' { 
                    $result.VerifyRevocationUsingCachedClientCertificateOnly = [NetShell]::ParseEnabled($Value) 
                }

                'Usage Check' {
                    $result.UsageCheck = [NetShell]::ParseEnabled($Value)
                }

                'Revocation Freshness Time' {
                    $result.RevocationFreshnessTime = [NetShell]::ParseSeconds($Value)
                }

                'URL Retrieval Timeout' {
                    $result.UrlRetrievalTimeout = [NetShell]::ParseMilliseconds($Value)
                }

                'Ctl Identifier' {
                    $result.CtlIdentifier = [NetShell]::ParseStringList($Value)
                }

                'Ctl Store Name' {
                    $result.CertificateStoreName = [NetShell]::ParseString($Value)
                }

                'DS Mapper Usage' {
                    $result.DsMapperUsage = [NetShell]::ParseEnabled($Value)
                }

                'Negotiate Client Certificate' {
                    $result.NegotiateClientCertificate = [NetShell]::ParseEnabled($Value)
                }

                'Reject Connections' {
                    $result.RejectConnections = [NetShell]::ParseEnabled($Value)
                }

                default {
                    throw [System.ArgumentException]::new('Unhandled parameter passed to ParseShowSSLCert : ' + $Name, '$input')
                }
            }
        }

        if($null -eq $result.PortBinding) {
            return $null
        }

        return $result
    }

    <#
        .SYNOPSIS
            Parses the full output of 'netsh http show sslcert' on Windows
    #>

    hidden static [NetshellSSLBinding[]] ParseShowSSLCert([string]$input)
    {
        [string[]]$blocks = [regex]::Split($input, '(\r?\n)(\r?\n)+') | Where-Object { $_.trim() -ne '' }

        # TODO : Find out how to make a proper array or list with typecasting here.
        $parseResult = [System.Collections.ArrayList]::new()

        foreach ($block in $blocks)
        {
            if ($block -match 'SSL Certificate bindings:') {
                continue
            }

            [NetshellSSLBinding]$binding = [NetShell]::ParseShowSSLCertBlock($block)
            if ($null -eq $binding) {
                throw [System.ArgumentException]::new('Unable to parse output from netsh http show sslcert', '$input')
            } else {
                $parseResult.Add($binding)
            }
        }

        return $parseResult
    }

    <#
        .SYNOPSIS
            Runs a program and receives the output of stdout or generates a 'meaningful exception'
    #>

    hidden static [string] ExecuteCommand([string]$path, [string[]]$arguments)
    {
        [string]$stdout = ''
        [string]$stderr = ''

        try {
            $processInfo = New-Object System.Diagnostics.ProcessStartInfo
            $processInfo.FileName = $path
            $processInfo.RedirectStandardError = $true
            $processInfo.RedirectStandardOutput = $true
            $processInfo.UseShellExecute = $false
            $processInfo.Arguments = $arguments

            $process = New-Object System.Diagnostics.Process
            $process.StartInfo = $processInfo
            $process.Start()
            $stdout = $process.StandardOutput.ReadToEnd()
            $stderr = $process.StandardError.ReadToEnd()
            $process.WaitForExit()
        } catch {
            throw [System.Exception]::new(('Failed to execute [' + $path + ']`n' + $stderr), $_.Exception)
        }

        return $stdout
    }

    <#
        .SYNOPSIS
            Executes 'netsh http show sslcert' and parses the output as an object
    #>

    static [NetshellSSLBinding[]] ShowSSLCert()
    {
        $commandResult = [NetShell]::ExecuteCommand(([NetShell]::NetshPath()), @('http','show','sslcert'))

        return [NetShell]::ParseShowSSLCert($commandResult)
    }
}

<#
$DebugPreference = "Continue"
$VerbosePreference = "Continue"
[NetShell]::ShowSSLCert()
#>