IntegrisWindowsUpdate.psm1

### General Support Functions
FUNCTION IPM-Set-RegistryKey {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)]
        [ValidateSet("HKLM", "HKCU", "HKCR", "HKU", "HKCC")]
        [string]$Hive,

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

        [Parameter(Mandatory=$true)]
        [string]$Name,

        [Parameter(Mandatory=$true)]
        $Value,

        [Parameter(Mandatory=$true)]
        [ValidateSet("String", "DWord", "QWord", "MultiString", "Binary", "ExpandString")]
        [string]$Type
    )

    try {
        # Convert hive shorthand to full registry path
        $hivePath = switch ($Hive) {
            "HKLM" { "HKLM:\" }
            "HKCU" { "HKCU:\" }
            "HKCR" { "HKEY_CLASSES_ROOT\" }
            "HKU"  { "HKEY_USERS\" }
            "HKCC" { "HKEY_CURRENT_CONFIG\" }
        }

        # Construct full registry path
        $fullPath = Join-Path $hivePath $Path

        # Check if path exists, create if it doesn't
        if (-not (Test-Path $fullPath)) {
            New-Item -Path $fullPath -Force -ErrorAction SilentlyContinue | Out-Null
        }

        # Set the registry value
        Set-ItemProperty -Path $fullPath -Name $Name -Value $Value -Type $Type -ErrorAction SilentlyContinue -Verbose:$False   

        # Verify the value was set
        $setValue = Get-ItemProperty -Path $fullPath -Name $Name -ErrorAction SilentlyContinue -Verbose:$False   
        $actualValue = $setValue.$Name

        # Compare set value with requested value
        if ($actualValue -eq $Value) {
            Write-Verbose "Registry key set successfully: $fullPath\$Name = $actualValue"
            return $true
        } else {
            Write-Verbose "Verification failed: Expected '$Value', but got '$actualValue'"
            return $false
        }



    }
    catch {
        Write-Verbose "Failed to set registry key: $($_.Exception.Message)"
        return $false
    }
}
FUNCTION Ensure-Module {
    param (
        [Parameter(Mandatory = $true)]
        [string]$ModuleName,

        [string]$Version = "latest",  # Optional: specify a version, defaults to latest
        [switch]$Force,               # Optional: force reinstall if already installed
        [switch]$ScopeCurrentUser     # Optional: install for current user only
    )

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
    try {
    
        # Step 1: Check and install NuGet provider if not present
        $nugetProvider = Get-PackageProvider -ErrorAction SilentlyContinue -Verbose:$false | Where-Object { $_.Name -eq "NuGet" }
        if (-not $nugetProvider -or $Force) {
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            Write-Verbose "NuGet provider not found or Force specified. Installing NuGet provider..."
            # Check if running with admin rights for global install
            $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
            $scope = if ($ScopeCurrentUser -or -not $isAdmin) { "CurrentUser" } else { "AllUsers" }
            Install-PackageProvider -Name "NuGet" -MinimumVersion "2.8.5.201" -Scope $scope -Force -ErrorAction Stop -Confirm:$false -Verbose:$false | Out-Null       
            Write-Verbose "NuGet provider installed successfully."
        } else {
            Write-Verbose "NuGet provider is already installed (Version: $($nugetProvider.Version))."
        }      
        
        # Step 2: Ensure PowerShell Gallery is registered as a trusted repository
        $psGallery = Get-PSRepository -Name "PSGallery" -ErrorAction SilentlyContinue -Verbose:$false
        if (-not $psGallery -or $psGallery.InstallationPolicy -ne "Trusted") {
            Write-Verbose "Configuring PSGallery as a trusted repository..."
            Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted -ErrorAction Stop -Verbose:$false
            Write-Verbose "PSGallery configured successfully."
        } else {
            Write-Verbose "PSGallery is already configured and trusted."
        }       

        # Step 3: Check if the module is already imported in the current session
        $importedModule = Get-Module -Name $ModuleName -ErrorAction SilentlyContinue -Verbose:$false
        if ($importedModule) {
            Write-Verbose "Module '$ModuleName' is already imported (Version: $($importedModule.Version))."
            return $true
        }

        # Step 4: Check if the module is installed (available but not necessarily imported)
        $installedModule = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue -Verbose:$false
        if ($installedModule -and -not $Force) {
            Write-Verbose "Module '$ModuleName' is installed but not imported. Importing now..."
            Import-Module -Name $ModuleName -ErrorAction Stop -Verbose:$false
            Write-Verbose "Module '$ModuleName' imported successfully (Version: $((Get-Module -Name $ModuleName -Verbose:$false).Version))."
            return $true
        }

        # Step 5: If not installed or Force is specified, install the module
        Write-Verbose "Module '$ModuleName' is not installed or Force specified. Installing now..."
        
        $scope = if ($ScopeCurrentUser -or -not $isAdmin) { "CurrentUser" } else { "AllUsers" }

        if ($Version -eq "latest") {
            Install-Module -Name $ModuleName -Scope $scope -Force:$Force -AllowClobber -ErrorAction Stop -Verbose:$false
        } else {
            Install-Module -Name $ModuleName -RequiredVersion $Version -Scope $scope -Force:$Force -AllowClobber -ErrorAction Stop -Verbose:$false
        }
        
        # Import the newly installed module
        Import-Module -Name $ModuleName -ErrorAction Stop -Verbose:$false
        $installedVersion = (Get-Module -Name $ModuleName).Version
        Write-Verbose "Module '$ModuleName' installed and imported successfully (Version: $installedVersion)."
        return $true
    }
    catch {
        Write-Error "Failed to ensure module '$ModuleName': $_"
        return $false
    }
}
FUNCTION Get-TimeInfo {

    [CmdletBinding()]
    Param (
        [switch]$NoTimeZone = $False
    )

    ### ===============================
    ### Temp Vars
    ### ===============================
    $Results = @()
    $NTPTime = ""
    $TimezoneDiff = ""

    ### ===============================
    ### Results Vars
    ### ===============================
    $Hostname = $env:COMPUTERNAME
    $ApproximateLocation = ""
    $LocalTime = ""
    $WindowsTime = ""            
    $WindowsTimeGMT = ""
    $GMTTime = ""        
    $SecondsDifference =  ""
    $WindowsTimeZone = ""
    $WindowsUTCOffSet = ""
    $LocalTimeZone = ""
    $LocalUTCOffSet = ""            
    $NTPServer = ""
    $TimeDifference = ""


    ### ===============================
    ### GET NTP Server Info
    ### ===============================
    IF ($True) {

        TRY { 
            $NTPConfig = & "w32tm" /query /configuration
            $ServerList = @()
            IF (($NTPConfig | Select-String "NTPServer:") -eq $null) {
                $ServerList = "None"
            }
            ELSE {
                $ServerString = ($NTPConfig | Select-String "NTPServer:")
                $ServerList = $ServerString.ToString().Replace("NtpServer: ","").Replace(" (Local)","").Split(" ")
            }

            $NTPServer = $ServerList
        }
        CATCH {
            $NTPServer = "[Error.Command.w32tm.Unknown]"
        }
    }        


    ### ===============================
    ### GET Actual Time
    ### ===============================
    IF ($True) {
        Import-Module NTPTime -ErrorAction SilentlyContinue -Force -Verbose:$false   
        IF ((Ensure-Module NTPTime -Verbose:$False) -eq $False) { 
            Write-Host "Required module NTPTime is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/NTPTime" 
            $LocalTime = "[Error.Module.NTPTime.NotInstalled]" 
            $GMTTime = "[Error.Module.NTPTime.NotInstalled]"
            $SecondsDifference = "[Error.Module.NTPTime.NotInstalled]"
        }
        TRY { $NTPTime = (Get-NTPTime -MaxOffset 1999999999 -ErrorAction Continue).NTPTime }
        CATCH { 
            Write-Host "NTP request to pool.ntp.org failed. If this error persists, check connectivity and/or security tools."
            $LocalTime = "[Error.Module.NTPTime.RequestFailed]" 
            $GMTTime = "[Error.Module.NTPTime.RequestFailed]"
            $SecondsDifference = "[Error.Module.NTPTime.RequestFailed]"
        }
    }


    ### ===============================
    ### GET Geo Location Data
    ### ===============================
    IF ($True) {
        TRY { $GeoData = Invoke-RestMethod -Method Get -Uri "http://ip-api.com/json/$PublicIPv4" }
        CATCH { 
            TRY { $GeoData = Invoke-RestMethod -Method Get -Uri "http://ip-api.com/json/$PublicIPv4" 
            }
            CATCH { 
                Write-Host "WebRequest failed. Check firewall settings or other web security applications, such as ThreatLocker or OpenDNS. IP-API.com must be whitelisted on port 80."
            
                $ApproximateLocation = "[Error.WebRequest.IP-API.com:80.RequestFailed]"
            } 
           
        }
    }

  
    ### ===============================
    ### GET TimeZone Data
    ### ===============================
    IF ($NoTimeZone -eq $False) {
        TRY { [xml]$TimezoneDataXML = Invoke-RestMethod -Method Get -Uri "http://api.geonames.org/timezone?lat=$($GeoData.lat)&lng=$($GeoData.lon)&username=integrispowershell" }
        CATCH { 
            TRY { [xml]$TimezoneDataXML = Invoke-RestMethod -Method Get -Uri "http://api.geonames.org/timezone?lat=$($GeoData.lat)&lng=$($GeoData.lon)&username=integrispowershell" }
            CATCH { 
                Write-Host "WebRequest failed. Check firewall settings or other web security applications, such as ThreatLocker or OpenDNS. API.Geonames.org must be whitelisted on port 80."; 
                $LocalTimeZone = "[Error.WebRequest.API.Geonames.org:80.RequestFailed]"
                $LocalUTCOffSet = "[Error.WebRequest.API.Geonames.org:80.RequestFailed]"
            } 
        }
    }

    ### ===============================
    ### GET Current Windows Time
    ### ===============================
    $WindowsTime = GET-Date
  

    $TimeDifference = $NTPTime - $WindowsTime


    IF ($ApproximateLocation -notlike "*Error.*") { $ApproximateLocation = $GeoData.City+", "+$GeoData.RegionName+" "+$GeoData.Zip }    
    IF ($GMTTime -notlike "*Error.*") { $GMTTime = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId($NTPTime, 'Greenwich Standard Time') }
    IF ($SecondsDifference -notlike "*Error.*") { $TimeDifference = $NTPTime - $WindowsTime; $SecondsDifference = [Math]::Round([Math]::Abs($TimeDifference.TotalSeconds),2) }
    TRY { $WindowsTimeZone = (Get-TimeZone).StandardName } CATCH { $WindowsTimeZone = "[Error.Command.Get-TimeZone.Unknown]" }
    TRY { 
        [int]$WindowsUTCOffSet = (Get-TimeZone).BaseUtcOffset.ToString().Replace("0","").Replace(":","") 
        $WindowsTimeGMT = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId($WindowsTime, 'Greenwich Standard Time')
    } CATCH { 
        $WindowsUTCOffSet = "[Error.Command.Get-TimeZone.Unknown]" 
        $WindowsTimeGMT = "[Error.Command.Get-TimeZone.Unknown]" 
    }    
    IF ($LocalTimeZone -notlike "*Error.*") { $LocalTimeZone = $TimezoneDataXML.geonames.timezone.timezoneId }
    IF ($LocalUTCOffSet -notlike "*Error.*") { $LocalUTCOffSet = [int]$TimezoneDataXML.geonames.timezone.rawOffset }
    IF ($WindowsUTCOffSet -like "*Error.*") { "[Error.Command.Get-TimeZone.Unknown]" }
    ELSEIF ($LocalTimeZone -like "*Error.*" ) { "[Error.WebRequest.API.Geonames.org:80.RequestFailed]" }
    ELSE { $TimezoneDiff = $LocalUTCOffSet - $WindowsUTCOffSet }
    IF ($LocalTime -notlike "*Error.*") { $LocalTime = $NTPTime.AddHours($TimezoneDiff) }
    
    $Results += New-Object PSObject -WarningAction SilentlyContinue -Property @{
        Hostname = $Hostname
        ApproximateLocation = $ApproximateLocation
        LocalTime = $LocalTime
        WindowsTime = $WindowsTime           
        WindowsTimeGMT = $WindowsTimeGMT
        LocalTimeGMT = $GMTTime
        SecondsDifference = $SecondsDifference
        WindowsTimeZone = $WindowsTimeZone
        WindowsUTCOffSet = $WindowsUTCOffSet
        LocalTimeZone = $LocalTimeZone
        LocalUTCOffSet = $LocalUTCOffSet       
        NTPServer = $NTPServer
    }
    
    RETURN $Results | Select Hostname, ApproximateLocation, WindowsTime, LocalTime, WindowsTimeGMT, LocalTimeGMT, SecondsDifference, WindowsTimeZone, WindowsUTCOffSet, LocalTimeZone, LocalUTCOffSet, NTPServer
}
FUNCTION Test-WindowsUpdateServiceIsRunning {

    [CmdletBinding()]
    PARAM (

    )

    IF (!(Test-AdministratorElevation)) { RETURN $False }

    $UpdateService = Get-Service WUAUSERV -ErrorAction SilentlyContinue

    IF ($UpdateService.Status -eq "Running") { Write-Host "Verified Windows Update service is running."; RETURN $True }
    ELSE { 
        Set-Service WUAUSERV -StartupType Automatic -ErrorAction SilentlyContinue
        Restart-Service WUAUSERV -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 70
        IF ($UpdateService.Status -eq "Running") { Write-Host "Restarted Windows update service and it is now running."; RETURN $True }
        ELSE { Write-Host "Attempted to restart Windows update service but failed."; RETURN $False }
    }
}
FUNCTION Test-AdministratorElevation {

    [CmdletBinding()]
    PARAM (

    )

    IF (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(` [Security.Principal.WindowsBuiltInRole] “Administrator”)) {
        Write-Host
        Write-Warning “You must run this command in an elevated privilege context.`nNo action was taken. Please re-run this command with administrator elevation.”
        RETURN $False
    }
    RETURN $True
}
FUNCTION Get-WindowsInfo {

    [CmdletBinding()]
    PARAM (

    )

    $Results = @()

    $ContextNotElevated = IF (!(Test-AdministratorElevation)) { RETURN }

    $OS = GET-CIMInstance Win32_OperatingSystem -Verbose:$False
    $TimeZone = GET-TimeZone
    $DomainType = ""
    $PowerMode = ""

    ### Check DomainJoin Type
    IF ($True) {
        $DomainServicesRegistration = dsregcmd /status

        IF ($DomainServicesRegistration -like "*DomainJoined : YES*") { $DomainType = "Local AD Domain" }
        ELSEIF ($DomainServicesRegistration -like "*AzureAdJoined : YES*" -and $DomainServicesRegistration -like "*DomainJoined : NO*") { $DomainType = "AzureAD" }
        ELSEIF ($DomainServicesRegistration -like "*AzureAdJoined : NO*" -and $DomainServicesRegistration -like "*DomainJoined : NO*") { $DomainType = "None (Workgroup)" }
        ELSE { $DomainType = "Error" }
    }

    IF ($ContextNotElevated -eq $False) {
        TRY { 
            $BitLockerEncryptedDrives = Get-BitLockerVolume -ErrorAction SilentlyContinue | Where-Object { $_.VolumeStatus -like "*encrypted*" } 
            $BitLockerEncryptedDrives = $BitLockerEncryptedDrives.MountPoint 
        } CATCH { $BitLockerEncryptedDrives = "[Error.FunctionNotFound.Get-BitLockerVolume]" }
        TRY { 
            $UnencryptedDrives = Get-BitLockerVolume -ErrorAction SilentlyContinue | Where-Object { $_.VolumeStatus -like "*decrypted*" }
            $UnencryptedDrives = $UnencryptedDrives.MountPoint
        } CATCH { $UnencryptedDrives = "[Error.FunctionNotFound.Get-BitLockerVolume]" }
    }
    ELSE {
        $BitLockerEncryptedDrives = "[Elevation Required]"
        $UnencryptedDrives = "[Elevation Required]"
    }

    $ProductType = "Unknown"
    IF ($OS.ProductType -eq 1) { $ProductType = "Workstation" }
    IF ($OS.ProductType -eq 2) { $ProductType = "Domain Controller" }
    IF ($OS.ProductType -eq 3) { $ProductType = "Server" }


    $LastBootTimeSpan = (Get-Date) - ($OS.LastBootUpTime)

    IF ($LastBootTimeSpan.Days -eq 1) { $LastBootTimeSpanString = "$([math]::round($($LastBootTimeSpan.Days),0)) Days + $([math]::round($LastBootTimeSpan.Hours,0).ToString().Padleft(2,"0")):$([math]::round($LastBootTimeSpan.Minutes,0).ToString().Padleft(2,"0")):$([math]::round($LastBootTimeSpan.Seconds,0).ToString().Padleft(2,"0"))" }
    ELSE { $LastBootTimeSpanString = "$([math]::round($($LastBootTimeSpan.Days),0)) Days + $([math]::round($LastBootTimeSpan.Hours,0).ToString().Padleft(2,"0")):$([math]::round($LastBootTimeSpan.Minutes,0).ToString().Padleft(2,"0")):$([math]::round($LastBootTimeSpan.Seconds,0).ToString().Padleft(2,"0"))" }
 
    #IF (IsLaptop) { $PowerMode = (GET-PowerMode).PowerModeName }

    $Results += New-Object PSObject -WarningAction SilentlyContinue -Property @{
        Hostname = $env:COMPUTERNAME
        OSName = $OS.Caption
        Type = $ProductType        
        DomainType = $DomainType
        PowerMode = $PowerMode
        Architecture = $OS.OSArchitecture 
        BuildNumber = $OS.BuildNumber
        TimeZone = $TimeZone.DisplayName
        InstallDate = $OS.InstallDate
        LastBootTime = $OS.LastBootUpTime
        LastBootTimeSpan = $LastBootTimeSpanString
        LastWindowsUpdate = (GET-HotFix -ErrorAction SilentlyContinue | Sort-Object -Property InstalledOn -Descending | Select-Object -First 1).InstalledOn
        BitLockerEncryptedDrives = $BitLockerEncryptedDrives
        UnencryptedDrives = $UnencryptedDrives
    }

    RETURN $Results | Select Hostname, OSName, Type, BuildNumber, PowerMode, DomainType, Architecture, TimeZone, InstallDate, LastBootTime, LastBootTimeSpan, LastWindowsUpdate, BitLockerEncryptedDrives, UnencryptedDrives
}
FUNCTION Test-IntegrisModuleDependency {
    <#
    .SYNOPSIS
    Tests and ensures the specified PowerShell module is installed and up to date.
 
    .DESCRIPTION
    This function checks if a specified PowerShell module is installed and meets the minimum version requirement. If not, it prompts the user to install or update the module.
 
    .PARAMETER Name
    Specifies the name of the module to test.
 
    .PARAMETER MinimumVersion
    Specifies the minimum version of the module required.
 
    .PARAMETER Confirm
    Prompts the user for confirmation before installing or updating the module. Defaults to $true. Use -Confirm:$False to override.
 
    .PARAMETER Force
    Forces the installation or update of the module.
 
    .EXAMPLE
    Test-IntegrisModuleDependency -Name "ModuleName" -MinimumVersion "1.0.0" -Confirm -Force
    Tests and ensures the specified module is installed and up to date, with user confirmation and force options.
 
    .NOTES
    The function checks for the module and handles installation or update as needed.
    #>


    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory)]
        [String]$Name,
        [String]$MinimumVersion,
        [Switch]$Confirm,
        [Switch]$Force = $True
    )

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

    # Set TLS Protocol for PowerShell Session
    TRY { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
    CATCH {}

    # Check if Module is installed
    $InstalledModules = Get-Module -ListAvailable -Name $Name -ErrorAction SilentlyContinue

    IF ($null -eq $InstalledModules) {
        # Check if user consent is required
        IF ($Confirm) {
            $UserInputInstall = Read-Host "Do you want to install module? [Y] Yes [N] No "
            IF ($UserInputInstall -match "[yY]") {
                $Install = $True
            }
            ELSE {
                $Install = $False
            }
        }

        # If user consent is granted, proceed to install
        IF ($Install -or !($Confirm)) {
            # Check if Package Provider and Source are available and registered
            TRY {
                IF ($null -eq (Get-PackageSource -Name nugetRepository -ErrorAction SilentlyContinue -Force)) {
                    Find-PackageProvider -Name NuGet -ErrorAction SilentlyContinue -Force | Install-PackageProvider -Force -Scope CurrentUser | Out-Null
                    Register-PackageSource -Provider NuGet -Name nugetRepository -Location https://www.nuget.org/api/v2 -Force | Out-Null
                }
            }
            CATCH {
                Write-Warning "Error validating NuGet package provider and source. This may cause the Module installation to fail."
            }

            # Install Module
            Write-Verbose "Installing $($Name) Module"
            IF($Force){ Install-Module $Name -Repository PSGallery -AllowClobber -Force }
            ELSE{ Install-Module $Name -Repository PSGallery -AllowClobber -Force }

            #Test if the install was successful
            $InstalledModules = Get-Module -ListAvailable -Name $Name -ErrorAction SilentlyContinue
            IF ($null -eq $InstalledModules) {
                Write-Error "Error installing $($Name) Module. Please install manually."
                Return $False
            }
        }
        ELSE {
            Write-Error "Please install $($Name) Module manually."
            Return $False
        }
    }
    
    # Check if MinimumVersion was defined
    IF ($MinimumVersion) {
        #Check if module matches the latest version. If it does not, remove the module from the session and update the module
        IF ($InstalledModules[0].Version -lt $MinimumVersion) {
            Write-Verbose "$($Name) Module not up to date." 
            
            # Check if user consent is required
            IF ($Confirm) {
                $UserInputUpdate = Read-Host "Do you want to update module? [Y] Yes [N] No "
                IF ($True) {
                    $Update = $True
                }
                ELSE {
                    $Update = $False
                }
            }

            # If user consent is granted, proceed to update
            IF ($Update -or !($Confirm)) {
                Write-Verbose "Removing $($Name) Module from current session."
                IF($Force){ Remove-Module -Name $Name -Force -ErrorAction SilentlyContinue }
                ELSE { Remove-Module -Name $Name -Force -ErrorAction SilentlyContinue }
                Write-Verbose "Updating Module: $($Name)"
                IF($Force){ Update-Module -Name $Name -Force -ErrorAction SilentlyContinue }
                ELSE { Update-Module -Name $Name -Force -ErrorAction SilentlyContinue }
            }
            ELSE {
                Write-Error "Please update $($Name) module manually."
                RETURN $False
            }
        }
    }
    
    #Import Module
    TRY {
        Write-Verbose "Importing Module: $($Name)"
        Import-Module -Name $Name -Force
        RETURN $True
    }
    CATCH {
        Write-Error "Error importing $($Name) Module."
        RETURN $False
    }
}
FUNCTION Test-NoUsersLoggedIn {

    [CmdletBinding()]
    PARAM ( )

    $Users = $null
    $Users = Get-Process -IncludeUserName | Where-Object {$_.ProcessName -eq 'explorer'} | Select-Object UserName -Unique

    TRY {
        IF ($null -like $Users) {
            RETURN $True
        }
        RETURN $False
    }
    CATCH { }
    RETURN $False
}


### Update Support Functions
FUNCTION Set-WindowsUpdateTargetVersion {
    [CmdletBinding()]
    PARAM (
        [Switch]$DoNotUpdateWin10toWin11 = $False
    )
    
    $OSName = (GET-CIMInstance -class Win32_OperatingSystem -Verbose:$false).Caption

    IF ($OSName -like "*Windows 10*" -and $DoNotUpdateWin10toWin11 -eq $True) {
        Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name BranchReadinessLevel -ErrorAction SilentlyContinue -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferQualityUpdates -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name TargetReleaseVersion -Value 1 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name TargetReleaseVersionInfo -Value "22H2" -Type String -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name ProductVersion -Value "Windows 10" -Type String -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DisableOSUpgrade -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferFeatureUpdates -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferQualityUpdates -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferFeatureUpdatesPeriodInDays -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferQualityUpdatesPeriodInDays -Value 0 -Type DWORD -Verbose:$False | Out-Null         
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name NoAutoUpdate -Value 1 -Type DWORD -Verbose:$False | Out-Null ### Prevents Automatic Updates
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name NoAutoRebootWithLoggedOnUsers -Value 1 -Type DWORD  -Verbose:$False | Out-Null ### Prevents No Auto Reboot When Logged In
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name AUOptions -Value 2 -Type DWORD -Verbose:$False | Out-Null ### No Automatic Download or Install
    }
    ELSE {
        Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name BranchReadinessLevel -ErrorAction SilentlyContinue -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferQualityUpdates -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name TargetReleaseVersion -Value 1 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name TargetReleaseVersionInfo -Value "24H2" -Type String -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name ProductVersion -Value "Windows 11" -Type String -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferFeatureUpdates -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferQualityUpdates -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferFeatureUpdatesPeriodInDays -Value 0 -Type DWORD -Verbose:$False | Out-Null
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name DeferQualityUpdatesPeriodInDays -Value 0 -Type DWORD -Verbose:$False | Out-Null        
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name NoAutoUpdate -Value 1 -Type DWORD -Verbose:$False | Out-Null ### Prevents Automatic Updates
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name NoAutoRebootWithLoggedOnUsers -Value 1 -Type DWORD -Verbose:$False | Out-Null ### Prevents No Auto Reboot When Logged In
        IPM-Set-RegistryKey -Hive HKLM -Path "SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name AUOptions -Value 2 -Type DWORD -Verbose:$False | Out-Null ### No Automatic Download or Install
    }
}
FUNCTION Run-WindowsUpdateServiceScan {
    USOClient StartInteractiveScan
}
FUNCTION Test-Windows11Compatibility {

    [CmdletBinding()]
    PARAM (
        [SWITCH]$GiveReason = $False
    )

$exitCode = 0

[int]$MinOSDiskSizeGB = 64
[int]$MinMemoryGB = 4
[Uint32]$MinClockSpeedMHz = 1000
[Uint32]$MinLogicalCores = 2
[Uint16]$RequiredAddressWidth = 64

$PASS_STRING = "PASS"
$FAIL_STRING = "FAIL"
$FAILED_TO_RUN_STRING = "FAILED TO RUN"
$UNDETERMINED_CAPS_STRING = "UNDETERMINED"
$UNDETERMINED_STRING = "Undetermined"
$CAPABLE_STRING = "Capable"
$NOT_CAPABLE_STRING = "Not capable"
$CAPABLE_CAPS_STRING = "CAPABLE"
$NOT_CAPABLE_CAPS_STRING = "NOT CAPABLE"
$STORAGE_STRING = "Storage"
$OS_DISK_SIZE_STRING = "OSDiskSize"
$MEMORY_STRING = "Memory"
$SYSTEM_MEMORY_STRING = "System_Memory"
$GB_UNIT_STRING = "GB"
$TPM_STRING = "TPM"
$TPM_VERSION_STRING = "TPMVersion"
$PROCESSOR_STRING = "Processor"
$SECUREBOOT_STRING = "SecureBoot"
$I7_7820HQ_CPU_STRING = "i7-7820hq CPU"

# 0=name of check, 1=attribute checked, 2=value, 3=PASS/FAIL/UNDETERMINED
$logFormat = '{0}: {1}={2}. {3}; '

# 0=name of check, 1=attribute checked, 2=value, 3=unit of the value, 4=PASS/FAIL/UNDETERMINED
$logFormatWithUnit = '{0}: {1}={2}{3}. {4}; '

# 0=name of check.
$logFormatReturnReason = '{0}, '

# 0=exception.
$logFormatException = '{0}; '

# 0=name of check, 1= attribute checked and its value, 2=PASS/FAIL/UNDETERMINED
$logFormatWithBlob = '{0}: {1}. {2}; '

# return returnCode is -1 when an exception is thrown. 1 if the value does not meet requirements. 0 if successful. -2 default, script didn't run.
$outObject = @{ returnCode = -2; returnResult = $FAILED_TO_RUN_STRING; returnReason = ""; logging = "" }

# NOT CAPABLE(1) state takes precedence over UNDETERMINED(-1) state
function Private:UpdateReturnCode {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateRange(-2, 1)]
        [int] $ReturnCode
    )

    Switch ($ReturnCode) {

        0 {
            if ($outObject.returnCode -eq -2) {
                $outObject.returnCode = $ReturnCode
            }
        }
        1 {
            $outObject.returnCode = $ReturnCode
        }
        -1 {
            if ($outObject.returnCode -ne 1) {
                $outObject.returnCode = $ReturnCode
            }
        }
    }
}

$Source = @"
using Microsoft.Win32;
using System;
using System.Runtime.InteropServices;
 
    public class CpuFamilyResult
    {
        public bool IsValid { get; set; }
        public string Message { get; set; }
    }
 
    public class CpuFamily
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct SYSTEM_INFO
        {
            public ushort ProcessorArchitecture;
            ushort Reserved;
            public uint PageSize;
            public IntPtr MinimumApplicationAddress;
            public IntPtr MaximumApplicationAddress;
            public IntPtr ActiveProcessorMask;
            public uint NumberOfProcessors;
            public uint ProcessorType;
            public uint AllocationGranularity;
            public ushort ProcessorLevel;
            public ushort ProcessorRevision;
        }
 
        [DllImport("kernel32.dll")]
        internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo);
 
        public enum ProcessorFeature : uint
        {
            ARM_SUPPORTED_INSTRUCTIONS = 34
        }
 
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature);
 
        private const ushort PROCESSOR_ARCHITECTURE_X86 = 0;
        private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12;
        private const ushort PROCESSOR_ARCHITECTURE_X64 = 9;
 
        private const string INTEL_MANUFACTURER = "GenuineIntel";
        private const string AMD_MANUFACTURER = "AuthenticAMD";
        private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc";
 
        public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture)
        {
            CpuFamilyResult cpuFamilyResult = new CpuFamilyResult();
 
            if (string.IsNullOrWhiteSpace(manufacturer))
            {
                cpuFamilyResult.IsValid = false;
                cpuFamilyResult.Message = "Manufacturer is null or empty";
                return cpuFamilyResult;
            }
 
            string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0";
            SYSTEM_INFO sysInfo = new SYSTEM_INFO();
            GetNativeSystemInfo(ref sysInfo);
 
            switch (processorArchitecture)
            {
                case PROCESSOR_ARCHITECTURE_ARM64:
 
                    if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
                    {
                        bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS);
 
                        if (!isArmv81Supported)
                        {
                            string registryName = "CP 4030";
                            long registryValue = (long)Registry.GetValue(registryPath, registryName, -1);
                            long atomicResult = (registryValue >> 20) & 0xF;
 
                            if (atomicResult >= 2)
                            {
                                isArmv81Supported = true;
                            }
                        }
 
                        cpuFamilyResult.IsValid = isArmv81Supported;
                        cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction";
                    }
                    else
                    {
                        cpuFamilyResult.IsValid = false;
                        cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11";
                    }
 
                    break;
 
                case PROCESSOR_ARCHITECTURE_X64:
                case PROCESSOR_ARCHITECTURE_X86:
 
                    int cpuFamily = sysInfo.ProcessorLevel;
                    int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF;
                    int cpuStepping = sysInfo.ProcessorRevision & 0xFF;
 
                    if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
                    {
                        try
                        {
                            cpuFamilyResult.IsValid = true;
                            cpuFamilyResult.Message = "";
 
                            if (cpuFamily >= 6 && cpuModel <= 95 && !(cpuFamily == 6 && cpuModel == 85))
                            {
                                cpuFamilyResult.IsValid = false;
                                cpuFamilyResult.Message = "";
                            }
                            else if (cpuFamily == 6 && (cpuModel == 142 || cpuModel == 158) && cpuStepping == 9)
                            {
                                string registryName = "Platform Specific Field 1";
                                int registryValue = (int)Registry.GetValue(registryPath, registryName, -1);
 
                                if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8))
                                {
                                    cpuFamilyResult.IsValid = false;
                                }
                                cpuFamilyResult.Message = "PlatformId " + registryValue;
                            }
                        }
                        catch (Exception ex)
                        {
                            cpuFamilyResult.IsValid = false;
                            cpuFamilyResult.Message = "Exception:" + ex.GetType().Name;
                        }
                    }
                    else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
                    {
                        cpuFamilyResult.IsValid = true;
                        cpuFamilyResult.Message = "";
 
                        if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17)))
                        {
                            cpuFamilyResult.IsValid = false;
                        }
                    }
                    else
                    {
                        cpuFamilyResult.IsValid = false;
                        cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision;
                    }
 
                    break;
 
                default:
                    cpuFamilyResult.IsValid = false;
                    cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision;
                    break;
            }
            return cpuFamilyResult;
        }
    }
