Private/Select-AppDetectionRule.ps1

function Select-AppDetectionRule {
    <#
    .SYNOPSIS
        Builds Intune detection rules from MSI product codes in the package and recent
        Start Menu shortcuts on this machine, letting the user pick them in a grid.
    .DESCRIPTION
        File-based candidates come from the 15 most recently modified Start Menu shortcuts
        on the packaging machine, so the app should be installed locally first. The icon of
        the first selected file is saved as AppIcon.png in the working directory.
        If nothing is selected, a PLACEHOLDER rule is returned that must be fixed in Intune.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)][bool]$RequireAdmin,
        [Parameter(Mandatory)][string]$Context,
        [Parameter(Mandatory)][hashtable]$Paths
    )

    $PossibleDetections = [System.Collections.Generic.List[object]]::new()

    # MSI detection
    $MSIPaths = Get-ChildItem -LiteralPath (Join-Path $Paths.PSADTDirectory 'Files') -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Extension -eq '.msi' }
    foreach ($MSIPath in $MSIPaths) {
        $Property = ([string](Get-MSIMetaData -Path $MSIPath.FullName -Property ProductCode)).Trim()
        $PossibleDetections.Add([PSCustomObject]@{
                'Type'         = 'MSI'
                'Property'     = $Property
                'VersionValue' = $null
                'Path'         = $MSIPath.FullName
            })
    }

    # File detection via shortcuts
    if ($RequireAdmin) {
        $ShortcutSearchPath = Join-Path -Path $env:ProgramData -ChildPath 'Microsoft\Windows\Start Menu'
    }
    else {
        $ShortcutSearchPath = Join-Path -Path $env:APPDATA -ChildPath 'Microsoft\Windows\Start Menu\Programs'
    }
    $ShortcutPaths = Get-ChildItem -LiteralPath $ShortcutSearchPath -Filter '*.lnk' -Recurse -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 15

    # Targets under %WINDIR% are never app files: advertised MSI shortcuts resolve to icon stubs in
    # %WINDIR%\Installer, and "Uninstall"/console shortcuts point at msiexec.exe or cmd.exe
    $WindowsPath = $env:windir.TrimEnd('\') + '\'
    $seenTargets = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

    $shell = New-Object -ComObject WScript.Shell
    try {
        foreach ($ShortcutPath in $ShortcutPaths) {
            $AppPath = $shell.CreateShortcut($ShortcutPath.FullName).TargetPath
            # Shell-item shortcuts (UWP apps, Control Panel items) have no file target
            if ([string]::IsNullOrWhiteSpace($AppPath)) {
                Write-Verbose "Skipping shortcut without a file target: $($ShortcutPath.FullName)"
                continue
            }
            if ($AppPath.StartsWith($WindowsPath, [System.StringComparison]::OrdinalIgnoreCase)) {
                Write-Verbose "Skipping shortcut to a Windows system file: $($ShortcutPath.FullName) -> $AppPath"
                continue
            }
            if (-not $seenTargets.Add($AppPath)) {
                continue
            }
            if (-not (Test-Path -LiteralPath $AppPath -PathType Leaf)) {
                Write-Warning "Skipping invalid shortcut path: $AppPath"
                continue
            }
            $Name = Split-Path -Path $AppPath -Leaf
            $VersionInfo = (Get-Item -LiteralPath $AppPath).VersionInfo
            $Version = "$($VersionInfo.FileMajorPart).$($VersionInfo.FileMinorPart).$($VersionInfo.FileBuildPart).$($VersionInfo.FilePrivatePart)"
            # Files without version info report 0.0.0.0 - use an Exists rule instead
            if ($Version -notmatch '^\d+(\.\d+)+$' -or $Version -eq '0.0.0.0') {
                $Version = $null
            }
            $PossibleDetections.Add([PSCustomObject]@{
                    'Type'         = 'File'
                    'Property'     = $Name
                    'VersionValue' = $Version
                    'Path'         = Split-Path -Path $AppPath -Parent
                })
        }
    }
    finally {
        [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null
        $shell = $null
    }

    # User selection
    $selected = @($PossibleDetections | Out-ConsoleGridView -Title 'Select detection rule(s)')

    $DetectionRules = [System.Collections.ArrayList]::new()

    # No selection made - warn and add placeholder
    if ($selected.Count -eq 0) {
        Write-Warning "No detection rule selected. Adding placeholder rule - you MUST update this in Intune before deploying."
        $DetectionRule = New-IntuneWin32AppDetectionRuleFile -Path 'C:\Program Files\PLACEHOLDER' -FileOrFolder 'PLACEHOLDER.exe' -DetectionType Exists -Existence -Check32BitOn64System $false
        $DetectionRules.Add($DetectionRule) | Out-Null
        return $DetectionRules
    }

    # Multiple MSI selected - error
    if (@($selected | Where-Object { $_.Type -eq 'MSI' }).Count -gt 1) {
        throw "Multiple MSI detection rules selected. Please select only one MSI detection rule."
    }

    $iconExtracted = $false
    foreach ($item in $selected) {
        if ($item.Type -eq 'MSI') {
            $DetectionRule = New-IntuneWin32AppDetectionRuleMSI -ProductCode $item.Property
        }
        elseif ($item.Type -eq 'File') {
            # Use the first selected executable's icon as the app icon
            if (-not $iconExtracted) {
                try {
                    Export-AppIcon -Path (Join-Path $item.Path $item.Property) -Destination $Paths.WorkingDirectory -Name 'AppIcon' -Format 'png' | Out-Null
                    $iconExtracted = $true
                }
                catch {
                    Write-Warning "Could not extract icon from '$($item.Property)': $_"
                }
            }
            # Replace dynamic username with %username%
            if ($Context -eq 'user') {
                $item.Path = $item.Path -replace '(?<=^[A-Z]:\\Users\\)[^\\]+', '%username%'
            }
            $DetectionParams = @{
                Path                 = $item.Path
                FileOrFolder         = $item.Property
                Check32BitOn64System = $false
            }
            if ($null -ne $item.VersionValue) {
                $DetectionParams['Version'] = $true
                $DetectionParams['Operator'] = 'Equal'
                $DetectionParams['VersionValue'] = $item.VersionValue
            }
            else {
                $DetectionParams['DetectionType'] = 'Exists'
                $DetectionParams['Existence'] = $true
            }
            $DetectionRule = New-IntuneWin32AppDetectionRuleFile @DetectionParams
        }
        $DetectionRules.Add($DetectionRule) | Out-Null
    }

    return $DetectionRules
}