MSIX.AppIsolation.ps1

# =============================================================================
# Win32 App Isolation
# -----------------------------------------------------------------------------
# Adds the rescap capabilities + iso namespace that turn a regular MSIX-packaged
# Win32 app into an "isolated" one. The isolation feature provides an OS-level
# sandbox with broker-mediated access to filesystem, devices, and protected APIs.
#
# Reference:
# https://learn.microsoft.com/windows/win32/secauthz/app-isolation-overview
# https://learn.microsoft.com/windows/win32/secauthz/app-isolation-supported-capabilities
#
# Important: this is OPT-IN. Most MSIX packages should NOT enable isolation —
# many legacy apps will break because they rely on broad filesystem/registry
# access. Use this only after validating the app under isolation manually.
#
# Minimum runtime: Windows 11 24H2 (build 26100) or later.
# =============================================================================

# Documented isolated-app capabilities. Add more as Microsoft publishes them.
$script:KnownIsolationCapabilities = @(
    'isolatedWin32-promptForAccess',
    'isolatedWin32-accessFromLowIntegrityLevel',
    'isolatedWin32-userProfileMinimal',
    'isolatedWin32-userProfile',
    'isolatedWin32-printDocumentsFolder',
    'isolatedWin32-printDocumentsContents',
    'isolatedWin32-fullFileSystemAccess',
    'isolatedWin32-allowElevation',
    'isolatedWin32-attachToHostInterop',
    'isolatedWin32-internetClient',
    'isolatedWin32-internetClientServer',
    'isolatedWin32-privateNetworkClientServer',
    'isolatedWin32-bluetooth',
    'isolatedWin32-networking',
    'isolatedWin32-removableStorage'
)

function Get-MsixIsolationCapability {
    <#
    .SYNOPSIS
        Returns the set of well-known Win32-app-isolation capabilities the module
        is aware of. Use this list to decide what to pass into Add-MsixAppIsolation.
    #>

    return $script:KnownIsolationCapabilities
}