"@


# Storage
try {
    $osDrive = Get-WmiObject -Class Win32_OperatingSystem | Select-Object -Property SystemDrive
    $osDriveSize = Get-WmiObject -Class Win32_LogicalDisk -filter "DeviceID='$($osDrive.SystemDrive)'" | Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } }  

    if ($null -eq $osDriveSize) {
        UpdateReturnCode -ReturnCode 1
        $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING
        $outObject.logging += $logFormatWithBlob -f $STORAGE_STRING, "Storage is null", $FAIL_STRING
        $exitCode = 1
    }
    elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) {
        UpdateReturnCode -ReturnCode 1
        $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING
        $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $FAIL_STRING
        $exitCode = 1
    }
    else {
        $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $PASS_STRING
        UpdateReturnCode -ReturnCode 0
    }
}
catch {
    UpdateReturnCode -ReturnCode -1
    $outObject.logging += $logFormat -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
    $exitCode = 1
}

# Memory (bytes)
try {
    $memory = Get-WmiObject Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } }

    if ($null -eq $memory) {
        UpdateReturnCode -ReturnCode 1
        $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING
        $outObject.logging += $logFormatWithBlob -f $MEMORY_STRING, "Memory is null", $FAIL_STRING
        $exitCode = 1
    }
    elseif ($memory.SizeGB -lt $MinMemoryGB) {
        UpdateReturnCode -ReturnCode 1
        $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING
        $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $FAIL_STRING
        $exitCode = 1
    }
    else {
        $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $PASS_STRING
        UpdateReturnCode -ReturnCode 0
    }
}
catch {
    UpdateReturnCode -ReturnCode -1
    $outObject.logging += $logFormat -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
    $exitCode = 1
}

