DSCResources/MSFT_xWindowsUpdate/MSFT_xWindowsUpdate.psm1

$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common'

Import-Module -Name $script:resourceHelperModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

$script:cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xWindowsUpdate"

# Get-TargetResource function
function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Id
    )

    $uri, $kbId = Test-StandardArguments -Path $Path -Id $Id

    Write-Verbose -Message ($script:localizedData.GettingHotfixMessage -f $Id)

    $hotfix = Get-HotFix -Id "KB$kbId"

    $returnValue = @{
        Path = ''
        Id   = $hotfix.HotFixId
        Log  = ''
    }

    $returnValue
}

# The Set-TargetResource cmdlet
function Set-TargetResource
{
    # should be [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "DSCMachineStatus")], but it doesn't work
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Id,

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

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    if (!$Log)
    {
        $Log = [IO.Path]::GetTempFileName()
        $Log += '.etl'

        Write-Verbose -Message ($script:localizedData.LogNotSpecified -f $Log)
    }

    $uri, $kbId = Test-StandardArguments -Path $Path -Id $Id

    if ($Ensure -eq 'Present')
    {
        $filePath = Test-WindowsUpdatePath -uri $uri -Credential $Credential

        Write-Verbose -Message "$($script:localizedData.StartKeyWord) $($script:localizedData.ActionInstallUsingWsusa)"

        Start-Process -FilePath 'wusa.exe' -ArgumentList "`"$filepath`" /quiet /norestart /log:`"$Log`"" -Wait -NoNewWindow -ErrorAction SilentlyContinue

        $errorOccurred = Get-WinEvent -Path $Log -Oldest | Where-Object -FilterScript { $_.Id -eq 3 }

        if ($errorOccurred)
        {
            $errorMessage = $script:localizedData.ErrorOccurredOnHotfixInstall -f $Log, $errorOccurred.Message

            New-InvalidOperationException -Message $errorMessage
        }

        Write-Verbose -Message "$($script:localizedData.EndKeyWord) $($script:localizedData.ActionInstallUsingWsusa)"
    }
    else
    {
        $argumentList = "/uninstall /KB:$kbId /quiet /norestart /log:`"$Log`""

        Write-Verbose -Message "$($script:localizedData.StartKeyWord) $($script:localizedData.ActionUninstallUsingWsusa) Arguments: $ArgumentList"

        Start-Process -FilePath 'wusa.exe' -ArgumentList $argumentList  -Wait -NoNewWindow  -ErrorAction SilentlyContinue

        # Read the log and see if there was an error event
        $errorOccurred = Get-WinEvent -Path $Log -Oldest | Where-Object -FilterScript { $_.Id -eq 3 }

        if ($errorOccurred)
        {
            $errorMessage = $script:localizedData.ErrorOccurredOnHotfixUninstall -f $Log, $errorOccurred.Message

            New-InvalidOperationException -Message $errorMessage
        }

        Write-Verbose -Message "$($script:localizedData.EndKeyWord) $($script:localizedData.ActionUninstallUsingWsusa)"
    }

    if (Test-Path -Path 'variable:\LASTEXITCODE')
    {
        if ($LASTEXITCODE -eq 3010)
        {
            # reboot machine if exitcode indicates reboot.
            $global:DSCMachineStatus = 1
        }
    }
}

# Function to test if Hotfix is installed.
function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Id,

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

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    Write-Verbose -Message ($script:localizedData.TestingEnsure -f $Ensure)

    $uri, $kbId = Test-StandardArguments -Path $Path -Id $Id

    <#
        This is not the correct way to test to see if an update is applicable to a machine
        but, WUSA does not currently expose a way to ask.
    #>

    $result = Get-HotFix -Id "KB$kbId" -ErrorAction SilentlyContinue

    $returnValue = [System.Boolean] $result

    if ($Ensure -eq 'Present')
    {
        return $returnValue
    }
    else
    {
        return !$returnValue
    }
}

function Test-StandardArguments
{
    param
    (
        [Parameter()]
        [System.String]
        $Path,

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

    Write-Verbose -Message ($script:localizedData.TestStandardArgumentsPathWasPath -f $Path)

    $uri = $null

    try
    {
        $uri = [uri] $Path
    }
    catch
    {
        $errorMessage = $script:localizedData.InvalidPath -f $Path

        New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage
    }

    if (-not @('file', 'http', 'https') -contains $uri.Scheme)
    {
        $errorMessage = $script:localizedData.InvalidPath -f $Path, $uri.Scheme

        New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage
    }

    $pathExt = [System.IO.Path]::GetExtension($Path)

    Write-Verbose -Message ($script:localizedData.ThePathExtensionWasPathExt -f $pathExt)

    if (-not @('.msu') -contains $pathExt.ToLower())
    {
        $errorMessage = $script:localizedData.InvalidBinaryType -f $Path

        New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage
    }

    if (-not $Id)
    {
        $errorMessage = $script:localizedData.NeedsMoreInfo -f $Path

        New-InvalidArgumentException -ArgumentName 'Id' -Message $errorMessage
    }
    else
    {
        if ($Id -match 'kb[0-9]+')
        {
            if ($Matches[0] -eq $id)
            {
                $kbId = $id.Substring(2)
            }
            else
            {
                $errorMessage = $script:localizedData.InvalidIdFormat -f $Path

                New-InvalidArgumentException -ArgumentName 'Id' -Message $errorMessage
            }
        }
        elseif ($id -match '[0-9]+')
        {
            if ($Matches[0] -eq $id)
            {
                $kbId = $id
            }
            else
            {
                $errorMessage = $script:localizedData.InvalidIdFormat -f $Path

                New-InvalidArgumentException -ArgumentName 'Id' -Message $errorMessage
            }
        }
    }

    return @($uri, $kbId)
}

<#
    .SYNOPSIS
        Validate path, if necessary, cache file and return the location to be accessed
#>

function Test-WindowsUpdatePath
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Uri]
        $uri,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    if ($uri.IsUnc)
    {
        $psDriveArgs = @{
            Name       = ([guid]::NewGuid())
            PSProvider = 'FileSystem'
            Root       = (Split-Path $uri.LocalPath)
        }

        if ($Credential)
        {
            #We need to optionally include these and then splat the hash otherwise
            #we pass a null for Credential which causes the cmdlet to pop a dialog up
            $psDriveArgs['Credential'] = $Credential
        }

        $psDrive = New-PSDrive @psDriveArgs

        $path = Join-Path $psDrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary?
    }
    elseif (@('http', 'https') -contains $uri.Scheme)
    {
        $scheme = $uri.Scheme
        $outStream = $null
        $responseStream = $null

        try
        {
            Write-Verbose -Message ($script:localizedData.CreatingCacheLocation)

            if (-not (Test-Path -PathType Container $script:cacheLocation))
            {
                mkdir $script:cacheLocation | Out-Null
            }

            $destName = Join-Path $script:cacheLocation (Split-Path -Leaf $uri.LocalPath)

            Write-Verbose -Message ($script:localizedData.NeedToDownloadFileFromSchemeDestinationWillBeDestName -f $scheme, $destName)

            try
            {
                Write-Verbose -Message ($script:localizedData.CreatingTheDestinationCacheFile)

                $outStream = New-Object System.IO.FileStream $destName, 'Create'
            }
            catch
            {
                # Should never happen since we own the cache directory
                $errorMessage = $script:localizedData.CouldNotOpenDestFile -f $destName

                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
            }

            try
            {
                Write-Verbose -Message ($script:localizedData.CreatingTheSchemeStream -f $scheme)

                $request = [System.Net.WebRequest]::Create($uri)

                Write-Verbose -Message ($script:localizedData.SettingDefaultCredential)

                $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials

                if ($scheme -eq 'http')
                {
                    Write-Verbose -Message ($script:localizedData.SettingAuthenticationLevel)

                    # default value is MutualAuthRequested, which applies to https scheme
                    $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None
                }
                if ($scheme -eq 'https')
                {
                    Write-Verbose -Message ($script:localizedData.IgnoringBadCertificates)

                    $request.ServerCertificateValidationCallBack = { $true }
                }

                Write-Verbose -Message ($script:localizedData.GettingTheSchemeResponseStream -f $scheme)

                $responseStream = (([System.Net.HttpWebRequest] $request).GetResponse()).GetResponseStream()
            }
            catch
            {
                $errorMessage = $script:localizedData.CouldNotGetHttpStream -f $scheme, $path

                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
            }

            try
            {
                Write-Verbose -Message ($script:localizedData.CopyingTheSchemeStreamBytesToTheDiskCache -f $scheme)

                $responseStream.CopyTo($outStream)
                $responseStream.Flush()
                $outStream.Flush()
            }
            catch
            {
                $errorMessage = $script:localizedData.ErrorCopyingDataToFile -f $path, $destName

                New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
            }
        }
        finally
        {
            if ($outStream)
            {
                $outStream.Close()
            }

            if ($responseStream)
            {
                $responseStream.Close()
            }
        }

        Write-Verbose -Message ($script:localizedData.RedirectingPackagePathToCacheFileLocation)

        $Path = $destName
    }

    return $path
}

Export-ModuleMember -Function *-TargetResource