Invoke-Command alternative for Intune-managed Windows devices

Invoke-Command alternative for Intune-managed Windows devices

ยท

6 min read

Continue reading if you want to have the ability to run PowerShell code (similar to Invoke-Command) against your Windows Intune-managed hybrid and cloud-only clients in a nearly real-time fashion and at the same time get the code results back ๐Ÿ˜Ž

๐Ÿ’ก
When I say "nearly real-time", Iโ€™m referring to a delay of just a few minutes, which, in the context of Intune, is practically instantaneous ๐Ÿ˜

I came across a thread on Reddit discussing the use of on-demand remediations in Intune to retrieve basic client data. However, I couldnโ€™t find any existing PowerShell function for this purpose, so I decided to create one myself.

Meet my shiny, brand-new Invoke-IntuneCommand function (part of the IntuneStuff module).

Use cases

  • You want to run some one-time PowerShell code against your Windows devices ASAP

  • You want to get some Windows clients' data back (is a service running, is an application installed,...)


How does Invoke-IntuneCommand work?

  • New Intune remediation is created in the background utilizing the code to be invoked within the detection component

  • Remediation is invoked via an on-demand request for each selected device

  • The function either waits for the remediation to complete or for the specified timeout to be reached (10 minutes by default, but can be overridden) in a which-comes-first manner

  • Remediation code results are retrieved (and converted to an object if the result is a JSON string)

  • Remediation is deleted (this step can vary based on used parameters and remediation invocation results)

The whole process can be nicely seen when Verbose parameter is used ๐Ÿ‘‡


What does the Invoke-IntuneCommand output look like

Function Invoke-IntuneCommand returns a custom object for each device where the command was run.

The returned object contains several properties

  • DeviceId - managed device ID (Intune ID)

  • DeviceName - name of the device

  • LastSyncDateTime - when it contacted Intune the last time

  • ProcessedOutput - returned output converted to object if compressed JSON was returned originally

  • Output - string output of the invoked command (just last line!)

  • Error - thrown error if any

  • Status - overall invoked remediation status

    • pending if not being run

    • fail if some error was thrown

    • success if everything was ok

If your code throws an error, you will see the error message in Error property and Status will be fail


Requirements

  • Module IntuneStuff

  • Graph permissions

    • DeviceManagementConfiguration.Read.All

    • DeviceManagementManagedDevices.Read.All

    • DeviceManagementManagedDevices.PrivilegedOperations.All

  • License to use Intune Remediation

    • Windows 10/11 Enterprise E3 or E5 (included in Microsoft 365 F3, E3, or E5)

    • Windows 10/11 Education A3 or A5 (included in Microsoft 365 A3 or A5)

    • Windows 10/11 Virtual Desktop Access (VDA) per user


Examples

Authenticate to Graph API with correct scopes

Connect-MgGraph -Scopes DeviceManagementConfiguration.Read.All, DeviceManagementManagedDevices.Read.All, DeviceManagementManagedDevices.PrivilegedOperations.All

Run some command against all clients

$deviceNameList = Get-MgBetaDeviceManagementManagedDevice -Filter "OperatingSystem eq 'Windows' and OwnerType eq 'company'" -all -Property DeviceName | select -ExpandProperty DeviceName

Invoke-Intunecommand -deviceName $deviceNameList -command "New-Item C:\temp2 -ItemType Directory" -Verbose

Create a folder on the selected clients

Invoke-Intunecommand -deviceName "PC-01" -command "New-Item C:\temp2 -ItemType Directory" -Verbose

In a few minutes (if the device is online) you should see an output similar to this one ๐Ÿ‘‡

Hence folder C:\temp2 was created.

Check if the folder exists and return a simple string

$command = @'
if (Test-Path "C:\temp2") {
    Write-Output "Folder exists"
} else {
    Write-Output "Folder doesn't exist"
}
'@

$result = Invoke-Intunecommand -deviceName "PC-01" -command $command -Verbose

In a few minutes (if the device is online) you should see an output similar to this one ๐Ÿ‘‡

Return an array of objects (of all running client PowerShell processes)

$command = @'
$result = Get-Process powershell | select processname, id
$result | ConvertTo-Json -Compress
'@

$result = Invoke-Intunecommand -deviceName "PC-01" -command $command -Verbose

# output the results
$result

# output just returned JSON string converted back to the object
$result.processedOutput

In a few minutes (if the device is online) you should see an output similar to this one ๐Ÿ‘‡

Because we have converted the output to compressed JSON, it was automatically converted back to an object and saved in the ProcessedOutput property.

Maximize the amount of returned data by compression

If the output you need is over the 2048 chars hard limit, you can reduce its length by compressing it using ConvertTo-CompressedString function (part of the CommonStuff module). This can make it 10x times smaller (but it depends on the output content solely)!

Invoke-Intunecommand function automatically tries to decompress the received output, so you don't have to worry about this part.

$command = @'
    $result = Get-Process powershell | select processname, id
    $result | ConvertTo-Json -Compress | ConvertTo-CompressedString
'@

# invoke selected command
$result = Invoke-Intunecommand -deviceName "PC-01" -command $command -Verbose

# output the results
$result

# output just returned compressed JSON string converted back to the object
$result.processedOutput

Adding command definition to the code block

Any command you run on the remote device needs to exist there. This specifically applies to the custom functions. You can add such a function directly to the invoked command like this

$command = @'
    # Get-SomeData definition is inside the code block defined manually
    function Get-SomeData {
        do some stuff...
    }

    Get-SomeData | ConvertTo-Json -Compress
'@

Or use parameter prependCommandDefinition ๐Ÿ˜Ž like this

$command = @'
    # ConvertTo-CompressedString definition is added automatically thanks to 'prependCommandDefinition' parameter 
    $result = Get-Process powershell | select processname, id
    $result | ConvertTo-Json -Compress | ConvertTo-CompressedString
'@

# text definition of the ConvertTo-CompressedString function will be added to the command, so it doesn't matter whether it is available on the remote system
Invoke-Intunecommand -deviceName "PC-01" -prependCommandDefinition ConvertFrom-CompressedString -command $command -Verbose

Limitations

  • Returned output is limited to 2048 chars (Intune inner limitation)

  • Only the last code output line is returned

    • if you run the following code write-output 'aaa'; write-output 'bbb'

    • the returned output will be just 'bbb'!

  • Write-Host cannot be used, instead use Write-Output

  • If the "helper" remediation is removed, any clients that have not already invoked it will be unable to do so


Tips

  • Definitely check the function help (Get-Help Invoke-IntuneCommand -Full) to get more details & examples

  • To get as much data back as possible, convert the output using ConvertTo-CompressedString function (part of the CommonStuff module) after converting it to the compressed JSON. This way you can get 10x more data back. And the Invoke-IntuneCommand will try to decompress it automatically ๐Ÿ‘

  • Invoke the function with the Verbose parameter to obtain more detailed information, such as the remaining time until the deadline is reached

  • If you wish to transform the result back into an object, ensure that your command returns a single result, specifically the compressed JSON

    • Get-Process powershell | select processname, id | ConvertTo-Json -Compress
  • If your command throws an error, the whole invocation takes more time, because a dummy remediation command (exit 0) will be run too (because we are using remediation and if the detection part fails, the remediation part takes place)

  • Helper remediations are named like _invCmd_<yyyy.MM.dd_HH:mm>

Did you find this article valuable?

Support Ondrej Sebela by becoming a sponsor. Any amount is appreciated!

ย