Public/Functions/Dism.ps1

function Add-WindowsPackageSSU {
    <#
    .SYNOPSIS
    Adds a Servicing Stack Update package to Windows.
 
    .DESCRIPTION
    Extracts SSU cabinet files from a .cab or .msu package and applies them to an online or offline Windows image using Add-WindowsPackage.
 
    .PARAMETER PackagePath
    Full path to the source .cab or .msu package file.
 
    .PARAMETER Path
    Full path to the root directory of the offline mounted Windows image.
 
    .PARAMETER Online
    Applies the SSU to the currently running operating system.
 
    .PARAMETER LogPath
    Full path to the DISM log file used during package application.
 
    .EXAMPLE
    Add-WindowsPackageSSU -PackagePath C:\Updates\windows10.0-kbxxxx.msu -Path C:\Mount
    Extracts SSU content from the MSU and applies it to the mounted image at C:\Mount.
 
    .EXAMPLE
    Add-WindowsPackageSSU -PackagePath C:\Updates\ssu.cab -Online
    Applies SSU cab content to the running operating system.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-11 - Moved help block inside function and expanded sections
    #>

    [CmdletBinding(DefaultParameterSetName = 'Offline')]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackagePath,

        [Parameter(ParameterSetName = 'Offline', Mandatory = $true)]
        [string]$Path,

        [Parameter(ParameterSetName = 'Online', Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]$Online,

        [string]$LogPath = "$env:windir\Logs\Dism\dism.log"
    )
    #=================================================
    # Blocks
    #=================================================
    $CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $CurrentPrincipal = [Security.Principal.WindowsPrincipal]::new($CurrentIdentity)
    if (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Administrative rights are required"
        return
    }
    Block-WindowsVersionNe10
    #=================================================
    # Test PackagePath
    #=================================================
    if (!(Test-Path "$PackagePath" -PathType Leaf)) {
        Write-Warning "Add-WindowsPackageLCU could not find $Path"; Continue
    }
    $PackagePathItem = Get-Item $PackagePath
    #=================================================
    # SSU Temp Path
    #=================================================
    $SSUTemp = Join-Path $env:Temp 'SSU'

    #See if the path already exists and remove it
    if (Test-Path $SSUTemp) {
        Remove-Item -Path $SSUTemp -Recurse -Force -ErrorAction Ignore | Out-Null
    }

    #Create the SSU Temp Path
    New-Item -Path $SSUTemp -ItemType Directory -Force -ErrorAction Ignore | Out-Null

    #Bail if SSU Temp Path doesn't exist
    if (!(Test-Path $SSUTemp)) {
        Write-Warning "Add-WindowsPackageLCU could not create $SSUTemp"; Continue
    }
    #=================================================
    # Expand MSU
    #=================================================
    if ($PackagePathItem.Extension -match '.msu') {
        & Expand.exe "$($PackagePathItem.FullName)" /f:Windows*.cab "$SSUTemp"
        Get-ChildItem -Path $SSUTemp *.cab | Where-Object {$_.Name -notmatch 'SSU'} | foreach {
            & Expand.exe $_.FullName /f:SSU*.cab "$SSUTemp"
        }
    }
    else {
        #Write-Host -ForegroundColor DarkGray "Expand SSU: $PackagePath"
        & Expand.exe "$($PackagePathItem.FullName)" /f:SSU*.cab "$SSUTemp"
    }
    #=================================================
    # Apply SSU
    #=================================================
    if ($Online.IsPresent) {
        Get-ChildItem -Path $SSUTemp SSU*.cab | foreach {
            Write-Host -ForegroundColor DarkGray $_.FullName
            Add-WindowsPackage -PackagePath $_.FullName -Online -LogPath $LogPath -Verbose | Out-Null
        }
    }
    else {
        Get-ChildItem -Path $SSUTemp SSU*.cab | foreach {
            Write-Host -ForegroundColor DarkGray $_.FullName
            Add-WindowsPackage -PackagePath $_.FullName -Path $Path -LogPath $LogPath -Verbose | Out-Null
        }
    }
    #=================================================
}
function Copy-PSModuleToWindowsImage {
    <#
    .SYNOPSIS
    Copies PowerShell modules to a mounted Windows image
 
    .DESCRIPTION
    Copies specified PowerShell modules from the running operating system to a mounted Windows image for offline servicing.
 
    .PARAMETER Name
    Name of the PowerShell module(s) to copy. Wildcard patterns are supported. This parameter is mandatory.
 
    .PARAMETER ExecutionPolicy
    Sets the PowerShell Execution Policy in the Windows image. Valid values are Restricted, AllSigned, RemoteSigned, Unrestricted, Bypass, and Undefined.
 
    .PARAMETER Path
    Path to the mounted Windows image. If not specified, will use the currently mounted image.
 
    .EXAMPLE
    Copy-PSModuleToWindowsImage -Name 'OSD' -Path 'C:\Mount'
    Copies the OSD module to the mounted image at C:\\Mount
 
    .EXAMPLE
    Copy-PSModuleToWindowsImage -Name 'OSD','ActiveDirectory' -ExecutionPolicy Bypass -Path 'C:\Mount'
    Copies multiple modules and sets execution policy
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-10 - Added comment-based help
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [SupportsWildcards()]
        [String[]]$Name,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Restricted','AllSigned','RemoteSigned','Unrestricted','Bypass','Undefined')]
        [string]$ExecutionPolicy,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string[]]$Path
    )

    begin {
        #=================================================
        # Require Admin Rights
        #=================================================
        if ((Get-OSDGather -Property IsAdmin) -eq $false) {
            Write-Warning "$($MyInvocation.MyCommand) requires Admin Rights ELEVATED"
            Break
        }
        #=================================================
        # Get-WindowsImage Mounted
        #=================================================
        if ($null -eq $Path) {
            $Path = (Get-WindowsImage -Mounted | Select-Object -Property Path).Path
        }
        #=================================================
    }
    process {
        foreach ($Input in $Path) {
            #=================================================
            # Path
            #=================================================
            $MountPath = (Get-Item -Path $Input | Select-Object FullName).FullName
            Write-Verbose "Path: $MountPath"
            #=================================================
            # Validate Mount Path
            #=================================================
            if (-not (Test-Path $Input -ErrorAction SilentlyContinue)) {
                Write-Warning "Unable to locate Mounted WindowsImage at $Input"
                Break
            }
            #=================================================
            # Copy-PSoduleToFolder
            #=================================================
            Copy-PSModuleToFolder -Name $Name -Destination "$MountPath\Program Files\WindowsPowerShell\Modules" -RemoveOldVersions
            #=================================================
            # Set-WindowsImageExecutionPolicy
            #=================================================
            if ($ExecutionPolicy) {
                Set-WindowsImageExecutionPolicy -ExecutionPolicy $ExecutionPolicy -Path $MountPath
            }
            #=================================================
            # Return for PassThru
            #=================================================
            Return Get-WindowsImage -Mounted | Where-Object {$_.Path -eq $MountPath}
            #=================================================
        }
    }
    end {}
}
function Dismount-MyWindowsImage {
<#
.SYNOPSIS
Dismounts MyWindowsImage and finalizes changes.
 
.DESCRIPTION
Commits or discards changes to MyWindowsImage and then unmounts the image.
 
.PARAMETER Path
Specifies the Path to use when running Dismount-MyWindowsImage.
 
.PARAMETER Discard
Specifies the Discard to use when running Dismount-MyWindowsImage.
 
.PARAMETER Save
Specifies the Save to use when running Dismount-MyWindowsImage.
 
.EXAMPLE
Dismount-MyWindowsImage -Path <value>
Demonstrates a common way to run Dismount-MyWindowsImage.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'DismountDiscard')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]]$Path,

        [Parameter(ParameterSetName = 'DismountDiscard', Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]$Discard,

        [Parameter(ParameterSetName = 'DismountSave', Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]$Save
    )

    begin {
        #=================================================
        # Require Admin Rights
        #=================================================
        if ((Get-OSDGather -Property IsAdmin) -eq $false) {
            Write-Warning "$($MyInvocation.MyCommand) requires Admin Rights ELEVATED"
            Break
        }
        #=================================================
        # Get-WindowsImage Mounted
        #=================================================
        if ($null -eq $Path) {
            $Path = (Get-WindowsImage -Mounted | Select-Object -Property Path).Path
        }
        #=================================================
    }
    process {
        foreach ($Input in $Path) {
            #=================================================
            # Path
            #=================================================
            $MountPath = (Get-Item -Path $Input | Select-Object FullName).FullName
            Write-Verbose "Path: $MountPath"
            #=================================================
            # Validate Mount Path
            #=================================================
            if (-not (Test-Path $Input -ErrorAction SilentlyContinue)) {
                Write-Warning "Dismount-MyWindowsImage: Unable to locate Mounted WindowsImage at $Input"
                Break
            }
            #=================================================
            # Dismount-WindowsImage
            #=================================================
            if ($Discard.IsPresent) {
                if ($PSCmdlet.ShouldProcess($Input, "Dismount-MyWindowsImage -Discard")) {
                    Dismount-WindowsImage -Path $Input -Discard | Out-Null
                }
            }
            if ($Save.IsPresent) {
                if ($PSCmdlet.ShouldProcess($Input, "Dismount-MyWindowsImage -Save")) {
                    Dismount-WindowsImage -Path $Input -Save | Out-Null
                }
            }
        }
    }
    end {}
}
function Edit-MyWindowsImage {
<#
.SYNOPSIS
Edits MyWindowsImage content.
 
.DESCRIPTION
Applies modifications to MyWindowsImage in the current servicing workflow.
 
.PARAMETER Path
Specifies the Path to use when running Edit-MyWindowsImage.
 
.PARAMETER CleanupImage
Specifies the CleanupImage to use when running Edit-MyWindowsImage.
 
.PARAMETER Online
Specifies the Online to use when running Edit-MyWindowsImage.
 
.PARAMETER GridRemoveAppx
Specifies the GridRemoveAppx to use when running Edit-MyWindowsImage.
 
.PARAMETER GridRemoveAppxPP
Specifies the GridRemoveAppxPP to use when running Edit-MyWindowsImage.
 
.PARAMETER RemoveAppx
Specifies the RemoveAppx to use when running Edit-MyWindowsImage.
 
.PARAMETER RemoveAppxPP
Specifies the RemoveAppxPP to use when running Edit-MyWindowsImage.
 
.PARAMETER DismountSave
Specifies the DismountSave to use when running Edit-MyWindowsImage.
 
.EXAMPLE
Edit-MyWindowsImage -Path <value>
Demonstrates a common way to run Edit-MyWindowsImage.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding(DefaultParameterSetName = 'Offline')]
    param (
        #Specifies the full path to the root directory of the offline Windows image that you will service.
        #If the directory named Windows is not a subdirectory of the root directory, -WindowsDirectory must be specified.
        [Parameter(ParameterSetName = 'Offline', ValueFromPipelineByPropertyName)]
        [string[]]$Path,

        #Dism Actions
        #Analyze cannot be used for PassThru
        [Parameter(ParameterSetName = 'Offline')]
        [ValidateSet('Analyze','Cleanup','CleanupResetBase')]
        [string]$CleanupImage,

        #Specifies that the action is to be taken on the operating system that is currently running on the local computer.
        [Parameter(ParameterSetName = 'Online', Mandatory = $true)]
        [System.Management.Automation.SwitchParameter]$Online,

        #Appx Packages selected in GridView will be removed from the Windows Image
        [Parameter(ParameterSetName = 'Online')]
        [System.Management.Automation.SwitchParameter]$GridRemoveAppx,

        #Appx Provisioned Packages selected in GridView will be removed from the Windows Image
        [System.Management.Automation.SwitchParameter]$GridRemoveAppxPP,

        #Appx Packages matching the string will be removed
        [Parameter(ParameterSetName = 'Online')]
        [string[]]$RemoveAppx,

        #Appx Provisioned Packages matching the string will be removed
        [string[]]$RemoveAppxPP,

        [System.Management.Automation.SwitchParameter]$DismountSave
    )

    begin {
        #=================================================
        # Require Admin Rights
        #=================================================
        if ((Get-OSDGather -Property IsAdmin) -eq $false) {
            Write-Warning "$($MyInvocation.MyCommand) requires Admin Rights ELEVATED"
            Break
        }
        #=================================================
        # Get Registry Information
        #=================================================
        $GetRegCurrentVersion = Get-RegCurrentVersion
        #=================================================
        # Require OSMajorVersion 10
        #=================================================
        if ($GetRegCurrentVersion.CurrentMajorVersionNumber -ne 10) {
            Write-Warning "$($MyInvocation.MyCommand) requires OS MajorVersion 10"
            Break
        }
        #=================================================
    }
    process {
        if ($PSCmdlet.ParameterSetName -eq 'Online') {
            #=================================================
            # Get Registry Information
            #=================================================
            $GetRegCurrentVersion = Get-RegCurrentVersion
            #=================================================
            # Require OSMajorVersion 10
            #=================================================
            if ($GetRegCurrentVersion.CurrentMajorVersionNumber -ne 10) {
                Write-Warning "Edit-MyWindowsImage: OS MajorVersion 10 is required"
                Break
            }
            #=================================================
            # GridRemoveAppx
            #=================================================
            if ($GridRemoveAppx.IsPresent) {
                Get-AppxPackage | Select-Object * | Where-Object {$_.NonRemovable -ne $true} | Out-GridView -PassThru -Title "Select Appx Packages to Remove from Online Windows Image" | ForEach-Object {
                    Remove-AppPackage -AllUsers -Package $_.PackageFullName -Verbose
                }
            }
            #=================================================
            # GridRemoveAppxPP
            #=================================================
            if ($GridRemoveAppxPP.IsPresent) {
                Get-AppxProvisionedPackage -Online | Select-Object DisplayName, PackageName | Out-GridView -PassThru -Title "Select Appx Provisioned Packages to Remove from Online Windows Image" | Remove-AppProvisionedPackage -Online -AllUsers
            }
            #=================================================
            # RemoveAppx
            #=================================================
            if ($RemoveAppx) {
                foreach ($Item in $RemoveAppx) {
                    Get-AppxPackage | Where-Object {$_.Name -Match $Item} | ForEach-Object {
                        Write-Verbose "$($_.Name): Removing Appx Package $($_.PackageFullName)" -Verbose
                        Try {Remove-AppxPackage -AllUsers -Package $_.PackageFullName | Out-Null}
                        Catch {Write-Warning "$($_.Name): Removing Appx Package $($_.PackageFullName) did not complete successfully"}
                    }
                }
            }
            #=================================================
            # RemoveAppxPP
            #=================================================
            if ($RemoveAppxPP) {
                foreach ($Item in $RemoveAppxPP) {
                    Get-AppxProvisionedPackage -Online | Where-Object {$_.DisplayName -Match $Item} | ForEach-Object {
                        Write-Verbose "$($_.DisplayName): Removing Appx Provisioned Package $($_.PackageName)" -Verbose
                        Try {Remove-AppxProvisionedPackage -Online -AllUsers -PackageName $_.PackageName | Out-Null}
                        Catch {Write-Warning "$($_.DisplayName): Removing Appx Provisioned Package $($_.PackageName) did not complete successfully"}
                    }
                }
            }
            #=================================================
            # Continue for PassThru
            #=================================================
            Continue

        }
        if ($PSCmdlet.ParameterSetName -eq 'Offline') {
            #=================================================
            # Get-WindowsImage Mounted
            #=================================================
            if ($null -eq $Path) {
                $Path = (Get-WindowsImage -Mounted | Select-Object -Property Path).Path
            }

            foreach ($Input in $Path) {
                Write-Verbose "Edit-MyWindowsImage $Input"
                #=================================================
                # Get-Item
                #=================================================
                if (Get-Item $Input -ErrorAction SilentlyContinue) {
                    $GetItemInput = Get-Item -Path $Input
                } else {
                    Write-Warning "Unable to locate WindowsImage at $Input"
                    Continue
                }
                #=================================================
                # Mount-MyWindowsImage
                #=================================================
                try {
                    $MountMyWindowsImage = Mount-MyWindowsImage -ImagePath $Input -Index $Index
                }
                catch {
                    Write-Warning "Could not mount this WIM for some reason"
                    Continue
                }

                if ($null -eq $MountMyWindowsImage) {
                    Write-Warning "Could not mount this WIM for some reason"
                    Continue
                }
                #=================================================
                # Make sure WinPE is Major Version 10
                #=================================================
                Write-Verbose "Verifying WinPE 10"
                $GetRegCurrentVersion = Get-RegCurrentVersion -Path $MountMyWindowsImage.Path

                if ($GetRegCurrentVersion.CurrentMajorVersionNumber -ne 10) {
                    Write-Warning "$($MyInvocation.MyCommand) can only service WinPE with MajorVersion 10"

                    $MountMyWindowsImage | Dismount-MyWindowsImage -Discard
                    Continue
                }
                #=================================================
                # GridRemoveAppxPP
                #=================================================
                if ($GridRemoveAppxPP.IsPresent) {
                    $CurrentLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Edit-MyWindowsImage.log"
                    Get-AppxProvisionedPackage -Path $Input | Select-Object DisplayName, PackageName | Out-GridView -PassThru -Title "Select Appx Provisioned Packages to Remove from $Input" | Remove-AppProvisionedPackage -Path $Input -LogPath $CurrentLog
                }
                #=================================================
                # RemoveAppxPP
                #=================================================
                if ($RemoveAppxPP) {
                    foreach ($Item in $RemoveAppxPP) {
                        Write-Verbose "RemoveAppxPP: $Item"
                        Get-AppxProvisionedPackage -Path $Input | Where-Object {$_.DisplayName -Match $Item} | ForEach-Object {
                            $DismLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Edit-MyWindowsImage.log"
                            Write-Verbose "$($_.DisplayName): Removing Appx Provisioned Package $($_.PackageName)" -Verbose
                            Remove-AppxProvisionedPackage -Path $_.Path -PackageName $_.PackageName -LogPath $DismLog | Out-Null
                        }
                    }
                }
                #=================================================
                # Cleanup
                #=================================================
                if ($CleanupImage -eq 'Analyze') {
                    $DismLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Cleanup-Image-Analyze-Dism.log"
                    $ConsoleLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Cleanup-Image-Analyze-Console.log"
                    Write-Verbose "DISM /Image:$Input /Cleanup-Image /AnalyzeComponentStore" -Verbose
                    Write-Warning "Console Output is being redirected to $ConsoleLog"
                    DISM /Image:"$Input" /Cleanup-Image /AnalyzeComponentStore /LogPath:"$DismLog" *> $ConsoleLog
                }
                if ($CleanupImage -eq 'Cleanup') {
                    $DismLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Cleanup-Image-Cleanup-Dism.log"
                    $ConsoleLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Cleanup-Image-Cleanup-Console.log"
                    Write-Verbose "DISM /Image:$Input /Cleanup-Image /StartComponentCleanup" -Verbose
                    Write-Warning "This process will take between 1 - 200 minutes to complete, depending on the number of Updates"
                    Write-Warning "Console Output is being redirected to $ConsoleLog"
                    DISM /Image:"$Input" /Cleanup-Image /StartComponentCleanup /LogPath:"$DismLog" *> $ConsoleLog
                }
                if ($CleanupImage -eq 'CleanupResetBase') {
                    $DismLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Cleanup-Image-CleanupResetBase-Dism.log"
                    $ConsoleLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Cleanup-Image-CleanupResetBase-Console.log"
                    Write-Verbose "DISM /Image:$Input /Cleanup-Image /StartComponentCleanup /ResetBase" -Verbose
                    Write-Warning "This process will take between 1 - 200 minutes to complete, depending on the number of Updates"
                    Write-Warning "Console Output is being redirected to $ConsoleLog"
                    DISM /Image:"$Input" /Cleanup-Image /StartComponentCleanup /ResetBase /LogPath:"$DismLog" *> $ConsoleLog

                }
                #=================================================
                # Dismount-MyWindowsImage
                #=================================================
                if ($DismountSave) {
                    $MountMyWindowsImage | Dismount-MyWindowsImage -Save
                } else {
                    $MountMyWindowsImage
                }
                #=================================================
            }
        }
    }
    end {}
}
function Get-MyWindowsCapability {
<#
.SYNOPSIS
Gets MyWindowsCapability information.
 
.DESCRIPTION
Returns MyWindowsCapability data for the current system or OSD session context.
 
.PARAMETER Path
Specifies the Path to use when running Get-MyWindowsCapability.
 
.PARAMETER State
Specifies the State to use when running Get-MyWindowsCapability.
 
.PARAMETER Category
Specifies the Category to use when running Get-MyWindowsCapability.
 
.PARAMETER Culture
Specifies the Culture to use when running Get-MyWindowsCapability.
 
.PARAMETER Like
Specifies the Like to use when running Get-MyWindowsCapability.
 
.PARAMETER Match
Specifies the Match to use when running Get-MyWindowsCapability.
 
.PARAMETER Detail
Specifies the Detail to use when running Get-MyWindowsCapability.
 
.PARAMETER DisableWSUS
Specifies the DisableWSUS to use when running Get-MyWindowsCapability.
 
.EXAMPLE
Get-MyWindowsCapability -Path <value>
Demonstrates a common way to run Get-MyWindowsCapability.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding(DefaultParameterSetName = 'Online')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Offline", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)]
        [string]$Path,

        [ValidateSet('Installed','NotPresent')]
        [string]$State,

        [ValidateSet('Language','Rsat','Other')]
        [string]$Category,

        [string[]]$Culture,

        [string[]]$Like,
        [string[]]$Match,

        [System.Management.Automation.SwitchParameter]$Detail,

        [Parameter(ParameterSetName = "Online")]
        [System.Management.Automation.SwitchParameter]$DisableWSUS
    )
    begin {
        #=================================================
        # Require Admin Rights
        #=================================================
        if ((Get-OSDGather -Property IsAdmin) -eq $false) {
            Write-Warning "$($MyInvocation.MyCommand) requires Admin Rights ELEVATED"
            Break
        }
        #=================================================
        # Test Get-WindowsCapability
        #=================================================
        if (Get-Command -Name Get-WindowsCapability -ErrorAction SilentlyContinue) {
            Write-Verbose 'Verified command Get-WindowsCapability'
        } else {
            Write-Warning 'Get-MyWindowsCapability requires Get-WindowsCapability which is not present'
            Break
        }
        #=================================================
        # Verify BuildNumber
        #=================================================
        $MinimumBuildNumber = 17763
        $CurrentBuildNumber = (Get-CimInstance -Class Win32_OperatingSystem).BuildNumber
        if ($MinimumBuildNumber -gt $CurrentBuildNumber) {
            Write-Warning "The current Windows BuildNumber is $CurrentBuildNumber"
            Write-Warning "Get-MyWindowsCapability requires Windows BuildNumber greater than $MinimumBuildNumber"
            Break
        }
        #=================================================
        # UseWUServer
        # Original code from Martin Bengtsson
        # https://www.imab.dk/deploy-rsat-remote-server-administration-tools-for-windows-10-v2004-using-configmgr-and-powershell/
        # https://github.com/imabdk/Powershell/blob/master/Install-RSATv1809v1903v1909v2004v20H2.ps1
        #=================================================
        $WUServer = (Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name WUServer -ErrorAction Ignore).WUServer
        $UseWUServer = (Get-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -ErrorAction Ignore).UseWuServer
        if ($PSCmdlet.ParameterSetName -eq 'Online') {

            if (($WUServer -ne $null) -and ($UseWUServer -eq 1) -and ($DisableWSUS -eq $false)) {
                Write-Warning "This computer is configured to receive updates from WSUS Server $WUServer"
                Write-Warning "Piping to Add-WindowsCapability may not function properly"
                Write-Warning "Local Source: Get-MyWindowsCapability | Add-WindowsCapability -Source"
                Write-Warning "Windows Update: Get-MyWindowsCapability -DisableWSUS | Add-WindowsCapability"
            }

            if (($DisableWSUS -eq $true) -and ($UseWUServer -eq 1)) {
                Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWuServer" -Value 0
                Restart-Service wuauserv
            }
        }
        #=================================================
        # Get Module Path
        #=================================================
        $GetModuleBase = Get-OSDModulePath
        #=================================================
    }
    process {
        #=================================================
        # Get-WindowsCapability
        #=================================================
        if ($PSCmdlet.ParameterSetName -eq 'Online') {
            $GetAllItems = Get-WindowsCapability -Online
        }
        if ($PSCmdlet.ParameterSetName -eq 'Offline') {
            $GetAllItems = Get-WindowsCapability -Path $Path
        }
        #=================================================
        # Like
        #=================================================
        foreach ($Item in $Like) {
            $GetAllItems = $GetAllItems | Where-Object {$_.Name -like "$Item"}
        }
        #=================================================
        # Match
        #=================================================
        foreach ($Item in $Match) {
            $GetAllItems = $GetAllItems | Where-Object {$_.Name -match "$Item"}
        }
        #=================================================
        # State
        #=================================================
        if ($State) {$GetAllItems = $GetAllItems | Where-Object {$_.State -eq $State}}
        #=================================================
        # Category
        #=================================================
        if ($Category -eq 'Other') {
            $GetAllItems = $GetAllItems | Where-Object {$_.Name -notmatch 'Language'}
            $GetAllItems = $GetAllItems | Where-Object {$_.Name -notmatch 'Rsat'}
        }
        if ($Category -eq 'Language') {
            $GetAllItems = $GetAllItems | Where-Object {$_.Name -match 'Language'}
        }
        if ($Category -eq 'Rsat') {
            $GetAllItems = $GetAllItems | Where-Object {$_.Name -match 'Rsat'}
        }
        #=================================================
        # Culture
        #=================================================
        $FilteredItems = @()
        if ($Culture) {
            foreach ($Item in $Culture) {
                $FilteredItems += $GetAllItems | Where-Object {$_.Name -match $Item}
            }
        } else {
            $FilteredItems = $GetAllItems
        }
        #=================================================
        # Dictionary
        #=================================================
        if (Test-Path "$GetModuleBase\Resources\Dictionary\Get-MyWindowsCapability.json") {
            $GetAllItemsDictionary = Get-Content "$GetModuleBase\Resources\Dictionary\Get-MyWindowsCapability.json" | ConvertFrom-Json
        }
        #=================================================
        # Create Object
        #=================================================
        if ($Detail -eq $true) {
            $Results = foreach ($Item in $FilteredItems) {
                $ItemProductName   = ($Item.Name -split ',*~')[0]
                $ItemCulture    = ($Item.Name -split ',*~')[3]
                $ItemVersion    = ($Item.Name -split ',*~')[4]

                $ItemDetails = $null
                $ItemDetails = $GetAllItemsDictionary | `
                    Where-Object {($_.ProductName -eq $ItemProductName)} | `
                    Where-Object {($_.Culture -eq $ItemCulture)} | `
                    Select-Object -First 1

                if ($null -eq $ItemDetails) {
                    Write-Verbose "$($Item.Name) ... gathering details" -Verbose
                    if ($PSCmdlet.ParameterSetName -eq 'Online') {
                        $ItemDetails = Get-WindowsCapability -Name $Item.Name -Online
                    }
                    if ($PSCmdlet.ParameterSetName -eq 'Offline') {
                        $ItemDetails = Get-WindowsCapability -Name $Item.Name -Path $Path
                    }
                }

                if ($PSCmdlet.ParameterSetName -eq 'Online') {
                    [PSCustomObject] @{
                        DisplayName     = $ItemDetails.DisplayName
                        Culture         = $ItemCulture
                        Version         = $ItemVersion
                        State           = $Item.State
                        Description     = $ItemDetails.Description
                        Name            = $Item.Name
                        Online          = $Item.Online
                        ProductName     = $ItemProductName
                    }
                }
                if ($PSCmdlet.ParameterSetName -eq 'Offline') {
                    [PSCustomObject] @{
                        DisplayName     = $ItemDetails.DisplayName
                        Culture         = $ItemCulture
                        Version         = $ItemVersion
                        State           = $Item.State
                        Description     = $ItemDetails.Description
                        Name            = $Item.Name
                        Path            = $Item.Path
                        ProductName     = $ItemProductName
                    }
                }
            }
        } else {
            $Results = foreach ($Item in $FilteredItems) {
                $ItemProductName   = ($Item.Name -split ',*~')[0]
                $ItemCulture   = ($Item.Name -split ',*~')[3]
                $ItemVersion    = ($Item.Name -split ',*~')[4]

                if ($PSCmdlet.ParameterSetName -eq 'Online') {
                    [PSCustomObject] @{
                        ProductName     = $ItemProductName
                        Culture         = $ItemCulture
                        Version         = $ItemVersion
                        State           = $Item.State
                        Name            = $Item.Name
                        Online          = $Item.Online
                    }
                }
                if ($PSCmdlet.ParameterSetName -eq 'Offline') {
                    [PSCustomObject] @{
                        ProductName     = $ItemProductName
                        Culture         = $ItemCulture
                        Version         = $ItemVersion
                        State           = $Item.State
                        Name            = $Item.Name
                        Path            = $Item.Path
                    }
                }
            }
        }
        #=================================================
        # Rebuild Dictionary
        #=================================================
        $Results | `
        Sort-Object ProductName, Culture | `
        Select-Object Name, ProductName, Culture, DisplayName, Description | `
        ConvertTo-Json | `
        Out-File "$env:TEMP\Get-MyWindowsCapability.json" -Width 2000 -Force
        #=================================================
        # Install / Return
        #=================================================
        if ($Install -eq $true) {
            foreach ($Item in $Results) {
                if ($_.State -eq 'Installed') {
                    Write-Verbose "$_.Name is already installed" -Verbose
                } else {
                    $Item | Add-WindowsCapability -Online
                }
            }
        } else {
            Return $Results
        }
        #=================================================
    }
    end {
        if (($DisableWSUS -eq $true) -and ($UseWUServer -eq 1)) {
            Set-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "UseWuServer" -Value $UseWUServer
            Restart-Service wuauserv
        }
    }
}
function Get-MyWindowsPackage {
<#
.SYNOPSIS
Gets MyWindowsPackage information.
 
.DESCRIPTION
Returns MyWindowsPackage data for the current system or OSD session context.
 
.PARAMETER Path
Specifies the Path to use when running Get-MyWindowsPackage.
 
.PARAMETER PackageState
Specifies the PackageState to use when running Get-MyWindowsPackage.
 
.PARAMETER ReleaseType
Specifies the ReleaseType to use when running Get-MyWindowsPackage.
 
.PARAMETER Category
Specifies the Category to use when running Get-MyWindowsPackage.
 
.PARAMETER Culture
Specifies the Culture to use when running Get-MyWindowsPackage.
 
.PARAMETER Like
Specifies the Like to use when running Get-MyWindowsPackage.
 
.PARAMETER Match
Specifies the Match to use when running Get-MyWindowsPackage.
 
.PARAMETER Detail
Specifies the Detail to use when running Get-MyWindowsPackage.
 
.EXAMPLE
Get-MyWindowsPackage -Path <value>
Demonstrates a common way to run Get-MyWindowsPackage.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding(DefaultParameterSetName = 'Online')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Offline", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)]
        [string]$Path,

        [ValidateSet('Installed','Superseded')]
        [string]$PackageState,

        [ValidateSet('FeaturePack','Foundation','LanguagePack','OnDemandPack','SecurityUpdate','Update')]
        [string]$ReleaseType,

        [ValidateSet('FOD','Language','LanguagePack','Update','Other')]
        [string]$Category,

        [string[]]$Culture,

        [string[]]$Like,
        [string[]]$Match,

        [System.Management.Automation.SwitchParameter]$Detail
    )
    #=================================================
    # Require Admin Rights
    #=================================================
    if ((Get-OSDGather -Property IsAdmin) -eq $false) {
        Write-Warning "$($MyInvocation.MyCommand) requires Admin Rights ELEVATED"
        Break
    }
    #=================================================
    # Test Get-WindowsPackage
    #=================================================
    if (Get-Command -Name Get-WindowsPackage -ErrorAction SilentlyContinue) {
        Write-Verbose 'Verified command Get-WindowsPackage'
    } else {
        Write-Warning 'Get-MyWindowsPackage requires Get-WindowsPackage which is not present'
        Break
    }
    #=================================================
    # Get Module Path
    #=================================================
    $GetModuleBase = Get-OSDModulePath
    #=================================================
    # Get-WindowsPackage
    #=================================================
    if ($PSCmdlet.ParameterSetName -eq 'Online') {
        $GetAllItems = Get-WindowsPackage -Online
    }
    if ($PSCmdlet.ParameterSetName -eq 'Offline') {
        $GetAllItems = Get-WindowsPackage -Path $Path
    }
    #=================================================
    # Like
    #=================================================
    foreach ($Item in $Like) {
        $GetAllItems = $GetAllItems | Where-Object {$_.PackageName -like "$Item"}
    }
    #=================================================
    # Match
    #=================================================
    foreach ($Item in $Match) {
        $GetAllItems = $GetAllItems | Where-Object {$_.PackageName -match "$Item"}
    }
    #=================================================
    # PackageState
    #=================================================
    if ($PackageState) {$GetAllItems = $GetAllItems | Where-Object {$_.PackageState -eq $PackageState}}
    #=================================================
    # ReleaseType
    #=================================================
    if ($ReleaseType) {$GetAllItems = $GetAllItems | Where-Object {$_.ReleaseType -eq $ReleaseType}}
    #=================================================
    # Category
    #=================================================
    #Get-MyWindowsPackage -Category FOD
    if ($Category -eq 'FOD') {
        $GetAllItems = $GetAllItems | Where-Object {$_.PackageName -match 'FOD'}
    }
    #Get-MyWindowsPackage -Category Language
    if ($Category -eq 'Language') {
        $GetAllItems = $GetAllItems | Where-Object {$_.ReleaseType -ne 'LanguagePack'}
        $GetAllItems = $GetAllItems | Where-Object {($_.PackageName -split ',*~')[3] -ne ''}
    }
    #Get-MyWindowsPackage -Category LanguagePack
    if ($Category -eq 'LanguagePack') {
        $GetAllItems = $GetAllItems | Where-Object {$_.ReleaseType -eq 'LanguagePack'}
    }
    #Get-MyWindowsPackage -Category Update
    if ($Category -eq 'Update') {
        $GetAllItems = $GetAllItems | Where-Object {$_.ReleaseType -match 'Update'}
    }
    #Get-MyWindowsPackage -Category Other
    if ($Category -eq 'Other') {
        $GetAllItems = $GetAllItems | Where-Object {$_.PackageName -notmatch 'FOD'}
        $GetAllItems = $GetAllItems | Where-Object {($_.PackageName -split ',*~')[3] -eq ''}
        $GetAllItems = $GetAllItems | Where-Object {$_.ReleaseType -ne 'LanguagePack'}
        $GetAllItems = $GetAllItems | Where-Object {$_.ReleaseType -notmatch 'Update'}
    }
    #=================================================
    # Culture
    #=================================================
    $FilteredItems = @()
    if ($Culture) {
        foreach ($Item in $Culture) {
            $FilteredItems += $GetAllItems | Where-Object {$_.PackageName -match "$Item"}
        }
    } else {
        $FilteredItems = $GetAllItems
    }
    #=================================================
    # Dictionary
    #=================================================
    if (Test-Path "$GetModuleBase\Resources\Dictionary\Get-MyWindowsPackage.json") {
        $GetAllItemsDictionary = Get-Content "$GetModuleBase\Resources\Dictionary\Get-MyWindowsPackage.json" | ConvertFrom-Json
    }
    #=================================================
    # Create Object
    #=================================================
    if ($Detail -eq $true) {
        $Results = foreach ($Item in $FilteredItems) {
            $ItemProductName    = ($Item.PackageName -split ',*~')[0]
            $ItemArchitecture   = ($Item.PackageName -split ',*~')[2]
            $ItemCulture        = ($Item.PackageName -split ',*~')[3]
            $ItemVersion        = ($Item.PackageName -split ',*~')[4]

            $ItemDetails = $null
            $ItemDetails = $GetAllItemsDictionary | `
                Where-Object {($_.ProductName -notmatch 'Package_for_DotNetRollup')} | `
                Where-Object {($_.ProductName -notmatch 'Package_for_RollupFix')} | `
                Where-Object {($_.ProductName -eq $ItemProductName)} | `
                Where-Object {($_.Culture -eq $ItemCulture)} | `
                Select-Object -First 1

            if ($null -eq $ItemDetails) {
                Write-Verbose "$($Item.PackageName) ... gathering details" -Verbose
                if ($PSCmdlet.ParameterSetName -eq 'Online') {
                    $ItemDetails = Get-WindowsPackage -PackageName $Item.PackageName -Online
                }
                if ($PSCmdlet.ParameterSetName -eq 'Offline') {
                    $ItemDetails = Get-WindowsPackage -PackageName $Item.PackageName -Path $Path
                }
            }

            $DisplayName = $ItemDetails.DisplayName
            if ($DisplayName -eq '') {$DisplayName = $ItemProductName}
            if ($ItemProductName -match 'Package_for_DotNetRollup') {$DisplayName = 'DotNet_Cumulative_Update'}
            if ($ItemProductName -match 'Package_for_RollupFix') {$DisplayName = 'Latest_Cumulative_Update'}
            if ($ItemProductName -match 'Package_for_KB') {$DisplayName = ("$ItemProductName" -replace "Package_for_")}

            if ($PSCmdlet.ParameterSetName -eq 'Online') {
                [PSCustomObject] @{
                    DisplayName     = $DisplayName
                    Architecture    = $ItemArchitecture
                    Culture         = $ItemCulture
                    Version         = $ItemVersion
                    ReleaseType     = $Item.ReleaseType
                    PackageState    = $Item.PackageState
                    InstallTime     = $Item.InstallTime
                    CapabilityId    = $ItemDetails.CapabilityId
                    Description     = $ItemDetails.Description
                    PackageName     = $Item.PackageName
                    Online          = $Item.Online
                    ProductName     = $ItemProductName
                }
            }
            if ($PSCmdlet.ParameterSetName -eq 'Offline') {
                [PSCustomObject] @{
                    DisplayName     = $DisplayName
                    Architecture    = $ItemArchitecture
                    Culture         = $ItemCulture
                    Version         = $ItemVersion
                    ReleaseType     = $Item.ReleaseType
                    PackageState    = $Item.PackageState
                    InstallTime     = $Item.InstallTime
                    CapabilityId    = $ItemDetails.CapabilityId
                    Description     = $ItemDetails.Description
                    PackageName     = $Item.PackageName
                    Path            = $Item.Path
                    ProductName     = $ItemProductName
                }
            }
        }
    } else {
        #Build Object
        $Results = foreach ($Item in $FilteredItems) {
            $ItemProductName    = ($Item.PackageName -split ',*~')[0]
            $ItemArchitecture   = ($Item.PackageName -split ',*~')[2]
            $ItemCulture        = ($Item.PackageName -split ',*~')[3]
            $ItemVersion        = ($Item.PackageName -split ',*~')[4]

            if ($PSCmdlet.ParameterSetName -eq 'Online') {
                [PSCustomObject] @{
                    ProductName     = $ItemProductName
                    Architecture    = $ItemArchitecture
                    Culture         = $ItemCulture
                    Version         = $ItemVersion
                    ReleaseType     = $Item.ReleaseType
                    PackageState    = $Item.PackageState
                    InstallTime     = $Item.InstallTime
                    PackageName     = $Item.PackageName
                    Online          = $Item.Online
                }
            }
            if ($PSCmdlet.ParameterSetName -eq 'Offline') {
                [PSCustomObject] @{
                    ProductName     = $ItemProductName
                    Architecture    = $ItemArchitecture
                    Culture         = $ItemCulture
                    Version         = $ItemVersion
                    ReleaseType     = $Item.ReleaseType
                    PackageState    = $Item.PackageState
                    InstallTime     = $Item.InstallTime
                    PackageName     = $Item.PackageName
                    Path            = $Item.Path
                }
            }
        }
    }
    #=================================================
    # Rebuild Dictionary
    #=================================================
    $Results | `
    Sort-Object ProductName, Culture | `
    Where-Object {$_.Architecture -notmatch 'wow64'} | `
    Where-Object {$_.ProductName -notmatch 'Package_for_DotNetRollup'} | `
    Where-Object {$_.ProductName -notmatch 'Package_for_RollupFix'} | `
    Where-Object {$_.PackageState -ne 'Superseded'} | `
    Select-Object PackageName, ProductName, Architecture, Culture, DisplayName, CapabilityId, Description | `
    ConvertTo-Json | `
    Out-File "$env:TEMP\Get-MyWindowsPackage.json" -Width 2000 -Force
    #=================================================
    # Return
    #=================================================
    Return $Results
    #=================================================
}
function Mount-MyWindowsImage {
<#
.SYNOPSIS
Mounts MyWindowsImage for servicing.
 
.DESCRIPTION
Mounts MyWindowsImage and prepares it for offline servicing tasks.
 
.PARAMETER ImagePath
Specifies the ImagePath to use when running Mount-MyWindowsImage.
 
.PARAMETER Index
Specifies the Index to use when running Mount-MyWindowsImage.
 
.PARAMETER ReadOnly
Specifies the ReadOnly to use when running Mount-MyWindowsImage.
 
.PARAMETER Explorer
Specifies the Explorer to use when running Mount-MyWindowsImage.
 
.EXAMPLE
Mount-MyWindowsImage -ImagePath <value>
Demonstrates a common way to run Mount-MyWindowsImage.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            ValueFromPipelineByPropertyName
        )]
        [string[]]$ImagePath,

        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [UInt32]$Index = 1,

        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [System.Management.Automation.SwitchParameter]$ReadOnly,

        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [System.Management.Automation.SwitchParameter]$Explorer
    )

    begin {
        #=================================================
        # Require Admin Rights
        #=================================================
        if ((Get-OSDGather -Property IsAdmin) -eq $false) {
            Write-Warning "$($MyInvocation.MyCommand) requires Admin Rights ELEVATED"
            Break
        }
        #=================================================
        # Get Registry Information
        #=================================================
        $GetRegCurrentVersion = Get-RegCurrentVersion
        #=================================================
        # Require OSMajorVersion 10
        #=================================================
        if ($GetRegCurrentVersion.CurrentMajorVersionNumber -ne 10) {
            Write-Warning "$($MyInvocation.MyCommand) requires OS MajorVersion 10"
            Break
        }
        #=================================================
    }
    process {
        foreach ($Input in $ImagePath) {
            Write-Verbose "$Input"
            #=================================================
            # Get-Item
            #=================================================
            if (Get-Item $Input -ErrorAction SilentlyContinue) {
                $GetItemInput = Get-Item -Path $Input
            } else {
                Write-Warning "Unable to locate WindowsImage at $Input"
                Break
            }
            #=================================================
            # Directory
            #=================================================
            if ($GetItemInput.PSIsContainer) {
                Write-Verbose "Directory was not expected"

                if (Test-WindowsImageMountPath -Path $GetItemInput.FullName) {
                    Write-Verbose "Windows Image is already mounted in this Directory"
                    Write-Verbose "Returning Mount-WindowsImage Object"
                    Get-WindowsImage -Mounted | Where-Object {($_.Path -eq $GetItemInput.FullName) -and ($_.ImageIndex -eq $Index)}
                    Continue
                } else {
                    Write-Warning "There isn't really anything that I can do with this directory. Goodbye!"
                    Continue
                }
            }
            #=================================================
            # Read Only
            #=================================================
            if ($GetItemInput.IsReadOnly) {
                Write-Warning "Cannot Mount this Read Only Image. Goodbye!"
                Continue
            }
            #=================================================
            # Already Mounted
            #=================================================
            if (Test-WindowsImageMounted -ImagePath $GetItemInput.FullName -Index $Index) {
                Write-Verbose "Windows Image is already mounted"
                Write-Verbose "Returning Mount-WindowsImage Object"
                Get-WindowsImage -Mounted | Where-Object {($_.ImagePath -eq $GetItemInput.FullName) -and ($_.ImageIndex -eq $Index)}
                Continue
            }
            #=================================================
            # Not a Windows Image
            #=================================================
            if (-Not (Test-WindowsImage -ImagePath $GetItemInput.FullName)) {
                Write-Warning "Does not appear to be a Windows Image. Goodbye!"
                Continue
            }
            #=================================================
            # Set Mount Path
            #=================================================
            $MyWindowsImageMountPath = $env:Temp + '\Mount' + (Get-Random)
            if (-NOT (Test-Path $MyWindowsImageMountPath)) {
                New-Item $MyWindowsImageMountPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
            }
            $Path = (Get-Item $MyWindowsImageMountPath).FullName
            #=================================================
            # Mount-WindowsImage
            #=================================================
            $Params = @{
                Path        = $Path
                ImagePath   = $GetItemInput.FullName
                Index       = $Index
                ReadOnly    = $ReadOnly
            }
            Mount-WindowsImage @Params | Out-Null
            #=================================================
            # Explorer
            #=================================================
            if ($Explorer.IsPresent) {explorer $Path}
            #=================================================
            # Return for PassThru
            #=================================================
            Get-WindowsImage -Mounted | Where-Object {$_.Path -eq $Path}
        }
    }
    end {}
}
function Remove-AppxOnline {
<#
.SYNOPSIS
Removes AppxOnline items.
 
.DESCRIPTION
Deletes matching AppxOnline items from the current context.
 
.PARAMETER GridRemoveAppx
Specifies the GridRemoveAppx to use when running Remove-AppxOnline.
 
.PARAMETER GridRemoveAppxPP
Specifies the GridRemoveAppxPP to use when running Remove-AppxOnline.
 
.PARAMETER Name
Specifies the Name to use when running Remove-AppxOnline.
 
.EXAMPLE
Remove-AppxOnline -GridRemoveAppx <value>
Demonstrates a common way to run Remove-AppxOnline.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        #Appx Packages selected in GridView will be removed from the Windows Image
        [System.Management.Automation.SwitchParameter]$GridRemoveAppx,

        #Appx Provisioned Packages selected in GridView will be removed from the Windows Image
        [System.Management.Automation.SwitchParameter]$GridRemoveAppxPP,

        #Appx Packages matching the string will be removed
        [string[]]$Name
    )

    begin {
        #=================================================
        # Blocks
        #=================================================
        $CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $CurrentPrincipal = [Security.Principal.WindowsPrincipal]::new($CurrentIdentity)
        if (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Administrative rights are required"
            return
        }
        Block-WindowsVersionNe10
        #=================================================
    }
    process {
        #=================================================
        # AppxPackage
        #=================================================
        if (Get-Command Get-AppxPackage) {
            if ($GridRemoveAppx.IsPresent) {
                Get-AppxPackage | Select-Object * | Where-Object {$_.NonRemovable -ne $true} | Out-GridView -PassThru -Title "Select Appx Packages to Remove from Online Windows Image" | ForEach-Object {
                    Write-Verbose "$($_.Name): Removing Appx Package $($_.PackageFullName)" -Verbose
                    Remove-AppPackage -AllUsers -Package $_.PackageFullName -Verbose
                }
            }
        }
        #=================================================
        # AppxProvisionedPackage
        #=================================================
        if (Get-Command Get-AppxProvisionedPackage) {
            if ($GridRemoveAppxPP.IsPresent) {
                Get-AppxProvisionedPackage -Online | Select-Object DisplayName, PackageName | Out-GridView -PassThru -Title "Select Appx Provisioned Packages to Remove from Online Windows Image" | ForEach-Object {
                    Write-Verbose "$($_.DisplayName): Removing Appx Provisioned Package $($_.PackageName)" -Verbose
                    Remove-AppProvisionedPackage -Online -AllUsers -PackageName $_.PackageName
                }
            }
        }
        #=================================================
        # RemoveAppx
        #=================================================
        foreach ($Item in $Name) {
            if (Get-Command Get-AppxPackage) {
                if ((Get-Command Get-AppxPackage).Parameters.ContainsKey('AllUsers')) {
                    Get-AppxPackage -AllUsers | Select-Object * | Where-Object {$_.NonRemovable -ne $true} | Where-Object {$_.Name -Match $Item} | ForEach-Object {

                        Write-Host -ForegroundColor DarkCyan $_.Name
                        if ((Get-Command Remove-AppxPackage).Parameters.ContainsKey('AllUsers')) {
                            Try {Remove-AppxPackage -AllUsers -Package $_.PackageFullName | Out-Null}
                            Catch {
                                #Write-Warning "AllUsers Appx Package $($_.PackageFullName) did not remove successfully"
                        }
                        }
                        else {
                            Try {Remove-AppxPackage -Package $_.PackageFullName | Out-Null}
                            Catch {
                                #Write-Warning "Appx Package $($_.PackageFullName) did not remove successfully"
                        }
                        }
                    }
                } else {
                    Get-AppxPackage | Select-Object * | Where-Object {$_.NonRemovable -ne $true} | Where-Object {$_.Name -Match $Item} | ForEach-Object {

                        Write-Host -ForegroundColor DarkCyan $_.Name
                        if ((Get-Command Remove-AppxPackage).Parameters.ContainsKey('AllUsers')) {
                            Try {Remove-AppxPackage -AllUsers -Package $_.PackageFullName | Out-Null}
                            Catch {
                                #Write-Warning "AllUsers Appx Package $($_.PackageFullName) did not remove successfully"
                        }
                        }
                        else {
                            Try {Remove-AppxPackage -Package $_.PackageFullName | Out-Null}
                            Catch {
                                #Write-Warning "Appx Package $($_.PackageFullName) did not remove successfully"
                            }
                        }
                    }
                }
            }
            if (Get-Command Get-AppxProvisionedPackage) {
                Get-AppxProvisionedPackage -Online | Where-Object {$_.DisplayName -Match $Item} | ForEach-Object {

                    Write-Host -ForegroundColor DarkCyan $_.Name
                    if ((Get-Command Remove-AppxProvisionedPackage).Parameters.ContainsKey('AllUsers')) {
                        Try {Remove-AppxProvisionedPackage -Online -AllUsers -PackageName $_.PackageName | Out-Null}
                        Catch {
                            #Write-Warning "AllUsers Appx Provisioned Package $($_.PackageName) did not remove successfully"
                    }
                    }
                    else {
                        Try {Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName | Out-Null}
                        Catch {
                            #Write-Warning "Appx Provisioned Package $($_.PackageName) did not remove successfully"
                    }
                    }
                }
            }
        }
    }
    end {}
}
function Test-WindowsImage {
<#
.SYNOPSIS
Tests WindowsImage conditions.
 
.DESCRIPTION
Evaluates WindowsImage state and returns a validation result for scripting decisions.
 
.PARAMETER ImagePath
Specifies the ImagePath to use when running Test-WindowsImage.
 
.PARAMETER Index
Specifies the Index to use when running Test-WindowsImage.
 
.PARAMETER Extension
Specifies the Extension to use when running Test-WindowsImage.
 
.EXAMPLE
Test-WindowsImage -ImagePath <value>
Demonstrates a common way to run Test-WindowsImage.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias('FullName')]
        [string]$ImagePath,

        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [Alias('ImageIndex')]
        [UInt32]$Index = $null,

        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [ValidateSet('.esd','.wim')]
        [string]$Extension = $null
    )
    #=================================================
    # Test-Path
    #=================================================
    if (! (Test-Path $ImagePath)) {
        Write-Warning "Test-WindowsImage: Test-Path failed $ImagePath"
        Return $false
    }
    #=================================================
    # Get-Item
    #=================================================
    try {
        $GetItem = Get-Item -Path $ImagePath -ErrorAction Stop
    }
    catch {
        Write-Warning "Test-WindowsImage: Get-Item failed $ImagePath"
        Return $false
    }
    #=================================================
    # Get-Item Extension
    #=================================================
    if ($Extension) {
        if (($Extension -eq '.esd') -and ($GetItem.Extension -ne '.esd')) {
            Write-Warning "Test-WindowsImage: Get-Item Extension is not $Extension"
            Return $false
        }
        if (($Extension -eq '.wim') -and ($GetItem.Extension -ne '.wim')) {
            Write-Warning "Test-WindowsImage: Get-Item Extension is not $Extension"
            Return $false
        }
    }
    else {
        if (($GetItem.Extension -ne '.esd') -and ($GetItem.Extension -ne '.wim')) {
            Write-Warning "Test-WindowsImage: Get-Item Extension failed. File must be .esd or .wim"
            Return $false
        }
    }
    #=================================================
    # Get-WindowsImage
    #=================================================
    if ($Index) {
        try {
            $GetWindowsImage = Get-WindowsImage -ImagePath $GetItem.FullName -Index $Index -ErrorAction Stop | Out-Null
            Return $true
        }
        catch {
            Write-Warning "Test-WindowsImage: Get-WindowsImage failed $ImagePath"
            Return $false
        }
        finally {
            $Error.Clear()
        }
    }
    else {
        try {
            $GetWindowsImage = Get-WindowsImage -ImagePath $GetItem.FullName -ErrorAction Stop | Out-Null
            Return $true
        }
        catch {
            Write-Warning "Test-WindowsImage: Get-WindowsImage failed $ImagePath"
            Return $false
        }
        finally {
            $Error.Clear()
        }
    }
    #=================================================
    # Something didn't work right if this is run
    #=================================================
    Return $false
    #=================================================
}
function Test-WindowsImageMounted {
<#
.SYNOPSIS
Tests WindowsImageMounted conditions.
 
.DESCRIPTION
Evaluates WindowsImageMounted state and returns a validation result for scripting decisions.
 
.PARAMETER ImagePath
Specifies the ImagePath to use when running Test-WindowsImageMounted.
 
.PARAMETER Index
Specifies the Index to use when running Test-WindowsImageMounted.
 
.EXAMPLE
Test-WindowsImageMounted -ImagePath <value>
Demonstrates a common way to run Test-WindowsImageMounted.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            ValueFromPipelineByPropertyName
        )]
        [string]$ImagePath,

        [Parameter(ValueFromPipelineByPropertyName)]
        [UInt32]$Index = 1
    )

    if (Get-WindowsImage -Mounted | Where-Object {($_.ImagePath -eq $ImagePath) -and ($_.ImageIndex -eq $Index)}) {
        Return $true
    } else {
        Return $false
    }
}
function Test-WindowsImageMountPath {
<#
.SYNOPSIS
Tests WindowsImageMountPath conditions.
 
.DESCRIPTION
Evaluates WindowsImageMountPath state and returns a validation result for scripting decisions.
 
.PARAMETER Path
Specifies the Path to use when running Test-WindowsImageMountPath.
 
.EXAMPLE
Test-WindowsImageMountPath -P <value>
Demonstrates a common way to run Test-WindowsImageMountPath.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            ValueFromPipelineByPropertyName
        )]
        [string]$Path
    )

    if (Get-WindowsImage -Mounted | Where-Object {$_.Path -eq $Path}) {
        Return $true
    } else {
        Return $false
    }
}
function Test-WindowsPackageCAB {
<#
.SYNOPSIS
Tests WindowsPackageCAB conditions.
 
.DESCRIPTION
Evaluates WindowsPackageCAB state and returns a validation result for scripting decisions.
 
.PARAMETER PackagePath
Specifies the PackagePath to use when running Test-WindowsPackageCAB.
 
.PARAMETER Path
Specifies the Path to use when running Test-WindowsPackageCAB.
 
.EXAMPLE
Test-WindowsPackageCAB -PackagePath <value>
Demonstrates a common way to run Test-WindowsPackageCAB.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.String]
        $PackagePath,

        [Parameter()]
        [System.String]
        $Path
    )

    try {
        $WinPackage = $null
        if ($Path) {
            $WinPackage = Get-WindowsPackage -Path $Path -PackagePath $PackagePath -ErrorAction SilentlyContinue
        }
        else {
            $WinPackage = Get-WindowsPackage -Online -PackagePath $PackagePath -ErrorAction SilentlyContinue
        }
    }
    catch {
        Write-Verbose -Message $_.Exception.Message
    }

    Write-Verbose -Message $PackagePath
    Write-Verbose -Message $Path

    [string]$returnVal = [string]::Empty

    if ([string]::IsNullOrWhiteSpace($WinPackage.PackageName)) {
        Write-Verbose -Message 'Could not extract PackageName from PackagePath.'
    }
    else {
        switch ($WinPackage.PackageName) {
            { $_ -match 'OnePackage' } { $returnVal = 'CombinedMSU'; Break; }
            { $_ -match 'Multiple_Packages' } { $returnVal = 'CombinedLCU'; Break; }
            { $_ -match 'DotNetRollup' } { $returnVal = 'DotNetCU'; Break; }
            { $_ -match 'ServicingStack' } { $returnVal = 'SSU'; Break; }
            Default { $returnVal = $WinPackage.PackageName; Break; }
        }
        Write-Verbose -Message $returnVal
    }
    Return $returnVal
}
function Update-MyWindowsImage {
<#
.SYNOPSIS
Updates MyWindowsImage content.
 
.DESCRIPTION
Installs updates into MyWindowsImage according to the supplied parameters.
 
.PARAMETER Path
Specifies the Path to use when running Update-MyWindowsImage.
 
.PARAMETER Update
Specifies the Update to use when running Update-MyWindowsImage.
 
.PARAMETER BitsTransfer
Specifies the BitsTransfer to use when running Update-MyWindowsImage.
 
.PARAMETER Force
Specifies the Force to use when running Update-MyWindowsImage.
 
.EXAMPLE
Update-MyWindowsImage -Path <value>
Demonstrates a common way to run Update-MyWindowsImage.
 
.LINK
https://github.com/OSDeploy/OSD/tree/master/docs
 
.NOTES
Author: David Segura - Recast Software
2026-07-13 - Initial help block created
2026-07-13 - Refined generated help text
#>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String[]]$Path,

        [ValidateSet('Check','All','AdobeSU','DotNet','DotNetCU','LCU','SSU')]
        [System.String]$Update = 'Check',

        [System.Management.Automation.SwitchParameter]$BitsTransfer,

        [System.Management.Automation.SwitchParameter]$Force
    )

    begin {
        #=================================================
        # Block
        #=================================================
        $CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $CurrentPrincipal = [Security.Principal.WindowsPrincipal]::new($CurrentIdentity)
        if (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Administrative rights are required"
            return
        }
        Block-WindowsVersionNe10
        #=================================================
        # Get-WindowsImage Mounted
        #=================================================
        if ($null -eq $Path) {
            $Path = (Get-WindowsImage -Mounted | Select-Object -Property Path).Path
        }
        #=================================================
    }
    process {
        foreach ($Input in $Path) {
            #=================================================
            # Path
            #=================================================
            $MountPath = (Get-Item -Path $Input | Select-Object FullName).FullName
            Write-Verbose "Path: $MountPath" -Verbose
            #=================================================
            # Validate Mount Path
            #=================================================
            if (-not (Test-Path $Input -ErrorAction SilentlyContinue)) {
                Write-Warning "Update-MyWindowsImage: Unable to locate Mounted WindowsImage at $Input"
                Break
            }
            #=================================================
            # Get Registry Information
            #=================================================
            $global:GetRegCurrentVersion = Get-RegCurrentVersion -Path $Input
            #=================================================
            # Require OSMajorVersion 10
            #=================================================
            if ($global:GetRegCurrentVersion.CurrentMajorVersionNumber -ne 10) {
                Write-Warning "Update-MyWindowsImage: OS MajorVersion 10 is required"
                Break
            }

            Write-Verbose -Verbose $global:GetRegCurrentVersion.ReleaseId
            #=================================================
            # Get-WSUSXML and Filter Results
            #=================================================
            $global:GetWSUSXML = Get-WSUSXML -Catalog Windows -Silent | Sort-Object UpdateGroup -Descending

            if ($global:GetRegCurrentVersion.ReleaseId -gt 0) {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateBuild -eq $global:GetRegCurrentVersion.DisplayVersion}
            }
            else {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateBuild -eq $global:GetRegCurrentVersion.ReleaseId}
            }

            if ($global:GetRegCurrentVersion.BuildLabEx -match 'amd64') {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateArch -eq 'x64'}
            } else {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateArch -eq 'x86'}
            }
            if ($global:GetRegCurrentVersion.InstallationType -match 'WindowsPE') {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateOS -eq 'Windows 10'}
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateGroup -notmatch 'Adobe'}
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateGroup -notmatch 'DotNet'}
            }
            if ($global:GetRegCurrentVersion.InstallationType -match 'Core') {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateGroup -notmatch 'Adobe'}
            }
            if ($global:GetRegCurrentVersion.InstallationType -match 'Client') {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateOS -notmatch 'Server'}
            }
            if ($global:GetRegCurrentVersion.InstallationType -match 'Server') {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateOS -match 'Server'}
            }

            #Don't install Optional Updates
            $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateGroup -ne ''}

            if ($Update -ne 'Check' -and $Update -ne 'All') {
                $global:GetWSUSXML = $global:GetWSUSXML | Where-Object {$_.UpdateGroup -match $Update}
            }
            #=================================================
            # Get-SessionsXml
            #=================================================
            $global:GetSessionsXml = Get-SessionsXml -Path "$Input" | Where-Object {$_.targetState -eq 'Installed'} | Sort-Object id
            #=================================================
            # Apply Update
            #=================================================
            foreach ($item in $global:GetWSUSXML) {
                if (! ($Force.IsPresent)) {
                    if ($global:GetSessionsXml | Where-Object {$_.KBNumber -match "$($item.FileKBNumber)"}) {
                        Write-Verbose "Installed: $($item.Title) $($item.FileName)" -Verbose
                        Continue
                    } else {
                        Write-Warning "Not Installed: $($item.Title) $($item.FileName)"
                    }
                }

                if ($Update -eq 'Check') {Continue}

<# if ($BitsTransfer.IsPresent) {
                    $UpdateFile = Save-OSDDownload -SourceUrl $item.OriginUri -BitsTransfer -Verbose
                } else {
                    $UpdateFile = Save-OSDDownload -SourceUrl $item.OriginUri -Verbose
                } #>

                $UpdateFile = Save-WebFile -SourceUrl $item.OriginUri
                $CurrentLog = "$env:TEMP\OSD\$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-Update-MyWindowsImage.log"

                if (! (Test-Path "$env:TEMP\OSD")) {New-Item -Path "$env:TEMP\OSD" -Force | Out-Null}

                if (Test-Path $UpdateFile.FullName) {
                    #Write-Verbose "Add-WindowsPackage -PackagePath $($UpdateFile.FullName) -Path $Input" -Verbose
                    Try {
                        Write-Verbose "Add-WindowsPackage -Path $Input -PackagePath $($UpdateFile.FullName)" -Verbose
                        Add-WindowsPackage -Path $Input -PackagePath $UpdateFile.FullName -LogPath $CurrentLog | Out-Null
                    }
                    Catch {
                        if ($_.Exception.Message -match '0x800f081e') {
                        Write-Verbose "Update-MyWindowsImage: 0x800f081e The package is not applicable to this image" -Verbose}
                        Write-Verbose $CurrentLog -Verbose
                    }
                } else {
                    Write-Warning "Unable to download $($UpdateFile.FullName)"
                }
            }
            #=================================================
            # Return for PassThru
            #=================================================
            Get-WindowsImage -Mounted | Where-Object {$_.Path -eq $MountPath}
            #=================================================
        }
    }
    end {}
}