Private/Test-LocalPackageDetection.ps1
|
#Requires -Version 5.1 <#! .SYNOPSIS Evaluates App.json DetectionRule entries on the local machine. .DESCRIPTION Supports File, Registry, and MSI rule types and returns whether all rules pass, with the best available detected version. .PARAMETER DefinitionObject Parsed App.json definition object. .OUTPUTS PSCustomObject with: Succeeded : bool Installed : bool DetectedVersion : string Status : string RuleCount : int RulePassCount : int Error : string !#> function Test-LocalPackageDetection { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [PSCustomObject]$DefinitionObject ) $result = [PSCustomObject]@{ Succeeded = $false Installed = $false DetectedVersion = '' Status = '' RuleCount = 0 RulePassCount = 0 Error = '' } if ($null -eq $DefinitionObject) { $result.Error = 'Definition object is null.' $result.Status = 'Detection failed' return $result } $rules = @($DefinitionObject.DetectionRule) if ($rules.Count -eq 0) { $result.Succeeded = $true $result.Status = 'No detection rules' return $result } $compareValues = { param( [string]$Operator, [string]$Left, [string]$Right ) $op = if ([string]::IsNullOrWhiteSpace($Operator)) { 'equal' } else { $Operator.Trim() } $leftVersion = $null $rightVersion = $null $leftIsVersion = [version]::TryParse(($Left -replace '[^0-9\.]', '').Trim('.'), [ref]$leftVersion) $rightIsVersion = [version]::TryParse(($Right -replace '[^0-9\.]', '').Trim('.'), [ref]$rightVersion) if ($leftIsVersion -and $rightIsVersion) { switch -Regex ($op.ToLowerInvariant()) { '^equal$|^eq$' { return $leftVersion -eq $rightVersion } '^notequal$|^ne$' { return $leftVersion -ne $rightVersion } '^greaterthan$|^gt$' { return $leftVersion -gt $rightVersion } '^greaterthanorequal$|^greaterthanorequals$|^ge$' { return $leftVersion -ge $rightVersion } '^lessthan$|^lt$' { return $leftVersion -lt $rightVersion } '^lessthanorequal$|^lessthanorequals$|^le$' { return $leftVersion -le $rightVersion } default { return $leftVersion -eq $rightVersion } } } switch -Regex ($op.ToLowerInvariant()) { '^equal$|^eq$' { return [string]::Equals($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase) } '^notequal$|^ne$' { return -not [string]::Equals($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase) } '^contains$' { return ([string]$Left).IndexOf([string]$Right, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 } default { return [string]::Equals($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase) } } } $resolveRegistryPath = { param( [string]$RawPath, [bool]$Check32BitOn64System ) if ([string]::IsNullOrWhiteSpace($RawPath)) { return '' } $normalized = $RawPath.Trim() if ($normalized -match '^HKEY_LOCAL_MACHINE' -or $normalized -match '^HKLM') { $normalized = $normalized -replace '^HKEY_LOCAL_MACHINE', 'HKLM:' -replace '^HKLM', 'HKLM:' } elseif ($normalized -match '^HKEY_CURRENT_USER' -or $normalized -match '^HKCU') { $normalized = $normalized -replace '^HKEY_CURRENT_USER', 'HKCU:' -replace '^HKCU', 'HKCU:' } if ($Check32BitOn64System -and $normalized -like 'HKLM:*' -and $normalized -notmatch 'WOW6432Node') { $normalized = $normalized -replace '^HKLM:\Software', 'HKLM:\Software\WOW6432Node' } return $normalized } $detectedVersion = '' $ruleCount = 0 $rulePassCount = 0 try { foreach ($rule in $rules) { if ($null -eq $rule) { continue } $ruleCount++ $ruleType = [string]$rule.Type $rulePassed = $false Write-Verbose "EvergreenUI: Detection rule $ruleCount - type=$ruleType" switch ($ruleType) { 'File' { $basePath = [string]$rule.Path $fileOrFolder = [string]$rule.FileOrFolder $targetPath = if ([string]::IsNullOrWhiteSpace($fileOrFolder)) { $basePath } else { Join-Path -Path $basePath -ChildPath $fileOrFolder } if (-not [string]::IsNullOrWhiteSpace($targetPath) -and (Test-Path -LiteralPath $targetPath)) { $method = [string]$rule.DetectionMethod if ($method -eq 'Version') { $fileInfo = Get-Item -LiteralPath $targetPath -ErrorAction SilentlyContinue $currentVersion = if ($null -ne $fileInfo -and $null -ne $fileInfo.VersionInfo) { [string]$fileInfo.VersionInfo.FileVersion } else { '' } if (-not [string]::IsNullOrWhiteSpace($currentVersion)) { $detectedVersion = $currentVersion } $rulePassed = & $compareValues -Operator ([string]$rule.Operator) -Left $currentVersion -Right ([string]$rule.VersionValue) Write-Verbose "EvergreenUI: File rule - path='$targetPath' detectedVersion='$currentVersion' expectedVersion='$([string]$rule.VersionValue)' passed=$rulePassed" } else { $rulePassed = $true Write-Verbose "EvergreenUI: File rule (existence) - path='$targetPath' passed=$rulePassed" } } else { Write-Verbose "EvergreenUI: File rule - path='$targetPath' not found; rule failed" } } 'Registry' { $registryPath = & $resolveRegistryPath -RawPath ([string]$rule.KeyPath) -Check32BitOn64System ([string]$rule.Check32BitOn64System -ieq 'true') if (-not [string]::IsNullOrWhiteSpace($registryPath) -and (Test-Path -LiteralPath $registryPath)) { $method = [string]$rule.DetectionMethod if ($method -eq 'Existence') { $rulePassed = $true Write-Verbose "EvergreenUI: Registry rule (existence) - key='$registryPath' passed=$rulePassed" } else { $valueName = [string]$rule.ValueName $item = Get-ItemProperty -LiteralPath $registryPath -ErrorAction SilentlyContinue $currentValue = if ($null -ne $item -and -not [string]::IsNullOrWhiteSpace($valueName)) { [string]$item.$valueName } else { '' } if ($method -eq 'Version' -and -not [string]::IsNullOrWhiteSpace($currentValue)) { $detectedVersion = $currentValue } $expectedValue = if ($method -eq 'Version') { [string]$rule.DetectionValue } else { [string]$rule.DetectionValue } $rulePassed = & $compareValues -Operator ([string]$rule.Operator) -Left $currentValue -Right $expectedValue Write-Verbose "EvergreenUI: Registry rule ($method) - key='$registryPath' value='$valueName' detected='$currentValue' expected='$expectedValue' passed=$rulePassed" } } else { Write-Verbose "EvergreenUI: Registry rule - key='$registryPath' not found; rule failed" } } 'MSI' { $productCode = [string]$rule.ProductCode if (-not [string]::IsNullOrWhiteSpace($productCode)) { $normalizedCode = $productCode.Trim('{}').ToUpperInvariant() $uninstallKeys = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' ) $matchedVersion = '' foreach ($keyRoot in $uninstallKeys) { if (-not (Test-Path -LiteralPath $keyRoot)) { continue } $candidates = @(Get-ChildItem -LiteralPath $keyRoot -ErrorAction SilentlyContinue) foreach ($candidate in $candidates) { $leaf = [string]$candidate.PSChildName if ([string]::IsNullOrWhiteSpace($leaf)) { continue } if ($leaf.Trim('{}').ToUpperInvariant() -ne $normalizedCode) { continue } $entry = Get-ItemProperty -LiteralPath $candidate.PSPath -ErrorAction SilentlyContinue if ($null -ne $entry -and -not [string]::IsNullOrWhiteSpace([string]$entry.DisplayVersion)) { $matchedVersion = [string]$entry.DisplayVersion } else { $matchedVersion = 'Installed' } break } if (-not [string]::IsNullOrWhiteSpace($matchedVersion)) { break } } if (-not [string]::IsNullOrWhiteSpace($matchedVersion)) { if ($matchedVersion -ne 'Installed') { $detectedVersion = $matchedVersion } $expectedVersion = [string]$rule.ProductVersion if ([string]::IsNullOrWhiteSpace($expectedVersion)) { $rulePassed = $true } else { $rulePassed = & $compareValues -Operator ([string]$rule.ProductVersionOperator) -Left $matchedVersion -Right $expectedVersion } Write-Verbose "EvergreenUI: MSI rule - productCode='{$normalizedCode}' detectedVersion='$matchedVersion' expectedVersion='$expectedVersion' passed=$rulePassed" } else { Write-Verbose "EvergreenUI: MSI rule - productCode='{$normalizedCode}' not found in uninstall keys; rule failed" } } } default { $rulePassed = $false Write-Verbose "EvergreenUI: Unknown rule type '$ruleType'; rule failed" } } if ($rulePassed) { $rulePassCount++ } } $result.RuleCount = $ruleCount $result.RulePassCount = $rulePassCount $result.Succeeded = $true $result.Installed = ($ruleCount -gt 0 -and $rulePassCount -eq $ruleCount) $result.DetectedVersion = $detectedVersion if ($result.Installed) { $result.Status = if ([string]::IsNullOrWhiteSpace($detectedVersion)) { 'Installed' } else { "Installed ($detectedVersion)" } } else { $result.Status = 'Not installed' } } catch { $result.Succeeded = $false $result.Error = $_.Exception.Message $result.Status = 'Detection failed' } return $result } |