Setting the Computer Description During Windows Autopilot

I’ve been getting to grips with Windows Autopilot recently and, having a long history working with SCCM, I’ve found it hard not to compare it with the power of traditional OSD using a task sequence. In fact, one of my goals was to basically try to reproduce what I’m doing in OSD with Autopilot in order to end up with the same result – and it’s been a challenge.

I like the general concept of Autopilot and don’t get me wrong – it’s getting better all the time – but it still has its shortcomings that require a bit of creativity to work around. One of the things I do during OSD is to set the computer description in AD. That’s fairly easy to do in a task sequence; you can just script it and run the step using credentials that have the permission to make that change.

In Autopilot however (hybrid AAD join scenario), although you can run Powershell scripts too, they will only run in SYSTEM context during the Autopilot process. That means you either need to give computer accounts the permission to change their own properties in AD, or you have to find a way to run that code using alternate credentials. You can run scripts in the context of the logged-on user, but I don’t want to do that – in fact I disable the user ESP – I want to use a specific account that has those permissions.

You could use SCCM to do it post-deployment if you are co-managing the device, but ideally I want everything to be native to Autopilot where possible, and move away from the hybrid mentality of do what you can with Intune, and use SCCM for the rest.

It is possible to execute code in another user context from SYSTEM context, but when making changes in AD the DirectoryEntry operation kept erroring with “An operations error occurred”. After researching, I realized it is due to AD not accepting the authentication token as it’s being passed a second time and not directly. I tried creating a separate powershell process, a background job, a runspace with specific credentials – nothing would play ball. Anyway, I found a way to get around that by using the AccountManagement .Net class, which allows you to create a context using specific credentials.

In this example, I’m setting the computer description based on the model and serial number of the device. You need to provide the username and password for the account you will perform the AD operation with. I’ve put the password in clear text in this example, but in the real world we store the credentials in an Azure Keyvault and load them in dynamically at runtime with some POSH code to avoid storing them in the script. I hope in the future we will be able to run Powershell scripts with Intune in a specific user context, as you can with steps in an SCCM task sequence.

# Set credentials
$ADAccount = "mydomain\myADaccount"
$ADPassword = "Pa$$w0rd"

# Set initial description
$Model = Get-WMIObject -Class Win32_ComputerSystem -Property Model -ErrorAction Stop| Select -ExpandProperty Model
$SerialNumber = Get-WMIObject -Class Win32_BIOS -Property SerialNumber -ErrorAction Stop | Select -ExpandProperty SerialNumber
$Description = "$Model - $SerialNumber"

# Set some type accelerators
Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop
$Accelerators = [PowerShell].Assembly.GetType("System.Management.Automation.TypeAccelerators")
$Accelerators::Add("PrincipalContext",[System.DirectoryServices.AccountManagement.PrincipalContext])
$Accelerators::Add("ContextType",[System.DirectoryServices.AccountManagement.ContextType])
$Accelerators::Add("Principal",[System.DirectoryServices.AccountManagement.ComputerPrincipal])
$Accelerators::Add("IdentityType",[System.DirectoryServices.AccountManagement.IdentityType])

# Connect to AD and set the computer description
$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PrincipalContext = [PrincipalContext]::new([ContextType]::Domain,$Domain,$ADAccount,$ADPassword)
$Account = [Principal]::FindByIdentity($PrincipalContext,[IdentityType]::Name,$env:COMPUTERNAME)
$LDAPObject = $Account.GetUnderlyingObject()
If ($LDAPObject.Properties["description"][0])
{
    $LDAPObject.Properties["description"][0] = $Description
}
Else
{
    [void]$LDAPObject.Properties["description"].Add($Description)
}
$LDAPObject.CommitChanges()
$Account.Dispose()

Querying for Devices in Azure AD and Intune with PowerShell and Microsoft Graph

Recently I needed to get a list of devices in both Azure Active Directory and Intune and I found that using the online portals I could not filter devices by the parameters that I needed. So I turned to Microsoft Graph to get the data instead. You can use the Microsoft Graph Explorer to query via the Graph REST API, however, the query capabilities of the API are still somewhat limited. To find the data I needed, I had to query the Graph REST API using PowerShell, where I can take advantage of the greater filtering capabilities of PowerShell’s Where-Object.

To use the Graph API, you need to authenticate first. A cool guy named Dave Falkus has published a number of PowerShell scripts on GitHub that use the Graph API with Intune, and these contain some code to authenticate with the API. Rather than re-invent the wheel, we can use his functions to get the authentication token that we need.

First, we need the AzureRM or Azure AD module installed as we use the authentication libraries that are included with it.

Next, open one of the scripts that Dave has published on GitHub, for example here, and copy the function Get-AuthToken into your script.

The also copy the Authentication code region into your script, ie the section between the following:


#region Authentication
...
#endregion

If you run this code it’ll ask you for an account name to authenticate with from your Azure AD. Once authenticated, we have a token we can use with the Graph REST API saved as a globally-scoped variable $authToken.

