a cartoon style image of a computer with a quirky 16 9

Have you ever needed to deploy a Win32 app via Microsoft Intune to a group of users but wanted the app to install only on their new devices? I recently faced this challenge and found a unique solution. Unlike previous situations where detecting the Autopilot Enrollment Status Page (ESP) was enough, this time I had to account for a variety of scenarios: some users would use Autopilot, some wouldn’t, and others would use Windows 365 (Cloud PCs), which do not support Autopilot.

To address this, I devised a PowerShell script to use as a requirement rule for the Win32 app. This script determines if a computer is “new” by checking the Intune enrollment date from the registry and comparing it to the current date and time. If the enrollment date is within a specified number of hours, the script deems the computer as new.

The Script

Here’s the PowerShell script that accomplishes this:

PowerShell
<#
.SYNOPSIS
    Finds the oldest Intune enrollment timestamp in the registry, compares it to the current date/time, and outputs true/false if the device was enrolled within the specified timeframe.

.DESCRIPTION
    This script recursively searches the specified registry path for FirstScheduleTimestamp values,
    finds the oldest timestamp, and compares it to the current date and time. It outputs a boolean
    value indicating whether the oldest timestamp is within a defined number of hours from the current date and time.

.PARAMETER REGPath
    The root registry path to search.

.PARAMETER Value
    The registry value name to search for.

.PARAMETER CompareHours
    The number of hours to compare the oldest timestamp against the current date and time.

.EXAMPLE
    .\Check-FirstScheduleTimestamp.ps1 -REGPath 'HKLM:\SOFTWARE\Microsoft\Enrollments' -Value 'FirstScheduleTimestamp' -CompareHours 24

.NOTES
    Script_Name: Check-FirstScheduleTimestamp.ps1
    Author: John Marcum (PJM)
    Date: 7/2/2024
    Version: 1.0

    This script can be used as a requirement rule for Intune Win32 app deployments to control installation timing based on maintenance windows.

# LEGAL DISCLAIMER
# This script is provided "as is" without any warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. The entire risk as to the quality and performance of the script is with you.
# In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the script or the use or other dealings in the script.
# You should never run any script from the Internet without understanding its contents and effects. It is highly recommended that you thoroughly test the script in a safe environment before running it in production.
#>

param (
    [string]$REGPath = 'HKLM:\SOFTWARE\Microsoft\Enrollments',
    [string]$Value = 'FirstScheduleTimestamp',
    [int]$CompareHours = 24
)

### BEGIN FUNCTIONS ###

function Get-KeyPath {
    # Get-ChildItem skips top level key, use Get-Item for that
    param([parameter(ValueFromPipeline)]$key)
    process {
        $key.GetValueNames() | ForEach-Object {
            $value = $_
            [pscustomobject] @{
                Path  = $key.PSPath
                Name  = $value
                Value = $key.GetValue($value)
                Type  = $key.GetValueKind($value)
            }
        }
    }
}

function Convert-BytesToDateTime {
    param (
        [byte[]]$bytes
    )
    function GetValue ($array){
        return [uint32]('0x' + (($array | ForEach-Object { $_.ToString("X2") }) -join ''))
    }

    [array]::Reverse($bytes)
    $dateTime = New-Object DateTime (
        (GetValue $bytes[14..15]), 
        (GetValue $bytes[12..13]), 
        (GetValue $bytes[8..9]), 
        (GetValue $bytes[6..7]), 
        (GetValue $bytes[4..5]), 
        (GetValue $bytes[2..3]), 
        (GetValue $bytes[0..1])
    )
    return $dateTime
}

### END FUNCTIONS ###

### SCRIPT ENTRY POINT ###

# Find all paths with the specified registry value
$TimestampPaths = Get-ChildItem -Recurse $REGPath | Get-KeyPath | Where-Object Name -eq $Value | Select-Object -ExpandProperty Path -ErrorAction SilentlyContinue

# If one or more Intune enrollment times were found, get the oldest
if ($TimestampPaths) {
    $oldestTimestamp = $null
    $oldestPath = $null

    foreach ($Path in $TimestampPaths) {
        Write-Host "Searching $($Path)"
        $Data = Get-ItemProperty -Path $Path -Name $Value | Select-Object -ExpandProperty $Value -ErrorAction SilentlyContinue
        if ($Data) {
            # Convert the binary data to DateTime
            $scheduleDateTime = Convert-BytesToDateTime -bytes $Data

            # Determine if this is the oldest timestamp found
            if (-not $oldestTimestamp -or $scheduleDateTime -lt $oldestTimestamp) {
                $oldestTimestamp = $scheduleDateTime
                $oldestPath = $Path
            }
        }
    }

    if ($oldestTimestamp) {
        Write-Host "Oldest FirstScheduleTimestamp found in $($oldestPath): $($oldestTimestamp)"

        # Get the current date and time
        $currentDateTime = Get-Date

        # Compare the oldest DateTime to the current date and time
        $timeDifference = $currentDateTime - $oldestTimestamp
        if ($timeDifference.TotalHours -le $CompareHours) {
            # Device was enrolled within the specified timeframe
            Write-Output $true
        } else {
            # Device was not enrolled within the specified timeframe
            Write-Output $false
        }
    } else {
        # No oldest timestamp found. We should never land here.
        Write-Output $false
    }
} else {
    # No enrollment timestamps found. The device is likely not enrolled in Intune.
    Write-Output $false
}

Using the Script in Intune Win32 App Requirement Rule

To use this script as a requirement rule in an Intune Win32 app deployment, follow these steps:

  1. Create the PowerShell Script: Save the provided PowerShell script as Check-InstallationWindow.ps1.
  2. Create the Win32 App:
    • Package your application using the Microsoft Win32 Content Prep Tool.
    • In the Intune portal, go to Apps > Windows > Add.
    • Select Windows app (Win32) and upload the .intunewin file created by the Content Prep Tool.
  3. Define the required settings for the Program.
  4. In the Requirements section:
    1. Select the Operating system architecture and the minimum operating system requirements.
    2. Add the Requirement Script:
      1. Select + Add to add a new requirement rule.
      2. Choose Script as the requirement type.
      3. Select the Check-InstallationWindow.ps1 script to upload it.
      4. Set Select output data type to Boolean.
      5. Set Operator to Equals.
      6. Set the Value to Yes.
  5. Define the Detection rules.
  6. Assign the App: Assign the application to the desired user or device groups.