Triggering Graph commands from a remediation – Securely!

Picture the scene, you have developed a remediation to do some sort of device magic involving Graph API and device.

Then you find out that whilst you can absolutely add app registration secrets in your remediation, they are displayed in plain text on the device, certainly not an ideal situation!

Fortunately there is a solution using Azure runbooks and webhooks which lets us either:
a) Store the app reg details securely within Azure
b) Connect using a managed identity

If you aren’t familiar with setting up an Azure runbook and connecting, you can learn about that in my previous post:

In this example I am going to harvest the device hash and upload to Autopilot after watching Steve’s video here:

Other examples could be:

  • Triggering user logout
  • Setting a grouptag
  • Adding to a group

The opportunities are endless, anything that can be done in Graph!

Automation Account & Runbook

In this example, we need an Azure automation account where we need to add the “Microsoft.Graph.Authentication” module onto it:

Make sure it matches the PowerShell version you use to add the script!

If you are using the more secure managed identity for connection, make sure you also give the identity the permissions required for the task, in this example “DeviceManagementServiceConfig.ReadWrite.All”

To do this, we navigate to Enterprise Applications in Entra and filter on Managed Identities:

Then follow this guide and script to add the permissions:

Now we have our automation account configured, we need to add the script. I’ve taken Steve’s script and amended it slightly, changing the device queries to instead retrieve them from the webhook data:

[cmdletbinding()]
    
param
(
    [object] $WebHookData #Webhook data for Azure Automation

    )

##WebHook Data

if ($WebHookData){
$bodyData = ConvertFrom-Json -InputObject $WebHookData.RequestBody
$serialNumber = ((($bodyData.serialNumber) | out-string).trim())
$hardwareId = ((($bodyData.hardwareId) | out-string).trim())
$groupTag = ((($bodyData.groupTag) | out-string).trim())
}

Connect-MgGraph -Identity
 
 # CONSTRUCT JSON
 $json = @"
 {
     "@odata.type": "#microsoft.graph.importedWindowsAutopilotDeviceIdentity",
     "groupTag":"$groupTag",
     "serialNumber":"$serialNumber",
     "productKey":"",
     "hardwareIdentifier":"$hardwareId",
     "assignedUserPrincipalName":"",
     "state":{
         "@odata.type":"microsoft.graph.importedWindowsAutopilotDeviceIdentityState",
         "deviceImportStatus":"pending",
         "deviceRegistrationId":"",
         "deviceErrorCode":0,
         "deviceErrorName":""
     }
 }
"@
 
 # POST DEVICE
 Invoke-MgGraphRequest -Method Post -Body $json -ContentType "application/json" -Uri "https://graph.microsoft.com/beta/deviceManagement/importedWindowsAutopilotDeviceIdentities"

As you can see, the only parameter is the $webhookdata itself which is one large JSON of data.

We then convert that from JSON and retrieve our individual values from it. Using this you can add pretty much anything to the JSON and just grab it within the runbook.

Next, we connect to Graph using the managed identity (see, no secrets here).

Construct the JSON and send it to Graph.

Remediation Script

As this stands, we have a runbook which won’t do anything, we need to trigger it and pass some data which is where the remediations come in, for those not licensed for remediations, this would also work just as well from a platform script!

One secret with remediations is that a remediation script isn’t strictly required, you can do everything in a detection. BUT, I don’t want to run this more than once or we will just get Graph errors so I’ll look for a registry key which is created in the remediation script once completed.

We need to query WMI so this is a local machine level script, therefore add the registry key at HKLM

$regkey = "HKLM:\Software\Harvester"
$regname = "Harvested"
$regvalue = "completed"
Try {
    $Registry = Get-ItemProperty -Path $regkey -Name $regname -ErrorAction Stop | Select-Object -ExpandProperty $regname
    If ($Registry -eq $regvalue){
        Write-Output "Compliant"
        Exit 0
    } 
    Write-Warning "Not Compliant"
    Exit 1
} 
Catch {
    Write-Warning "Not Compliant"
    Exit 1
}

That’s the easy bit done, now for the remediation.

##Create a reg key in HKLM
$regkey = "HKLM:\Software\Harvester"
$regname = "Harvested"
$regvalue = "completed"
   New-Item -Path $regkey -Force | Out-Null
   New-ItemProperty -Path $regkey -Name "$regname" -Value "$regvalue" -PropertyType String -Force | Out-Null

##Get Hardware Details
# GET HARDWARE INFO
$serialNumber = (Get-WmiObject -Class Win32_BIOS).SerialNumber
$hardwareId = ((Get-WmiObject -Namespace root/cimv2/mdm/dmmap -Class MDM_DevDetail_Ext01 -Filter "InstanceID='Ext' AND ParentID='./DevDetail'").DeviceHardwareData)
$groupTag = "M365"

$webhook = "WEBHOOK URL HERE"

##Create webhook array with username
$webhookData = @{
    serialNumber = $serialNumber
    hardwareId = $hardwareId
    groupTag = $groupTag
}

##Convert to JSON
$body = $webhookData | ConvertTo-Json

##Invoke Webhook
Invoke-WebRequest -Method Post -Uri $webhook -Body $body -UseBasicParsing
write-output "Hardware hash sent to Azure"

Create the reg key to stop it triggering again.

Then grab the hardware info into our variables which will be used to create the hash.

Update the URL to the webhook created earlier.

Then it’s simply a case of creating an array containing the hardware info and convert it to JSON, remember we convertfrom-json in the runbook.

Finally, call the webhook with our data.

I’ve also added a “write-output” at the end so we can see the status in the remediations panel.

You can find all of the scripts on GitHub here:

https://github.com/andrew-s-taylor/public/tree/main/Powershell%20Scripts/Intune/Harvest-hash-remediation

7 thoughts on “Triggering Graph commands from a remediation – Securely!”

  1. Is that effectively “security by obscurity”, i.e. if anyone discovered the URL they could add devices to your tenant?

    Reply
    • It’s always a risk. In any live ones I have I usually send a passkey of some sort to inspect as well, reading a file or a reg key the users can’t access as an example.

      Reply
      • Hello Andrew, I agree with Michael, although I have used the similar approach as well. When calling a webhook, I am also sending a string (not to call it a password), that is checked in the runbook. Therefore, when someone would guess the link, he would also have to know the agreed string, to proceed with the runbook (I know, Intune stores the script locally, unencrypted, so it is not really a security barrier, just a small obstacle).
        I have been thinking recently how to maybe use the Intune device certificate to add more security to webhook calls, but have not gone into the details yet. What would be your thoughts on such approach please?

        Reply
        • It’s always a tricky one, obviously depending on the remediation. Ideally they would be encrypted (if anyone from Microsoft is reading, please encrypt scripts).
          Storing the string somewhere on the device the user can’t access might help and just grab it directly in the string.
          Device cert would probably work in some situations, but it’s how you would read it at the runbook side. You could also grab the device ID from the registry and pass that to the runbook and then query graph that the ID does exist and matches the hostname.
          None are perfect fixes, it’s just making it tricky enough to stop most people

          Reply
    • good point, what if we try to convert the script to Base64 encoding? I haven’t tried it on this scenario I’ve used inside of PPGK (not protected by password) so if anyone would try to get its content it’d give them a hard time. sometimes I don’t want automation piece of code related to intune/autopilot/other stuff used in PPKGs being wide open

      Reply
    • Would running the script inside of a Win32 app expose the webhook at all? I would think not.

      Obviously, this wouldn’t work with remediation, but with one-time jobs.

      Reply

Leave a Comment