# TPM
try {
    $tpm = Get-Tpm

    if ($null -eq $tpm) {
        UpdateReturnCode -ReturnCode 1
        $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
        $outObject.logging += $logFormatWithBlob -f $TPM_STRING, "TPM is null", $FAIL_STRING
        $exitCode = 1
    }
    elseif ($tpm.TpmPresent) {
        $tpmVersion = Get-WmiObject -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion

        if ($null -eq $tpmVersion.SpecVersion) {
            UpdateReturnCode -ReturnCode 1
            $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, "null", $FAIL_STRING
            $exitCode = 1
        }

        $majorVersion = $tpmVersion.SpecVersion.Split(",")[0] -as [int]
        if ($majorVersion -lt 2) {
            UpdateReturnCode -ReturnCode 1
            $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $FAIL_STRING
            $exitCode = 1
        }
        else {
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $PASS_STRING
            UpdateReturnCode -ReturnCode 0
        }
    }
    else {
        if ($tpm.GetType().Name -eq "String") {
            UpdateReturnCode -ReturnCode -1
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
            $outObject.logging += $logFormatException -f $tpm
        }
        else {
            UpdateReturnCode -ReturnCode  1
            $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpm.TpmPresent), $FAIL_STRING
        }
        $exitCode = 1
    }
}
catch {
    UpdateReturnCode -ReturnCode -1
    $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
    $exitCode = 1
}

