Intro

In this post I would like to show an automation that might assist when you are requested to create an Intune demo. Usually this is something that might take hours if you know what you are doing. I would like to show you how to reduce this time

Challenge

We need to find a way to automatically deploy device configurations, device compliance policies and win32 applications to an empty Intune tenant

Solution

Create an Azure DevOps build pipeline (CI) that will execute a PowerShell script to provision items from an DevOps repository to Intune. This solution is based on Microsoft Intune scripts that you can find at this location

Requirements

For this you will need the following

  • DevOps Account with right to create a new project
  • Empty Intune Tenant (demo or trial)

Let us discuss the steps:

  • Create project
  • Clone repository
  • Create app registration
  • Create pipeline
  • Execute pipeline

Create Project

As listed in requirements, for this task you will need to have an DevOps account with permissions to create projects

  • Enter a project name and a short description
  • Click Create

Clone repository

For the automation we need to clone this repository where we have the PowerShell Script and some sample applications and configurations. If you plan to use this in your work, just add items to the corresponding folder. Before explaining the script let us go ahead and clone the repository

  • Click Import and wait for the task to complete

Upon successful import your repository structure should look like this:

CompliancePolicies, ConfigurationPolicies and win32app, each of them holds 2 sample items.

In the Function folder, we have all the functions used in the script.

While most of them are copied from MSFT repository, there are some changes and additions:

  • Get-AuthToken.ps1 has been changed to perform unattended authentication.

Note the parameters required for unattended authentication.

function Get-AuthToken {

param ( 
    [Parameter(Mandatory = $true)]
    [string]$client, 
    [Parameter(Mandatory = $true)]
    [string]$secret, 
    [Parameter(Mandatory = $true)]
    [string]$tenant 
    )

<#
.SYNOPSIS
This function is used to authenticate with the Graph API REST interface
.DESCRIPTION
The function authenticate with the Graph API Interface with the tenant name
.EXAMPLE
Get-AuthToken
Authenticates you with the Graph API interface
.NOTES
NAME: Get-AuthToken
#>

$client_id = $client
$client_secret = $secret
$tenant_id = $tenant

$resource = "https://graph.microsoft.com"
$authority = "https://login.microsoftonline.com/$tenant_id"
$tokenEndpointUri = "$authority/oauth2/token"

$content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource"


$response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing
        

$authHeader = @{
'Content-Type'='application/json'
'Authorization'="Bearer " + $response.access_token
'ExpiresOn'=$response.expires_on
}
return $authHeader
} 

I also added 2 import functions: Import_DeviceConfigurationPolicies.ps1 and Import_DeviceCompliance.ps1

The main script that we will use in the pipeline looks as following:

[CmdletBinding()]

param (
    [Parameter(Mandatory = $true)]
    [string]$tenantId, 
    [Parameter(Mandatory =$true)]
    [string]$client_id, 
    [Parameter(Mandatory=$true)]
    [string]$client_secret
)

#$rootLocation =  $env:BUILD_ARTIFACTSTAGINGDIRECTORY
$rootLocation = $env:Build_SourcesDirectory
Write-Host "Working Directory is " + $rootLocation

Write-Host "PSScriptRoot is" $PSScriptRoot

#$Functions = @(gci -Path  $rootLocation\Functions\*.ps1 -ErrorAction SilentlyContinue)

# Source Functions

$Functions = @(Get-ChildItem -Path $rootLocation\Functions\*.ps1 -ErrorAction SilentlyContinue)
foreach ($f in $Functions){
    try {
        Write-Host "Importing function "  $($f.FullName)
        . $($f.FullName)

    }
    catch{
        Write-Error -Message "Failed to import function " + $($f.FullName)
    }
}

#################################
####### Create win32 apps  ######
################################# 

# Test-AuthToken

Write-Host "Getting Token"
$global:authToken = Get-AuthToken -client $($client_id) -secret $($client_secret) -tenant $($tenantId)
Write-Host "Token is " $global:authToken.Authorization

$baseUrl = "https://graph.microsoft.com/beta/deviceAppManagement/"

$logRequestUris = $true;
$logHeaders = $false;
$logContent = $true;

$azureStorageUploadChunkSizeInMb = 6l;

$sleep = 30



$apps = @(Get-ChildItem -Path $rootLocation\win32apps -Directory )