function Add-MsixAppIsolation {
    <#
    .SYNOPSIS
        Enables Win32 App Isolation on an MSIX package by adding the rescap
        namespace and the requested isolated-Win32 capabilities to the manifest.
 
    .DESCRIPTION
        Modifies AppxManifest.xml to:
          - Declare the `rescap` namespace if not already present.
          - Add a <Capabilities> block (or augment the existing one).
          - Insert one <rescap:Capability Name="…"/> element per capability.
          - Bump MaxVersionTested to 10.0.26100.0 (the documented minimum).
 
        Repacks and re-signs the package.
 
        WARNING: this is opt-in. Many existing MSIX packages will break under
        isolation because the app expects broad filesystem/registry access.
        Validate with the Application Capability Profiler (ACP) first:
        https://github.com/microsoft/win32-app-isolation/releases
 
    .PARAMETER PackagePath
        .msix file to modify.
 
    .PARAMETER Capabilities
        Capabilities to add. Defaults to a conservative starter set:
        promptForAccess + accessFromLowIntegrityLevel.
 
    .PARAMETER OutputPath
        Write the modified package here instead of overwriting -PackagePath.
 
    .PARAMETER SkipSigning
        Do not sign the resulting package.
 
    .PARAMETER Pfx / PfxPassword
        Signing certificate.
 
    .EXAMPLE
        Add-MsixAppIsolation -PackagePath app.msix `
            -Capabilities 'isolatedWin32-promptForAccess','isolatedWin32-userProfileMinimal' `
            -Pfx cert.pfx -PfxPassword 'P@ss'
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string]$PackagePath,
        [string[]]$Capabilities = @(
            'isolatedWin32-promptForAccess',
            'isolatedWin32-accessFromLowIntegrityLevel'
        ),
        [string]$OutputPath,
        [Alias('NoSign')]
        [switch]$SkipSigning,
        [string]$Pfx,
        [SecureString]$PfxPassword
    )

    foreach ($c in $Capabilities) {
        if ($c -notmatch '^isolatedWin32-') {
            Write-MsixLog Warning "'$c' doesn't look like a Win32 isolation capability (expected 'isolatedWin32-*'). Adding anyway."
        }
        if ($script:KnownIsolationCapabilities -notcontains $c) {
            Write-MsixLog Warning "'$c' is not in the documented capability set. Verify against MS Learn before publishing."
        }
    }

    $isWhatIf = -not $PSCmdlet.ShouldProcess($PackagePath, 'Add App Isolation capabilities')

    _MsixMutateManifest -PackagePath $PackagePath -OutputPath $OutputPath `
        -SkipSigning:$SkipSigning -Pfx $Pfx -PfxPassword $PfxPassword `
        -WhatIfPreview:$isWhatIf `
        -Activity 'Add App Isolation capabilities' -Mutate {
        param([xml]$manifest)

        Add-MsixManifestNamespace $manifest 'rescap'
        # Win32 App Isolation requires Win11 24H2 (build 26100)
        Set-MsixManifestMaxVersionTested $manifest -MinBuild 26100

        $rescapUri = Get-MsixManifestNamespaceUri 'rescap'
        $capsNode  = $manifest.Package.Capabilities
        if (-not $capsNode) {
            $capsNode = $manifest.CreateElement('Capabilities', $manifest.Package.NamespaceURI)
            $null     = $manifest.Package.AppendChild($capsNode)
        }

        foreach ($cap in $Capabilities) {
            # Idempotent: skip if already present
            $existing = $capsNode.ChildNodes | Where-Object {
                $_.LocalName -eq 'Capability' -and $_.Name -eq $cap
            }
            if ($existing) {
                Write-MsixLog Info "Capability already present: $cap"
                continue
            }
            $node = $manifest.CreateElement('rescap:Capability', $rescapUri)
            $node.SetAttribute('Name', $cap)
            $null = $capsNode.AppendChild($node)
            Write-MsixLog Info "Capability added: $cap"
        }
    }
}


function Remove-MsixAppIsolation {
    <#
    .SYNOPSIS
        Removes all `isolatedWin32-*` capabilities from a package, undoing
        Add-MsixAppIsolation.
 
    .PARAMETER PackagePath
        .msix file to modify.
 
    .PARAMETER OutputPath
        Write the modified package here instead of overwriting -PackagePath.
 
    .PARAMETER SkipSigning
        Do not sign the resulting package.
 
    .PARAMETER Pfx / PfxPassword
        Signing certificate.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string]$PackagePath,
        [string]$OutputPath,
        [Alias('NoSign')]
        [switch]$SkipSigning,
        [string]$Pfx,
        [SecureString]$PfxPassword
    )

    PROCESS {
        # Quick pre-check: does the package even have isolation caps?
        $preCheck = Get-MsixManifest -Path $PackagePath
        $hasCaps = @($preCheck.Package.Capabilities.ChildNodes) |
            Where-Object { $_.LocalName -eq 'Capability' -and $_.Name -like 'isolatedWin32-*' }
        if (-not $hasCaps) {
            Write-MsixLog Info 'No isolation capabilities found; nothing to do.'
            return
        }

        $isWhatIf = -not $PSCmdlet.ShouldProcess($PackagePath, 'Remove App Isolation capabilities')

        _MsixMutateManifest -PackagePath $PackagePath -OutputPath $OutputPath `
            -SkipSigning:$SkipSigning -Pfx $Pfx -PfxPassword $PfxPassword `
            -WhatIfPreview:$isWhatIf `
            -Activity 'Remove App Isolation capabilities' -Mutate {
            param([xml]$manifest)
            $capsNode = $manifest.Package.Capabilities
            if ($capsNode) {
                foreach ($n in @($capsNode.ChildNodes)) {
                    if ($n.LocalName -eq 'Capability' -and $n.Name -like 'isolatedWin32-*') {
                        $null = $capsNode.RemoveChild($n)
                        Write-MsixLog Info "Removed: $($n.Name)"
                    }
                }
            }
        }
    }
}


# Backward-compatible plural aliases
Set-Alias Get-MsixIsolationCapabilities Get-MsixIsolationCapability