# CPU Details
#$cpuDetails;
try {
    $cpuDetails = @(Get-WmiObject -Class Win32_Processor)[0]

    if ($null -eq $cpuDetails) {
        UpdateReturnCode -ReturnCode 1
        $exitCode = 1
        $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING
        $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, "CpuDetails is null", $FAIL_STRING
    }
    else {
        $processorCheckFailed = $false

        # AddressWidth
        if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) {
            UpdateReturnCode -ReturnCode 1
            $processorCheckFailed = $true
            $exitCode = 1
        }

        # ClockSpeed is in MHz
        if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -le $MinClockSpeedMHz) {
            UpdateReturnCode -ReturnCode 1;
            $processorCheckFailed = $true
            $exitCode = 1
        }

        # Number of Logical Cores
        if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) {
            UpdateReturnCode -ReturnCode 1
            $processorCheckFailed = $true
            $exitCode = 1
        }

        # CPU Family
        Add-Type -TypeDefinition $Source
        $cpuFamilyResult = [CpuFamily]::Validate([String]$cpuDetails.Manufacturer, [uint16]$cpuDetails.Architecture)

        $cpuDetailsLog = "{AddressWidth=$($cpuDetails.AddressWidth); MaxClockSpeed=$($cpuDetails.MaxClockSpeed); NumberOfLogicalCores=$($cpuDetails.NumberOfLogicalProcessors); Manufacturer=$($cpuDetails.Manufacturer); Caption=$($cpuDetails.Caption); $($cpuFamilyResult.Message)}"

        if (!$cpuFamilyResult.IsValid) {
            UpdateReturnCode -ReturnCode 1
            $processorCheckFailed = $true
            $exitCode = 1
        }

        if ($processorCheckFailed) {
            $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING
            $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $FAIL_STRING
        }
        else {
            $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $PASS_STRING
            UpdateReturnCode -ReturnCode 0
        }
    }
}
catch {
    UpdateReturnCode -ReturnCode -1
    $outObject.logging += $logFormat -f $PROCESSOR_STRING, $PROCESSOR_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
    $exitCode = 1
}

