Private/New-StandaloneDetectShim.ps1
|
<#
.SYNOPSIS Generates an enterprise detection script using dual 64/32-bit registry view enumeration and SemVer evaluation. .DESCRIPTION Enumerates subkeys in both 64-bit and 32-bit registry hives via OpenBaseKey, matching ProductCode GUIDs or DisplayName regexes, and evaluates versions using segment-by-segment integer comparison. #> function New-StandaloneDetectShim { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PackageId, [Parameter()] [string]$DisplayName = '', [Parameter()] [string]$ProductCode = '', [Parameter()] [string]$MinVersion = '', [Parameter()] [string]$DetectionStrategy = 'RegistryDisplayVersion' ) $scriptTemplate = @' <# .SYNOPSIS Intune Win32 Detection Script for __PACKAGE_ID__ Generated by WingetIntune (Dual-View Registry Enumeration & SemVer Evaluator) #> $packageId = '__PACKAGE_ID__' $displayName = '__DISPLAY_NAME__' $productCode = '__PRODUCT_CODE__' $minVersion = '__MIN_VERSION__' $installed = $false $detectedVersion = $null # Helper: Compare Version Strings Segment-by-Segment (Avoids "1.10.0" < "1.9.0" string bugs) function Compare-SemVer { param([string]$Installed, [string]$Required) if (-not $Installed -or -not $Required) { return $true } # Strip leading 'v' or build tags $cleanInst = ($Installed -replace '^[vV]', '') -replace '-.+$', '' $cleanReq = ($Required -replace '^[vV]', '') -replace '-.+$', '' $instParts = @($cleanInst.Split('.') | ForEach-Object { $val = 0; [int]::TryParse($_, [ref]$val) | Out-Null; $val }) $reqParts = @($cleanReq.Split('.') | ForEach-Object { $val = 0; [int]::TryParse($_, [ref]$val) | Out-Null; $val }) $maxLen = [Math]::Max($instParts.Count, $reqParts.Count) for ($i = 0; $i -lt $maxLen; $i++) { $iVal = if ($i -lt $instParts.Count) { $instParts[$i] } else { 0 } $rVal = if ($i -lt $reqParts.Count) { $reqParts[$i] } else { 0 } if ($iVal -gt $rVal) { return $true } if ($iVal -lt $rVal) { return $false } } return $true } # 1. Dual-View Registry Hive Enumeration (Bypasses 32-bit IME WOW64 Redirection) $views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) foreach ($view in $views) { try { $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view) $uninstallKey = $base.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') if ($uninstallKey) { foreach ($subName in $uninstallKey.GetSubKeyNames()) { $sub = $uninstallKey.OpenSubKey($subName) if ($sub) { $dName = [string]$sub.GetValue('DisplayName') $dVer = [string]$sub.GetValue('DisplayVersion') $matched = $false if ($productCode -and $subName -eq $productCode) { $matched = $true } elseif ($displayName -and $dName -match [regex]::Escape($displayName)) { $matched = $true } elseif ($packageId -and $dName -match [regex]::Escape($packageId)) { $matched = $true } if ($matched) { $installed = $true $detectedVersion = $dVer $sub.Close() break } $sub.Close() } } $uninstallKey.Close() } $base.Close() } catch { } if ($installed) { break } } # 2. Local Winget List Fallback if (-not $installed) { try { $wingetCmd = Get-Command 'winget.exe' -ErrorAction SilentlyContinue if ($wingetCmd) { $output = & $wingetCmd.Source list --exact --id $packageId --source winget --accept-source-agreements 2>$null if ($output -match [regex]::Escape($packageId)) { $installed = $true } } } catch { } } # 3. Final Version Compliance Evaluation if ($installed) { if ($minVersion -and $detectedVersion) { $isCompliant = Compare-SemVer -Installed $detectedVersion -Required $minVersion if (-not $isCompliant) { Write-Output "Non-Compliant: Detected version $detectedVersion is below required $minVersion." exit 1 } } Write-Output "Compliant: $packageId detected (Version: $detectedVersion)." exit 0 } else { exit 1 } '@ $content = $scriptTemplate.Replace('__PACKAGE_ID__', $PackageId).Replace('__DISPLAY_NAME__', $DisplayName).Replace('__PRODUCT_CODE__', $ProductCode).Replace('__MIN_VERSION__', $MinVersion).Replace('__STRATEGY__', $DetectionStrategy) return $content } |