Contents

Getting Intune device config Powershell scripts via the Graph API

Since a few months, it’s possible to upload PowerShell scripts to Intune as a part of you device configuration policies. These scripts will then be pushed to the linked Windows devies and run either under the SYSTEM account, or as the logged on user.

While working on an Intune deployment, I wanted to check the PowerShell scripts that are currently in use, and found out you can’t do that through the portal. You can change the properties of the script and upload a new file, but can’t view the current script.

View PowerShell script within Intune GUI

Looking for a way to make the script visible, I started playing around with the Graph API, to see if we can do it via this route. Spoiler: we can! 🙂

Getting started with authenticating

First of all, we need to authenticate to the graph API. There is some great example code on the Microsoft Graph GitHub pages that explains how to do this, so I won’t go into any detail here. The scriptblock I use to authenticate result in a $authHeader hashtable, that we can include in our REST-calls to the Graph.

Setting up variables

First, I set a few variables in my script that I can re-use in my calls:

1
2
$graphApiVersion = "Beta"
$Resource = "deviceManagement/deviceManagementScripts"

We need to use the beta version of the API, because the resources we need (deviceManagement/deviceManagementScrips) are not exposed in the currently stable version.

Calling the API

So, lets make our first call to the API to see what results we get back.

1
2
$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource/"
Invoke-RestMethod -Uri $uri -Headers $authHeader -Method Get

We set the URI we want to call, including the API version and resources specified earlier. Next, we call this URI using the invoke-restmethod cmdlet, including our authentication header we retrieved in the beginning. We use the ‘GET’ method, because we want to retrieve data. Because we set the resource to be deviceManagementScripts, the response will include the deviceManagementScripts currently in use.

Response

Narrowing down the results

The response is a PSObject with several properties. Of course, we have the most interest in the ‘value’ property, as this has the actual data we are looking for. So, rewrite our line of code to get just the ‘value’.

1
(Invoke-RestMethod -Uri $uri -Headers $authHeader -Method Get).value

This returns the actual deviceManagementScripts that are currently in use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
id                    : 63717e92-c46a-4ab8-8073-691445e9154c
displayName           : FolderRedirectionOneDrive
description           : Used to redirect common folders like documents and desktop to OneDrive
runSchedule           :
scriptContent         :
createdDateTime       : 2018-05-02T11:24:59.1103193Z
lastModifiedDateTime  : 2018-05-02T11:24:59.1103193Z
runAsAccount          : user
enforceSignatureCheck : False
fileName              : OneDriveFolderRedirectv4.ps1

In my case, this is only one script that apparently is used to redirect certain folders to OneDrive.

Getting the content

By referencing the id for this script in our API call, we can get more information on this particular object.

1
2
$detailuri = "https://graph.microsoft.com/$graphApiVersion/$Resource/63717e92-c46a-4ab8-8073-691445e9154c"
Invoke-RestMethod -Uri $detailuri -Headers $authHeader -Method Get

Again, we use the GET method to retrieve the information from the Graph API. Because we are referencing this single object, now we don’t need to distinctly retrieve the value property to get the actual data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@odata.context        : https://graph.microsoft.com/Beta/$metadata#deviceManagement/deviceManagementScripts/$entity
id                    : 63717e92-c46a-4ab8-8073-691445e9154c
displayName           : FolderRedirectionOneDrive
description           : Used to redirect common folders like documents and desktop to OneDrive
runSchedule           :
scriptContent         : 77u/I3JlZ2lvbiBmdW <snip> ZXJyb3IiDQp9DQo=
createdDateTime       : 2018-05-02T11:24:59.1103193Z
lastModifiedDateTime  : 2018-05-02T11:24:59.1103193Z
runAsAccount          : user
enforceSignatureCheck : False
fileName              : OneDriveFolderRedirectv4.ps1

For ease of reading in this output I truncated the scriptContent property shown here a bit, but as you can see we can retrieve all the information we have in the portal: the description, the runAsAccount (that can be either ‘user’ or ‘system’), if a signature check is enforced, and of course the filename of the script and the actual content.

Making it readable