# SecureBooot
try {
    $isSecureBootEnabled = Confirm-SecureBootUEFI
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $CAPABLE_STRING, $PASS_STRING
    UpdateReturnCode -ReturnCode 0
}
catch [System.PlatformNotSupportedException] {
    # PlatformNotSupportedException "Cmdlet not supported on this platform." - SecureBoot is not supported or is non-UEFI computer.
    UpdateReturnCode -ReturnCode 1
    $outObject.returnReason += $logFormatReturnReason -f $SECUREBOOT_STRING
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $NOT_CAPABLE_STRING, $FAIL_STRING
    $exitCode = 1
}
catch [System.UnauthorizedAccessException] {
    UpdateReturnCode -ReturnCode -1
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
    $exitCode = 1
}
catch {
    UpdateReturnCode -ReturnCode -1
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
    $exitCode = 1
}

# i7-7820hq CPU
try {
    $supportedDevices = @('surface studio 2', 'precision 5520')
    $systemInfo = @(Get-WmiObject -Class Win32_ComputerSystem)[0]

    if ($null -ne $cpuDetails) {
        if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz'){
            $modelOrSKUCheckLog = $systemInfo.Model.Trim()
            if ($supportedDevices -contains $modelOrSKUCheckLog){
                $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $modelOrSKUCheckLog, $PASS_STRING
                $outObject.returnCode = 0
                $exitCode = 0
            }
        }
    }
}
catch {
    if ($outObject.returnCode -ne 0){
        UpdateReturnCode -ReturnCode -1
        $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
        $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
        $exitCode = 1
    }
}

Switch ($outObject.returnCode) {

    0 { $outObject.returnResult = $CAPABLE_CAPS_STRING }
    1 { $outObject.returnResult = $NOT_CAPABLE_CAPS_STRING }
    -1 { $outObject.returnResult = $UNDETERMINED_CAPS_STRING }
    -2 { $outObject.returnResult = $FAILED_TO_RUN_STRING }
}
    IF($GiveReason) { Return $outObject }
    ELSE {
        IF ($outObject.returnresult -eq "CAPABLE") { RETURN $True }
        ELSE { RETURN $False }
    }

    Write-Error "Unknown Error"
}


### Shutdown with Updates App
FUNCTION Download-ShutdownWithUpdates  {

    [CmdletBinding()]
    PARAM (

    )

    IF (Get-ChildItem -Path "C:\Integris\Temp\ShutdownWithUpdates.exe" -ErrorAction SilentlyContinue) { Write-Host "$($(Get-Date).ToShortTimeString()): ShutdownWithUpdates.exe app downloaded."; RETURN }
    #Write-Host "$($(Get-Date).ToShortTimeString()): Downloading"
    $SavePath = "C:\Integris\Temp"
    New-Item -ItemType Directory -Force -Path $SavePath | Out-Null
    $DownloadLink = "https://integristech-my.sharepoint.com/:u:/g/personal/david_mcvicker_integrisit_com/EeUprOD4AVVGt2K0AMLlfnQBmqTzNkpsfliz2hP6EzfZjQ?Download=1"
    $DownloadName = "ShutdownWithUpdates.Zip"
    Invoke-WebRequest $DownloadLink -OutFile ("$SavePath\$DownloadName") -UseBasicParsing 
    Expand-Archive -Path ("$SavePath\$DownloadName") -DestinationPath $SavePath -Force
    Write-Host "$($(Get-Date).ToShortTimeString()): ShutdownWithUpdates.exe app downloaded."
    RETURN
}
FUNCTION Run-ShutdownWithUpdates  {

    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory)]
        [ValidateSet("ForceReboot","NoReboot","IfNoUserLoggedIn")]
        [String]$RebootSetting
    )

    IF ($RebootSetting -eq "NoReboot") {
        Write-Host "$($(Get-Date).ToShortTimeString()): Ready to reboot and install. Reboot option set to no reboot. Done."; RETURN;
    }
    IF ($RebootSetting -eq "ForceReboot") {
        Write-Host "$($(Get-Date).ToShortTimeString()): Ready to reboot and install. Reboot option set to force reboot. Forcing reboot in 5 seconds."
        CMD.exe /C "C:\Integris\Temp\ShutdownWithUpdates.exe /r /t 5 /irr /f"
        Start-Sleep -Seconds 120
        Write-Host "$($(Get-Date).ToShortTimeString()): Reboot failed. Update failed to finalize? Trying again."
        Run-InstallFeatureUpdate -RebootSetting ForceReboot
        RETURN
    }
    IF ($RebootSetting -eq "IfNoUserLoggedIn") {
        Write-Host "$($(Get-Date).ToShortTimeString()): Ready to reboot and install. Reboot option set to IfNoUserLoggedIn. Reboot now if no user logged in."
        CMD.exe /C "C:\Integris\Temp\ShutdownWithUpdates.exe /r /t 5 /irr"
        Start-Sleep -Seconds 120
        Write-Host "$($(Get-Date).ToShortTimeString()): Reboot failed. User is currently logged in? Trying again."
        Run-InstallFeatureUpdate -RebootSetting IfNoUserLoggedIn
        RETURN
    }
}


