WSManDsc.psm1

#Region '.\prefix.ps1' -1

using module .\Modules\DscResource.Base

# Import nested, 'DscResource.Common' module
$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules\DscResource.Common'
Import-Module -Name $script:dscResourceCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'
#EndRegion '.\prefix.ps1' 8
#Region '.\Enum\005.WSManSubjectFormat.ps1' -1

<#
    .SYNOPSIS
        The possible states for the DSC resource parameter WSManSubjectFormat.
#>


enum WSManSubjectFormat
{
    Both = 1
    FQDNOnly
    NameOnly
}
#EndRegion '.\Enum\005.WSManSubjectFormat.ps1' 12
#Region '.\Enum\005.WSManTransport.ps1' -1

<#
    .SYNOPSIS
        The possible states for the DSC resource parameter WSManTransport.
#>


enum WSManTransport
{
    HTTP = 1
    HTTPS
}
#EndRegion '.\Enum\005.WSManTransport.ps1' 11
#Region '.\Classes\001.WSManReason.ps1' -1

<#
    .SYNOPSIS
        The reason a property of a DSC resource is not in desired state.
 
    .DESCRIPTION
        A DSC resource can have a read-only property `Reasons` that the compliance
        part (audit via Azure Policy) of Azure AutoManage Machine Configuration
        uses. The property Reasons holds an array of WSManReason. Each WSManReason
        explains why a property of a DSC resource is not in desired state.
#>


class WSManReason
{
    [DscProperty()]
    [System.String]
    $Code

    [DscProperty()]
    [System.String]
    $Phrase
}
#EndRegion '.\Classes\001.WSManReason.ps1' 22
#Region '.\Classes\020.WSManListener.ps1' -1

<#
    .SYNOPSIS
        The `WSManListener` DSC resource is used to create, modify, or remove
        WSMan listeners.
 
    .DESCRIPTION
        This resource is used to create, edit or remove WS-Management HTTP/HTTPS listeners.
 
        ### SubjectFormat Parameter Notes
 
        The subject format is used to determine how the certificate for the listener
        will be identified. It must be one of the following:
 
        - **Both**: Look for a certificate with a subject matching the computer FQDN.
            If one can't be found the flat computer name will be used. If neither
            can be found then the listener will not be created.
        - **FQDN**: Look for a certificate with a subject matching the computer FQDN
            only. If one can't be found then the listener will not be created.
        - **ComputerName**: Look for a certificate with a subject matching the computer
        FQDN only. If one can't be found then the listener will not be created.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
 
    .PARAMETER Ensure
        Specifies whether the WS-Man Listener should exist.
 
    .PARAMETER Port
        The port the WS-Man Listener should use. Defaults to 5985 for HTTP and 5986 for HTTPS listeners.
 
    .PARAMETER Address
        The Address that the WS-Man Listener will be bound to. The default is * (any address).
 
    .PARAMETER Issuer
        The Issuer of the certificate to use for the HTTPS WS-Man Listener if a thumbprint is
        not specified.
 
    .PARAMETER SubjectFormat
        The format used to match the certificate subject to use for an HTTPS WS-Man Listener
        if a thumbprint is not specified.
 
    .PARAMETER MatchAlternate
        Should the FQDN/Name be used to also match the certificate alternate subject for an HTTPS WS-Man
        Listener if a thumbprint is not specified.
 
    .PARAMETER BaseDN
        This is the BaseDN (path part of the full Distinguished Name) used to identify the certificate
        to use for the HTTPS WS-Man Listener if a thumbprint is not specified.
 
    .PARAMETER CertificateThumbprint
        The Thumbprint of the certificate to use for the HTTPS WS-Man Listener.
 
    .PARAMETER HostName
        The HostName of WS-Man Listener.
 
    .PARAMETER Enabled
        Returns true if the existing WS-Man Listener is enabled.
 
    .PARAMETER URLPrefix
        The URL Prefix of the existing WS-Man Listener.
 
    .PARAMETER Reasons
        Returns the reason a property is not in desired state.
#>


[DscResource()]
class WSManListener : ResourceBase
{
    [DscProperty(Key)]
    [WSManTransport]
    $Transport

