DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1
data LocalizedData { # culture="en-US" # TODO: Support WhatIf ConvertFrom-StringData @' InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file NeedsMoreInfo=Either Name or ProductId is required InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported CouldNotOpenLog=The specified LogPath ({0}) could not be opened CouldNotStartProcess=The process {0} could not be started UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct PathDoesNotExist=The given Path ({0}) could not be found CouldNotOpenDestFile=Could not open the file {0} for writing CouldNotGetHttpStream=Could not get the {0} stream for file {1} ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1} PackageConfigurationComplete=Package configuration finished PackageConfigurationStarting=Package configuration starting InstalledPackage=Installed package UninstalledPackage=Uninstalled package NoChangeRequired=Package found in desired state, no action required RemoveExistingLogFile=Remove existing log file CreateLogFile=Create log file MountSharePath=Mount share to get media DownloadHTTPFile=Download the media over HTTP or HTTPS StartingProcessMessage=Starting process {0} with arguments {1} RemoveDownloadedFile=Remove the downloaded file PackageInstalled=Package has been installed PackageUninstalled=Package has been uninstalled MachineRequiresReboot=The machine requires a reboot PackageDoesNotAppearInstalled=The package {0} is not installed PackageAppearsInstalled=The package {0} is already installed PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details CheckingFileHash=Checking file '{0}' for expected {2} hash value of {1} InvalidFileHash=File '{0}' does not match expected {2} hash value of {1}. CheckingFileSignature=Checking file '{0}' for valid digital signature. FileHasValidSignature=File '{0}' contains a valid digital signature. Signer Thumbprint: {1}, Subject: {2} InvalidFileSignature=File '{0}' does not have a valid Authenticode signature. Status: {1} WrongSignerSubject=File '{0}' was not signed by expected signer subject '{1}' WrongSignerThumbprint=File '{0}' was not signed by expected signer certificate thumbprint '{1}' CreatingRegistryValue=Creating package registry value of {0}. '@ } $Debug = $true Function Trace-Message { param([string] $Message) if($Debug) { Write-Verbose $Message } } $CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource" Function Throw-InvalidArgumentException { param( [string] $Message, [string] $ParamName ) $exception = new-object System.ArgumentException $Message,$ParamName $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null throw $errorRecord } Function Throw-InvalidNameOrIdException { param( [string] $Message ) $exception = new-object System.ArgumentException $Message $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null throw $errorRecord } Function Throw-TerminatingError { param( [string] $Message, [System.Management.Automation.ErrorRecord] $ErrorRecord ) $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception $errorRecord = New-Object System.Management.Automation.ErrorRecord ($exception.ToString()),"MachineStateIncorrect","InvalidOperation",$null throw $errorRecord } Function Set-RegistryValue { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Microsoft.Win32.RegistryHive] $RegistryHive, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Key, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Value, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Data ) try { $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) $subKey = $baseKey.OpenSubKey($Key, $true) ## Opens the subkey with write access if($subKey -eq $null) { $subKey = $baseKey.CreateSubKey($Key) } $subKey.SetValue($Value, $Data) } catch { $exceptionText = ($_ | Out-String).Trim() Write-Verbose "Exception occured in Set-RegistryValue: $exceptionText" } } Function Get-RegistryValueIgnoreError { param ( [parameter(Mandatory = $true)] [Microsoft.Win32.RegistryHive] $RegistryHive, [parameter(Mandatory = $true)] [System.String] $Key, [parameter(Mandatory = $true)] [System.String] $Value, [parameter(Mandatory = $true)] [Microsoft.Win32.RegistryView] $RegistryView ) try { $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) $subKey = $baseKey.OpenSubKey($Key) if($subKey -ne $null) { return $subKey.GetValue($Value) } } catch { $exceptionText = ($_ | Out-String).Trim() Write-Verbose "Exception occured in Get-RegistryValueIgnoreError: $exceptionText" } return $null } Function Validate-StandardArguments { param( $Path, $ProductId, $Name ) Trace-Message "Validate-StandardArguments, Path was $Path" $uri = $null try { $uri = [uri] $Path } catch { Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" } if(-not @("file", "http", "https") -contains $uri.Scheme) { Trace-Message "The uri scheme was $uri.Scheme" Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" } $pathExt = [System.IO.Path]::GetExtension($Path) Trace-Message "The path extension was $pathExt" if(-not @(".msi",".exe") -contains $pathExt.ToLower()) { Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path" } $identifyingNumber = $null if(-not $Name -and -not $ProductId) { #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId" } elseif($ProductId) { try { Trace-Message "Parsing $ProductId as an identifyingNumber" $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() Trace-Message "Parsed $ProductId as $identifyingNumber" } catch { Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId } } return $uri, $identifyingNumber } Function Get-ProductEntry { param ( [string] $Name, [string] $IdentifyingNumber, [string] $InstalledCheckRegHive = 'LocalMachine', [string] $InstalledCheckRegKey, [string] $InstalledCheckRegValueName, [string] $InstalledCheckRegValueData ) $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" if($IdentifyingNumber) { $keyLocation = "$uninstallKey\$identifyingNumber" $item = Get-Item $keyLocation -EA SilentlyContinue if(-not $item) { $keyLocation = "$uninstallKeyWow64\$identifyingNumber" $item = Get-Item $keyLocation -EA SilentlyContinue } return $item } foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64)) { if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName")) { return $item } } if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) { $installValue = $null #if 64bit OS, check 64bit registry view first if ((Get-WmiObject -Class Win32_OperatingSystem -ComputerName "localhost" -ea 0).OSArchitecture -eq '64-bit') { $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64 } if($installValue -eq $null) { $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32 } if($installValue) { if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) { return @{ Installed = $true } } } } return $null } function Test-TargetResource { [OutputType([System.Boolean])] param ( [ValidateSet("Present", "Absent")] [string] $Ensure = "Present", [parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Name, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ProductId, [string] $Arguments, [pscredential] $Credential, [System.UInt32[]] $ReturnCode, [string] $LogPath, [pscredential] $RunAsCredential, [ValidateSet('LocalMachine','CurrentUser')] [string] $InstalledCheckRegHive = 'LocalMachine', [string] $InstalledCheckRegKey, [string] $InstalledCheckRegValueName, [string] $InstalledCheckRegValueData, [boolean] $CreateCheckRegValue, [string] $FileHash, [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')] [string] $HashAlgorithm, [string] $SignerSubject, [string] $SignerThumbprint, [string] $ServerCertificateValidationCallback ) $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData Trace-Message "Ensure is $Ensure" if($product) { Trace-Message "product found" } else { Trace-Message "product installation cannot be determined" } Trace-Message ("product as boolean is {0}" -f [boolean]$product) $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent") # install registry test overrides the product id test and there is no true product information # when doing a lookup via registry key if ($product -and $InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) { Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name) } else { if ($product -ne $null) { $name = Get-LocalizableRegKeyValue $product "DisplayName" Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name) } else { $displayName = $null if($Name) { $displayName = $Name } else { $displayName = $ProductId } Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) } } return $res } function Get-LocalizableRegKeyValue { param( [object] $RegKey, [string] $ValueName ) $res = $RegKey.GetValue("{0}_Localized" -f $ValueName) if(-not $res) { $res = $RegKey.GetValue($ValueName) } return $res } function Get-TargetResource { [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Name, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ProductId, [ValidateSet('LocalMachine','CurrentUser')] [string] $InstalledCheckRegHive = 'LocalMachine', [string] $InstalledCheckRegKey, [string] $InstalledCheckRegValueName, [string] $InstalledCheckRegValueData, [boolean] $CreateCheckRegValue ) #If the user gave the ProductId then we derive $identifyingNumber $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name $localMsi = $uri.IsFile -and -not $uri.IsUnc $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData if(-not $product) { return @{ Ensure = "Absent" Name = $Name ProductId = $identifyingNumber Installed = $false InstalledCheckRegHive = $InstalledCheckRegHive InstalledCheckRegKey = $InstalledCheckRegKey InstalledCheckRegValueName = $InstalledCheckRegValueName InstalledCheckRegValueData = $InstalledCheckRegValueData } } if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) { return @{ Ensure = "Present" Name = $Name ProductId = $identifyingNumber Installed = $true InstalledCheckRegHive = $InstalledCheckRegHive InstalledCheckRegKey = $InstalledCheckRegKey InstalledCheckRegValueName = $InstalledCheckRegValueName InstalledCheckRegValueData = $InstalledCheckRegValueData } } #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE) #If the user gave a ProductId just pass it through, otherwise fill it from the product if(-not $identifyingNumber) { $identifyingNumber = Split-Path -Leaf $product.Name } $date = $product.GetValue("InstallDate") if($date) { try { $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date } catch { $date = $null } } $publisher = Get-LocalizableRegKeyValue $product "Publisher" $size = $product.GetValue("EstimatedSize") if($size) { $size = $size/1024 } $version = $product.GetValue("DisplayVersion") $description = $product.GetValue("Comments") $name = Get-LocalizableRegKeyValue $product "DisplayName" return @{ Ensure = "Present" Name = $name Path = $Path InstalledOn = $date ProductId = $identifyingNumber Size = $size Installed = $true Version = $version PackageDescription = $description Publisher = $publisher } } Function Get-MsiTools { if($script:MsiTools) { return $script:MsiTools } $sig = @' [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] private static extern uint MsiCloseHandle(IntPtr hAny); [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); private static string GetPackageProperty(string msi, string property) { IntPtr MsiHandle = IntPtr.Zero; try { var res = MsiOpenPackageW(msi, out MsiHandle); if (res != 0) { return null; } int length = 256; var buffer = new StringBuilder(length); res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); return buffer.ToString(); } finally { if (MsiHandle != IntPtr.Zero) { MsiCloseHandle(MsiHandle); } } } public static string GetProductCode(string msi) { return GetPackageProperty(msi, "ProductCode"); } public static string GetProductName(string msi) { return GetPackageProperty(msi, "ProductName"); } '@ $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.xPackageResource ` -Name MsiTools -Using System.Text -MemberDefinition $sig return $script:MsiTools } Function Get-MsiProductEntry { param ( [string] $Path ) if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi")) { Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) } $tools = Get-MsiTools $pn = $tools::GetProductName($Path) $pc = $tools::GetProductCode($Path) return $pn,$pc } function Set-TargetResource { [CmdletBinding(SupportsShouldProcess=$true)] param ( [ValidateSet("Present", "Absent")] [string] $Ensure = "Present", [parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Name, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ProductId, [string] $Arguments, [pscredential] $Credential, [System.UInt32[]] $ReturnCode, [string] $LogPath, [pscredential] $RunAsCredential, [ValidateSet('LocalMachine','CurrentUser')] [string] $InstalledCheckRegHive = 'LocalMachine', [string] $InstalledCheckRegKey, [string] $InstalledCheckRegValueName, [string] $InstalledCheckRegValueData, [boolean] $CreateCheckRegValue, [string] $FileHash, [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')] [string] $HashAlgorithm, [string] $SignerSubject, [string] $SignerThumbprint, [string] $ServerCertificateValidationCallback ) $ErrorActionPreference = "Stop" if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId ` -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName ` -InstalledCheckRegValueData $InstalledCheckRegValueData)) { return } $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name #Path gets overwritten in the download code path. Retain the user's original Path in case the install succeeded #but the named package wasn't present on the system afterward so we can give a better message $OrigPath = $Path Write-Verbose $LocalizedData.PackageConfigurationStarting if(-not $ReturnCode) { $ReturnCode = @(0) } $logStream = $null $psdrive = $null $downloadedFileName = $null try { $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() if($LogPath) { try { if($fileExtension -eq ".msi") { #We want to pre-verify the path exists and is writable ahead of time #even in the MSI case, as detecting WHY the MSI log doesn't exist would #be rather problematic for the user if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null)) { rm $LogPath } if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) { New-Item -Type File $LogPath | Out-Null } } elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) { $logStream = new-object "System.IO.StreamWriter" $LogPath,$false } } catch { Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_ } } #Download or mount file as necessary if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent")) { if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) { $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 -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) { $scheme = $uri.Scheme $outStream = $null $responseStream = $null try { Trace-Message "Creating cache location" if(-not (Test-Path -PathType Container $CacheLocation)) { mkdir $CacheLocation | Out-Null } $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath) Trace-Message "Need to download file from $scheme, destination will be $destName" try { Trace-Message "Creating the destination cache file" $outStream = New-Object System.IO.FileStream $destName, "Create" } catch { #Should never happen since we own the cache directory Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_ } try { Trace-Message "Creating the $scheme stream" $request = [System.Net.WebRequest]::Create($uri) Trace-Message "Setting default credential" $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials if ($scheme -eq "http") { Trace-Message "Setting authentication level" # default value is MutualAuthRequested, which applies to https scheme $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None } if ($scheme -eq "https" -and -not [string]::IsNullOrEmpty($ServerCertificateValidationCallback)) { Trace-Message "Assigning user-specified certificate verification callback" $scriptBlock = [scriptblock]::Create($ServerCertificateValidationCallback) $request.ServerCertificateValidationCallBack = $scriptBlock } Trace-Message "Getting the $scheme response stream" $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream() } catch { Trace-Message ("Error: " + ($_ | Out-String)) Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_ } try { Trace-Message "Copying the $scheme stream bytes to the disk cache" $responseStream.CopyTo($outStream) $responseStream.Flush() $outStream.Flush() } catch { Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_ } } finally { if($outStream) { $outStream.Close() } if($responseStream) { $responseStream.Close() } } Trace-Message "Redirecting package path to cache file location" $Path = $downloadedFileName = $destName } } if (-not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi")) { #At this point the Path ought to be valid unless it's an MSI uninstall case if(-not (Test-Path -PathType Leaf $Path)) { Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) } ValidateFile -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint } $startInfo = New-Object System.Diagnostics.ProcessStartInfo $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea $process = New-Object System.Diagnostics.Process $process.StartInfo = $startInfo $errLogPath = $LogPath + ".err" #Concept only, will never touch disk if($fileExtension -eq ".msi") { $startInfo.FileName = "$env:windir\system32\msiexec.exe" if($Ensure -eq "Present") { # check if Msi package contains the ProductName and Code specified $pName,$pCode = Get-MsiProductEntry -Path $Path if ( ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) ` -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode)) ) { Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) } $startInfo.Arguments = '/i "{0}"' -f $Path } else { $product = Get-ProductEntry $Name $identifyingNumber $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID $startInfo.Arguments = ("/x{0}" -f $id) } if($LogPath) { $startInfo.Arguments += ' /log "{0}"' -f $LogPath } $startInfo.Arguments += " /quiet" if($Arguments) { $startInfo.Arguments += " " + $Arguments } } else #EXE { Trace-Message "The binary is an EXE" $startInfo.FileName = $Path $startInfo.Arguments = $Arguments if($LogPath) { Trace-Message "User has requested logging, need to attach event handlers to the process" $startInfo.RedirectStandardError = $true $startInfo.RedirectStandardOutput = $true Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath } } Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments) if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) { try { $exitCode = 0 if($PSBoundParameters.ContainsKey("RunAsCredential")) { CallPInvoke [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, ` $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, ` $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode) } else { $process.Start() | Out-Null if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath { $process.BeginOutputReadLine(); $process.BeginErrorReadLine(); } $process.WaitForExit() if($process) { $exitCode = $process.ExitCode } } } catch { Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_ } if($logStream) { #We have to re-mux these since they appear to us as different streams #The underlying Win32 APIs prevent this problem, as would constructing a script #on the fly and executing it, but the former is highly problematic from PowerShell #and the latter doesn't let us get the return code for UI-based EXEs $outputEvents = Get-Event -SourceIdentifier $LogPath $errorEvents = Get-Event -SourceIdentifier $errLogPath $masterEvents = @() + $outputEvents + $errorEvents $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated foreach($event in $masterEvents) { $logStream.Write($event.SourceEventArgs.Data); } Remove-Event -SourceIdentifier $LogPath Remove-Event -SourceIdentifier $errLogPath } if(-not ($ReturnCode -contains $exitCode)) { Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) } } } finally { if($psdrive) { Remove-PSDrive -Force $psdrive } if($logStream) { $logStream.Dispose() } } if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) { #This is deliberately not in the Finally block. We want to leave the downloaded file on disk #in the error case as a debugging aid for the user rm $downloadedFileName } $operationString = $LocalizedData.PackageUninstalled if($Ensure -eq "Present") { $operationString = $LocalizedData.PackageInstalled } if($CreateCheckRegValue -eq $true) { $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName Write-Verbose ($LocalizedData.CreatingRegistryValue -f $registryValueString) Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData } # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore if(($featureData -and $featureData.RequiresReboot) -or $regData) { Write-Verbose $LocalizedData.MachineRequiresReboot $global:DSCMachineStatus = 1 } if($Ensure -eq "Present") { $productEntry = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData if(-not $productEntry) { Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath) } } Write-Verbose $operationString Write-Verbose $LocalizedData.PackageConfigurationComplete } function CallPInvoke { $script:ProgramSource = @" using System; using System.Collections.Generic; using System.Text; using System.Security; using System.Runtime.InteropServices; using System.Diagnostics; using System.Security.Principal; using System.ComponentModel; using System.IO; namespace Source { [SuppressUnmanagedCodeSecurity] public static class NativeMethods { //The following structs and enums are used by the various Win32 API's that are used in the code below [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public Int32 cb; public string lpReserved; public string lpDesktop; public string lpTitle; public Int32 dwX; public Int32 dwY; public Int32 dwXSize; public Int32 dwXCountChars; public Int32 dwYCountChars; public Int32 dwFillAttribute; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public Int32 dwProcessID; public Int32 dwThreadID; } [Flags] public enum LogonType { LOGON32_LOGON_INTERACTIVE = 2, LOGON32_LOGON_NETWORK = 3, LOGON32_LOGON_BATCH = 4, LOGON32_LOGON_SERVICE = 5, LOGON32_LOGON_UNLOCK = 7, LOGON32_LOGON_NETWORK_CLEARTEXT = 8, LOGON32_LOGON_NEW_CREDENTIALS = 9 } [Flags] public enum LogonProvider { LOGON32_PROVIDER_DEFAULT = 0, LOGON32_PROVIDER_WINNT35, LOGON32_PROVIDER_WINNT40, LOGON32_PROVIDER_WINNT50 } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public Int32 Length; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } public enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } public enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct TokPriv1Luid { public int Count; public long Luid; public int Attr; } public const int GENERIC_ALL_ACCESS = 0x10000000; public const int CREATE_NO_WINDOW = 0x08000000; internal const int SE_PRIVILEGE_ENABLED = 0x00000002; internal const int TOKEN_QUERY = 0x00000008; internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern bool CloseHandle(IntPtr handle); [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, ref PROCESS_INFORMATION lpProcessInformation ); [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] public static extern bool DuplicateTokenEx( IntPtr hExistingToken, Int32 dwDesiredAccess, ref SECURITY_ATTRIBUTES lpThreadAttributes, Int32 ImpersonationLevel, Int32 dwTokenType, ref IntPtr phNewToken ); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern Boolean LogonUser( String lpszUserName, String lpszDomain, String lpszPassword, LogonType dwLogonType, LogonProvider dwLogonProvider, out IntPtr phToken ); [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool AdjustTokenPrivileges( IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen ); [DllImport("kernel32.dll", ExactSpelling = true)] internal static extern IntPtr GetCurrentProcess(); [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool OpenProcessToken( IntPtr h, int acc, ref IntPtr phtok ); [DllImport("kernel32.dll", ExactSpelling = true)] internal static extern int WaitForSingleObject( IntPtr h, int milliseconds ); [DllImport("kernel32.dll", ExactSpelling = true)] internal static extern bool GetExitCodeProcess( IntPtr h, out int exitcode ); [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool LookupPrivilegeValue( string host, string name, ref long pluid ); public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) { var hToken = IntPtr.Zero; var hDupedToken = IntPtr.Zero; TokPriv1Luid tp; var pi = new PROCESS_INFORMATION(); var sa = new SECURITY_ATTRIBUTES(); sa.Length = Marshal.SizeOf(sa); Boolean bResult = false; try { bResult = LogonUser( strName, strDomain, strPassword, LogonType.LOGON32_LOGON_BATCH, LogonProvider.LOGON32_PROVIDER_DEFAULT, out hToken ); if (!bResult) { throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); } IntPtr hproc = GetCurrentProcess(); IntPtr htok = IntPtr.Zero; bResult = OpenProcessToken( hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok ); if(!bResult) { throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); } tp.Count = 1; tp.Luid = 0; tp.Attr = SE_PRIVILEGE_ENABLED; bResult = LookupPrivilegeValue( null, SE_INCRASE_QUOTA, ref tp.Luid ); if(!bResult) { throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); } bResult = AdjustTokenPrivileges( htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero ); if(!bResult) { throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); } bResult = DuplicateTokenEx( hToken, GENERIC_ALL_ACCESS, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref hDupedToken ); if(!bResult) { throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); } var si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); si.lpDesktop = ""; bResult = CreateProcessAsUser( hDupedToken, null, strCommand, ref sa, ref sa, false, 0, IntPtr.Zero, null, ref si, ref pi ); if(!bResult) { throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); } int status = WaitForSingleObject(pi.hProcess, -1); if(status == -1) { throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); } bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); if(!bResult) { throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); } } finally { if (pi.hThread != IntPtr.Zero) { CloseHandle(pi.hThread); } if (pi.hProcess != IntPtr.Zero) { CloseHandle(pi.hProcess); } if (hDupedToken != IntPtr.Zero) { CloseHandle(hDupedToken); } } } } } "@ Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" } function ValidateFile { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Path, [string] $FileHash, [string] $HashAlgorithm, [string] $SignerThumbprint, [string] $SignerSubject ) if ($FileHash) { ValidateFileHash -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm } if ($SignerThumbprint -or $SignerSubject) { ValidateFileSignature -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject } } function ValidateFileHash { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Hash, [string] $Algorithm ) if ([string]::IsNullOrEmpty($Algorithm)) { $Algorithm = 'SHA256' } Trace-Message ($LocalizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction Stop if ($fileHash.Hash -ne $Hash) { throw ($LocalizedData.InvalidFileHash -f $Path, $Hash, $Algorithm) } } function ValidateFileSignature { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Path, [string] $Thumbprint, [string] $Subject ) Trace-Message ($LocalizedData.CheckingFileSignature -f $Path) $signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction Stop if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { throw ($LocalizedData.InvalidFileSignature -f $Path, $signature.Status) } else { Trace-Message ($LocalizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) } if ($Subject -and ($signature.SignerCertificate.Subject -notlike $Subject)) { throw ($LocalizedData.WrongSignerSubject -f $Path, $Subject) } if ($Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) { throw ($LocalizedData.WrongSignerThumbprint -f $Path, $Thumbprint) } } Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource |