Public/UpdateBlocking.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
    Windows Update blocking and hiding functions
.DESCRIPTION
    Provides functions to block, unblock, and manage Windows Update visibility
    using PSWindowsUpdate module integration.
#>


function Block-WindowsUpdate {
    <#
    .SYNOPSIS
        Hides/blocks specific Windows Updates by KB article ID
    .DESCRIPTION
        Uses PSWindowsUpdate to hide updates, preventing them from being installed.
        Hidden updates won't appear in Windows Update scans.
    .PARAMETER KBArticleID
        One or more KB article IDs to block (e.g., 'KB5001234', 'KB5005678')
    .PARAMETER Title
        Block updates matching this title pattern (supports wildcards)
    .EXAMPLE
        Block-WindowsUpdate -KBArticleID 'KB5001234'
    .EXAMPLE
        Block-WindowsUpdate -KBArticleID 'KB5001234', 'KB5005678'
    .EXAMPLE
        Block-WindowsUpdate -Title '*NVIDIA*'
    .OUTPUTS
        Array of blocked update objects
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByKB')]
        [string[]]$KBArticleID,
        
        [Parameter(Mandatory, ParameterSetName = 'ByTitle')]
        [string]$Title
    )
    
    Assert-Elevation -Operation "Blocking Windows Updates"
    
    # Ensure PSWindowsUpdate is available
    Initialize-PSWindowsUpdate
    
    $blockedUpdates = @()
    
    try {
        if ($PSCmdlet.ParameterSetName -eq 'ByKB') {
            foreach ($kb in $KBArticleID) {
                # Normalize KB format
                $normalizedKB = if ($kb -match '^KB') { $kb } else { "KB$kb" }
                
                if ($PSCmdlet.ShouldProcess($normalizedKB, "Block Windows Update")) {
                    Write-DriverLog -Message "Blocking Windows Update: $normalizedKB" -Severity Info
                    
                    $result = Hide-WindowsUpdate -KBArticleID $normalizedKB -Confirm:$false -ErrorAction Stop
                    
                    if ($result) {
                        $blockedUpdates += $result
                        Write-DriverLog -Message "Successfully blocked: $normalizedKB" -Severity Info
                        
                        # Also add to local blocklist
                        Add-ToLocalBlocklist -KBArticleID $normalizedKB
                    }
                }
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ByTitle') {
            if ($PSCmdlet.ShouldProcess($Title, "Block Windows Updates matching title")) {
                Write-DriverLog -Message "Blocking Windows Updates matching: $Title" -Severity Info
                
                $result = Hide-WindowsUpdate -Title $Title -Confirm:$false -ErrorAction Stop
                
                if ($result) {
                    $blockedUpdates += $result
                    foreach ($update in $result) {
                        Write-DriverLog -Message "Successfully blocked: $($update.Title)" -Severity Info
                        Add-ToLocalBlocklist -KBArticleID $update.KB -Title $update.Title
                    }
                }
            }
        }
    }
    catch {
        Write-DriverLog -Message "Failed to block update: $($_.Exception.Message)" -Severity Error
        throw
    }
    
    return $blockedUpdates
}

