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 }

    $UpdateService = Get-Service WUAUSERV -ErrorAction SilentlyContinue

    IF ($UpdateService.Status -eq "Running") { Write-Host "$($(Get-Date).ToShortTimeString()): Verified Windows Update service is running." }
    ELSE { 
        Set-Service WUAUSERV -StartupType Automatic -ErrorAction SilentlyContinue
        Restart-Service WUAUSERV -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 70
        IF ($UpdateService.Status -eq "Running") { Write-Host "$($(Get-Date).ToShortTimeString()): Restarted Windows Update service and it is now running." }
        ELSE { Write-Host "$($(Get-Date).ToShortTimeString()): Error: Unable to Start Windows Update Service. Trying again..." }
    }
}
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
}


### 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-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 { 

    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory)]
        [ValidateSet("ForceReboot","NoReboot","IfNoUserLoggedIn")]
        [String]$RebootSetting,
        [Int]$MinutesToRun = 120,
        [DateTime]$ScheduledStart
    )

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

    CLS
    Write-Host "Checking for Updates. Logging Enabled:"
    Write-Host "==========================================="
    Write-Host "$($(Get-Date).ToShortTimeString()): Setting Execution Policy to Remote Signed for this PowerShell process."
    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force

    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
        }
    }
     
    Write-Host "$($(Get-Date).ToShortTimeString()): Attempting to Download Updates using the Windows Update Service..."

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

    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows Update Service is Running..."
    While ((Test-WindowsUpdateServiceIsRunning) -like "*Error*") { Start-Sleep -Seconds 15 }

    ### Check if Windows Updates are Available for Download
    Start-Sleep -Seconds 3
    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows Updates are available for download."

    IF(!(Test-WindowsUpdatesPendingDownload)) {
        Write-Host "$($(Get-Date).ToShortTimeString()): No Updates Found for Download."
        IF ($RebootSetting -eq "NoReboot") {
            Write-Host "$($(Get-Date).ToShortTimeString()): NoReboot requested. Done."
            RETURN
        }
        ELSE {
            Write-Host "$($(Get-Date).ToShortTimeString()): Attempting Reboot if Updates are Available."
            Run-ShutdownWithUpdates -RebootSetting $RebootSetting -Verbose
            RETURN
        }
    }
    Write-Host "$($(Get-Date).ToShortTimeString()): Windows Updates are available for download from Windows Update."
    Start-Sleep -Seconds 5

    While ($True) {
    
        ### Run Stage 1
        Write-Host "$($(Get-Date).ToShortTimeString()): Running Windows Update. Waiting $($MinutestoRun) minutes..."
        Run-WindowsUpdateProcessStage1 
        Start-Sleep -Seconds (60 * $MinutestoRun)
       
        ### Run Stage 2
        Write-Host "$($(Get-Date).ToShortTimeString()): Processing Windows Updates. Waiting 10 minutes..."
        Run-WindowsUpdateProcessStage2
        Start-Sleep -Seconds 300    

        ### Run Stage 3
        Write-Host "$($(Get-Date).ToShortTimeString()): Finalizing Windows Updates. Waiting 10 minutes..."
        Run-WindowsUpdateProcessStage3
        Start-Sleep -Seconds 300

        IF ((Test-WindowsUpdatePendingReboot) -eq $False) {
            Write-Host "$($(Get-Date).ToShortTimeString()): No Updates installed and ready for reboot."
        } 
        ELSE {
            IF ($RebootSetting -eq "NoReboot") {
                Write-Host "$($(Get-Date).ToShortTimeString()): NoReboot requested. Done."
                RETURN
            }
            ELSE {
                Write-Host "$($(Get-Date).ToShortTimeString()): Attempting Reboot if Updates are Available."
                Run-ShutdownWithUpdates -RebootSetting $RebootSetting -Verbose
                RETURN
            }
        }
    }
}
FUNCTION Run-InstallFeatureUpdate { 

    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory)]
        [ValidateSet("ForceReboot","NoReboot","IfNoUserLoggedIn")]
        [STRING]$RebootSetting,
        [SWITCH]$CheckOnly,
        [SWITCH]$DoNotUpdateWin10toWin11,
        [SWITCH]$OverrideIncompatibility,
        [DateTime]$ScheduledStart,
        [SWITCH]$IgnoreStorageRequirement
    )  
    $VerbosePreference = "Continue"   
    
    
    ### ==================
    ### === Log Header ===
    ### ==================
    $Callstack = Get-PSCallStack 
    IF (!((($Callstack).Command) -contains "Run-DeploymentSet")) {
        $PackageName = ($Callstack | Where-Object { ($_.Command -like "App-*" -or $_.Command -like "Driver-*" -or $_.Command -like "Font-*") -and $_.Command -notlike "*PackageHandler*" }).Command

        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
        IF ($DoNotUpdateWin10toWin11) { 
        Write-Host "### Windows 10 Feature Update ###" -ForegroundColor Red -BackgroundColor Black }
        ELSE { 
        Write-Host "### Windows 11 Feature Update ###" -ForegroundColor Red -BackgroundColor Black }
        Write-Host "$($(Get-Date).ToShortTimeString()): " -NoNewline
        Write-Host "### ==================================================== ###" -ForegroundColor Red -BackgroundColor Black
        Write-Host ""
    }
    


    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force

    

    
    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

    Write-Host "$($(Get-Date).ToShortTimeString()): Checking if Windows Update Service is Running..."
    While ((Test-WindowsUpdateServiceIsRunning) -like "*Error*") { Start-Sleep -Seconds 15 }
    Write-Host "$($(Get-Date).ToShortTimeString()): Windows Update Service is Running."

    ### 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."

        Test-WindowsUpdateServiceIsRunning
    }
}
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
        
    )
    
    $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
    }
}