foreach ($appItem in $apps){
    Write-Host "Creating Application " $appItem
    $appInfofile = (Get-ChildItem -Path "$rootLocation\win32apps\$appItem\$appItem.json").FullName
    $sourceFile = (Get-ChildItem -Path "$rootLocation\win32apps\$appItem\$appItem.intunewin").FullName

    $appData = Get-Content $appInfoFile -Raw | ConvertFrom-Json

    Write-Host "AppinfoFile is " $appInfofile
    Write-Host "SourceFile is " $sourceFile
    Write-Host "Application DisplayName is " $($appData.displayName)

    # Defining Detection Rules
    if ($($appData.DetectionRules.type) -eq "#microsoft.graph.win32LobAppFileSystemDetection"){
        $FileRule = New-DetectionRule -File -Path ($($appData.DetectionRules.path)) -FileOrFolderName ($($appData.DetectionRules.fileOrFolderName)) -FileDetectionType ($($appData.DetectionRules.detectionType)) -check32BitOn64System ($($appData.DetectionRules.check32BitOn64System))
    }
    
    $DetectionRule = @($FileRule)

    $ReturnCodes = Get-DefaultReturnCodes

    Upload-Win32Lob -SourceFile "$SourceFile" -publisher $($appData.publisher) `
    -displayName $($appData.displayName) `
    -description $($appData.description) -detectionRules $DetectionRule -returnCodes $ReturnCodes `
    -installCmdLine "$($appData.installCommandLine)" `
    -uninstallCmdLine "$($appData.uninstallCommandLine)"

}

#########################################
#### Import Configuration Policies ######
#########################################

$Configs = (Get-ChildItem -Path "$rootLocation\ConfigurationPolicies\*.json").FullName
foreach ($uniqueConfig in $Configs) {
    Write-Host "Importing Configuration " $uniqueConfig
    Import_DeviceConfigurationPolicy -ImportPath $uniqueConfig
}

#########################################
#### Import Compliance Policies ######
#########################################

$Compliances = (Get-ChildItem -Path "$rootLocation\CompliancePolicies\*.json").FullName
foreach ($uniqueCompliance in $Compliances) {
    Write-Host "Importing Compliance " $uniqueCompliance
    Import_DeviceCompliancePolicy -ImportPath $uniqueCompliance
}

The script is amazingly simple and does the following

  • Create a Graph token using client application parameters
  • Import win32 applications
  • Import device configuration profiles
  • Import device compliance

Create Application Registration

Our script needs to run unattended to have value as an automation. Therefore, we need to create an application registration:

  • Go to the azure portal aad.portal.azure.com
  • Clock Azure Active Directory
  • Click App registration
  • Click New registration
  • Enter an application name – DevOpsIntune
  • Keep the API access to single tenant
  • Click Register

There are 3 parameters we need to perform unattended application authentication in Graph

  • Application (client) ID
  • Directory (tenant) ID
  • Application (client) secret

The first 2 just appeared once you created the application

To create the secret:

  • click certificates and secret
  • new client secret
  • enter a name then click add

Make sure you copy the secret in a text editor. This is the only time you have access to it.

The client application needs permissions in MSFT Graph to create applications, profiles and policies.

  • Click API permissions
  • Click Add permission
  •  Select Microsoft Graph
  • Select Application Permission
  • Add DeviceManagementApps.ReadWrite.All
  • Add DeviceManagementConfiguration.ReadWrite.all
  • Click Add permissions

Click Grant admin consent for Intune Provisioning then yes

Now we have everything we need to set up the pipeline

In DevOps portal

  • Pipelines then Create Pipeline

Click Use classic editor at the bottom of the page

Leave the source as it is and click continue

  • In the newly created page click start with an Empty Job
  • In the new window click the + sign on Agent job 1
  • Search for PowerShell and click Add
  • Select the newly created job
  • Enter a name in the Display name field
  • Select the Provision-Intune.ps1 from the repository
  • Enter the following in the arguments filed:   -tenantId $(tenantId) -client_id $(client_id) -client_secret $(client_secret)
  • Click the down arrow and select Save then again Save

-tenantId $(tenantId) -client_id $(client_id) -client_secret $(client_secret)

The pipeline was created, but we need to define the parameters in the variables tab

  • Click Variables
  • Click add to add all the parameters we collected when created the application registration:
    • Application (client) ID
    • Tenant ID
    • Application Secret
  • Click Save and Queue
  • In the Run pipeline windows click Save and run at the top of the page

Execute Pipeline

If everything was ok so far, the last command queued the pipeline

Click the Agent job

Wait for the script to finish. It should take less than 2 minutes

If everything goes well, you should have something like this

Now we can check for the results in the endpoint manager portal

In compliance policies we have the newly created policies

In Configuration Profiles we have the policies we added with the pipeline automation

And finally, the applications

Summary

In this post we learned how to automatically provision an Intune tenant using DevOps repository and DevOps pipelines