Public/windowsupdate/Hide-WindowsUpdate.ps1

#Requires -Version 5.1
function Hide-WindowsUpdate {
    <#
        .SYNOPSIS
            Hides one or more Windows Updates to prevent them from being installed
 
        .DESCRIPTION
            Hides (declines) specified Windows Updates by setting the IsHidden property
            to true on matching IUpdate COM objects. Hidden updates are excluded from
            automatic installation and from Get-WindowsUpdate results unless -IncludeHidden
            is specified. Use Show-WindowsUpdate to unhide them later.
            Searches both visible and already-hidden updates to accurately report
            AlreadyHidden status.
 
        .PARAMETER ComputerName
            One or more computer names to target. Defaults to the local computer.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not required for local operations.
 
        .PARAMETER KBArticleID
            One or more KB article IDs to hide. Accepts values with or without
            the KB prefix (e.g., 'KB5034441' or '5034441').
 
        .PARAMETER MicrosoftUpdate
            When specified, queries the full Microsoft Update catalog instead of the
            machine's configured source.
 
        .EXAMPLE
            Hide-WindowsUpdate -KBArticleID 'KB5034441'
 
            Hides KB5034441 on the local computer.
 
        .EXAMPLE
            Hide-WindowsUpdate -ComputerName 'SRV01' -KBArticleID 'KB5034441', 'KB5035432'
 
            Hides two updates on SRV01.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Hide-WindowsUpdate -KBArticleID 'KB5034441'
 
            Hides KB5034441 on SRV01 and SRV02 via pipeline.
 
        .OUTPUTS
            PSWinOps.WindowsUpdateHideResult
            Returns objects with ComputerName, Title, KBArticle, Result, and Timestamp.
            Result is one of: Hidden, AlreadyHidden, NotFound.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-04-08
            Requires: PowerShell 5.1+ / Windows only
            Requires: Administrator privileges
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate-get_ishidden
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType('PSWinOps.WindowsUpdateHideResult')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$KBArticleID,

        [Parameter(Mandatory = $false)]
        [switch]$MicrosoftUpdate
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        $normalizedKBIds = $KBArticleID | ForEach-Object -Process { $_ -replace '^KB', '' }
        $kbCsv = $normalizedKBIds -join ','
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] KB targets: $($normalizedKBIds | ForEach-Object -Process { "KB$_" }) "

        $hideScriptBlock = {
            param(
                [string]$KBCsv,
                [bool]$UseMicrosoftUpdate
            )

            $kbTargets = $KBCsv -split ','
            $session = New-Object -ComObject 'Microsoft.Update.Session'
            $session.ClientApplicationID = 'PSWinOps'
            $searcher = $session.CreateUpdateSearcher()

            if ($UseMicrosoftUpdate) {
                $serviceManager = New-Object -ComObject 'Microsoft.Update.ServiceManager'
                $serviceManager.ClientApplicationID = 'PSWinOps'
                $service = $serviceManager.AddService2('7971f918-a847-4430-9279-4a52d1efe18d', 7, '')
                $searcher.ServerSelection = 3
                $searcher.ServiceID = $service.ServiceID
            }

            $processedKB = @{}

            # Search available (non-hidden) updates
            $availableResult = $searcher.Search('IsInstalled=0')
            foreach ($update in $availableResult.Updates) {
                foreach ($kbId in $update.KBArticleIDs) {
                    if ($kbTargets -contains $kbId -and -not $processedKB.ContainsKey($kbId)) {
                        $update.IsHidden = $true
                        $processedKB[$kbId] = [PSCustomObject]@{
                            Title  = [string]$update.Title
                            Result = 'Hidden'
                        }
                    }
                }
            }

            # Search already-hidden updates
            $hiddenResult = $searcher.Search('IsInstalled=0 AND IsHidden=1')
            foreach ($update in $hiddenResult.Updates) {
                foreach ($kbId in $update.KBArticleIDs) {
                    if ($kbTargets -contains $kbId -and -not $processedKB.ContainsKey($kbId)) {
                        $processedKB[$kbId] = [PSCustomObject]@{
                            Title  = [string]$update.Title
                            Result = 'AlreadyHidden'
                        }
                    }
                }
            }

            # Build results preserving input order
            foreach ($kb in $kbTargets) {
                if ($processedKB.ContainsKey($kb)) {
                    [PSCustomObject]@{
                        KBArticle = "KB$kb"
                        Title     = $processedKB[$kb].Title
                        Result    = $processedKB[$kb].Result
                    }
                } else {
                    [PSCustomObject]@{
                        KBArticle = "KB$kb"
                        Title     = $null
                        Result    = 'NotFound'
                    }
                }
            }
        }
    }

    process {
        foreach ($computer in $ComputerName) {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Processing '$computer'"

            if (-not $PSCmdlet.ShouldProcess("$computer — KB$($normalizedKBIds -join ', KB')", 'Hide Windows Update')) {
                continue
            }

            try {
                $invokeParams = @{
                    ComputerName = $computer
                    ScriptBlock  = $hideScriptBlock
                    ArgumentList = @($kbCsv, [bool]$MicrosoftUpdate)
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $invokeParams['Credential'] = $Credential
                }

                $results = Invoke-RemoteOrLocal @invokeParams

                foreach ($entry in $results) {
                    [PSCustomObject]@{
                        PSTypeName   = 'PSWinOps.WindowsUpdateHideResult'
                        ComputerName = $computer
                        Title        = $entry.Title
                        KBArticle    = $entry.KBArticle
                        Result       = $entry.Result
                        Timestamp    = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                    }
                }
            } catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${computer}': $_"
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}