robjschroeder
Contributor

robjschroeder_0-1734631877648.jpeg

 


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 

  1. Sign in to the Azure Portal 
  1. From the top menu, select + Create a resource 
  2. Under Categories, select IT & Management Tools, and then select Automation (If Automation is not listed, you can search for it in the Search Services and Marketplace bar)  
  3. On the Basics tab, provide the essential 
  4. for your Automation account. After completing this tab, you can choose to further customize your Automation account by setting options on the other tabs, or you can select Review + create to accept the default options and proceed with creating the account. (For more information: https://learn.microsoft.com/en-us/azure/automation/quickstarts/create-azure-automationaccount-portal) 

Create a Runbook 

  1. Within your Automation account page, under Process Automation, select Runbooks 

robjschroeder_0-1734631061571.jpeg

 

2.  Select + Create a runbook 

  • Give your runbook a name 
  • From the Runbook type drop-down menu, select PowerShell  
  • From the Runtime version drop-down, select 7.2  
  • (Optional) Add a description for your runbook  
  • Select Review + create 

 

robjschroeder_1-1734631106557.jpeg

 

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: 

  1. Navigate to Settings > System > API Roles and clients 
  1. Select + New in the top-right corner to create a new API Role  
  1. Give your API Role a name and assign the following privileges: 
  • Read Computer Check-In 
  • Read Computers 
  • Flush Policy Logs 
  • Send Computer Remote Command to Install Package 
  1. And Save 
  1. Next, click on the API Clients tab and select + New in the top-right corner to create a new API Client 
  1. Give your API Client a name 
  1. Assign your API Role that you just created and select to enable your API Client. Take note of your API client ID and secret. You will not be able to retrieve the secret again. 

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 

  1. From your Automation account page, make sure to select the Runbook and go to the Overview page. 
  2. Select Add webhook to open the Add Webhook page. 

    robjschroeder_2-1734631567155.jpeg

     

    3. On the Add Webhook page, select Create new webhookrobjschroeder_3-1734631610459.jpeg4. Enter the Name for your webhook and give it an expiration date
    5. Make sure to copy your webhook’s URL as you won’t be able to retrieve it after you leave this page. 

 

robjschroeder_4-1734631744481.jpeg

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 

  1. The URL that you copied before needs to be decoded before it can be added to Jamf Pro. You can use the following page to help decode your URL: https://www.urldecoder.org 
  1. Now, in Jamf Pro, navigate to Settings > Global > Webhooks  
  1. Click New 
  1. Enter a display name for the webhook 
  1. Paste your decoded URL into the URL section of the webhook 
  1. Authentication can be set to none.  
  1. Change the Content Type to JSON  
  1. Change the Webhook Event to SmartGroupComputerMembershipChange  
  1. Change the target to a Smart Computer Group that identifies computers that have not checked-in for 30 days. 
  1. Save 

 

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? 

 



 

 

 

1 Comment
Contributors