Public/Add-MSIXPSFRegLegacyFixup.ps1

function Add-MSIXPSFRegLegacyFixup {
<#
.SYNOPSIS
    Adds a RegLegacyFixups remediation entry to an MSIX package.

.DESCRIPTION
    Appends one remediation rule to the RegLegacyFixups configuration in
    config.json.xml. Call the function once per rule; multiple calls
    accumulate entries in the same remediation array.

    The DLL is always referenced as RegLegacyFixups.dll (no architecture
    suffix); the PSF launcher selects the correct 32- or 64-bit file at
    runtime.

    config.json structure produced:
        "config": [ { "remediation": [ ... ] } ]

    Parameter sets:
      -ModifyKeyAccess Adjusts requested access rights on registry key opens.
                        Supported by Microsoft PSF and Tim Mangan PSF.
      -FakeDelete Returns success for key deletions that fail with
                        ACCESS_DENIED. Both PSF versions.
      -DeletionMarker Hides keys/values beneath a marked deletion point.
                        Both PSF versions.
      -HKLM2HKCU Redirects HKLM writes to virtual HKCU.
                        Tim Mangan PSF only.
      -JavaBlocker Blocks detection of Java versions above a threshold.
                        Tim Mangan PSF only.

.PARAMETER MSIXFolder
    Path to the expanded MSIX package folder (must contain config.json.xml).

.PARAMETER Executable
    Regex pattern for the process entry. Default: ".*" (all processes).

.PARAMETER ModifyKeyAccess
    Selects the ModifyKeyAccess remediation type.

.PARAMETER FakeDelete
    Selects the FakeDelete remediation type.

.PARAMETER DeletionMarker
    Selects the DeletionMarker remediation type.

.PARAMETER HKLM2HKCU
    Selects the HKLM2HKCU remediation type (Tim Mangan PSF only).

.PARAMETER JavaBlocker
    Selects the JavaBlocker remediation type (Tim Mangan PSF only).

.PARAMETER Hive
    Registry hive: HKCU or HKLM.

.PARAMETER Patterns
    Array of regex patterns matching the registry key paths.

.PARAMETER Access
    Access transformation for ModifyKeyAccess.
    Full2MaxAllowed Full access requested -> MAXIMUM_ALLOWED
    RW2MaxAllowed ReadWrite requested -> MAXIMUM_ALLOWED
    Full2R Full access requested -> Read only
    RW2R ReadWrite requested -> Read only
    Full2RW Full access requested -> ReadWrite (removes KEY_CREATE_LINK)

.PARAMETER Key
    Base key pattern for DeletionMarker (the root beneath which entries are hidden).

.PARAMETER MajorVersion
    Major Java version threshold for JavaBlocker.

.PARAMETER MinorVersion
    Minor Java version threshold for JavaBlocker. Default: 0.

.PARAMETER UpdateVersion
    Update Java version threshold for JavaBlocker. Default: 0.

.EXAMPLE
    Add-MSIXPSFRegLegacyFixup -MSIXFolder $f -ModifyKeyAccess -Hive HKCU -Patterns @('.*') -Access Full2MaxAllowed

.EXAMPLE
    Add-MSIXPSFRegLegacyFixup -MSIXFolder $f -FakeDelete -Hive HKCU -Patterns @('.*')

.EXAMPLE
    Add-MSIXPSFRegLegacyFixup -MSIXFolder $f -HKLM2HKCU -Hive HKLM

.EXAMPLE
    Add-MSIXPSFRegLegacyFixup -MSIXFolder $f -JavaBlocker -MajorVersion '1' -MinorVersion '8' -UpdateVersion '133'

.NOTES
    Microsoft PSF: https://github.com/microsoft/MSIX-PackageSupportFramework/tree/main/fixups/RegLegacyFixups
    Tim Mangan PSF: https://github.com/TimMangan/MSIX-PackageSupportFramework/wiki/Fixup:-RegLegacyFixup
    https://www.nick-it.de
    Andreas Nick, 2026
#>

    [CmdletBinding(DefaultParameterSetName = 'ModifyKeyAccess')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0,
            ParameterSetName = 'ModifyKeyAccess')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0,
            ParameterSetName = 'FakeDelete')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0,
            ParameterSetName = 'DeletionMarker')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0,
            ParameterSetName = 'HKLM2HKCU')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0,
            ParameterSetName = 'JavaBlocker')]
        [System.IO.DirectoryInfo] $MSIXFolder,

        [Parameter(ParameterSetName = 'ModifyKeyAccess')]
        [Parameter(ParameterSetName = 'FakeDelete')]
        [Parameter(ParameterSetName = 'DeletionMarker')]
        [Parameter(ParameterSetName = 'HKLM2HKCU')]
        [Parameter(ParameterSetName = 'JavaBlocker')]
        [String] $Executable = '.*',

        # --- type discriminators ---
        [Parameter(ParameterSetName = 'ModifyKeyAccess', Mandatory = $true)]
        [Switch] $ModifyKeyAccess,

        [Parameter(ParameterSetName = 'FakeDelete', Mandatory = $true)]
        [Switch] $FakeDelete,

        [Parameter(ParameterSetName = 'DeletionMarker', Mandatory = $true)]
        [Switch] $DeletionMarker,

        [Parameter(ParameterSetName = 'HKLM2HKCU', Mandatory = $true)]
        [Switch] $HKLM2HKCU,

        [Parameter(ParameterSetName = 'JavaBlocker', Mandatory = $true)]
        [Switch] $JavaBlocker,

        # --- shared parameters ---
        [Parameter(ParameterSetName = 'ModifyKeyAccess', Mandatory = $true)]
        [Parameter(ParameterSetName = 'FakeDelete', Mandatory = $true)]
        [Parameter(ParameterSetName = 'DeletionMarker', Mandatory = $true)]
        [Parameter(ParameterSetName = 'HKLM2HKCU', Mandatory = $true)]
        [ValidateSet('HKCU', 'HKLM')]
        [String] $Hive,

        [Parameter(ParameterSetName = 'ModifyKeyAccess', Mandatory = $true)]
        [Parameter(ParameterSetName = 'FakeDelete', Mandatory = $true)]
        [Parameter(ParameterSetName = 'DeletionMarker')]
        [String[]] $Patterns,

        # --- ModifyKeyAccess ---
        [Parameter(ParameterSetName = 'ModifyKeyAccess', Mandatory = $true)]
        [ValidateSet('Full2MaxAllowed', 'RW2MaxAllowed', 'Full2R', 'RW2R', 'Full2RW')]
        [String] $Access,

        # --- DeletionMarker ---
        [Parameter(ParameterSetName = 'DeletionMarker', Mandatory = $true)]
        [String] $Key,

        # --- JavaBlocker (Tim Mangan only) ---
        [Parameter(ParameterSetName = 'JavaBlocker', Mandatory = $true)]
        [String] $MajorVersion,

        [Parameter(ParameterSetName = 'JavaBlocker')]
        [String] $MinorVersion = '0',

        [Parameter(ParameterSetName = 'JavaBlocker')]
        [String] $UpdateVersion = '0'
    )

    process {
        if (-not (Test-Path $MSIXFolder.FullName -PathType Container)) {
            Write-Error "MSIXFolder not found: $($MSIXFolder.FullName)"
            return
        }

        try { $null = [System.Text.RegularExpressions.Regex]::new($Executable) }
        catch {
            Write-Error "-Executable '$Executable' is not a valid regular expression: $_"
            return
        }

        if ($PSBoundParameters.ContainsKey('Patterns')) {
            foreach ($pat in $Patterns) {
                if ([string]::IsNullOrEmpty($pat)) {
                    Write-Warning "-Patterns contains an empty entry."
                    continue
                }
                try { $null = [System.Text.RegularExpressions.Regex]::new($pat) }
                catch { Write-Warning "-Patterns entry '$pat' is not a valid regular expression: $_" }
            }
        }

        if ($PSBoundParameters.ContainsKey('Key') -and [string]::IsNullOrWhiteSpace($Key)) {
            Write-Error "-Key must not be empty for DeletionMarker."
            return
        }

        # Tim Mangan-only feature guard
        if ($PSCmdlet.ParameterSetName -in @('HKLM2HKCU', 'JavaBlocker')) {
            if ($Script:PsfBasePath -notlike '*TimMangan*') {
                Write-Error "$($PSCmdlet.ParameterSetName) requires Tim Mangan PSF. Run Set-MSIXActivePSFFramework -Framework TimManganPSF first."
                return
            }
        }

        $configXmlPath = Join-Path $MSIXFolder 'config.json.xml'
        if (-not (Test-Path $configXmlPath)) {
            Write-Warning "config.json.xml not found in: $($MSIXFolder.FullName)"
            return
        }

        $conxml = New-Object xml
        $conxml.Load($configXmlPath)

        Initialize-MSIXPSFProcessSection -ConXml $conxml

        # Find or create the target process node
        $execNode = $conxml.SelectSingleNode("//processes/process/executable[text()='$Executable']")
        if (-not $execNode) {
            $proc = $conxml.CreateElement('process')
            $exec = $conxml.CreateElement('executable')
            $exec.InnerText = $Executable
            $proc.AppendChild($exec) | Out-Null
            $conxml.SelectSingleNode('//processes').AppendChild($proc) | Out-Null
            $execNode = $conxml.SelectSingleNode("//processes/process/executable[text()='$Executable']")
        }
        $processNode = $execNode.ParentNode

        if ($null -eq $processNode.SelectSingleNode('fixups')) {
            $processNode.AppendChild($conxml.CreateElement('fixups')) | Out-Null
        }

        # Find or create the RegLegacyFixups fixup element
        $dllNode = $processNode.SelectSingleNode("fixups/fixup/dll[text()='RegLegacyFixups.dll']")
        if ($null -eq $dllNode) {
            $fixup  = $conxml.CreateElement('fixup')
            $dllEl  = $conxml.CreateElement('dll')
            $dllEl.InnerText = 'RegLegacyFixups.dll'
            $fixup.AppendChild($dllEl) | Out-Null
            $cfgEl  = $conxml.CreateElement('config')
            $fixup.AppendChild($cfgEl) | Out-Null
            $rgEl   = $conxml.CreateElement('remediationGroup')
            $cfgEl.AppendChild($rgEl) | Out-Null
            $processNode.SelectSingleNode('fixups').AppendChild($fixup) | Out-Null
            $dllNode = $processNode.SelectSingleNode("fixups/fixup/dll[text()='RegLegacyFixups.dll']")
        }
        $remGroup = $dllNode.ParentNode.SelectSingleNode('config/remediationGroup')

        # Build the remediation element
        $rem    = $conxml.CreateElement('remediation')
        $typeEl = $conxml.CreateElement('type')
        $typeEl.InnerText = $PSCmdlet.ParameterSetName
        $rem.AppendChild($typeEl) | Out-Null

        if ($PSBoundParameters.ContainsKey('Hive')) {
            $hiveEl = $conxml.CreateElement('hive')
            $hiveEl.InnerText = $Hive
            $rem.AppendChild($hiveEl) | Out-Null
        }

        if ($PSBoundParameters.ContainsKey('Patterns') -and $Patterns.Count -gt 0) {
            $patsEl = $conxml.CreateElement('patterns')
            foreach ($p in $Patterns) {
                $patEl = $conxml.CreateElement('pattern')
                $patEl.InnerText = $p
                $patsEl.AppendChild($patEl) | Out-Null
            }
            $rem.AppendChild($patsEl) | Out-Null
        }

        if ($PSBoundParameters.ContainsKey('Access')) {
            $accEl = $conxml.CreateElement('access')
            $accEl.InnerText = $Access
            $rem.AppendChild($accEl) | Out-Null
        }

        if ($PSBoundParameters.ContainsKey('Key')) {
            $keyEl = $conxml.CreateElement('key')
            $keyEl.InnerText = $Key
            $rem.AppendChild($keyEl) | Out-Null
        }

        if ($PSCmdlet.ParameterSetName -eq 'JavaBlocker') {
            $majEl = $conxml.CreateElement('majorVersion'); $majEl.InnerText = $MajorVersion
            $minEl = $conxml.CreateElement('minorVersion'); $minEl.InnerText = $MinorVersion
            $updEl = $conxml.CreateElement('updateVersion'); $updEl.InnerText = $UpdateVersion
            $rem.AppendChild($majEl) | Out-Null
            $rem.AppendChild($minEl) | Out-Null
            $rem.AppendChild($updEl) | Out-Null
        }

        # Skip if an identical entry already exists (idempotent across repeated runs)
        $dupXPath = "remediation[type='" + $PSCmdlet.ParameterSetName + "'"
        if ($PSBoundParameters.ContainsKey('Hive'))   { $dupXPath += " and hive='" + $Hive + "'" }
        if ($PSBoundParameters.ContainsKey('Access')) { $dupXPath += " and access='" + $Access + "'" }
        $dupXPath += ']'
        if ($null -ne $remGroup.SelectSingleNode($dupXPath)) {
            Write-Verbose "RegLegacyFixups [$($PSCmdlet.ParameterSetName)] already present for '$Executable' — skipped."
            return
        }

        $remGroup.AppendChild($rem) | Out-Null

        $conxml.PreserveWhiteSpace = $false
        $conxml.Save($configXmlPath)
        Write-Verbose "RegLegacyFixups [$($PSCmdlet.ParameterSetName)] added for executable: $Executable"
    }
}