### Update Checks
FUNCTION Get-WindowsUpdateInfo {

    <#
    .SYNOPSIS
    Retrieves information about Windows updates that have not yet been installed.
 
    .DESCRIPTION
    This function collects and returns information about Windows updates that have not yet been installed, including their type, status, and release date.
 
    .PARAMETER IncludeDriver
    Includes information about Windows Updates marked as driver updates. Excluded by default.
 
    .PARAMETER IncludeOptional
    Includes information about Windows Updates marked as optional updates. Excluded by default.
 
    .PARAMETER All
    Includes information about all updates. By default only software and securty updates are shown.
 
    .EXAMPLE
    Get-WindowsUpdateInfo
    Retrieves and displays information about all pending Windows updates.
 
    .NOTES
    The function requires the PSWindowsUpdate module to be installed.
    #>


    [CmdletBinding(DefaultParameterSetName='Default')]
    PARAM ( 
        [Parameter(ParameterSetName = 'Default')]
        [SWITCH]$IncludeDriver = $False,
        [Parameter(ParameterSetName = 'Default')]
        [SWITCH]$IncludeOptional = $False,
        [Parameter(ParameterSetName = 'All')]
        [SWITCH]$All = $False
    )

    IF (!(Test-AdministratorElevation)) { Write-Warning "This command must be run with administrator elevation. Please elevate and try again."; RETURN }
    IF ((Test-IntegrisModuleDependency -Name PSWindowsUpdate) -ne $True) { 
        Write-Warning "Required module PSWindowsUpdate is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/PSWindowsUpdate"
    }

    $Results = @()
    $Updates = Get-WindowsUpdate

    FOREACH ($Update in ($Updates | Sort LastDeploymentChangeTime -Descending)) {
        
        $ReleaseDate = $null
        $Type = ""

        IF ($Update.Title -like "*Microsoft Defender Antivirus*") { $Type = "Antivirus" }
        ELSEIF ($Update.Title -like "*Security Update*") { $Type = "Security" }
        ELSEIF ($Update.DriverClass -ne "" -and $Update.DriverClass -ne $null) { $Type = "Driver" }
        ELSEIF ($Update.Type -eq 2) { $Type = "Optional" }
        ELSE { $Type = "Software" }

        ### Skip Types if Not Included
        IF ($Type -eq "Driver" -and $IncludeDriver -eq $False -and $All -eq $False) { continue }        
        IF ($Type -eq "Optional" -and $IncludeOptional -eq $False -and $All -eq $False) { continue }

        TRY { $ReleaseDate = $Update.LastDeploymentChangeTime.ToString("MM/dd/yyy") } CATCH { $ReleaseDate = "" }
            
        $Results += New-Object PSObject -WarningAction SilentlyContinue -Property @{
            PSTypeName = 'IntegrisWindowsUpdate.WindowsUpdateInfo'
            KB = $Update.KB
            Downloaded = $Update.IsDownloaded
            Installed = $Update.IsInstalled
            RebootRequired = $Update.RebootRequired
            Title = $Update.Title
            Type = $Type            
            ReleaseDate = $ReleaseDate
            Size = $Update.Size            
        }
    }
    
    RETURN $Results
}
FUNCTION Get-WindowsUpdatesPendingDownload {
 
    [CmdletBinding()]
    PARAM (

    )

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
    IF (!(Ensure-Module PSWindowsUpdate -Verbose:$False)) { Write-Warning "Required module PSWindowsUpdate is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/PSWindowsUpdate" }
    IF (!(Test-AdministratorElevation)) { RETURN }

    $Results = @()
    $Updates = Get-WindowsUpdate -ErrorAction SilentlyContinue -Verbose:$false   

    FOREACH ($Update in ($Updates | Sort LastDeploymentChangeTime -Descending)) {
        
        $ReleaseDate = $null
        $Type = ""

        IF ($Update.Title -like "*Microsoft Defender Antivirus*") { $Type = "Antivirus" }
        ELSEIF ($Update.Title -like "*Security Update*") { $Type = "Security" }
        ELSEIF ($Update.DriverClass -ne "" -and $Update.DriverClass -ne $null) { $Type = "Driver" }
        ELSEIF ($Update.Type -eq 2) { $Type = "Optional" }
        ELSE { $Type = "Software" }

        TRY { $ReleaseDate = $Update.LastDeploymentChangeTime.ToString("MM/dd/yyy") } CATCH { $ReleaseDate = "" }
            
        $Results += New-Object PSObject -WarningAction SilentlyContinue -Property @{
            KB = $Update.KB
            Status = $Update.Status
            Title = $Update.Title
            Type = $Type            
            ReleaseDate = $ReleaseDate
            Size = $Update.Size            
        }
    }
    
    RETURN $Results | Select KB, Status, Type, ReleaseDate, Size, Title
}
FUNCTION Test-WindowsUpdatesPendingDownload {
 
    [CmdletBinding()]
    PARAM (

    )

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
    IF (!(Ensure-Module PSWindowsUpdate -Verbose:$false)) { Write-Warning "Required module PSWindowsUpdate is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/PSWindowsUpdate" }
    IF (!(Test-AdministratorElevation)) { RETURN }

    $Results = @()
    $Updates = Get-WindowsUpdate -ErrorAction SilentlyContinue -Verbose:$false  

    FOREACH ($Update in ($Updates | Sort LastDeploymentChangeTime -Descending)) {
        
        $ReleaseDate = $null
        $Type = ""

        IF ($Update.Title -like "*Microsoft Defender Antivirus*") { $Type = "Antivirus" }
        ELSEIF ($Update.Title -like "*Security Update*") { $Type = "Security" }
        ELSEIF ($Update.DriverClass -ne "" -and $Update.DriverClass -ne $null) { $Type = "Driver" }
        ELSEIF ($Update.Type -eq 2) { $Type = "Optional" }
        ELSE { $Type = "Software" }

        TRY { $ReleaseDate = $Update.LastDeploymentChangeTime.ToString("MM/dd/yyy") } CATCH { $ReleaseDate = "" }
            
        $Results += New-Object PSObject -WarningAction SilentlyContinue -Property @{
            KB = $Update.KB
            Status = $Update.Status
            Title = $Update.Title
            Type = $Type            
            ReleaseDate = $ReleaseDate
            Size = $Update.Size            
        }
    }
    
    IF (($Results | Where-Object { $_.Type -ne "Driver" }).Count -eq 0) { RETURN $False }
    ELSE { RETURN $True }
}
FUNCTION Test-WindowsUpdatePendingReboot {
    
    [CmdletBinding()]
    PARAM (

    )

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
    IF (!(Ensure-Module PSWindowsUpdate -Verbose:$False)) { Write-Warning "Required module PSWindowsUpdate is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/PSWindowsUpdate" }
    IF (!(Test-AdministratorElevation)) { RETURN }

    $Result = $False
    $UpdateSession = New-Object -ComObject Microsoft.Update.Session
    $UpdateSearcher = $UpdateSession.CreateupdateSearcher()
    $Updates = @($UpdateSearcher.Search("IsHidden=0 and IsInstalled=0").Updates)
    FOREACH ($Update in $Updates) {
        IF ($Update.RebootRequired -eq "True") { $Result = $True }
        IF ($Update.Title.ToString().ToUpper() -like "*FEATURE UPDATE*") {
            IF ($Update.RebootRequired -eq "True") { 
                $Result = $True
                break
            }
        }
    }

    RETURN $Result
}
FUNCTION Test-FeatureUpdatePendingDownload {

    [CmdletBinding()]
    PARAM (

    )
    
    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force    
    IF (!(Ensure-Module PSWindowsUpdate -Verbose:$false)) { Write-Warning "Required module PSWindowsUpdate is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/PSWindowsUpdate" }
    IF (!(Test-AdministratorElevation)) { RETURN }

    $Result = $False
    $UpdateSession = New-Object -ComObject Microsoft.Update.Session
    $UpdateSearcher = $UpdateSession.CreateupdateSearcher()
    $Updates = @($UpdateSearcher.Search("IsHidden=0 and IsInstalled=0").Updates)
    FOREACH ($Update in $Updates) {       
        IF ($Update.Title -like "*FEATURE UPDATE*" -or 
            $Update.Title -eq "Windows 11" -or 
            ($Update.Title -like "*version*" -and 
            $Update.Title -match "\d\dH\d" -and 
            $Update.Title -notlike "*Cumulative*")) {
            IF ($Update.RebootRequired -ne "True") { 
                $Result = $True
                break
            }
        }
    }

    RETURN $Result
}
FUNCTION Test-FeatureUpdatePendingReboot {
    
    [CmdletBinding()]
    PARAM (

    )

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
    IF (!(Ensure-Module PSWindowsUpdate -Verbose:$false)) { Write-Warning "Required module PSWindowsUpdate is missing and could not be installed. Please install module an try again: https://www.powershellgallery.com/packages/PSWindowsUpdate" }
    IF (!(Test-AdministratorElevation)) { RETURN }

    $Result = $False
    $UpdateSession = New-Object -ComObject Microsoft.Update.Session
    $UpdateSearcher = $UpdateSession.CreateupdateSearcher()
    $Updates = @($UpdateSearcher.Search("IsHidden=0 and IsInstalled=0").Updates)
    FOREACH ($Update in $Updates) {       
        IF ($Update.Title -like "*FEATURE UPDATE*" -or 
            $Update.Title -eq "Windows 11" -or 
            ($Update.Title -like "*version*" -and 
            $Update.Title -match "\d\dH\d" -and 
            $Update.Title -notlike "*Cumulative*")) {
            IF ($Update.RebootRequired -eq "True") { 
                $Result = $True
                break
            }
        }
    }

    RETURN $Result
}


### Asynchronous Update Processors
FUNCTION Run-WindowsUpdateProcessStage1 {
    Start-Process $PSHOME\powershell.exe -ArgumentList "-windowstyle hidden","-command `"Start-Transcript -OutputDirectory C:\Windows\LTSvc\packages\PowerShellLogs\; Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Verbose:$false; Install-Module -Name PSWindowsUpdate -Force -Verbose:$false; Import-Module PSWindowsUpdate -Verbose:$false; `$Count = 0; WHILE (`$Count -lt 3) { Get-WUInstall -Download -Install -Verbose:$false -AcceptAll -IgnoreReboot; USOClient StartInteractiveScan; Start-Sleep -Seconds 150; `$Count++ } Stop-Transcript; USOClient StartInteractiveScan; exit`""
}
FUNCTION Run-WindowsUpdateProcessStage2 {
    USOClient StartInteractiveScan

    Start-Sleep -Seconds 300

    $Output = "NoUpdatePending"
    $UpdateSession = New-Object -ComObject Microsoft.Update.Session
    $UpdateSearcher = $UpdateSession.CreateupdateSearcher()
    $Updates = @($UpdateSearcher.Search("IsHidden=0 and IsInstalled=0").Updates)
    FOREACH ($Update in $Updates) {
        IF ($Update.RebootRequired -eq "True") { $Output = "WindowsUpdatePending" }
        IF ($Update.Title.ToString().ToUpper() -like "*FEATURE UPDATE*") {
            IF ($Update.RebootRequired -eq "True") { 
                $Output = "FeatureUpdatePending"
                break
            }
        }
    }

    Start-Sleep -Seconds 10

    USOClient StartInstall
}
FUNCTION Run-WindowsUpdateProcessStage3 { 
    USOClient StartInteractiveScan

    Start-Sleep -Seconds 30

    USOClient StartScan

    Start-Sleep -Seconds 30

    USOClient StartWork

    Start-Sleep -Seconds 30

    USOClient StartInstall

    Start-Sleep -Seconds 30

    USOClient StartInteractiveScan

    Start-Sleep -Seconds 30
}