    [DscProperty(Mandatory)]
    [Ensure]
    $Ensure

    [DscProperty()]
    [ValidateRange(0, 65535)]
    [Nullable[System.UInt16]]
    $Port

    [DscProperty()]
    [System.String]
    $Address

    [DscProperty()]
    [System.String]
    $Issuer

    [DscProperty()]
    [WSManSubjectFormat]
    $SubjectFormat

    [DscProperty()]
    [Nullable[System.Boolean]]
    $MatchAlternate

    [DscProperty()]
    [System.String]
    $BaseDN

    [DscProperty()]
    [System.String]
    $CertificateThumbprint

    [DscProperty()]
    [System.String]
    $HostName

    [DscProperty(NotConfigurable)]
    [System.Boolean]
    $Enabled

    [DscProperty(NotConfigurable)]
    [System.String]
    $URLPrefix

    [DscProperty(NotConfigurable)]
    [WSManReason[]]
    $Reasons

    WSManListener () : base ($PSScriptRoot)
    {
        # Enable use of Enums as optional properties
        $this.FeatureOptionalEnums = $true

        # These properties will not be enforced.
        $this.ExcludeDscProperties = @(
            'Issuer'
            'SubjectFormat'
            'MatchAlternate'
            'BaseDN'
        )
    }

    [WSManListener] Get()
    {
        # Call the base method to return the properties.
        return ([ResourceBase] $this).Get()
    }

    # Base method Get() call this method to get the current state as a Hashtable.
    [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties)
    {
        $getParameters = @{
            Transport = $properties.Transport
        }

        $state = @{}

        $getCurrentStateResult = Get-Listener @getParameters

        if ($getCurrentStateResult)
        {
            $state = @{
                Transport             = [WSManTransport] $getCurrentStateResult.Transport
                Port                  = [System.UInt16] $getCurrentStateResult.Port
                Address               = $getCurrentStateResult.Address

                CertificateThumbprint = $getCurrentStateResult.CertificateThumbprint
                Hostname              = $getCurrentStateResult.Hostname

                Enabled               = $getCurrentStateResult.Enabled
                URLPrefix             = $getCurrentStateResult.URLPrefix
            }

            if ($getCurrentStateResult.CertificateThumbprint)
            {
                $state.Issuer = (Find-Certificate -CertificateThumbprint $getCurrentStateResult.CertificateThumbprint).Issuer
            }
        }

        return $state
    }

    [void] Set()
    {
        # Call the base method to enforce the properties.
        ([ResourceBase] $this).Set()
    }

    <#
        Base method Set() call this method with the properties that should be
        enforced and that are not in desired state.
    #>

    hidden [void] Modify([System.Collections.Hashtable] $properties)
    {
        if ($properties.ContainsKey('Ensure') -and $properties.Ensure -eq [Ensure]::Absent -and $this.Ensure -eq [Ensure]::Absent)
        {
            # Ensure was not in desired state so the resource should be removed
            $this.RemoveInstance()
        }
        elseif ($properties.ContainsKey('Ensure') -and $properties.Ensure -eq [Ensure]::Present -and $this.Ensure -eq [Ensure]::Present)
        {
            # Ensure was not in the desired state so the resource should be created
            $this.NewInstance()
        }
        elseif ($this.Ensure -eq [Ensure]::Present)
        {
            # Resource exists but one or more properties are not in the desired state
            $this.RemoveInstance()
            $this.NewInstance()
        }
    }

    [System.Boolean] Test()
    {
        # Call the base method to test all of the properties that should be enforced.
        return ([ResourceBase] $this).Test()
    }

    <#
        Base method Assert() call this method with the properties that was assigned
        a value.
    #>

    hidden [void] AssertProperties([System.Collections.Hashtable] $properties)
    {
        $assertBoundParameterParameters = @{
            BoundParameterList     = $properties
            MutuallyExclusiveList1 = @(
                'Issuer'
                'BaseDN'
                'SubjectFormat'
                'MatchAlternate'
            )
            MutuallyExclusiveList2 = @(
                'CertificateThumbprint'
                'HostName'
            )
        }

        Assert-BoundParameter @assertBoundParameterParameters
    }