function Unblock-WindowsUpdate {
    <#
    .SYNOPSIS
        Shows/unblocks previously hidden Windows Updates
    .DESCRIPTION
        Uses PSWindowsUpdate to show hidden updates, making them available for installation again.
    .PARAMETER KBArticleID
        One or more KB article IDs to unblock
    .PARAMETER Title
        Unblock updates matching this title pattern
    .PARAMETER All
        Unblock all hidden updates
    .EXAMPLE
        Unblock-WindowsUpdate -KBArticleID 'KB5001234'
    .EXAMPLE
        Unblock-WindowsUpdate -All
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByKB')]
        [string[]]$KBArticleID,
        
        [Parameter(Mandatory, ParameterSetName = 'ByTitle')]
        [string]$Title,
        
        [Parameter(Mandatory, ParameterSetName = 'All')]
        [switch]$All
    )
    
    Assert-Elevation -Operation "Unblocking Windows Updates"
    
    Initialize-PSWindowsUpdate
    
    $unblockedUpdates = @()
    
    try {
        if ($All) {
            if ($PSCmdlet.ShouldProcess("All hidden updates", "Unblock")) {
                Write-DriverLog -Message "Unblocking all hidden Windows Updates" -Severity Info
                
                $hiddenUpdates = Get-WindowsUpdate -IsHidden -ErrorAction SilentlyContinue
                
                foreach ($update in $hiddenUpdates) {
                    $result = Show-WindowsUpdate -KBArticleID $update.KB -Confirm:$false -ErrorAction Stop
                    if ($result) {
                        $unblockedUpdates += $result
                        Remove-FromLocalBlocklist -KBArticleID $update.KB
                    }
                }
                
                Write-DriverLog -Message "Unblocked $($unblockedUpdates.Count) updates" -Severity Info
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ByKB') {
            foreach ($kb in $KBArticleID) {
                $normalizedKB = if ($kb -match '^KB') { $kb } else { "KB$kb" }
                
                if ($PSCmdlet.ShouldProcess($normalizedKB, "Unblock Windows Update")) {
                    Write-DriverLog -Message "Unblocking Windows Update: $normalizedKB" -Severity Info
                    
                    $result = Show-WindowsUpdate -KBArticleID $normalizedKB -Confirm:$false -ErrorAction Stop
                    
                    if ($result) {
                        $unblockedUpdates += $result
                        Remove-FromLocalBlocklist -KBArticleID $normalizedKB
                        Write-DriverLog -Message "Successfully unblocked: $normalizedKB" -Severity Info
                    }
                }
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ByTitle') {
            if ($PSCmdlet.ShouldProcess($Title, "Unblock Windows Updates matching title")) {
                $result = Show-WindowsUpdate -Title $Title -Confirm:$false -ErrorAction Stop
                
                if ($result) {
                    $unblockedUpdates += $result
                    foreach ($update in $result) {
                        Remove-FromLocalBlocklist -KBArticleID $update.KB
                    }
                }
            }
        }
    }
    catch {
        Write-DriverLog -Message "Failed to unblock update: $($_.Exception.Message)" -Severity Error
        throw
    }
    
    return $unblockedUpdates
}

function Get-BlockedUpdates {
    <#
    .SYNOPSIS
        Lists all blocked/hidden Windows Updates
    .DESCRIPTION
        Returns a list of all updates that have been hidden from Windows Update.
        Also includes updates from the local blocklist that may not be currently available.
    .PARAMETER IncludeLocal
        Include entries from the local blocklist file that may not be currently hidden
    .EXAMPLE
        Get-BlockedUpdates
    .EXAMPLE
        Get-BlockedUpdates -IncludeLocal
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$IncludeLocal
    )
    
    Initialize-PSWindowsUpdate
    
    $blockedUpdates = @()
    
    # Get currently hidden updates from Windows Update
    try {
        $hiddenUpdates = Get-WindowsUpdate -IsHidden -ErrorAction SilentlyContinue
        
        if ($hiddenUpdates) {
            foreach ($update in $hiddenUpdates) {
                $blockedUpdates += [PSCustomObject]@{
                    KBArticleID  = $update.KB
                    Title        = $update.Title
                    Size         = $update.Size
                    Source       = 'WindowsUpdate'
                    HiddenDate   = $null
                    IsCurrentlyHidden = $true
                }
            }
        }
    }
    catch {
        Write-DriverLog -Message "Failed to query hidden updates: $($_.Exception.Message)" -Severity Warning
    }
    
    # Include local blocklist entries
    if ($IncludeLocal) {
        $localBlocklist = Get-LocalBlocklist
        
        foreach ($entry in $localBlocklist.BlockedKBs) {
            # Check if already in list
            if ($blockedUpdates.KBArticleID -notcontains $entry.KBArticleID) {
                $blockedUpdates += [PSCustomObject]@{
                    KBArticleID  = $entry.KBArticleID
                    Title        = $entry.Title
                    Size         = $null
                    Source       = 'LocalBlocklist'
                    HiddenDate   = $entry.DateBlocked
                    IsCurrentlyHidden = $false
                }
            }
        }
    }
    
    return $blockedUpdates
}

function Export-UpdateBlocklist {
    <#
    .SYNOPSIS
        Exports the update blocklist to a JSON file
    .DESCRIPTION
        Exports both the local blocklist and currently hidden updates to a portable JSON format.
    .PARAMETER Path
        Path to export the blocklist to
    .PARAMETER IncludeHidden
        Include currently hidden Windows Updates (not just local blocklist)
    .EXAMPLE
        Export-UpdateBlocklist -Path 'C:\Backup\blocklist.json'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path,
        
        [Parameter()]
        [switch]$IncludeHidden
    )
    
    $exportData = @{
        ExportDate = (Get-Date).ToString('o')
        ComputerName = $env:COMPUTERNAME
        LocalBlocklist = (Get-LocalBlocklist)
        HiddenUpdates = @()
    }
    
    if ($IncludeHidden) {
        Initialize-PSWindowsUpdate
        $hidden = Get-WindowsUpdate -IsHidden -ErrorAction SilentlyContinue
        if ($hidden) {
            $exportData.HiddenUpdates = $hidden | ForEach-Object {
                @{
                    KBArticleID = $_.KB
                    Title = $_.Title
                    Size = $_.Size
                }
            }
        }
    }
    
    $exportData | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Encoding UTF8
    
    Write-DriverLog -Message "Exported blocklist to: $Path" -Severity Info
    
    return $Path
}

