SharedResources/Src/InstallPrimaryHeadNode/xPSDesiredStateConfiguration/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 (1,346 lines of code) (raw):
# Suppress Global Vars PSSA Error because $global:DSCMachineStatus must be allowed
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
param()
data LocalizedData
{
# culture='en-US'
# TODO: Support WhatIf
ConvertFrom-StringData -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 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}.
RemovingRegistryValue = Removing package registry value of {0}.
ValidateStandardArgumentsPathwasPath = Validate-StandardArguments, Path was {0}
TheurischemewasuriScheme = The uri scheme was {0}
ThepathextensionwaspathExt = The path extension was {0}
ParsingProductIdasanidentifyingNumber = Parsing {0} as an identifyingNumber
ParsedProductIdasidentifyingNumber = Parsed {0} as {1}
EnsureisEnsure = Ensure is {0}
productisproduct = product {0} found
productasbooleanis = product as boolean is {0}
Creatingcachelocation = Creating cache location
NeedtodownloadfilefromschemedestinationwillbedestName = Need to download file from {0}, destination will be {1}
Creatingthedestinationcachefile = Creating the destination cache file
Creatingtheschemestream = Creating the {0} stream
Settingdefaultcredential = Setting default credential
Settingauthenticationlevel = Setting authentication level
Ignoringbadcertificates = Ignoring bad certificates
Gettingtheschemeresponsestream = Getting the {0} response stream
ErrorOutString = Error: {0}
Copyingtheschemestreambytestothediskcache = Copying the {0} stream bytes to the disk cache
Redirectingpackagepathtocachefilelocation = Redirecting package path to cache file location
ThebinaryisanEXE = The binary is an EXE
Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process
StartingwithstartInfoFileNamestartInfoArguments = Starting {0} with {1}
ProvideParameterForRegistryCheck = Please provide the {0} parameter in order to check for installation status from a registry key.
ErrorSettingRegistryValue = An error occured while attempting to set the registry key {0} value {1} to {2}
ErrorRemovingRegistryValue = An error occured while attempting to remove the registry key {0} value {1}
ExeCouldNotBeUninstalled = The .exe file found at {0} could not be uninstalled. The uninstall functionality may not be implemented in this .exe file.
'@
}
# Commented-out until more languages are supported
# Import-LocalizedData -BindingVariable 'LocalizedData' -FileName 'MSFT_xPackageResource.strings.psd1'
Import-Module -Name "$PSScriptRoot\..\CommonResourceHelper.psm1" -Force
$script:packageCacheLocation = "$env:programData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xPackageResource"
$script:msiTools = $null
<#
.SYNOPSIS
Asserts that the path extension is valid.
.PARAMETER Path
The path to validate the extension of.
#>
function Assert-PathExtensionValid
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Path
)
$pathExtension = [System.IO.Path]::GetExtension($Path)
Write-Verbose -Message ($LocalizedData.ThePathExtensionWasPathExt -f $pathExtension)
$validPathExtensions = @( '.msi', '.exe' )
if ($validPathExtensions -notcontains $pathExtension.ToLower())
{
New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidBinaryType -f $Path)
}
}
<#
.SYNOPSIS
Retrieves the product ID as an identifying number.
.PARAMETER ProductId
The product id to retrieve as an identifying number.
#>
function Convert-ProductIdToIdentifyingNumber
{
[OutputType([String])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$ProductId
)
try
{
Write-Verbose -Message ($LocalizedData.ParsingProductIdAsAnIdentifyingNumber -f $ProductId)
$identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper()
Write-Verbose -Message ($LocalizedData.ParsedProductIdAsIdentifyingNumber -f $ProductId, $identifyingNumber)
return $identifyingNumber
}
catch
{
New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($LocalizedData.InvalidIdentifyingNumber -f $ProductId)
}
}
<#
.SYNOPSIS
Converts the given path to a URI.
Throws an exception if the path's scheme as a URI is not valid.
.PARAMETER Path
The path to retrieve as a URI.
#>
function Convert-PathToUri
{
[OutputType([Uri])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Path
)
try
{
$uri = [Uri] $Path
}
catch
{
New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidPath -f $Path)
}
$validUriSchemes = @( 'file', 'http', 'https' )
if ($validUriSchemes -notcontains $uri.Scheme)
{
Write-Verbose -Message ($Localized.TheUriSchemeWasUriScheme -f $uri.Scheme)
New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidPath -f $Path)
}
return $uri
}
<#
.SYNOPSIS
Retrieves a value from a registry without throwing errors.
.PARAMETER Key
The key of the registry to get the value from.
.PARAMETER Value
The name of the value to retrieve.
.PARAMETER RegistryHive
The registry hive to retrieve the value from.
.PARAMETER RegistyView
The registry view to retrieve the value from.
#>
function Get-RegistryValueWithErrorsIgnored
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String]
$Key,
[Parameter(Mandatory = $true)]
[String]
$Value,
[Parameter(Mandatory = $true)]
[Microsoft.Win32.RegistryHive]
$RegistryHive,
[Parameter(Mandatory = $true)]
[Microsoft.Win32.RegistryView]
$RegistryView
)
$registryValue = $null
try
{
$baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView)
$subRegistryKey = $baseRegistryKey.OpenSubKey($Key)
if ($null -ne $subRegistryKey)
{
$registryValue = $subRegistryKey.GetValue($Value)
}
}
catch
{
$exceptionText = ($_ | Out-String).Trim()
Write-Verbose -Message "An exception occured while attempting to retrieve a registry value: $exceptionText"
}
return $registryValue
}
<#
.SYNOPSIS
Retrieves the product entry for the package with the given name and/or identifying number.
.PARAMETER Name
The name of the product entry to retrieve.
.PARAMETER CreateCheckRegValue
Indicates whether or not to retrieve the package installation status from a registry.
.PARAMETER IdentifyingNumber
The identifying number of the product entry to retrieve.
.PARAMETER InstalledCheckRegHive
The registry hive to check for package installation status.
.PARAMETER InstalledCheckRegKey
The registry key to open to check for package installation status.
.PARAMETER InstalledCheckRegValueName
The registry value name to check for package installation status.
.PARAMETER InstalledCheckRegValueData
The value to compare against the retrieved registry value to check for package installation.
#>
function Get-ProductEntry
{
[CmdletBinding()]
param
(
[String]
$Name,
[String]
$IdentifyingNumber,
[Switch]
$CreateCheckRegValue,
[ValidateSet('LocalMachine', 'CurrentUser')]
[String]
$InstalledCheckRegHive = 'LocalMachine',
[String]
$InstalledCheckRegKey,
[String]
$InstalledCheckRegValueName,
[String]
$InstalledCheckRegValueData
)
$uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
$uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
$productEntry = $null
if (-not [String]::IsNullOrEmpty($IdentifyingNumber))
{
$productEntryKeyLocation = Join-Path -Path $uninstallRegistryKey -ChildPath $IdentifyingNumber
$productEntry = Get-Item -Path $productEntryKeyLocation -ErrorAction 'SilentlyContinue'
if ($null -eq $productEntry)
{
$productEntryKeyLocation = Join-Path -Path $uninstallRegistryKeyWow64 -ChildPath $IdentifyingNumber
$productEntry = Get-Item $productEntryKeyLocation -ErrorAction 'SilentlyContinue'
}
}
else
{
foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' ))
{
if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName'))
{
$productEntry = $registryKeyEntry
break
}
}
}
if ($null -eq $productEntry)
{
if ($CreateCheckRegValue)
{
$installValue = $null
$win32OperatingSystem = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction 'SilentlyContinue'
# If 64-bit OS, check 64-bit registry view first
if ($win32OperatingSystem.OSArchitecture -ieq '64-bit')
{
$installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry64'
}
if ($null -eq $installValue)
{
$installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry32'
}
if ($null -ne $installValue)
{
if ($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData)
{
$productEntry = @{
Installed = $true
}
}
}
}
}
return $productEntry
}
function Test-TargetResource
{
[OutputType([Boolean])]
[CmdletBinding()]
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]
[System.Management.Automation.Credential()]
$Credential,
# Return codes 1641 and 3010 indicate success when a restart is requested per installation
[ValidateNotNullOrEmpty()]
[UInt32[]]
$ReturnCode = @( 0, 1641, 3010 ),
[String]
$LogPath,
[String]
$FileHash,
[ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')]
[String]
$HashAlgorithm,
[String]
$SignerSubject,
[String]
$SignerThumbprint,
[String]
$ServerCertificateValidationCallback,
[Boolean]
$CreateCheckRegValue = $false,
[ValidateSet('LocalMachine','CurrentUser')]
[String]
$InstalledCheckRegHive = 'LocalMachine',
[String]
$InstalledCheckRegKey,
[String]
$InstalledCheckRegValueName,
[String]
$InstalledCheckRegValueData
)
Assert-PathExtensionValid -Path $Path
$uri = Convert-PathToUri -Path $Path
if (-not [String]::IsNullOrEmpty($ProductId))
{
$identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId
}
$getProductEntryParameters = @{
Name = $Name
IdentifyingNumber = $identifyingNumber
}
$checkRegistryValueParameters = @{
CreateCheckRegValue = $CreateCheckRegValue
InstalledCheckRegHive = $InstalledCheckRegHive
InstalledCheckRegKey = $InstalledCheckRegKey
InstalledCheckRegValueName = $InstalledCheckRegValueName
InstalledCheckRegValueData = $InstalledCheckRegValueData
}
if ($CreateCheckRegValue)
{
Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData
$getProductEntryParameters += $checkRegistryValueParameters
}
$productEntry = Get-ProductEntry @getProductEntryParameters
Write-Verbose -Message ($LocalizedData.EnsureIsEnsure -f $Ensure)
if ($null -eq $productEntry)
{
Write-Verbose -Message ($LocalizedData.ProductIsProduct -f $productEntry)
}
else
{
Write-Verbose -Message 'Product installation cannot be determined'
}
Write-Verbose -Message ($LocalizedData.ProductAsBooleanIs -f [Boolean]$productEntry)
if ($null -ne $productEntry)
{
if ($CreateCheckRegValue)
{
Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $Name)
}
else
{
$displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName'
Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $displayName)
}
}
else
{
$displayName = $null
if (-not [String]::IsNullOrEmpty($Name))
{
$displayName = $Name
}
else
{
$displayName = $ProductId
}
Write-Verbose -Message ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName)
}
return ($null -ne $productEntry -and $Ensure -eq 'Present') -or ($null -eq $productEntry -and $Ensure -eq 'Absent')
}
<#
.SYNOPSIS
Retrieves a localized registry key value.
.PARAMETER RegistryKey
The registry key to retrieve the value from.
.PARAMETER ValueName
The name of the value to retrieve.
#>
function Get-LocalizedRegistryKeyValue
{
[CmdletBinding()]
param
(
[Object]
$RegistryKey,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$ValueName
)
$localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName)
if ($null -eq $localizedRegistryKeyValue)
{
$localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName)
}
return $localizedRegistryKeyValue
}
function Get-TargetResource
{
[OutputType([Hashtable])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[String]
$Name,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Path,
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[String]
$ProductId,
[Boolean]
$CreateCheckRegValue = $false,
[ValidateSet('LocalMachine','CurrentUser')]
[String]
$InstalledCheckRegHive = 'LocalMachine',
[String]
$InstalledCheckRegKey,
[String]
$InstalledCheckRegValueName,
[String]
$InstalledCheckRegValueData
)
Assert-PathExtensionValid -Path $Path
$uri = Convert-PathToUri -Path $Path
if (-not [String]::IsNullOrEmpty($ProductId))
{
$identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId
}
else
{
$identifyingNumber = $ProductId
}
$packageResourceResult = @{}
$getProductEntryParameters = @{
Name = $Name
IdentifyingNumber = $identifyingNumber
}
$checkRegistryValueParameters = @{
CreateCheckRegValue = $CreateCheckRegValue
InstalledCheckRegHive = $InstalledCheckRegHive
InstalledCheckRegKey = $InstalledCheckRegKey
InstalledCheckRegValueName = $InstalledCheckRegValueName
InstalledCheckRegValueData = $InstalledCheckRegValueData
}
if ($CreateCheckRegValue)
{
Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData
$getProductEntryParameters += $checkRegistryValueParameters
$packageResourceResult += $checkRegistryValueParameters
}
$productEntry = Get-ProductEntry @getProductEntryParameters
if ($null -eq $productEntry)
{
$packageResourceResult += @{
Ensure = 'Absent'
Name = $Name
ProductId = $identifyingNumber
Path = $Path
Installed = $false
}
return $packageResourceResult
}
elseif ($CreateCheckRegValue)
{
$packageResourceResult += @{
Ensure = 'Present'
Name = $Name
ProductId = $identifyingNumber
Path = $Path
Installed = $true
}
return $packageResourceResult
}
<#
Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE).
If the user gave a product ID just pass it through, otherwise get it from the product.
#>
if ($null -eq $identifyingNumber -and $null -ne $productEntry.Name)
{
$identifyingNumber = Split-Path -Path $productEntry.Name -Leaf
}
$installDate = $productEntry.GetValue('InstallDate')
if ($null -ne $installDate)
{
try
{
$installDate = '{0:d}' -f [DateTime]::ParseExact($installDate, 'yyyyMMdd',[System.Globalization.CultureInfo]::CurrentCulture).Date
}
catch
{
$installDate = $null
}
}
$publisher = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'Publisher'
$estimatedSize = $productEntry.GetValue('EstimatedSize')
if ($null -ne $estimatedSize)
{
$estimatedSize = $estimatedSize / 1024
}
$displayVersion = $productEntry.GetValue('DisplayVersion')
$comments = $productEntry.GetValue('Comments')
$displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName'
$packageResourceResult += @{
Ensure = 'Present'
Name = $displayName
Path = $Path
InstalledOn = $installDate
ProductId = $identifyingNumber
Size = $estimatedSize
Installed = $true
Version = $displayVersion
PackageDescription = $comments
Publisher = $publisher
}
return $packageResourceResult
}
<#
.SYNOPSIS
Retrieves the MSI tools type.
#>
function Get-MsiTool
{
[OutputType([System.Type])]
[CmdletBinding()]
param ()
if ($null -ne $script:msiTools)
{
return $script:msiTools
}
$msiToolsCodeDefinition = @'
[DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, 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 = MsiOpenPackageExW(msi, 1, 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");
}
'@
if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type)
{
$script:msiTools = ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type
}
else
{
$script:msiTools = Add-Type `
-Namespace 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' `
-Name 'MsiTools' `
-Using 'System.Text' `
-MemberDefinition $msiToolsCodeDefinition `
-PassThru
}
return $script:msiTools
}
<#
.SYNOPSIS
Retrieves the name of a product from an msi.
.PARAMETER Path
The path to the msi to retrieve the name from.
#>
function Get-MsiProductName
{
[OutputType([String])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Path
)
$msiTools = Get-MsiTool
$productName = $msiTools::GetProductName($Path)
return $productName
}
<#
.SYNOPSIS
Retrieves the code of a product from an msi.
.PARAMETER Path
The path to the msi to retrieve the code from.
#>
function Get-MsiProductCode
{
[OutputType([String])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Path
)
$msiTools = Get-MsiTool
$productCode = $msiTools::GetProductCode($Path)
return $productCode
}
<#
.SYNOPSIS
Asserts that the InstalledCheckRegKey, InstalledCheckRegValueName, and
InstalledCheckRegValueData parameter required for retrieving package installation status
from a registry are not null or empty.
.PARAMETER InstalledCheckRegKey
The InstalledCheckRegKey parameter to check.
.PARAMETER InstalledCheckRegValueName
The InstalledCheckRegValueName parameter to check.
.PARAMETER InstalledCheckRegValueData
The InstalledCheckRegValueData parameter to check.
.NOTES
This could be done with parameter validation.
It is implemented this way to provide a clearer error message.
#>
function Assert-RegistryParametersValid
{
[CmdletBinding()]
param
(
[String]
$InstalledCheckRegKey,
[String]
$InstalledCheckRegValueName,
[String]
$InstalledCheckRegValueData
)
foreach ($parameter in $PSBoundParameters.Keys)
{
if ([String]::IsNullOrEmpty($PSBoundParameters[$parameter]))
{
New-InvalidArgumentException -ArgumentName $parameter -Message ($LocalizedData.ProvideParameterForRegistryCheck -f $parameter)
}
}
}
<#
.SYNOPSIS
Sets the value of a registry key to the specified data.
.PARAMETER Key
The registry key that contains the value to set.
.PARAMETER Value
The value name of the registry key value to set.
.PARAMETER RegistryHive
The registry hive that contains the registry key to set.
.PARAMETER Data
The data to set the registry key value to.
#>
function Set-RegistryValue
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String]
$Key,
[Parameter(Mandatory = $true)]
[String]
$Value,
[Parameter(Mandatory = $true)]
[Microsoft.Win32.RegistryHive]
$RegistryHive,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Data
)
try
{
$baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default)
# Opens the subkey with write access
$subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true)
if ($null -eq $subRegistryKey)
{
Write-Verbose "Key: '$Key'"
$subRegistryKey = $baseRegistryKey.CreateSubKey($Key)
}
$subRegistryKey.SetValue($Value, $Data)
$subRegistryKey.Close()
}
catch
{
New-InvalidOperationException -Message ($LocalizedData.ErrorSettingRegistryValue -f $Key, $Value, $Data) -ErrorRecord $_
}
}
<#
.SYNOPSIS
Removes the specified value of a registry key.
.PARAMETER Key
The registry key that contains the value to remove.
.PARAMETER Value
The value name of the registry key value to remove.
.PARAMETER RegistryHive
The registry hive that contains the registry key to remove.
#>
function Remove-RegistryValue
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String]
$Key,
[Parameter(Mandatory = $true)]
[String]
$Value,
[Parameter(Mandatory = $true)]
[Microsoft.Win32.RegistryHive]
$RegistryHive
)
try
{
$baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default)
$subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true)
$subRegistryKey.DeleteValue($Value)
$subRegistryKey.Close()
}
catch
{
New-InvalidOperationException -Message ($LocalizedData.ErrorRemovingRegistryValue -f $Key, $Value) -ErrorRecord $_
}
}
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]
[System.Management.Automation.Credential()]
$Credential,
# Return codes 1641 and 3010 indicate success when a restart is requested per installation
[ValidateNotNullOrEmpty()]
[UInt32[]]
$ReturnCode = @( 0, 1641, 3010 ),
[String]
$LogPath,
[String]
$FileHash,
[ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')]
[String]
$HashAlgorithm,
[String]
$SignerSubject,
[String]
$SignerThumbprint,
[String]
$ServerCertificateValidationCallback,
[Boolean]
$CreateCheckRegValue = $false,
[ValidateSet('LocalMachine','CurrentUser')]
[String]
$InstalledCheckRegHive = 'LocalMachine',
[String]
$InstalledCheckRegKey,
[String]
$InstalledCheckRegValueName,
[String]
$InstalledCheckRegValueData
)
$ErrorActionPreference = 'Stop'
if (Test-TargetResource @PSBoundParameters)
{
return
}
Assert-PathExtensionValid -Path $Path
$uri = Convert-PathToUri -Path $Path
if (-not [String]::IsNullOrEmpty($ProductId))
{
$identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId
}
else
{
$identifyingNumber = $ProductId
}
$productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber
<#
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 error message.
#>
$originalPath = $Path
Write-Verbose -Message $LocalizedData.PackageConfigurationStarting
$logStream = $null
$psDrive = $null
$downloadedFileName = $null
try
{
$fileExtension = [System.IO.Path]::GetExtension($Path).ToLower()
if (-not [String]::IsNullOrEmpty($LogPath))
{
try
{
if ($fileExtension -eq '.msi')
{
<#
We want to pre-verify the log path exists and is writable ahead of time
even in the MSI case, as detecting WHY the MSI log path doesn't exist would
be rather problematic for the user.
#>
if ((Test-Path -Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile, $null, $null))
{
Remove-Item -Path $LogPath
}
if ($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null))
{
New-Item -Path $LogPath -Type 'File' | Out-Null
}
}
elseif ($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null))
{
$logStream = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $LogPath, $false )
}
}
catch
{
New-InvalidOperationException -Message ($LocalizedData.CouldNotOpenLog -f $LogPath) -ErrorRecord $_
}
}
# 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 -Path $uri.LocalPath
}
# If we pass a null for Credential, a dialog will pop up.
if ($null -ne $Credential)
{
$psDriveArgs['Credential'] = $Credential
}
$psDrive = New-PSDrive @psDriveArgs
$Path = Join-Path -Path $psDrive.Root -ChildPath (Split-Path -Path $uri.LocalPath -Leaf)
}
elseif (@( 'http', 'https' ) -contains $uri.Scheme -and $Ensure -eq 'Present' -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null))
{
$uriScheme = $uri.Scheme
$outStream = $null
$responseStream = $null
try
{
Write-Verbose -Message ($LocalizedData.CreatingCacheLocation)
if (-not (Test-Path -Path $script:packageCacheLocation -PathType 'Container'))
{
New-Item -Path $script:packageCacheLocation -ItemType 'Directory' | Out-Null
}
$destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Split-Path -Path $uri.LocalPath -Leaf)
Write-Verbose -Message ($LocalizedData.NeedtodownloadfilefromschemedestinationwillbedestName -f $uriScheme, $destinationPath)
try
{
Write-Verbose -Message ($LocalizedData.CreatingTheDestinationCacheFile)
$outStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $destinationPath, 'Create' )
}
catch
{
# Should never happen since we own the cache directory
New-InvalidOperationException -Message ($LocalizedData.CouldNotOpenDestFile -f $destinationPath) -ErrorRecord $_
}
try
{
Write-Verbose -Message ($LocalizedData.CreatingTheSchemeStream -f $uriScheme)
$webRequest = [System.Net.WebRequest]::Create($uri)
Write-Verbose -Message ($LocalizedData.SettingDefaultCredential)
$webRequest.Credentials = [System.Net.CredentialCache]::DefaultCredentials
if ($uriScheme -eq 'http')
{
# Default value is MutualAuthRequested, which applies to the https scheme
Write-Verbose -Message ($LocalizedData.SettingAuthenticationLevel)
$webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None
}
elseif ($uriScheme -eq 'https' -and -not [String]::IsNullOrEmpty($ServerCertificateValidationCallback))
{
Write-Verbose -Message 'Assigning user-specified certificate verification callback'
$serverCertificateValidationScriptBlock = [ScriptBlock]::Create($ServerCertificateValidationCallback)
$webRequest.ServerCertificateValidationCallBack = $serverCertificateValidationScriptBlock
}
Write-Verbose -Message ($LocalizedData.Gettingtheschemeresponsestream -f $uriScheme)
$responseStream = (([System.Net.HttpWebRequest]$webRequest).GetResponse()).GetResponseStream()
}
catch
{
Write-Verbose -Message ($LocalizedData.ErrorOutString -f ($_ | Out-String))
New-InvalidOperationException -Message ($LocalizedData.CouldNotGetHttpStream -f $uriScheme, $Path) -ErrorRecord $_
}
try
{
Write-Verbose -Message ($LocalizedData.CopyingTheSchemeStreamBytesToTheDiskCache -f $uriScheme)
$responseStream.CopyTo($outStream)
$responseStream.Flush()
$outStream.Flush()
}
catch
{
New-InvalidOperationException -Message ($LocalizedData.ErrorCopyingDataToFile -f $Path, $destinationPath) -ErrorRecord $_
}
}
finally
{
if ($null -ne $outStream)
{
$outStream.Close()
}
if ($null -ne $responseStream)
{
$responseStream.Close()
}
}
Write-Verbose -Message ($LocalizedData.RedirectingPackagePathToCacheFileLocation)
$Path = $destinationPath
$downloadedFileName = $destinationPath
}
# At this point the Path ought to be valid unless it's a MSI uninstall case
if (-not (Test-Path -Path $Path -PathType 'Leaf'))
{
New-InvalidOperationException -Message ($LocalizedData.PathDoesNotExist -f $Path)
}
Assert-FileValid -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint
}
$startInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo'
# Necessary for I/O redirection and just generally a good idea
$startInfo.UseShellExecute = $false
$process = New-Object -TypeName 'System.Diagnostics.Process'
$process.StartInfo = $startInfo
# Concept only, will never touch disk
$errorLogPath = $LogPath + ".err"
if ($fileExtension -eq '.msi')
{
$startInfo.FileName = "$env:winDir\system32\msiexec.exe"
if ($Ensure -eq 'Present')
{
# Check if the MSI package specifies the ProductName and Code
$productName = Get-MsiProductName -Path $Path
$productCode = Get-MsiProductCode -Path $Path
if ((-not [String]::IsNullOrEmpty($Name)) -and ($productName -ne $Name))
{
New-InvalidArgumentException -ArgumentName 'Name' -Message ($LocalizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode)
}
if ((-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $productCode))
{
New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($LocalizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode)
}
$startInfo.Arguments = '/i "{0}"' -f $Path
}
else
{
$productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber
# We may have used the Name earlier, now we need the actual ID
$id = Split-Path -Path $productEntry.Name -Leaf
$startInfo.Arguments = '/x{0}' -f $id
}
if ($LogPath)
{
$startInfo.Arguments += ' /log "{0}"' -f $LogPath
}
$startInfo.Arguments += " /quiet"
if ($Arguments)
{
$startInfo.Arguments += "$Arguments"
}
}
else
{
# EXE
Write-Verbose -Message $LocalizedData.TheBinaryIsAnExe
if ($Ensure -eq 'Present')
{
$startInfo.FileName = $Path
$startInfo.Arguments = $Arguments
if ($LogPath)
{
Write-Verbose -Message ($LocalizedData.UserHasRequestedLoggingNeedToAttachEventHandlersToTheProcess)
$startInfo.RedirectStandardError = $true
$startInfo.RedirectStandardOutput = $true
Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -SourceIdentifier $LogPath
Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -SourceIdentifier $errorLogPath
}
}
else
{
# Absent case
$startInfo.FileName = "$env:winDir\system32\msiexec.exe"
# We may have used the Name earlier, now we need the actual ID
if ($null -eq $productEntry.Name)
{
$id = $Path
}
else
{
$id = Split-Path -Path $productEntry.Name -Leaf
}
$startInfo.Arguments = "/x $id /quiet /norestart"
if ($LogPath)
{
$startInfo.Arguments += ' /log "{0}"' -f $LogPath
}
if ($Arguments)
{
$startInfo.Arguments += "$Arguments"
}
}
}
Write-Verbose -Message ($LocalizedData.StartingWithStartInfoFileNameStartInfoArguments -f $startInfo.FileName, $startInfo.Arguments)
if ($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null))
{
try
{
$exitCode = 0
$process.Start() | Out-Null
# Identical to $fileExtension -eq '.exe' -and $logPath
if ($logStream)
{
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
}
$process.WaitForExit()
if ($process)
{
$exitCode = $process.ExitCode
}
}
catch
{
New-InvalidOperationException -Message ($LocalizedData.CouldNotStartProcess -f $Path) -ErrorRecord $_
}
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))
{
# Some .exe files do not support uninstall
if ($Ensure -eq 'Absent' -and $fileExtension -eq '.exe' -and $exitCode -eq '1620')
{
Write-Warning -Message ($LocalizedData.ExeCouldNotBeUninstalled -f $Path)
}
else
{
New-InvalidOperationException ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString())
}
}
}
}
finally
{
if ($psDrive)
{
Remove-PSDrive -Name $psDrive -Force
}
if ($logStream)
{
$logStream.Dispose()
}
}
if ($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null))
{
<#
This is deliberately not in the finally block because we want to leave the downloaded
file on disk if an error occurred as a debugging aid for the user.
#>
Remove-Item -Path $downloadedFileName
}
$operationMessageString = $LocalizedData.PackageUninstalled
if ($Ensure -eq 'Present')
{
$operationMessageString = $LocalizedData.PackageInstalled
}
if ($CreateCheckRegValue)
{
$registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName
if ($Ensure -eq 'Present')
{
Write-Verbose -Message ($LocalizedData.CreatingRegistryValue -f $registryValueString)
Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData
}
else
{
Write-Verbose ($LocalizedData.RemovingRegistryValue -f $registryValueString)
Remove-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName
}
}
<#
Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is
missing on some client SKUs (worked on both Server and Client Skus in Windows 10).
#>
$serverFeatureData = Invoke-CimMethod -Name 'GetServerFeature' -Namespace 'root\microsoft\windows\servermanager' -Class 'MSFT_ServerManagerTasks' -Arguments @{ BatchSize = 256 } -ErrorAction 'Ignore'
$registryData = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction 'Ignore'
if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or $exitcode -eq 3010 -or $exitcode -eq 1641)
{
Write-Verbose $LocalizedData.MachineRequiresReboot
$global:DSCMachineStatus = 1
}
if ($Ensure -eq 'Present')
{
$getProductEntryParameters = @{
Name = $Name
IdentifyingNumber = $identifyingNumber
}
$checkRegistryValueParameters = @{
CreateCheckRegValue = $CreateCheckRegValue
InstalledCheckRegHive = $InstalledCheckRegHive
InstalledCheckRegKey = $InstalledCheckRegKey
InstalledCheckRegValueName = $InstalledCheckRegValueName
InstalledCheckRegValueData = $InstalledCheckRegValueData
}
if ($CreateCheckRegValue)
{
$getProductEntryParameters += $checkRegistryValueParameters
}
$productEntry = Get-ProductEntry @getProductEntryParameters
if ($null -eq $productEntry)
{
New-InvalidOperationException -Message ($LocalizedData.PostValidationError -f $originalPath)
}
}
Write-Verbose -Message $operationMessageString
Write-Verbose -Message $LocalizedData.PackageConfigurationComplete
}
<#
.SYNOPSIS
Asserts that the file at the given path is valid.
.PARAMETER Path
The path to the file to check.
.PARAMETER FileHash
The hash that should match the hash of the file.
.PARAMETER HashAlgorithm
The algorithm to use to retrieve the file hash.
.PARAMETER SignerThumbprint
The certificate thumbprint that should match the file's signer certificate.
.PARAMETER SignerSubject
The certificate subject that should match the file's signer certificate.
#>
function Assert-FileValid
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String]
$Path,
[String]
$FileHash,
[String]
$HashAlgorithm,
[String]
$SignerThumbprint,
[String]
$SignerSubject
)
if (-not [String]::IsNullOrEmpty($FileHash))
{
Assert-FileHashValid -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm
}
if (-not [String]::IsNullOrEmpty($SignerThumbprint) -or -not [String]::IsNullOrEmpty($SignerSubject))
{
Assert-FileSignatureValid -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject
}
}
<#
.SYNOPSIS
Asserts that the hash of the file at the given path matches the given hash.
.PARAMETER Path
The path to the file to check the hash of.
.PARAMETER Hash
The hash to check against.
.PARAMETER Algorithm
The algorithm to use to retrieve the file's hash.
#>
function Assert-FileHashValid
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String]
$Path,
[Parameter(Mandatory)]
[String]
$Hash,
[String]
$Algorithm = 'SHA256'
)
if ([String]::IsNullOrEmpty($Algorithm))
{
$Algorithm = 'SHA256'
}
Write-Verbose -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)
}
}
<#
.SYNOPSIS
Asserts that the signature of the file at the given path is valid.
.PARAMETER Path
The path to the file to check the signature of
.PARAMETER Thumbprint
The certificate thumbprint that should match the file's signer certificate.
.PARAMETER Subject
The certificate subject that should match the file's signer certificate.
#>
function Assert-FileSignatureValid
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String]
$Path,
[String]
$Thumbprint,
[String]
$Subject
)
Write-Verbose -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
{
Write-Verbose -Message ($LocalizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject)
}
if ($null -ne $Subject -and ($signature.SignerCertificate.Subject -notlike $Subject))
{
throw ($LocalizedData.WrongSignerSubject -f $Path, $Subject)
}
if ($null -ne $Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint))
{
throw ($LocalizedData.WrongSignerThumbprint -f $Path, $Thumbprint)
}
}
Export-ModuleMember -Function *-TargetResource