    hidden [void] NewInstance()
    {
        # Get the port if it's not provided
        if (-not $this.Port)
        {
            $this.Port = Get-DefaultPort -Transport $this.Transport
        }

        # Get the Address if it's not provided
        if (-not $this.Address)
        {
            $this.Address = '*'
        }

        Write-Verbose -Message ($this.localizedData.CreatingListenerMessage -f $this.Transport, $this.Port)

        $selectorSet = @{
            Transport = $this.Transport
            Address   = $this.Address
        }

        $valueSet = @{
            Port = $this.Port
        }


        if ($this.Transport -eq [WSManTransport]::HTTPS)
        {
            $findCertificateParams = $this | Get-DscProperty -Attribute @('Optional') -ExcludeName @('Port', 'Address') -HasValue

            $certificate = Find-Certificate @findCertificateParams
            [System.String] $thumbprint = $certificate.Thumbprint

            if ($thumbprint)
            {
                $valueSet.CertificateThumbprint = $thumbprint

                if ([System.String]::IsNullOrEmpty($this.Hostname))
                {
                    $valueSet.HostName = [System.Net.Dns]::GetHostEntry((Get-ComputerName)).Hostname
                }
                else
                {
                    $valueSet.HostName = $this.HostName
                }
            }
            else
            {
                # A certificate could not be found to use for the HTTPS listener
                New-InvalidArgumentException -Message (
                    $this.localizedData.ListenerCreateFailNoCertError -f $this.Transport, $this.Port
                ) -Argument 'Issuer'
            } # if
        }

        New-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorSet -ValueSet $valueSet -ErrorAction Stop
    }

    hidden [void] RemoveInstance()
    {
        Write-Verbose -Message ($this.localizedData.ListenerExistsRemoveMessage -f $this.Transport, $this.Address)

        $selectorSet = @{
            Transport = [System.String] $this.Transport
            Address   = '*'
        }

        if ($this.Address)
        {
            $selectorSet.Address = $this.Address
        }

        Remove-WSManInstance -ResourceURI 'winrm/config/Listener' -SelectorSet $selectorSet
    }
}
#EndRegion '.\Classes\020.WSManListener.ps1' 310
#Region '.\Private\Find-Certificate.ps1' -1

<#
    .SYNOPSIS
        Finds the certificate to use for the HTTPS WS-Man Listener
 
    .PARAMETER Issuer
        The Issuer of the certificate to use for the HTTPS WS-Man Listener if a thumbprint is
        not specified.
 
    .PARAMETER SubjectFormat
        The format used to match the certificate subject to use for an HTTPS WS-Man Listener
        if a thumbprint is not specified.
 
    .PARAMETER MatchAlternate
        Should the FQDN/Name be used to also match the certificate alternate subject for an HTTPS WS-Man
        Listener if a thumbprint is not specified.
 
    .PARAMETER BaseDN
        This is the BaseDN (path part of the full Distinguished Name) used to identify the certificate
        to use for the HTTPS WS-Man Listener if a thumbprint is not specified.
 
    .PARAMETER CertificateThumbprint
        The Thumbprint of the certificate to use for the HTTPS WS-Man Listener.
#>