Get Devices from Azure AD

To get devices from Azure AD, we can use the following function, which I take no credit for as I have simply modified a function written by Dave.


Function Get-AzureADDevices(){

[cmdletbinding()]

$graphApiVersion = "v1.0"
$Resource = "devices"
$QueryParams = ""

    try {

        $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)$QueryParams"
        Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get
    }

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

In the $graphAPIVersion parameter, you can use the current version of the API.

Now we can run the following code, which will use the API to return all devices in your Azure AD and save them to them a hash table which organizes the results by operating system version.


# Return the data
$ADDeviceResponse = Get-AzureADDevices
$ADDevices = $ADDeviceResponse.Value
$NextLink = $ADDeviceResponse.'@odata.nextLink'
# Need to loop the requests because only 100 results are returned each time
While ($NextLink -ne $null)
{
    $ADDeviceResponse = Invoke-RestMethod -Uri $NextLink -Headers $authToken -Method Get
    $NextLink = $ADDeviceResponse.'@odata.nextLink'
    $ADDevices += $ADDeviceResponse.Value
}

Write-Host "Found $($ADDevices.Count) devices in Azure AD" -ForegroundColor Yellow
$ADDevices.operatingSystem | group -NoElement

$DeviceTypes = $ADDevices.operatingSystem | group -NoElement | Select -ExpandProperty Name
$AzureADDevices = @{}
Foreach ($DeviceType in $DeviceTypes)
{
    $AzureADDevices.$DeviceType = $ADDevices | where {$_.operatingSystem -eq "$DeviceType"} | Sort displayName
}

Write-host "Devices have been saved to a variable. Enter '`$AzureADDevices' to view."

It will tell you how many devices it found, and how many devices there are by operating system version / device type.

2018-10-22 16_06_14-Windows PowerShell ISE

We can now use the $AzureADDevices hash table to query the data as we wish.

For example, here I search for an iPhone that belongs to a particular user:


$AzureADDevices.Iphone | where {$_.displayName -match 'nik'}

Here I am looking for the count of Windows devices that are hybrid Azure AD joined, and display the detail in the GridView.


($AzureADDevices.Windows | where {$_.trustType -eq 'ServerAd'}).Count
$AzureADDevices.Windows | where {$_.trustType -eq 'ServerAd'} | Out-GridView

And here I’m looking for all MacOS devices that are not compliant with policy.


($AzureADDevices.MacOS | where {$_.isCompliant -ne "True"}) | Out-GridView

Get Devices from Intune

To get devices from Intune, we can take a similar approach. Again no credit for this function as its modified from Dave’s code.


Function Get-IntuneDevices(){

[cmdletbinding()]

# Defining Variables
$graphApiVersion = "v1.0"
$Resource = "deviceManagement/managedDevices"

try {

    $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"
    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

Running the following code will return all devices in Intune and save them to a hash table again organised by operating system.


$MDMDevices = Get-IntuneDevices

Write-Host "Found $($MDMDevices.Count) devices in Intune" -ForegroundColor Yellow
$MDMDevices.operatingSystem | group -NoElement

$IntuneDeviceTypes = $MDMDevices.operatingSystem | group -NoElement | Select -ExpandProperty Name
$IntuneDevices = @{}
Foreach ($IntuneDeviceType in $IntuneDeviceTypes)
{
    $IntuneDevices.$IntuneDeviceType = $MDMDevices | where {$_.operatingSystem -eq "$IntuneDeviceType"} | Sort displayName
}

Write-host "Devices have been saved to a variable. Enter '`$IntuneDevices' to view."

Now we can query data using the $IntuneDevices variable.

Here I am querying for the count of compliant and non-compliant iOS devices.


$IntuneDevices.iOS | group complianceState -NoElement

Here I am querying for all non-compliant iOS devices, specifying the columns I want to see, sort the results and outputting into table format.


$IntuneDevices.iOS |
    where {$_.complianceState -eq "noncompliant"} |
    Select userDisplayName,deviceName,imei,managementState,complianceGracePeriodExpirationDateTime |
    Sort userDisplayName |
    ft

All Windows devices sorted by username:


$IntuneDevices.Windows | Select userDisplayName,deviceName | Sort userDisplayName

Windows devices managed by SCCM:


$IntuneDevices.Windows | where {$_.managementAgent -eq "ConfigurationManagerClientMdm"} | Out-GridView

Windows devices enrolled using Windows auto enrollment:


$IntuneDevices.Windows | where {$_.deviceEnrollmentType -eq "windowsAutoEnrollment"} | Out-GridView

Windows devices enrolled by SCCM co-management:


$IntuneDevices.Windows | where {$_.deviceEnrollmentType -eq "windowsCoManagement"} | Out-GridView

You can, of course, expand this into users and other resource types, not just devices. You just need the right URL construct for the data type you want to query.