### Priamry Functions
FUNCTION Run-InstallWindowsUpdates {

    <#
    .SYNOPSIS
    Installs Windows updates.
 
    .DESCRIPTION
    This function installs windows updates and optionally reboots the computer.
 
    .PARAMETER RetryCount
    This specifies additional install attempts if updates remain uninstalled after the first attempt. Default 2.
 
    .PARAMETER ForceReboot
    This specifies a forced reboot if needed when updates are finished installing.
 
    .PARAMETER RebootTime
    This specifies a future time to reboot the computer once updates are finished installing.
 
    .EXAMPLE
    Install-WindowsUpdates -ForceReboot -RebootTime "5/5/25 20:00"
    Installs updates and forces a reboot at 8PM on 5/5/25.
 
    .NOTES
    The function will install driver updates if available, but doesn't care if they don't install successfully.
    #>


    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory)]
        [ValidateSet("ForceReboot","NoReboot","IfNoUserLoggedIn")]
        [STRING]$RebootSetting,
        [INT]$RetryCount = 2,
        [DATETIME]$RebootTime = "1/1/1"
    )

    ### ==================
    ### === Log Header ===
    ### ==================
    Write-Host ""
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Starting Integris Windows Update Tool Session ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Install Windows Updates ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ©2025 Integris - https://IntegrisIT.com ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Exceptional IT, Elevated Outcomes ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host ""   
    
    ### Reboot Scheduler
    IF ($RebootTime -le (Get-Date) -and $RebootTime -ne "1/1/1") { Write-Host ""; Write-Warning "Specified time has already passed. Please specify a time in the future or no time for immediate reboot when done."; RETURN }
    ELSEIF ($RebootTime -ne "1/1/1") { Write-Host "Reboot scheduled for $RebootTime. (Approximately $([Math]::Round(($RebootTime - (Get-Date)).TotalHours,0)) Hour(s) from Now)" }
    

    ### Initialize Variables
    $TryCount = 0    
    $UpdatesRemaining = 0
    $RebootPendingCount = 0
    $Fail = $False


    ### Check if Windows Update Service is Running
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows update service is running... " -NoNewline
    While ((Test-WindowsUpdateServiceIsRunning) -eq $False) { Start-Sleep -Seconds 15 }


    ### Check Updates
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking for Windows updates... " -NoNewline  
    $Searcher = New-Object -ComObject Microsoft.Update.Searcher
    $SearchResult = $Searcher.Search("IsInstalled=0 and Type!='Driver'").Updates


    ### Count Reboots Pending
    FOREACH ($Result in $SearchResult) { 
        IF ($Result.RebootRequired -eq $True) { $RebootPendingCount++ } 
        IF ($Result.Type -eq 1) { $UpdatesRemaining++ } 
    }
    
    
    ### All Updates Installed, No Reboot Required
    IF (0 -eq $SearchResult.Count -or $null -eq $SearchResult -or "" -eq $SearchResult) { 
        Write-Host "No updates found. Done." 
        RETURN
    }
    ### All Updates Installed, Reboot Required
    ELSEIF ($SearchResult.Count -eq $RebootPendingCount) {
        Write-Host "All updates installed. Reboot required... " -NoNewline


        IF ($RebootSetting -eq "NoReboot") { Write-Host "NoReboot requested. Done:"
            $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT
            RETURN 
        }
        ELSEIF ($RebootTime -ne "1/1/1") { 
            IF ($RebootSetting -eq "ForceReboot") {
                [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                Write-Host "ForceReboot requested. Scheduled Reboot for $RebootTime`:"
                $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT
                Shutdown.exe /t $Delay /r 
                RETURN                
            }
            IF ($RebootSetting -eq "IfNoUserLoggedIn") {
                IF (Test-NoUsersLoggedIn) {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. No user logged in. Scheduled Reboot for $RebootTime`:"
                    $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT
                    Shutdown.exe /t $Delay /r 
                    RETURN
                }
                ELSE {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. User logged in. Done:"
                    $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT
                    RETURN
                }
            }
        }
        ELSE {
            IF ($RebootSetting -eq "ForceReboot") {
                [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                Write-Host "ForceReboot requested. Rebooting now..."
                Shutdown.exe /t 5 /r
                RETURN                
            }
            IF ($RebootSetting -eq "IfNoUserLoggedIn") {
                IF (Test-NoUsersLoggedIn) {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. No user logged in. Rebooting now..."
                    Shutdown.exe /t 5 /r 
                    RETURN
                }
                ELSE {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. User logged in. Done:"
                    $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT
                    RETURN
                }
            }
        }
        RETURN
    }
    ### All Pending Updates Left are Optional
    ELSEIF ($UpdatesRemaining -eq 0) { 
        Write-Host "Only optional updates found (Type 2). Done:"
        $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT    
        RETURN         
    }
    ### Else Update Pending Install
    ELSE {
        Write-Host "$($SearchResult.Count) updates found:"
        $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT   
    }

        
    ### Processing Updates
    WHILE ($UpdatesRemaining -gt 0 -and $TryCount -le $RetryCount) {
        
        ### Updating Try Count
        IF ($TryCount -gt 0) { 
            Write-Host "Some updates failed to install. Trying again:" 
            $SearchResult | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT

            ### Check if Windows Update Service is Running
            Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows Update Service is Running... " -NoNewline
            While ((Test-WindowsUpdateServiceIsRunning) -eq $False) { Start-Sleep -Seconds 15 }
        }
        $TryCount++

        
        ### Download Updates
        Write-Host "$($(Get-Date).ToShortTimeString()): Downloading updates... " -NoNewline
        $Session = New-Object -ComObject Microsoft.Update.Session
        $Downloader = $Session.CreateUpdateDownloader()
        $Downloader.Updates = $SearchResult
        $Downloader.Download() | Out-Null
        Write-Host "Finished downloading updates."

              
        ### Install Updates
        Write-Host "$($(Get-Date).ToShortTimeString()): Installing updates... " -NoNewline
        $Installer = New-Object -ComObject Microsoft.Update.Installer
        $Installer.Updates = $SearchResult
        $InstallerResults = $Installer.Install() | Out-Null
        Write-Host "Finished installing updates."
        

        ### Getting Results
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking results... " -NoNewline
        $Searcher = New-Object -ComObject Microsoft.Update.Searcher
        $SearchResult = $Searcher.Search("IsInstalled=0 and RebootRequired=0 and Type!='Driver'").Updates


        ### Checking Results
        $UpdatesRemaining = 0
        FOREACH ($Result in $SearchResult) { 
            IF ($Result.Type -eq 1) { $UpdatesRemaining++ } 
        }   
        IF ($UpdatesRemaining -eq 0) { continue }
    }

    ### Get Final Results
    $Searcher = New-Object -ComObject Microsoft.Update.Searcher
    $SearchResult2 = $Searcher.Search("IsInstalled=0 and Type!='Driver'").Updates
    

    ### Check Results
    FOREACH ($Result in $SearchResult) { 
        IF ($Result.Type -eq 1 -and $Result.IsInstalled -eq $False) { 
            Write-Host "Retry count exceeded. Some updates failed to install:" 
            
            $Fail = $True
            BREAK
        } 
    }
    IF ($Fail -eq $False) { Write-Host "Updates installed successfully:" }


    ### Display Final Results
    $SearchResult2 | Select IsDownloaded, IsInstalled, RebootRequired, Type, Title | FT


    ### Reboot if Required by Updates
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if reboot is needed... " -NoNewline 
    IF (($SearchResult2 | select RebootRequired).RebootRequired -contains $true)
    { 
        Write-Host "All updates installed. Reboot required... " -NoNewline
        IF ($RebootSetting -eq "NoReboot") { Write-Host "NoReboot requested. Done."; RETURN }
        ELSEIF ($RebootTime -ne "1/1/1") { 
            IF ($RebootSetting -eq "ForceReboot") {
                [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                Write-Host "ForceReboot requested. Scheduled Reboot for $RebootTime."
                Shutdown.exe /t $Delay /r 
                RETURN                
            }
            IF ($RebootSetting -eq "IfNoUserLoggedIn") {
                IF (Test-NoUsersLoggedIn) {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. No user logged in. Scheduled Reboot for $RebootTime."
                    Shutdown.exe /t $Delay /r 
                    RETURN
                }
                ELSE {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. User logged in. Done."
                    RETURN
                }
            }
        }
        ELSE {
            IF ($RebootSetting -eq "ForceReboot") {
                [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                Write-Host "ForceReboot requested. Rebooting now..."
                Shutdown.exe /t 5 /r
                RETURN                
            }
            IF ($RebootSetting -eq "IfNoUserLoggedIn") {
                IF (Test-NoUsersLoggedIn) {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. No user logged in. Rebooting now..."
                    Shutdown.exe /t 5 /r 
                    RETURN
                }
                ELSE {
                    [int]$Delay = [MATH]::Round(($RebootTime - (Get-Date)).TotalSeconds,0) 
                    Write-Host "IfNoUserLoggedIn requested. User logged in. Done."
                    RETURN
                }
            }
        }
        RETURN
    }
    ELSE {
        Write-Host "All updates installed. No reboot required. Done."
        RETURN
    }
    
    RETURN
}
FUNCTION Run-InstallFeatureUpdate { 

    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory)]
        [ValidateSet("ForceReboot","NoReboot","IfNoUserLoggedIn")]
        [STRING]$RebootSetting,
        [SWITCH]$CheckOnly,
        [SWITCH]$DoNotUpdateWin10toWin11,
        [SWITCH]$OverrideIncompatibility,
        [DateTime]$ScheduledStart,
        [SWITCH]$IgnoreStorageRequirement
    )      
    
    ### ==================
    ### === Log Header ===
    ### ==================
    Write-Host ""
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Starting Integris Windows Update Tool Session ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Install Feature Update ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ©2025 Integris - https://IntegrisIT.com ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Exceptional IT, Elevated Outcomes ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host ""   
    
    
    IF ($DoNotUpdateWin10toWin11 -eq $False) {
        $UpgradeCheck = Test-Windows11Compatibility
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking compatibility with Windows 11."
        IF ($UpgradeCheck -eq $False -and $OverrideIncompatibility -eq $false) { Write-Warning "$($(Get-Date).ToShortTimeString()): Unfortunately this computer is not compatabile with Windows 11 and cannot be upgraded."; RETURN }
        Write-Host "$($(Get-Date).ToShortTimeString()): Confirmed device is compatible with Windows 11."
        Write-Host "$($(Get-Date).ToShortTimeString()): Attempting to Update this computer to Windows 11 using the Windows Update Service..."
    } ELSE {
        Write-Host "$($(Get-Date).ToShortTimeString()): Attempting to Update this computer to Windows 10 using the Windows Update Service..."
    }


    IF (($ScheduledStart - (GET-Date)).TotalMinutes -lt 0 -and $ScheduledStart -ne $null -and $ScheduledStart -ne "") { Write-Host ""; Write-Warning "Scheduled update cannot occur in the past. Update process aborted."; RETURN }

    IF ($ScheduledStart -ne $null -and $ScheduledStart -ne "") {
        Write-Host "$($(Get-Date).ToShortTimeString()): Scheduled update for $ScheduledStart (Approximately $([MATH]::ROUND((($ScheduledStart - (GET-Date)).TotalHours),0)) Hours From Now)"
        While ((Get-Date) -lt $ScheduledStart) {
            Start-Sleep -Seconds 15
        }
    }
    
    ### Check if Windows 24H2 Already Installed
    IF ((GET-WindowsInfo).BuildNumber -eq 26100) { Write-Host "$($(Get-Date).ToShortTimeString()): Win 11 24H2 Already Installed. Done."; RETURN }
    IF ((GET-WindowsInfo).BuildNumber -eq 19045 -and ($DoNotUpdateWin10toWin11)) {
        Write-Host "$($(Get-Date).ToShortTimeString()): Win 10 22H2 Already Installed. Done."; RETURN
    }

    IF (((Get-Volume C).SizeRemaining / 1GB) -lt 64) { Write-Warning "OS volume has $([Math]::Round(((Get-Volume ($env:windir)[0]).SizeRemaining / 1GB),0)) GBs of free space. Microsoft says you need at least 64 GBs free and recommends at least 100 GBs free." 
        IF ($IgnoreStorageRequirement -eq $False) { 
             Write-Warning "Use the -IgnoreStorageRequirement switch to attempt an update on this computer anyway."
            RETURN 
        }
       
    } 
    ELSEIF (((Get-Volume C).SizeRemaining / 1GB) -lt 64) { Write-Warning "OS volume has [Math]::Round(((Get-Volume ($env:windir)[0]).SizeRemaining / 1GB),0) GBs of free space. Microsoft says you need at least 64 GBs free and recommends at least 100 GBs free." }


    ### Get Ready
    Download-ShutdownWithUpdates
    Start-Sleep -Seconds 3

    ### Check if Windows Update Service is Running
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows Update Service is Running... " -NoNewline
    While ((Test-WindowsUpdateServiceIsRunning) -eq $False) { Start-Sleep -Seconds 15 }

    ### Check if Feature Update is Installed and Needs Reboot
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Feature Update is installed and ready for reboot."
    IF (Test-FeatureUpdatePendingReboot) { 
        Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is installed and ready for reboot."
        Run-ShutdownWithUpdates -RebootSetting $RebootSetting -Verbose
        RETURN
    }
    Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is not installed and ready for reboot."

    ### Check if Feature Update is Available for Download
    Set-WindowsUpdateTargetVersion -DoNotUpdateWin10toWin11:$DoNotUpdateWin10toWin11
    Start-Sleep -Seconds 5
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Feature Update is available for download."
        
    WHILE (!(Test-FeatureUpdatePendingDownload)) {
        Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update not available for download. Checking again in 60 seconds..."
        Set-WindowsUpdateTargetVersion -DoNotUpdateWin10toWin11:$DoNotUpdateWin10toWin11
        Start-Sleep 60
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Feature Update is available to download..."
    }
    Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is available for download from Windows Update."
    Start-Sleep -Seconds 5

    IF ($CheckOnly -eq $true) {
        Write-Host "$($(Get-Date).ToShortTimeString()): Check only option specified. Done."
        RETURN
    }

    While ($True) {
    
        ### Run Stage 1
        Set-WindowsUpdateTargetVersion -DoNotUpdateWin10toWin11:$DoNotUpdateWin10toWin11
        Write-Host "$($(Get-Date).ToShortTimeString()): Running Windows Update. Waiting 120 minutes..."
        Run-WindowsUpdateProcessStage1 
        Start-Sleep -Seconds 2000
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Feature Update is installed and ready for reboot."
        IF (Test-FeatureUpdatePendingReboot) { Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is installed and ready for reboot. Rebooting to finalize feature update install."; Start-Sleep -Seconds 5; Run-ShutdownWithUpdates -Verbose -RebootSetting $RebootSetting; RETURN }
        Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is not installed and ready for reboot."

        ### Run Stage 2
        Set-WindowsUpdateTargetVersion -DoNotUpdateWin10toWin11:$DoNotUpdateWin10toWin11
        Write-Host "$($(Get-Date).ToShortTimeString()): Processing Windows Updates. Waiting 10 minutes..."
        Run-WindowsUpdateProcessStage2
        Start-Sleep -Seconds 300    
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Feature Update is installed and ready for reboot."
        IF (Test-FeatureUpdatePendingReboot) { Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is installed and ready for reboot. Rebooting to finalize feature update install."; Start-Sleep -Seconds 5; Run-ShutdownWithUpdates -Verbose -RebootSetting $RebootSetting; RETURN }
        Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is not installed and ready for reboot."

        ### Run Stage 3
        Set-WindowsUpdateTargetVersion -DoNotUpdateWin10toWin11:$DoNotUpdateWin10toWin11
        Write-Host "$($(Get-Date).ToShortTimeString()): Finalizing Windows Updates. Waiting 10 minutes..."
        Run-WindowsUpdateProcessStage3
        Start-Sleep -Seconds 330
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Feature Update is installed and ready for reboot."
        IF (Test-FeatureUpdatePendingReboot) { Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is installed and ready for reboot. Rebooting to finalize feature update install."; Start-Sleep -Seconds 5; Run-ShutdownWithUpdates -Verbose -RebootSetting $RebootSetting; RETURN }
        Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is not installed and ready for reboot."

        IF ((GET-WindowsInfo).BuildNumber -eq 26100) { Write-Host "$($(Get-Date).ToShortTimeString()): Win 11 Installed Successfully. Done."; RETURN }
        Write-Host "$($(Get-Date).ToShortTimeString()): Feature Update is not installed and ready for reboot. Starting over."

        ### Check if Windows Update Service is Running
        Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows Update Service is Running... " -NoNewline
        While ((Test-WindowsUpdateServiceIsRunning) -eq $False) { Start-Sleep -Seconds 15 }
    }
}
FUNCTION Run-Windows11InstallationAssistant {

    [CmdletBinding(DefaultParameterSetName='NotSilent')]
    PARAM (
        [Parameter(ParameterSetName = 'Silent')]
        [Switch]$SilentInstall = $False,        
        [Parameter(ParameterSetName = 'Silent')]
        [Parameter(ParameterSetName = 'NotSilent')]
        [SWITCH]$OverrideIncompatibility,
        [Parameter(ParameterSetName = 'Silent')]
        [DateTime]$ScheduledStart
        
    )

    ### ==================
    ### === Log Header ===
    ### ==================
    Write-Host ""
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Starting Integris Windows Update Tool Session ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Run Windows 11 Installation Assistant ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ©2025 Integris - https://IntegrisIT.com ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### Exceptional IT, Elevated Outcomes ###" -ForegroundColor Red -BackgroundColor Black 
    Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
    Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
    Write-Host ""   
    
    
    $UpgradeCheck = Test-Windows11Compatibility
    IF ($UpgradeCheck -eq $False -and $OverrideIncompatibility -eq $False) { Write-Host ""; Write-Warning "Unfortunately this computer is not compatabile with Windows 11 and cannot be upgraded."; RETURN }


    IF (($ScheduledStart - (GET-Date)).TotalMinutes -lt 0 -and $ScheduledStart -ne $null -and $ScheduledStart -ne "") { Write-Host ""; Write-Warning "Scheduled update cannot occur in the past. Update process aborted."; RETURN }

    IF ($ScheduledStart -ne $null -and $ScheduledStart -ne "") {
        Write-Host "$($(Get-Date).ToShortTimeString()): Scheduled update for $ScheduledStart (Approximately $([MATH]::ROUND((($ScheduledStart - (GET-Date)).TotalHours),0)) Hours From Now)"
        While ((Get-Date) -lt $ScheduledStart) {
            Start-Sleep -Seconds 15
        }
    }

    $SID = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value
    New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS -ErrorAction SilentlyContinue | Out-Null
    New-Item -Path "HKU:\$SID\Software\Microsoft\PCHC" -ErrorAction SilentlyContinue | Out-Null
    New-ItemProperty -Path "HKU:\$SID\Software\Microsoft\PCHC" -Name UpgradeEligibility -Value 1 -PropertyType DWORD -Force -ErrorAction SilentlyContinue | Out-Null
    Set-ItemProperty -Path "HKU:\$SID\Software\Microsoft\PCHC" -Name UpgradeEligibility -Value 1 -Force -ErrorAction SilentlyContinue | Out-Null
    New-Item -ItemType Directory -Path "C:\Temp" -ErrorAction SilentlyContinue | Out-Null
    $webClient = New-Object System.Net.WebClient
    $webClient.DownloadFile("https://go.microsoft.com/fwlink/?linkid=2171764","c:\Temp\Win1124H2Upgrade.exe")

    $Processes = (Get-Process *Windows* | Where { $_.ProcessName -like "*UpgraderApp*" })

    IF ( $Processes.Count -gt 0) {
        Write-Warning "Updater is already running. If you would like to restart it run the command: Stop-Process -Name $($Processes.ProcessName)"
        RETURN
    }

    IF ($SilentInstall -eq $true) { 
        & cmd.exe /c "c:\Temp\Win1124H2Upgrade.exe" /quietinstall /skipeula /auto upgrade 

        Write-Warning "Updater started. May take a while depending on the computer's internet speeds and hardware specs."
        Write-Warning "If update fails, rerun the updater with the GUI enabled to see the error message."

        RETURN $Processes
    }
    ELSE { 
        & cmd.exe /c "c:\Temp\Win1124H2Upgrade.exe" 
        
        RETURN
    }
}