function Find-Certificate
{
    [CmdletBinding()]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    param
    (
        [Parameter()]
        [System.String]
        $Issuer,

        [Parameter()]
        [WSManSubjectFormat]
        $SubjectFormat = [WSManSubjectFormat]::Both,

        [Parameter()]
        [System.Boolean]
        $MatchAlternate,

        [Parameter()]
        [System.String]
        $BaseDN,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

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

    if ($PSBoundParameters.ContainsKey('CertificateThumbprint'))
    {
        Write-Verbose -Message ($script:localizedData.FindCertificate_ByThumbprintMessage -f $CertificateThumbprint)

        $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                ($_.Thumbprint -eq $CertificateThumbprint)
        } | Select-Object -First 1
    }
    else
    {
        # First try and find a certificate that is used to the FQDN of the machine
        if ($SubjectFormat -in [WSManSubjectFormat]::Both, [WSManSubjectFormat]::FQDNOnly)
        {
            # Lookup the certificate using the FQDN of the machine
            if ([System.String]::IsNullOrEmpty($Hostname))
            {
                $Hostname = [System.Net.Dns]::GetHostEntry((Get-ComputerName)).Hostname
            }
            $Subject = "CN=$Hostname"

            if ($PSBoundParameters.ContainsKey('BaseDN'))
            {
                $Subject = "$Subject, $BaseDN"
            } # if

            if ($MatchAlternate)
            {
                # Try and lookup the certificate using the subject and the alternate name
                Write-Verbose -Message ($script:localizedData.FindCertificate_AlternateMessage -f $Subject, $Issuer, $Hostname)

                $certificate = (Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($Hostname -in $_.DNSNameList.Unicode) -and
                        ($_.Subject -eq $Subject)
                    } | Select-Object -First 1)
            }
            else
            {
                # Try and lookup the certificate using the subject name
                Write-Verbose -Message ($script:localizedData.FindCertificate_Message -f $Subject, $Issuer)

                $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($_.Subject -eq $Subject)
                } | Select-Object -First 1
            } # if
        }

        if (-not $certificate -and ($SubjectFormat -in [WSManSubjectFormat]::Both, [WSManSubjectFormat]::NameOnly))
        {
            # If could not find an FQDN cert, try for one issued to the computer name
            [System.String] $Hostname = Get-ComputerName
            [System.String] $Subject = "CN=$Hostname"

            if ($PSBoundParameters.ContainsKey('BaseDN'))
            {
                $Subject = "$Subject, $BaseDN"
            } # if

            if ($MatchAlternate)
            {
                # Try and lookup the certificate using the subject and the alternate name
                Write-Verbose -Message ($script:localizedData.FindCertificate_AlternateMessage -f $Subject, $Issuer, $Hostname)

                $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($Hostname -in $_.DNSNameList.Unicode) -and
                        ($_.Subject -eq $Subject)
                } | Select-Object -First 1
            }
            else
            {
                # Try and lookup the certificate using the subject name
                Write-Verbose -Message ($script:localizedData.FindCertificate_Message -f $Subject, $Issuer)

                $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                    ($_.Extensions.EnhancedKeyUsages.FriendlyName -contains 'Server Authentication') -and
                    $_.Issuer -eq $Issuer -and
                    $_.Subject -eq $Subject
                } | Select-Object -First 1
            } # if
        } # if
    } # if

    if ($certificate)
    {
        Write-Verbose -Message ($script:localizedData.FindCertificate_FoundMessage -f $certificate.thumbprint)
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.FindCertificate_NotFoundMessage)
    } # if

    return $certificate
}
#EndRegion '.\Private\Find-Certificate.ps1' 153
#Region '.\Private\Get-DefaultPort.ps1' -1

<#
    .SYNOPSIS
        Returns the port to use for the listener based on the transport and port.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
 
    .PARAMETER Port
        The port the WS-Man Listener should use. Defaults to 5985 for HTTP and 5986 for HTTPS listeners.
#>

function Get-DefaultPort
{
    [CmdletBinding()]
    [OutputType([System.UInt16])]
    param
    (
        [Parameter(Mandatory = $true)]
        [WSManTransport]
        $Transport,

        [Parameter()]
        [System.UInt16]
        $Port
    )

    process
    {
        if (-not $Port)
        {
            # Set the default port because none was provided
            if ($Transport -eq [WSManTransport]::HTTP)
            {
                $Port = 5985
            }
            else
            {
                $Port = 5986
            }
        }

        return $Port
    }
}
#EndRegion '.\Private\Get-DefaultPort.ps1' 44
#Region '.\Private\Get-Listener.ps1' -1

<#
    .SYNOPSIS
        Looks up a WS-Man listener on the machine and returns the details.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
#>

function Get-Listener
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [WSManTransport]
        $Transport
    )

    $listeners = @(Get-WSManInstance -ResourceURI 'winrm/config/Listener' -Enumerate)

    if ($listeners)
    {
        return $listeners.Where(
            { ($_.Transport -eq $Transport) -and ($_.Source -ne 'Compatibility') }
        )
    }
}
#EndRegion '.\Private\Get-Listener.ps1' 28