function Import-UpdateBlocklist {
    <#
    .SYNOPSIS
        Imports an update blocklist from a JSON file
    .DESCRIPTION
        Imports blocklist entries and optionally applies them (hides the updates).
    .PARAMETER Path
        Path to the blocklist JSON file
    .PARAMETER Apply
        Actually hide the imported updates (requires elevation)
    .EXAMPLE
        Import-UpdateBlocklist -Path 'C:\Backup\blocklist.json' -Apply
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,
        
        [Parameter()]
        [switch]$Apply
    )
    
    $importData = Get-Content -Path $Path -Raw | ConvertFrom-Json
    
    Write-DriverLog -Message "Importing blocklist from: $Path (exported: $($importData.ExportDate))" -Severity Info
    
    # Merge with existing local blocklist
    $localBlocklist = Get-LocalBlocklist
    
    foreach ($entry in $importData.LocalBlocklist.BlockedKBs) {
        if ($localBlocklist.BlockedKBs.KBArticleID -notcontains $entry.KBArticleID) {
            $localBlocklist.BlockedKBs += $entry
        }
    }
    
    foreach ($driver in $importData.LocalBlocklist.BlockedDrivers) {
        if ($localBlocklist.BlockedDrivers -notcontains $driver) {
            $localBlocklist.BlockedDrivers += $driver
        }
    }
    
    Save-LocalBlocklist -Blocklist $localBlocklist
    
    # Apply hidden updates if requested
    if ($Apply) {
        Assert-Elevation -Operation "Applying imported blocklist"
        Initialize-PSWindowsUpdate
        
        $allKBs = @()
        $allKBs += $importData.LocalBlocklist.BlockedKBs.KBArticleID
        $allKBs += $importData.HiddenUpdates.KBArticleID
        $allKBs = $allKBs | Where-Object { $_ } | Select-Object -Unique
        
        foreach ($kb in $allKBs) {
            if ($PSCmdlet.ShouldProcess($kb, "Hide imported update")) {
                try {
                    Hide-WindowsUpdate -KBArticleID $kb -Confirm:$false -ErrorAction SilentlyContinue
                    Write-DriverLog -Message "Applied block for: $kb" -Severity Info
                }
                catch {
                    Write-DriverLog -Message "Could not hide $kb (may not be available): $($_.Exception.Message)" -Severity Warning
                }
            }
        }
    }
    
    return $importData
}

#region Helper Functions

function Initialize-PSWindowsUpdate {
    <#
    .SYNOPSIS
        Ensures PSWindowsUpdate module is available
    #>

    [CmdletBinding()]
    param()
    
    if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) {
        Write-DriverLog -Message "Installing PSWindowsUpdate module" -Severity Info
        Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -ErrorAction Stop
    }
    
    Import-Module PSWindowsUpdate -Force -ErrorAction Stop
}

function Get-LocalBlocklistPath {
    $config = $script:ModuleConfig
    $basePath = Split-Path $config.CompliancePath -Parent
    return Join-Path $basePath "blocklist.json"
}

function Get-LocalBlocklist {
    <#
    .SYNOPSIS
        Gets the local blocklist from JSON file
    #>

    $path = Get-LocalBlocklistPath
    
    if (Test-Path $path) {
        return Get-Content $path -Raw | ConvertFrom-Json
    }
    
    # Return default structure
    return [PSCustomObject]@{
        Version = '1.0'
        LastModified = $null
        BlockedKBs = @()
        BlockedDrivers = @()
        ApprovedOnly = $false
        ApprovedUpdates = @()
    }
}

function Save-LocalBlocklist {
    param(
        [Parameter(Mandatory)]
        $Blocklist
    )
    
    $path = Get-LocalBlocklistPath
    $dir = Split-Path $path -Parent
    
    if (-not (Test-Path $dir)) {
        New-Item -Path $dir -ItemType Directory -Force | Out-Null
    }
    
    $Blocklist.LastModified = (Get-Date).ToString('o')
    $Blocklist | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8
}

function Add-ToLocalBlocklist {
    param(
        [string]$KBArticleID,
        [string]$Title,
        [string]$DriverInf
    )
    
    $blocklist = Get-LocalBlocklist
    
    if ($KBArticleID) {
        $entry = [PSCustomObject]@{
            KBArticleID = $KBArticleID
            Title = $Title
            DateBlocked = (Get-Date).ToString('o')
            BlockedBy = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
        }
        
        # Avoid duplicates
        $existing = $blocklist.BlockedKBs | Where-Object { $_.KBArticleID -eq $KBArticleID }
        if (-not $existing) {
            $blocklist.BlockedKBs = @($blocklist.BlockedKBs) + $entry
        }
    }
    
    if ($DriverInf) {
        if ($blocklist.BlockedDrivers -notcontains $DriverInf) {
            $blocklist.BlockedDrivers = @($blocklist.BlockedDrivers) + $DriverInf
        }
    }
    
    Save-LocalBlocklist -Blocklist $blocklist
}

function Remove-FromLocalBlocklist {
    param(
        [string]$KBArticleID,
        [string]$DriverInf
    )
    
    $blocklist = Get-LocalBlocklist
    
    if ($KBArticleID) {
        $blocklist.BlockedKBs = @($blocklist.BlockedKBs | Where-Object { $_.KBArticleID -ne $KBArticleID })
    }
    
    if ($DriverInf) {
        $blocklist.BlockedDrivers = @($blocklist.BlockedDrivers | Where-Object { $_ -ne $DriverInf })
    }
    
    Save-LocalBlocklist -Blocklist $blocklist
}

#endregion