Rudy Ooms wrote a nice blog post on block pre-provisioning of devices with an outdated OS version using the default device restriction. 


But what if you cannot change that policy as other services still need to enroll Windows 10 22H2 devices? In our case there is another team managing Teams Rooms Systems in the same tenant.



My colleague Lukasz Herod built a solution based on a separate ESP, an application and a device filter after we had a brainstorming session on this a few weeks ago. As of this week we implemented it in production. This is what we have built.

The Device filter

We started by creating a device filter to filter on Windows 10 (all versions) and Windows 11 21H1 like below:


Rule syntax:
(device.osVersion -startsWith "10.0.1") or (device.osVersion -startsWith "10.0.22000")

The Application

Next we created an application using the PowerShell App Deployment Toolkit (PSADK). The code part itself is small as it doesn't have to do that much.

The application will only show a message when the device is being enrolled and will exit with code 1603 to let the enrollment fail. When the device is not in enrolment, it will set the neccasary registry key and exit.

In the Deploy-Application.ps1 the installation  part looks like below:

##*===============================================
##* INSTALLATION
##*===============================================
[String]$installPhase = 'Installation'

$ifDefaultUser0 = $false
$LoggedOnUserSessions | ForEach-Object {
    If ($_.UserName -eq 'defaultuser0') {
        Write-Log -Message "$($_.UserName) user is signed in." -Source $appDeployToolkitName
        $ifDefaultUser0 = $true
    }
}

If ($ifDefaultUser0) {
    If (-not($IsOOBEComplete)) {
        Write-Log -Message "Requirement text: $RequirementText" -Severity 1 -Source $deployAppScriptFriendlyName
        Copy-File -Path "$dirFiles\Logo.png" -Destination "$configToolkitCachePath\EnrollemntRequirements"
        $RequirementText | Out-File -FilePath "$configToolkitCachePath\EnrollemntRequirements\RequirementText.txt"
        Copy-File -Path "$dirFiles\cmtrace.exe" -Destination "$envWinDir\System32"

        Start-Sleep -Seconds 3
        Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'ConsentPromptBehaviorAdmin' -Type 'DWord' -Value '0'
        Start-Sleep -Milliseconds 500
        [__ComObject]$WShell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'SilentlyContinue'
        $WShell.SendKeys('+{F10}')
        $i = 0
        Do {
            $i++
            Get-Process -Name cmd | Stop-Process -Force
            Start-Sleep -Milliseconds 500
        } Until ($i -ge 10)
        Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'ConsentPromptBehaviorAdmin' -Type 'DWord' -Value '1'
        Execute-Process -Path "$dirFiles\ServiceUI.exe" -Parameters "$dirFiles\popup.exe" -NoWait
    }
    Exit-Script -ExitCode 1603
} Else {
    Write-Log -Message "defaultuser0 user is not signed in. Skip." -Source $appDeployToolkitName
}


#endregion
 
##*===============================================
##* POST-INSTALLATION
##*===============================================
[String]$installPhase = 'Post-Installation'

#Remove unzipped folder
If ($ZIPName -and $ZIPRemove) {
    Remove-Folder -Path $ZIPDestination
}
#Region POST-INSTALL
## 

#endregion
#This should be the last task
If ($Set_INGAuditKey) {
    Set-RegistryKey -Key $AuditKey -Name 'ApplicationName' -Value $PKGName
    Set-RegistryKey -Key $AuditKey -Name 'InstallDate' -Value $currentDateTime
    Set-RegistryKey -Key $AuditKey -Name 'Version' -Value $appVersion
    Set-RegistryKey -Key $AuditKey -Name 'Language' -Value $appLang
    Set-RegistryKey -Key $AuditKey -Name 'Revision' -Value $appRevision_version

}
#Reboot section
If ($IsRebootRequired -or $IsRebootRequired_Install) {
    $mainExitCode = 3010 #By default, Intune sets 3010 as soft reboot
}

Next to the Deploy-Application.ps1 script the are 2 main files included: ServiceUI.exe and Popup.exe. This last one is created from the below popup.ps1 script.

$ErrorActionPreference = 'SilentlyContinue'

function Show-WPFWindow
{
	param
	(
		[Parameter(Mandatory)]
		[Windows.Window]
		$Window
	)
				
	$result = $null
	$null = $window.Dispatcher.InvokeAsync{
		$result = $window.ShowDialog()
		Set-Variable -Name result -Value $result -Scope 1
	}.Wait()
	$result
}