The content of the script is stored (and displayed) in a Base64-encoded string. To make this human readable, we need to decode it.

1
2
$script64 = (Invoke-RestMethod -Uri $detailuri -Headers $authHeader -Method Get).scriptContent
$decodedscript = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($script64))

First, we specify the $script64 variable to store the Base64-encoding string. Next, we decode the Base64-string to UTF8 and store it in the $decodedscript variable.

When we now display this $decodedscript variable, we see the contents of the script!

1
2
3
4
5
#region functions
function Set-KnownFolderPath {
<snip>    
    $strFoutmelding = $error[0]
    Log -Message "OneDrive map niet gevonden in het register, enable OneDrive script heeft hoogstwaarschijnlijk niet gedraaid. Foutmelding: $strFoutmelding" -Type "error"}

Again, I truncated to output for readability here. Since we have this variable, we can store the contents in a file and thus save the script to our local harddrive.

1
$decodedscript | out-file c:\temp\MyPowerShellScript.ps1

Pretty neat, right? But, if we can use this to download a script, why shouldn’t we be able to upload a script this way? Let’s check. The documentation by Microsoft is pretty clear: we call the same deviceManagementSripts resource, but with a POST-method in stead of a GET. In this POST we need to include a JSON in the body with the details of the script we would like to set, including the actual content of the script in the same Base64-encoding we saw earlier.

Putting it together

So, let’s put the building blocks together. I’ve created a mind blowing Powershell-script that I stored as c:\temp\testscript.ps1

1
write-host 'Hello world!'

We get the content for this file and then encode it using Base64

1
2
$UploadScript = get-content C:\temp\testscript.ps1
$UploadScriptEncoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($UploadScript))

So now we have the $UploadScriptEncoded variable, containing the Base64-encoded script. Next, we need to build the JSON file to include in the POST to the REST API. I do this by creating a hastable with all the needed information and piping this to the ConvertTo-JSON cmdlet.

1
2
3
4
5
6
7
8
$postbody = @{
    "@odata.type" = "#microsoft.graph.deviceManagementScript";
    "displayName" = "displayname";
    "description" = "This is the description for our script";
    "scriptContent" = $UploadScriptEncoded;
    "runAsAccount" = "user";
    "enforceSignatureCheck" = "false";
    "fileName" = "testscript.ps1" } | ConvertTo-Json

In the hashtable I specify the Displayname for the script and a short description, include the scriptContent we just encoded, specify it should run in the user-context and that we don’t want to check for a valid signature. Finaly, we give the filename for the script that will be displayed in the Intune portal where the script is referenced.

To finish up, we call the API with the given parameters to do the actual uploading.

1
Invoke-RestMethod -Uri $uri -Headers $authHeader -Method Post -Body $postbody -ContentType "application/JSON"

We call the URI specified earlier, including our authentication header with the POST-method. We include the JSON-file we stored in $postbody as the body of the request, specifying that this is indeed a JSON-file.

The response indicates that file was uploaded and configured.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@odata.context        : https://graph.microsoft.com/Beta/$metadata#deviceManagement/deviceManagementScripts/$entity
id                    : 136512bd-cce8-447a-9eba-4537e149297c
displayName           : displayname
description           : This is the description for our script
runSchedule           :
scriptContent         : dwByAGkAdABlAC0AaABvAHMAdAAgACcASABlAGwAbABvACAAdwBvAHIAbABkACEAJwA=
createdDateTime       : 2018-05-04T19:52:57.254547Z
lastModifiedDateTime  : 2018-05-04T19:52:57.254547Z
runAsAccount          : user
enforceSignatureCheck : False
fileName              : testscript.ps1

We can now check the Intune portal to double check if the script is there.

Uploaded script

There you have it: using the Graph API you can do stuff you can’t do in the portal and automate many things you do Intune. For example, you can place the PowerShell scripts you deploy using Intune in a repository in (for example) VSTS and create a build sequence that uses the Graph API to update the script in Intune every time you push to master. If you haven’t played around with the Graph API, now is the time do so. The possibilities are endless.

Happy scripting!