Previously, I have used Microsoft’s Power Automate platform to receive webhooks from Jamf Pro, have the webhook data parsed, and then use the Jamf Pro API to perform some sort of action on the devices included in the webhook data. While thinking about how I can make this process more robust, I wanted to begin moving my Power Automate workflows over to Azure Runbooks. Some benefits of using Runbooks over Power Automate flows include:
Ownership: Power Automate workflows are owned by the individual who creates them. This means that if anything were to happen to that user’s account, there is the possibility that the flow may not continue to function and others within the organization would not have access to them.
Advanced Scripting Capabilities: Runbooks support PowerShell and Python scripting, offering a higher level of control and flexibility for complex automation scripts.
Control and Logging: Runbooks provide extensive logging and tracking capabilities. The ability to monitor the execution of the runbooks, view detailed logs, and track their performance and resources used over time is available when using Runbooks.
Scalability and Reliability: Runbooks are designed to operate at scale, offering high reliability and performance. They are suitable for enterprise-level automation tasks.
There is still room for Power Automate, however. Power Automate is more focused on workflow automation across various applications and servers, often with a user-friendly, low-code approach. This makes it highly effective for integrating different applications, automating routine business tasks, and enabling those with less than advanced scripting knowledge to create these automations.
Next, we’ll dig into how to use a Runbook to redeploy the Jamf Management Framework to computers that have not checked in for 30 days.
If you’d like to see this process within Power Automate, take a look at this link: https://www.jamf.com/blog/how-to-reinstall-the-jamf-frameworkthrough-the-api-with-webhooks-and...y JNUC 2022 presentation on using Power Automate to keep Macs updated: https://techitout.xyz/2022/10/12/jnuc-2022/
I’d also like to thank [@Matthew Cornell](MacAdmins Slack) for his help in developing the PowerShell script listed below.
Getting Started:
Before you can create a new runbook in Azure, you will need to create an automation account resource. The runbooks will live within this automation account.
Create an Automation Account
Create a Runbook
2. Select + Create a runbook
Create a Jamf Pro API Role and Client
Since we are going to utilize the Jamf Pro API, it may be useful to create a dedicated API Role and Client for our purpose here. To create an API Role and Client, within Jamf Pro:
Creating the PowerShell Script:
Now that we have a runbook created, it’s time to create a PowerShell script that will be used to help redeploy the Jamf Management Framework to our computers that need it.
To set the script up to receive webhook data from Jamf Pro, we need to add the following to the top of the script:
param
(
[Parameter (Mandatory=$false)]
[object] $WebhookData
)
Setting up our Variables
There are certain variables that our script will need to run. This includes our Jamf API client id, client secret, and Jamf Pro URL. I would recommend using a Keyvault to store your client id and secret rather than putting them inside of the script itself. These secrets can be called using a PowerShell cmdlet inside of your script. Additionally, you can add your Jamf URL as a variable asset to your Automation Account which can be used across all your Runbooks (https://learn.microsoft.com/enus/azure/automation/shared-resources/variabl...).
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Version and Jamf Pro Parameters
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
$scriptFunctionalName = 'Redeploy JMF - 30 Days Inactive'
$scriptVersion = '1.0.1'
Write-Output "$scriptFunctionalName, version $scriptVersion"
# Jamf Pro API Credentials
$global:jamfProInformation = @{
client_id = "06d7c45b-4d7b-4479-9831-77892c0be41f"
client_secret = "7RdyZSpRf-gvzCqwanoBap-cXxB2-JL5bWru74ua73Ge9u33mhhb"
URI = "https://server.jamfcloud.com"
}
Webhook Data Parsing
Next, we need to parse the data that is being sent to our Runbook from Jamf Pro, specifically the group added devices ids that represent the computers that have not checked in for 30 days.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Webhook Data Parsing
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if ($WebhookData){
# Get the added computer device ids
$RequestBody = $WebhookData.RequestBody
$RequestBody = $RequestBody | ConvertFrom-Json
$groupAddedDevicesIds = $RequestBody.event | Select-Object -ExpandProperty "groupAddedDevicesIds"
} else {
# Error
Write-Output "This runbook is meant to be started from a Jamf Pro webhook only."
}
Defining Functions
There are several functions that need to be created for the workflow. These functions include:
Get-BearerToken
This function uses our cliend id and secret to obtain an authorization token from the Jamf Pro API that will later be used in our API call.
Clear-BearerToken
Once the script is completed, this function will invalidate our authorization token, making sure that it can no longer be used.
Redeploy-JamfManagementFramework
The Redeploy JMF function will loop through each of the computers’ ids from the webhook data and will send an MDM command to reinstall the Jamf Management Framework. While looking at a computer’s inventory record within Jamf Pro, this management command will show as InstallEnterpriseApplication.
###########################################################################################
#
# Functions
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # Bearer Token Functions
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get a bearer token for Jamf Pro API authentication
function Get-BearerToken {
# Prepare user credentials for generating a token
$global:bearerTokenInformation = @{
}
# Add header information to the bearerTokenInformation hash table
$bearerTokenAuthHeaders = @{
"Content-Type" = "application/x-www-form-urlencoded"
}
# Add body content for the request
$bodyContent = @{
client_id = $jamfProInformation['client_id']
client_secret = $jamfProInformation['client_secret']
grant_type = "client_credentials"
}
# Get a bearer token
$bearerTokenAuthResponse = Invoke-WebRequest -Uri "$($jamfProInformation['URI'])/api/oauth/token" -Headers $bearerTokenAuthHeaders -Method Post -Body $bodyContent -ContentType "application/x-www-form-urlencoded"
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
if ($($bearerTokenAuthResponse).StatusCode -ne 200) {
Write-Output ""
Write-Output "Error generating token. Status code: $($($bearerTokenAuthResponse).StatusCode)."
exit
}
else {
$bearerTokenInformation.Add("Token", "$(($bearerTokenAuthResponse).content | ConvertFrom-Json | Select-Object -ExpandProperty "access_token")")
}
}
# Invalidate the token when finished
function Clear-BearerToken {
# Set up headers
$authHeaders = @{
}
# Add headers to the hash table
# Add bearerToken to the hash table
$authHeaders.Add("accept", "application/json")
$authHeaders.Add("Authorization", "Bearer $($bearerTokenInformation['Token'])")
# Invalidate the token
$invalidateTokenResponse = Invoke-WebRequest -Uri "$($jamfProInformation['URI'])/api/v1/auth/invalidate-token" -Method POST -Headers $authHeaders
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
if ($($invalidateTokenResponse).StatusCode -ne 204) {
Write-Output ""
Write-Output "Error invalidating token. Status code: $($($invalidateTokenResponse).StatusCode)."
exit
}
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
else {
Write-Output ""
Write-Output "Token invalidated. Status code: $($($invalidateTokenResponse).StatusCode)."
Write-Output ""
}
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # Other Functions
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Redeploy the JMF Framework
function Redeploy-JamfManagementFramework {
# For logging, output which device ids we are working with
foreach ($id in $groupAddedDevicesIds)
{
Write-Output "Redploying the Jamf Managment Framework for computer id: $id"
# Set up headers
$authHeaders = @{
}
# Add headers to the hash table
# Add bearerToken to the hash table
$authHeaders.Add("accept", "application/json")
$authHeaders.Add("Authorization", "Bearer $($bearerTokenInformation['Token'])")
# Send MDM command to install the JMF framework
$redeployFrameworkResponse = Invoke-WebRequest -Uri "$($jamfProInformation['URI'])/api/v1/jamf-management-framework/redeploy/$id" -Headers $authHeaders -Method Post
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
if ($($redeployFrameworkResponse).StatusCode -ne 202) {
Write-Output ""
Write-Output "Error sending MDM command. Status code $($($redeployFrameworkResponse).StatusCode)."
}
else {
Write-Output "MDM command successfully sent. Status code: $($($redeployFrameworkResponse).StatusCode)."
Write-Output "Device ID: $(($redeployFrameworkResponse).content | ConvertFrom-Json | Select-Object -ExpandProperty "deviceId")"
Write-Output "Command UUID: $(($redeployFrameworkResponse).content | ConvertFrom-Json | Select-Object -ExpandProperty "commandUuid")"
Write-Output "#########################################################################################"
}
}
}
Calling the functions
Now that the variables and functions have been created, we need to call our functions. Here we are only calling the functions if there are device ids to work with, otherwise, there’s no work to do.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # Call the functions
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Call the functions
if ($groupAddedDevicesIds){
Get-BearerToken
Redeploy-JamfManagementFramework
Clear-BearerToken
} else {
# Error
Write-Output "No group added devices ids to do work on, exiting."
}
Putting it all together
Below is the completed script that we just went through
param
(
[Parameter (Mandatory=$false)]
[object] $WebhookData
)
###########################################################################################
#
# Redeploy Jamf Management Framework - 30 Day Inactive Computers
#
###########################################################################################
#
# HISTORY
#
#
# Version 0.1.1, 01.30.2024, Robert Schroeder (@robjschroeder)
# _ Thanks @whetmat (Matthew Cornell) for the PowerShell help!
# - This PowerShell script will receive a webhook from Jamf Pro each time a computer is inactive for 30 days.
# Once this event has occurred, this script will attempt to re-install the Jamf Management Framework as this may be
# the reason the computer has become stale.
#
# Version 1.0.1, 07.29.2024, Robert Schroeder (@robjschroeder)
# - Updated Get-BearerToken function
#
###########################################################################################
#
# Global Variables
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Version and Jamf Pro Parameters
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
$scriptFunctionalName = 'Redeploy JMF - 30 Days Inactive'
$scriptVersion = '1.0.1'
Write-Output "$scriptFunctionalName, version $scriptVersion"
# Jamf Pro API Credentials
$global:jamfProInformation = @{
client_id = "06d7c45b-4d7b-4479-9831-77892c0be41f"
client_secret = "7RdyZSpRf-gvzCqwanoBap-cXxB2-JL5bWru74ua73Ge9u33mhhb"
URI = "https://server.jamfcloud.com"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Webhook Data Parsing
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if ($WebhookData){
# Get the added computer device ids
$RequestBody = $WebhookData.RequestBody
$RequestBody = $RequestBody | ConvertFrom-Json
$groupAddedDevicesIds = $RequestBody.event | Select-Object -ExpandProperty "groupAddedDevicesIds"
} else {
# Error
Write-Output "This runbook is meant to be started from a Jamf Pro webhook only."
}
###########################################################################################
#
# Functions
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # Bearer Token Functions
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get a bearer token for Jamf Pro API authentication
function Get-BearerToken {
# Prepare user credentials for generating a token
$global:bearerTokenInformation = @{
}
# Add header information to the bearerTokenInformation hash table
$bearerTokenAuthHeaders = @{
"Content-Type" = "application/x-www-form-urlencoded"
}
# Add body content for the request
$bodyContent = @{
client_id = $jamfProInformation['client_id']
client_secret = $jamfProInformation['client_secret']
grant_type = "client_credentials"
}
# Get a bearer token
$bearerTokenAuthResponse = Invoke-WebRequest -Uri "$($jamfProInformation['URI'])/api/oauth/token" -Headers $bearerTokenAuthHeaders -Method Post -Body $bodyContent -ContentType "application/x-www-form-urlencoded"
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
if ($($bearerTokenAuthResponse).StatusCode -ne 200) {
Write-Output ""
Write-Output "Error generating token. Status code: $($($bearerTokenAuthResponse).StatusCode)."
exit
}
else {
$bearerTokenInformation.Add("Token", "$(($bearerTokenAuthResponse).content | ConvertFrom-Json | Select-Object -ExpandProperty "access_token")")
}
}
# Invalidate the token when finished
function Clear-BearerToken {
# Set up headers
$authHeaders = @{
}
# Add headers to the hash table
# Add bearerToken to the hash table
$authHeaders.Add("accept", "application/json")
$authHeaders.Add("Authorization", "Bearer $($bearerTokenInformation['Token'])")
# Invalidate the token
$invalidateTokenResponse = Invoke-WebRequest -Uri "$($jamfProInformation['URI'])/api/v1/auth/invalidate-token" -Method POST -Headers $authHeaders
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
if ($($invalidateTokenResponse).StatusCode -ne 204) {
Write-Output ""
Write-Output "Error invalidating token. Status code: $($($invalidateTokenResponse).StatusCode)."
exit
}
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
else {
Write-Output ""
Write-Output "Token invalidated. Status code: $($($invalidateTokenResponse).StatusCode)."
Write-Output ""
}
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # Other Functions
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Redeploy the JMF Framework
function Redeploy-JamfManagementFramework {
# For logging, output which device ids we are working with
foreach ($id in $groupAddedDevicesIds)
{
Write-Output "Redploying the Jamf Managment Framework for computer id: $id"
# Set up headers
$authHeaders = @{
}
# Add headers to the hash table
# Add bearerToken to the hash table
$authHeaders.Add("accept", "application/json")
$authHeaders.Add("Authorization", "Bearer $($bearerTokenInformation['Token'])")
# Send MDM command to install the JMF framework
$redeployFrameworkResponse = Invoke-WebRequest -Uri "$($jamfProInformation['URI'])/api/v1/jamf-management-framework/redeploy/$id" -Headers $authHeaders -Method Post
# Check the response code and see if it was not successful (IF) or if it was successful (ELSE)
if ($($redeployFrameworkResponse).StatusCode -ne 202) {
Write-Output ""
Write-Output "Error sending MDM command. Status code $($($redeployFrameworkResponse).StatusCode)."
}
else {
Write-Output "MDM command successfully sent. Status code: $($($redeployFrameworkResponse).StatusCode)."
Write-Output "Device ID: $(($redeployFrameworkResponse).content | ConvertFrom-Json | Select-Object -ExpandProperty "deviceId")"
Write-Output "Command UUID: $(($redeployFrameworkResponse).content | ConvertFrom-Json | Select-Object -ExpandProperty "commandUuid")"
Write-Output "#########################################################################################"
}
}
}
###########################################################################################
#
# Script Work
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # Call the functions
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Call the functions
if ($groupAddedDevicesIds){
Get-BearerToken
Redeploy-JamfManagementFramework
Clear-BearerToken
} else {
# Error
Write-Output "No group added devices ids to do work on, exiting."
}
Finishing Up
Now that we have our PowerShell Script for our Runbook, we need to save it and publish it. Once the runbook is published, we can create a webhook for it.
Create A Webhook
3. On the Add Webhook page, select Create new webhook4. Enter the Name for your webhook and give it an expiration date
6. Select OK to return to the Add Webhook page.
7. Review the Parameters page and Select OK to return to the Add Webhook page.
8. From the Add Webhook page, select Create. The webhook is now created.
Add the webhook to Jamf Pro
That’s It!
You now have an Azure Runbook up and running, obtaining webhook data from Jamf Pro and automatically attempting to remedy your stale computers.
Now that you have one down, what other automation can you create?