function Convert-XAMLtoWindow
{
	param
	(
		[Parameter(Mandatory=$true)]
		[string]
		$XAML
	)
				
	Add-Type -AssemblyName PresentationFramework
				
	$reader = [XML.XMLReader]::Create([IO.StringReader]$XAML)
	$result = [Windows.Markup.XAMLReader]::Load($reader)
	$reader.Close()
	$reader = [XML.XMLReader]::Create([IO.StringReader]$XAML)
	while ($reader.Read())
	{
		$name=$reader.GetAttribute('Name')
		if (!$name) { $name=$reader.GetAttribute('x:Name') }
		if($name)
		{$result | Add-Member NoteProperty -Name $name -Value $result.FindName($name) -Force}
	}
	$reader.Close()
	$result
}

function Show-PopupWindow {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [String[]]$NotificationTitle,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [String[]]$NotificationContent,
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [switch]$PassThru = $true
    )

#region XAML
$xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="Window" Width="1010" Height="355" Topmost="True" WindowStyle="None" ResizeMode="NoResize" AllowsTransparency="True" Background="Transparent" WindowStartupLocation="CenterScreen" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,0,0,0" ShowInTaskbar="False">
    <Grid Margin="0,0,0,0">       
        <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Center" Height="345" Width="1000" VerticalAlignment="Top" Margin="5,5,5,5" CornerRadius="5" Background="#f0f0f0">
            <Border BorderThickness="0" HorizontalAlignment="Center" Height="50" VerticalAlignment="Top" Width="1600" Margin="0,0,0,0" CornerRadius="5" Background="#d3d3d3">
                <TextBlock Name="TextTitle" FontSize="12" FontWeight="Bold"  Foreground="Black"  Margin="5,5,5,5" HorizontalAlignment="Center" TextWrapping="Wrap" Text="Global Workplace Services inform you"/>       
            </Border> 
        </Border>
        <Image Name="Logo" Height="200" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="30,80,0,0"/>
        <Image Name="ICPLogo" Height="40" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="25,12,0,0"/>
        <TextBlock Name="TextNotificationTitle" FontSize="17" FontWeight="Bold"  Foreground="Black"  Margin="5,27,5,5" HorizontalAlignment="Center" TextWrapping="Wrap"/>
        <TextBlock Name="TextNotificationContent" FontSize="30" FontWeight="Bold" Foreground="Black"  Margin="240,100,50,50" HorizontalAlignment="Center" TextWrapping="Wrap"/>

    </Grid>
</Window>
'@

#endregion

    $window = Convert-XAMLtoWindow -XAML $xaml
    $window.TextNotificationTitle.Text = $NotificationTitle
    $window.TextNotificationContent.Text = $NotificationContent
    $window.Logo.Source = "$env:ProgramFiles\SRC\EnrollemntRequirements\Logo.png"
    $window.ICPLogo.Source = "$env:WinDir\ImmersiveControlPanel\images\logo.png"

    $null = Show-WPFWindow -Window $window

}

[string]$Text = ''
[string]$RequirementText = ''
$RequirementTextFile = "$env:ProgramFiles\SRC\EnrollemntRequirements\\RequirementText.txt"
If (Test-Path -Path $RequirementTextFile -PathType Leaf) {
    $Text = Get-Content -Path $RequirementTextFile
}
$Text.Split(';') | ForEach-Object {
    $RequirementText = $RequirementText + $_ + " `r`n"
}

[string]$NotificationContent = "This device cannot be enrolled because it does not meet the minimum requirements `n" + $RequirementText

Show-PopupWindow -NotificationTitle 'Enrollment restrictions' -NotificationContent $NotificationContent

We created a nice Intinewin package out of it, so so we could create the application in Intune. Below the most important part of the app configuraction:

As you can see the application is assigned to all Windows 10 and Windows 11 21H2 devices, but on the already enrolled devices it will only set the registry value and exit.

The Enrollment Status Page

The last piece of the puzzle is the ESP, which is targetted to the all Windows 10 and Windows 11 21H2 devices again and will only have the above application as blocking app assigned to it.


Conclussion

When you have other teams also manaing Windows devices in the same Intune tetant and you cannot what till every teams is ready to block versions from being block, the bove setup can be used.

Engineers running the pre-provisiong will get a nice message and also users will get a message they can understand.


Comments

Popular posts from this blog

Why pre-provision Windows devices?

How to do phased deployments in a cloud native world?

Why are we using Autopilot group tags?