thanks for the tip that was decent. i recently had to tackle this issue for a dozen or so laptops that were enrolled prior to 11.5 of macOS; i ended up using the following script, it worked well enough after the dependencies wqere installed on the mac.
#!/usr/bin/env python3
####
#
# SetRecoveryLockJAMF.py
#
#
#
# Script to set recovery lock for macOS computers in JAMF Pro
# Requires:
# Python3
# Python module: requests (can be installed by running 'python3 -m pip install requests')
#
# Adapted from https://github.com/shbedev/jamf-recovery-lock
#
####
### User-edited Variables ###
# Define how we connect to JAMF
jamf_url = 'jamf.myorg.edu:8443'
jamf_api_username = 'jamf_api'
jamf_api_password = 'ApiPasswordGoesHere'
########## DO NOT EDIT BELOW THIS LINE ##########
import argparse, sys
# Initialize command line argument parser
parser = argparse.ArgumentParser()
# Adding command line arguments
parser.add_argument(
"SearchString",
help = "String to use to search JAMF computer names"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-p", "--Passcode",
help = "Specify Recovery Lock passcode (default is blank)"
)
group.add_argument(
"-r", "--RandomPasscode", nargs='?', const=20,
help = "Generate a different random Recovery Lock passcode for each computer (default length is 20, specify a value for a different length)"
)
# Read arguments from command line
args = parser.parse_args()
### From git project auth/basic_auth.py
import base64
def auth_token():
# create base64 encoded string of jamf API user credetinals
credentials_str = f'{jamf_api_username}:{jamf_api_password}'
data_bytes = credentials_str.encode("utf-8")
encoded_bytes = base64.b64encode(data_bytes)
encoded_str = encoded_bytes.decode("utf-8")
return encoded_str
### From git project auth/bearer_auth.py
import os, time, requests
# current working directory
cwd = os.path.dirname(os.path.realpath(__file__))
# path of token file in current working directory
token_file = f'{cwd}/token.txt'
def request_token():
"""Generate an auth token from API"""
headers = {
'Accept': 'application/json',
'Authorization': f'Basic {auth_token()}',
'Content-Type': 'application/json',
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/v1/auth/token', headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
return response.json()['token']
def get_token():
"""Returns a token from local cache or API request"""
current_time = int(time.time())
# check if token is cached and if it is less than 30 minutes old
if os.path.exists(token_file) and ((current_time - 1800) < os.stat(token_file)[-1]):
# return a cached token from file
return read_token_from_local()
else:
# return a token from API
return get_token_from_api()
def get_token_from_api():
"""Returns a token from an API request"""
token = request_token()
cache_token(token)
return token
def cache_token(token):
"""
Cache token to local file
Parameters:
token - str
"""
with open(token_file, 'w') as file_obj:
file_obj.write(token)
def read_token_from_local():
"""Read cached token from local file"""
with open(token_file, 'r') as file_obj:
token = file_obj.read().strip()
return token
### From git project computers.py
import requests
import math
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def get_computer_count():
"""
Returns the number of computers in Jamf Pro
"""
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page=0&page-size=1&sort=id%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
count = response.json()['totalCount']
#print("Number of computers in JAMF = " + str(count))
return count
computers_per_page = 1000
number_of_pages = math.ceil(get_computer_count() / computers_per_page)
def get_arm64(filter = None):
"""
Returns Jamf IDs of all arm64 type computers
Parameters:
filter - (e.g. 'filter=general.name=="jdoe-mbp"'). If empty, returns all computers.
Computer name in filter is not case sensitive
"""
computers_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page={pageIndex}&page-size={computers_per_page}&sort=id%3Aasc&{filter}',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer in computers:
if computer['hardware']['processorArchitecture'] == 'arm64':
computers_id.append(computer['id'])
if computers_id == []:
sys.exit("No Apple Silicon computers found in Jamf that match search string.")
return computers_id
def get_mgmt_id(computers_id):
"""
Returns Jamf computers management id
Parameters:
computers_id - (e.g. ['10', '12']]). List of Jamf computers id
"""
computers_mgmt_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url = f'https://{jamf_url}/api/preview/computers?page={pageIndex}&page-size={computers_per_page}&sort=name%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer_id in computers_id:
for computer in computers:
# Find computers that given computer id in list of computers
if computer['id'] == computer_id:
computer_mgmt_id = computer['managementId']
computer_name = computer['name']
# Add computer to list
computers_mgmt_id.append({
'id': computer_id,
'name': computer_name,
'mgmt_id': computer_mgmt_id
})
break
return computers_mgmt_id
### From git project recovery_lock.py
import requests
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def set_key(computer_name, management_id, recovery_lock_key):
"""Sets a Recovery Lock key for a given computer"""
print(f'Settings recovery lock key: {recovery_lock_key} for {computer_name}')
payload = {
'clientData': [
{
'managementId': f'{management_id}',
'clientType': 'COMPUTER'
}
],
'commandData': {
'commandType': 'SET_RECOVERY_LOCK',
'newPassword': f'{recovery_lock_key}'
}
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/preview/mdm/commands', headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
### From git project main.py
from random import randint
computers_id = get_arm64('filter=general.name=="*'+args.SearchString+'*"')
computers_mgmt_id = get_mgmt_id(computers_id)
print("These are the changes you will be making:")
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
print(" "+computer_name+" will have its Recovery Lock set to "+args.Passcode)
elif args.RandomPasscode:
print(" "+computer_name+" will have its Recovery Lock set to a random "+str(args.RandomPasscode)+"-digit number")
else:
print(" "+computer_name+" will have its Recovery Lock cleared")
print("")
go_ahead = input("Do you wish to proceed? (y/n)")
print("")
if go_ahead == ("y" or "Y"):
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
recovery_lock_key = args.Passcode
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
elif args.RandomPasscode:
rand_low_val=pow(10,(int(args.RandomPasscode) - 1))
rand_val_high=pow(10,int(args.RandomPasscode)) - 1
recovery_lock_key = randint(rand_low_val,rand_val_high)
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
else:
recovery_lock_key = ''
print(" Command sent to clear Recovery Lock on "+computer_name)
set_key(computer_name, computer_mgmt_id, recovery_lock_key)
print("")
thanks for the tip that was decent. i recently had to tackle this issue for a dozen or so laptops that were enrolled prior to 11.5 of macOS; i ended up using the following script, it worked well enough after the dependencies wqere installed on the mac.
#!/usr/bin/env python3
####
#
# SetRecoveryLockJAMF.py
#
#
#
# Script to set recovery lock for macOS computers in JAMF Pro
# Requires:
# Python3
# Python module: requests (can be installed by running 'python3 -m pip install requests')
#
# Adapted from https://github.com/shbedev/jamf-recovery-lock
#
####
### User-edited Variables ###
# Define how we connect to JAMF
jamf_url = 'jamf.myorg.edu:8443'
jamf_api_username = 'jamf_api'
jamf_api_password = 'ApiPasswordGoesHere'
########## DO NOT EDIT BELOW THIS LINE ##########
import argparse, sys
# Initialize command line argument parser
parser = argparse.ArgumentParser()
# Adding command line arguments
parser.add_argument(
"SearchString",
help = "String to use to search JAMF computer names"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-p", "--Passcode",
help = "Specify Recovery Lock passcode (default is blank)"
)
group.add_argument(
"-r", "--RandomPasscode", nargs='?', const=20,
help = "Generate a different random Recovery Lock passcode for each computer (default length is 20, specify a value for a different length)"
)
# Read arguments from command line
args = parser.parse_args()
### From git project auth/basic_auth.py
import base64
def auth_token():
# create base64 encoded string of jamf API user credetinals
credentials_str = f'{jamf_api_username}:{jamf_api_password}'
data_bytes = credentials_str.encode("utf-8")
encoded_bytes = base64.b64encode(data_bytes)
encoded_str = encoded_bytes.decode("utf-8")
return encoded_str
### From git project auth/bearer_auth.py
import os, time, requests
# current working directory
cwd = os.path.dirname(os.path.realpath(__file__))
# path of token file in current working directory
token_file = f'{cwd}/token.txt'
def request_token():
"""Generate an auth token from API"""
headers = {
'Accept': 'application/json',
'Authorization': f'Basic {auth_token()}',
'Content-Type': 'application/json',
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/v1/auth/token', headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
return response.json()['token']
def get_token():
"""Returns a token from local cache or API request"""
current_time = int(time.time())
# check if token is cached and if it is less than 30 minutes old
if os.path.exists(token_file) and ((current_time - 1800) < os.stat(token_file)[-1]):
# return a cached token from file
return read_token_from_local()
else:
# return a token from API
return get_token_from_api()
def get_token_from_api():
"""Returns a token from an API request"""
token = request_token()
cache_token(token)
return token
def cache_token(token):
"""
Cache token to local file
Parameters:
token - str
"""
with open(token_file, 'w') as file_obj:
file_obj.write(token)
def read_token_from_local():
"""Read cached token from local file"""
with open(token_file, 'r') as file_obj:
token = file_obj.read().strip()
return token
### From git project computers.py
import requests
import math
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def get_computer_count():
"""
Returns the number of computers in Jamf Pro
"""
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page=0&page-size=1&sort=id%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
count = response.json()['totalCount']
#print("Number of computers in JAMF = " + str(count))
return count
computers_per_page = 1000
number_of_pages = math.ceil(get_computer_count() / computers_per_page)
def get_arm64(filter = None):
"""
Returns Jamf IDs of all arm64 type computers
Parameters:
filter - (e.g. 'filter=general.name=="jdoe-mbp"'). If empty, returns all computers.
Computer name in filter is not case sensitive
"""
computers_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page={pageIndex}&page-size={computers_per_page}&sort=id%3Aasc&{filter}',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer in computers:
if computer['hardware']['processorArchitecture'] == 'arm64':
computers_id.append(computer['id'])
if computers_id == []:
sys.exit("No Apple Silicon computers found in Jamf that match search string.")
return computers_id
def get_mgmt_id(computers_id):
"""
Returns Jamf computers management id
Parameters:
computers_id - (e.g. ['10', '12']]). List of Jamf computers id
"""
computers_mgmt_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url = f'https://{jamf_url}/api/preview/computers?page={pageIndex}&page-size={computers_per_page}&sort=name%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer_id in computers_id:
for computer in computers:
# Find computers that given computer id in list of computers
if computer['id'] == computer_id:
computer_mgmt_id = computer['managementId']
computer_name = computer['name']
# Add computer to list
computers_mgmt_id.append({
'id': computer_id,
'name': computer_name,
'mgmt_id': computer_mgmt_id
})
break
return computers_mgmt_id
### From git project recovery_lock.py
import requests
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def set_key(computer_name, management_id, recovery_lock_key):
"""Sets a Recovery Lock key for a given computer"""
print(f'Settings recovery lock key: {recovery_lock_key} for {computer_name}')
payload = {
'clientData': [
{
'managementId': f'{management_id}',
'clientType': 'COMPUTER'
}
],
'commandData': {
'commandType': 'SET_RECOVERY_LOCK',
'newPassword': f'{recovery_lock_key}'
}
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/preview/mdm/commands', headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
### From git project main.py
from random import randint
computers_id = get_arm64('filter=general.name=="*'+args.SearchString+'*"')
computers_mgmt_id = get_mgmt_id(computers_id)
print("These are the changes you will be making:")
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
print(" "+computer_name+" will have its Recovery Lock set to "+args.Passcode)
elif args.RandomPasscode:
print(" "+computer_name+" will have its Recovery Lock set to a random "+str(args.RandomPasscode)+"-digit number")
else:
print(" "+computer_name+" will have its Recovery Lock cleared")
print("")
go_ahead = input("Do you wish to proceed? (y/n)")
print("")
if go_ahead == ("y" or "Y"):
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
recovery_lock_key = args.Passcode
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
elif args.RandomPasscode:
rand_low_val=pow(10,(int(args.RandomPasscode) - 1))
rand_val_high=pow(10,int(args.RandomPasscode)) - 1
recovery_lock_key = randint(rand_low_val,rand_val_high)
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
else:
recovery_lock_key = ''
print(" Command sent to clear Recovery Lock on "+computer_name)
set_key(computer_name, computer_mgmt_id, recovery_lock_key)
print("")
Cool, great to see that there is a batch operation script.
thanks for the tip that was decent. i recently had to tackle this issue for a dozen or so laptops that were enrolled prior to 11.5 of macOS; i ended up using the following script, it worked well enough after the dependencies wqere installed on the mac.
#!/usr/bin/env python3
####
#
# SetRecoveryLockJAMF.py
#
#
#
# Script to set recovery lock for macOS computers in JAMF Pro
# Requires:
# Python3
# Python module: requests (can be installed by running 'python3 -m pip install requests')
#
# Adapted from https://github.com/shbedev/jamf-recovery-lock
#
####
### User-edited Variables ###
# Define how we connect to JAMF
jamf_url = 'jamf.myorg.edu:8443'
jamf_api_username = 'jamf_api'
jamf_api_password = 'ApiPasswordGoesHere'
########## DO NOT EDIT BELOW THIS LINE ##########
import argparse, sys
# Initialize command line argument parser
parser = argparse.ArgumentParser()
# Adding command line arguments
parser.add_argument(
"SearchString",
help = "String to use to search JAMF computer names"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-p", "--Passcode",
help = "Specify Recovery Lock passcode (default is blank)"
)
group.add_argument(
"-r", "--RandomPasscode", nargs='?', const=20,
help = "Generate a different random Recovery Lock passcode for each computer (default length is 20, specify a value for a different length)"
)
# Read arguments from command line
args = parser.parse_args()
### From git project auth/basic_auth.py
import base64
def auth_token():
# create base64 encoded string of jamf API user credetinals
credentials_str = f'{jamf_api_username}:{jamf_api_password}'
data_bytes = credentials_str.encode("utf-8")
encoded_bytes = base64.b64encode(data_bytes)
encoded_str = encoded_bytes.decode("utf-8")
return encoded_str
### From git project auth/bearer_auth.py
import os, time, requests
# current working directory
cwd = os.path.dirname(os.path.realpath(__file__))
# path of token file in current working directory
token_file = f'{cwd}/token.txt'
def request_token():
"""Generate an auth token from API"""
headers = {
'Accept': 'application/json',
'Authorization': f'Basic {auth_token()}',
'Content-Type': 'application/json',
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/v1/auth/token', headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
return response.json()['token']
def get_token():
"""Returns a token from local cache or API request"""
current_time = int(time.time())
# check if token is cached and if it is less than 30 minutes old
if os.path.exists(token_file) and ((current_time - 1800) < os.stat(token_file)[-1]):
# return a cached token from file
return read_token_from_local()
else:
# return a token from API
return get_token_from_api()
def get_token_from_api():
"""Returns a token from an API request"""
token = request_token()
cache_token(token)
return token
def cache_token(token):
"""
Cache token to local file
Parameters:
token - str
"""
with open(token_file, 'w') as file_obj:
file_obj.write(token)
def read_token_from_local():
"""Read cached token from local file"""
with open(token_file, 'r') as file_obj:
token = file_obj.read().strip()
return token
### From git project computers.py
import requests
import math
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def get_computer_count():
"""
Returns the number of computers in Jamf Pro
"""
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page=0&page-size=1&sort=id%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
count = response.json()['totalCount']
#print("Number of computers in JAMF = " + str(count))
return count
computers_per_page = 1000
number_of_pages = math.ceil(get_computer_count() / computers_per_page)
def get_arm64(filter = None):
"""
Returns Jamf IDs of all arm64 type computers
Parameters:
filter - (e.g. 'filter=general.name=="jdoe-mbp"'). If empty, returns all computers.
Computer name in filter is not case sensitive
"""
computers_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page={pageIndex}&page-size={computers_per_page}&sort=id%3Aasc&{filter}',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer in computers:
if computer['hardware']['processorArchitecture'] == 'arm64':
computers_id.append(computer['id'])
if computers_id == []:
sys.exit("No Apple Silicon computers found in Jamf that match search string.")
return computers_id
def get_mgmt_id(computers_id):
"""
Returns Jamf computers management id
Parameters:
computers_id - (e.g. ['10', '12']]). List of Jamf computers id
"""
computers_mgmt_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url = f'https://{jamf_url}/api/preview/computers?page={pageIndex}&page-size={computers_per_page}&sort=name%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer_id in computers_id:
for computer in computers:
# Find computers that given computer id in list of computers
if computer['id'] == computer_id:
computer_mgmt_id = computer['managementId']
computer_name = computer['name']
# Add computer to list
computers_mgmt_id.append({
'id': computer_id,
'name': computer_name,
'mgmt_id': computer_mgmt_id
})
break
return computers_mgmt_id
### From git project recovery_lock.py
import requests
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def set_key(computer_name, management_id, recovery_lock_key):
"""Sets a Recovery Lock key for a given computer"""
print(f'Settings recovery lock key: {recovery_lock_key} for {computer_name}')
payload = {
'clientData': [
{
'managementId': f'{management_id}',
'clientType': 'COMPUTER'
}
],
'commandData': {
'commandType': 'SET_RECOVERY_LOCK',
'newPassword': f'{recovery_lock_key}'
}
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/preview/mdm/commands', headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
### From git project main.py
from random import randint
computers_id = get_arm64('filter=general.name=="*'+args.SearchString+'*"')
computers_mgmt_id = get_mgmt_id(computers_id)
print("These are the changes you will be making:")
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
print(" "+computer_name+" will have its Recovery Lock set to "+args.Passcode)
elif args.RandomPasscode:
print(" "+computer_name+" will have its Recovery Lock set to a random "+str(args.RandomPasscode)+"-digit number")
else:
print(" "+computer_name+" will have its Recovery Lock cleared")
print("")
go_ahead = input("Do you wish to proceed? (y/n)")
print("")
if go_ahead == ("y" or "Y"):
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
recovery_lock_key = args.Passcode
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
elif args.RandomPasscode:
rand_low_val=pow(10,(int(args.RandomPasscode) - 1))
rand_val_high=pow(10,int(args.RandomPasscode)) - 1
recovery_lock_key = randint(rand_low_val,rand_val_high)
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
else:
recovery_lock_key = ''
print(" Command sent to clear Recovery Lock on "+computer_name)
set_key(computer_name, computer_mgmt_id, recovery_lock_key)
print("")
Am I correct in that if this is in a policy scoped to devices without a recovery password set, it will assign a random key to each individually? Or is it designed to be run outside of Jamf and will pick out those devices needing a password and assign? Sorry for the noob questions - this looks really useful but I'm barely literate in python at this point. Was also wondering if you could repost with proper code formatting - was running it through an interpreter and it appears to be missing all its necessary indents. thanks much
Am I correct in that if this is in a policy scoped to devices without a recovery password set, it will assign a random key to each individually? Or is it designed to be run outside of Jamf and will pick out those devices needing a password and assign? Sorry for the noob questions - this looks really useful but I'm barely literate in python at this point. Was also wondering if you could repost with proper code formatting - was running it through an interpreter and it appears to be missing all its necessary indents. thanks much
Sure. As best I can recall in order to use the script; python3 and the associated dependencies mentioned in the script must be installed on whatever mac you are using ot administer your jamf instance. It can be on any network as long as it has an internet connection (assuming the instance is jamf cloud based), as it just needs to auth to your jamf instance and use some API calls.
After the dependencies have been installed and the script customized with your FQDN and API user; you can execute the script from the terminal as follows:
python3 SetRecoveryLockJAMF.py Tad -r
The above command would loop through your Macs and set a random recovery password, for the first computer it found with Tad in the name. Repeat the process as necessary, replacing the name to hit all the macs you need. Hope that makes sense. Here is the script in a code block:
#!/usr/bin/env python3
####
#
# SetRecoveryLockJAMF.py
#
#
# Script to set recovery lock for macOS computers in JAMF Pro
# Requires:
# Python3
# Python module: requests (can be installed by running 'python3 -m pip install requests')
#
# Adapted from https://github.com/shbedev/jamf-recovery-lock
#
####
### User-edited Variables ###
# Define how we connect to JAMF
jamf_url = ''
jamf_api_username = ''
jamf_api_password = ''
########## DO NOT EDIT BELOW THIS LINE ##########
import argparse, sys
# Initialize command line argument parser
parser = argparse.ArgumentParser()
# Adding command line arguments
parser.add_argument(
"SearchString",
help = "String to use to search JAMF computer names"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-p", "--Passcode",
help = "Specify Recovery Lock passcode (default is blank)"
)
group.add_argument(
"-r", "--RandomPasscode", nargs='?', const=20,
help = "Generate a different random Recovery Lock passcode for each computer (default length is 20, specify a value for a different length)"
)
# Read arguments from command line
args = parser.parse_args()
### From git project auth/basic_auth.py
import base64
def auth_token():
# create base64 encoded string of jamf API user credetinals
credentials_str = f'{jamf_api_username}:{jamf_api_password}'
data_bytes = credentials_str.encode("utf-8")
encoded_bytes = base64.b64encode(data_bytes)
encoded_str = encoded_bytes.decode("utf-8")
return encoded_str
### From git project auth/bearer_auth.py
import os, time, requests
# current working directory
cwd = os.path.dirname(os.path.realpath(__file__))
# path of token file in current working directory
token_file = f'{cwd}/token.txt'
def request_token():
"""Generate an auth token from API"""
headers = {
'Accept': 'application/json',
'Authorization': f'Basic {auth_token()}',
'Content-Type': 'application/json',
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/v1/auth/token', headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
return response.json()['token']
def get_token():
"""Returns a token from local cache or API request"""
current_time = int(time.time())
# check if token is cached and if it is less than 30 minutes old
if os.path.exists(token_file) and ((current_time - 1800) < os.stat(token_file)[-1]):
# return a cached token from file
return read_token_from_local()
else:
# return a token from API
return get_token_from_api()
def get_token_from_api():
"""Returns a token from an API request"""
token = request_token()
cache_token(token)
return token
def cache_token(token):
"""
Cache token to local file
Parameters:
token - str
"""
with open(token_file, 'w') as file_obj:
file_obj.write(token)
def read_token_from_local():
"""Read cached token from local file"""
with open(token_file, 'r') as file_obj:
token = file_obj.read().strip()
return token
### From git project computers.py
import requests
import math
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def get_computer_count():
"""
Returns the number of computers in Jamf Pro
"""
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page=0&page-size=1&sort=id%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
count = response.json()['totalCount']
#print("Number of computers in JAMF = " + str(count))
return count
computers_per_page = 1000
number_of_pages = math.ceil(get_computer_count() / computers_per_page)
def get_arm64(filter = None):
"""
Returns Jamf IDs of all arm64 type computers
Parameters:
filter - (e.g. 'filter=general.name=="jdoe-mbp"'). If empty, returns all computers.
Computer name in filter is not case sensitive
"""
computers_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page={pageIndex}&page-size={computers_per_page}&sort=id%3Aasc&{filter}',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer in computers:
if computer['hardware']['processorArchitecture'] == 'arm64':
computers_id.append(computer['id'])
if computers_id == []:
sys.exit("No Apple Silicon computers found in Jamf that match search string.")
return computers_id
def get_mgmt_id(computers_id):
"""
Returns Jamf computers management id
Parameters:
computers_id - (e.g. ['10', '12']]). List of Jamf computers id
"""
computers_mgmt_id = []
for pageIndex in range(number_of_pages):
try:
response = requests.get(
url = f'https://{jamf_url}/api/preview/computers?page={pageIndex}&page-size={computers_per_page}&sort=name%3Aasc',
headers=headers
)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
computers = response.json()['results']
for computer_id in computers_id:
for computer in computers:
# Find computers that given computer id in list of computers
if computer['id'] == computer_id:
computer_mgmt_id = computer['managementId']
computer_name = computer['name']
# Add computer to list
computers_mgmt_id.append({
'id': computer_id,
'name': computer_name,
'mgmt_id': computer_mgmt_id
})
break
return computers_mgmt_id
### From git project recovery_lock.py
import requests
headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}
def set_key(computer_name, management_id, recovery_lock_key):
"""Sets a Recovery Lock key for a given computer"""
print(f'Settings recovery lock key: {recovery_lock_key} for {computer_name}')
payload = {
'clientData': [
{
'managementId': f'{management_id}',
'clientType': 'COMPUTER'
}
],
'commandData': {
'commandType': 'SET_RECOVERY_LOCK',
'newPassword': f'{recovery_lock_key}'
}
}
try:
response = requests.request("POST", f'https://{jamf_url}/api/preview/mdm/commands', headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
### From git project main.py
from random import randint
computers_id = get_arm64('filter=general.name=="*'+args.SearchString+'*"')
computers_mgmt_id = get_mgmt_id(computers_id)
print("These are the changes you will be making:")
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
print(" "+computer_name+" will have its Recovery Lock set to "+args.Passcode)
elif args.RandomPasscode:
print(" "+computer_name+" will have its Recovery Lock set to a random "+str(args.RandomPasscode)+"-digit number")
else:
print(" "+computer_name+" will have its Recovery Lock cleared")
print("")
go_ahead = input("Do you wish to proceed? (y/n)")
print("")
if go_ahead == ("y" or "Y"):
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
recovery_lock_key = args.Passcode
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
elif args.RandomPasscode:
rand_low_val=pow(10,(int(args.RandomPasscode) - 1))
rand_val_high=pow(10,int(args.RandomPasscode)) - 1
recovery_lock_key = randint(rand_low_val,rand_val_high)
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
else:
recovery_lock_key = ''
print(" Command sent to clear Recovery Lock on "+computer_name)
set_key(computer_name, computer_mgmt_id, recovery_lock_key)
print("")
Finally got a chance to revisit this and wanted to share: I came up with a group/policy workflow that runs automatically if Macs somehow manage to go through PreStage enrollment without Recovery Lock being enabled.
First step is the script (full credit to DerFlounder for his API token sequence):
#! /usr/bin/env zsh
# Set default exit code
exitCode=0
# Explicitly set initial value for the api_token variable to null:
api_token=""
# Explicitly set initial value for the token_expiration variable to null:
token_expiration=""
## generate random 39-digit numerical passcode
recPasscode=$(jot -r -s '' 39 0 9)
## Get the Mac's UUID string
UUID=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}')
# Set basic api access info
jamfpro_url="https://myjssserver.jamfcloud.com"
# Use the encoded credentials with Basic Authorization to request a bearer token
authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/token" --silent --request POST --header "accept: application/json" --header "Authorization: Basic $4")
# Parse the returned output for the bearer token and store the bearer token as a variable.
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
api_token=$(/usr/bin/awk -F \\" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
else
api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "$authToken")
fi
# Verify that API authentication is using a valid token by running an API command
# which displays the authorization details associated with the current API user.
# The API call will only return the HTTP status code.
api_authentication_check=$(/usr/bin/curl --write-out %{http_code} --silent --output /dev/null "${jamfpro_url}/api/v1/auth" --request GET --header "Authorization: Bearer ${api_token}")
## Pull JSSID from "general" subsection
JSS_ID=$(curl -H "Accept: text/xml" -H "Authorization: Bearer ${api_token}" -sf "${jamfpro_url}/JSSResource/computers/udid/${UUID}/subset/general" | xmllint --xpath '/computer/general/id/text()' -)
## get mgmtID
mgmtID=$(curl -s -X GET "${jamfpro_url}/api/v1/computers-inventory-detail/${JSS_ID}" -H "accept: application/json" -H "Authorization: Bearer ${api_token}" | grep -i "managementid" | awk -F '"' '{print $4}')
#checkpoints for troubleshooting
#echo $api_token
#echo $api_authentication_check
#echo $JSS_ID
#echo $mgmtID
## set Recovery Lock password
curl --location --request POST "${jamfpro_url}/api/preview/mdm/commands" \\
--header "Authorization: Bearer ${api_token}" \\
--header 'Content-Type: application/json' \\
--data-raw '{
"clientData": [
{
"managementId": "'$mgmtID'",
"clientType": "COMPUTER"
}
],
"commandData": {
"commandType": "SET_RECOVERY_LOCK",
"newPassword": "'$recPasscode'"
}
}'
## tag Mac as Recovery Lock set by API
if [[ ! -f "/usr/local/mylocalorgdir/RLapiSet" ]] && [[ $? == "0" ]]; then
touch /usr/local/mylocalorgdir/RLapiSet
fi
Note that per the bearer token request, I'm using script parameters to pass along the encoded API creds.
2nd step is to create an extension attribute for the last section of the script so that computers can be detected in a smart group as having had the script's policy run:
#! /usr/bin/env zsh
if [[ -f "/usr/local/mylocalorgdir/RLapiSet" ]]; then
Result="Yes"
else
Result="No"
fi
echo "<result>$Result</result>"
Then create two smart groups - one for devices that don't have Recovery Lock enabled:

and one for those that have completed the script policy:

Finally, this will require two policies; one Ongoing policy triggered by Login and a Custom Event and scoped to both Smart Groups above:

And one triggered by check-in which runs the policy trigger for the other policy and is scoped only to the Recovery Lock not enabled group:


Appears to work beautifully - also created another smart group for Macs that might end up members of both of the above groups in order to detect ones that have had the policies run but for whatever reason haven't actually had the set Recovery Lock mgmt cmd go through.
Finally got a chance to revisit this and wanted to share: I came up with a group/policy workflow that runs automatically if Macs somehow manage to go through PreStage enrollment without Recovery Lock being enabled.
First step is the script (full credit to DerFlounder for his API token sequence):
#! /usr/bin/env zsh
# Set default exit code
exitCode=0
# Explicitly set initial value for the api_token variable to null:
api_token=""
# Explicitly set initial value for the token_expiration variable to null:
token_expiration=""
## generate random 39-digit numerical passcode
recPasscode=$(jot -r -s '' 39 0 9)
## Get the Mac's UUID string
UUID=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}')
# Set basic api access info
jamfpro_url="https://myjssserver.jamfcloud.com"
# Use the encoded credentials with Basic Authorization to request a bearer token
authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/token" --silent --request POST --header "accept: application/json" --header "Authorization: Basic $4")
# Parse the returned output for the bearer token and store the bearer token as a variable.
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
api_token=$(/usr/bin/awk -F \\" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
else
api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "$authToken")
fi
# Verify that API authentication is using a valid token by running an API command
# which displays the authorization details associated with the current API user.
# The API call will only return the HTTP status code.
api_authentication_check=$(/usr/bin/curl --write-out %{http_code} --silent --output /dev/null "${jamfpro_url}/api/v1/auth" --request GET --header "Authorization: Bearer ${api_token}")
## Pull JSSID from "general" subsection
JSS_ID=$(curl -H "Accept: text/xml" -H "Authorization: Bearer ${api_token}" -sf "${jamfpro_url}/JSSResource/computers/udid/${UUID}/subset/general" | xmllint --xpath '/computer/general/id/text()' -)
## get mgmtID
mgmtID=$(curl -s -X GET "${jamfpro_url}/api/v1/computers-inventory-detail/${JSS_ID}" -H "accept: application/json" -H "Authorization: Bearer ${api_token}" | grep -i "managementid" | awk -F '"' '{print $4}')
#checkpoints for troubleshooting
#echo $api_token
#echo $api_authentication_check
#echo $JSS_ID
#echo $mgmtID
## set Recovery Lock password
curl --location --request POST "${jamfpro_url}/api/preview/mdm/commands" \\
--header "Authorization: Bearer ${api_token}" \\
--header 'Content-Type: application/json' \\
--data-raw '{
"clientData": [
{
"managementId": "'$mgmtID'",
"clientType": "COMPUTER"
}
],
"commandData": {
"commandType": "SET_RECOVERY_LOCK",
"newPassword": "'$recPasscode'"
}
}'
## tag Mac as Recovery Lock set by API
if [[ ! -f "/usr/local/mylocalorgdir/RLapiSet" ]] && [[ $? == "0" ]]; then
touch /usr/local/mylocalorgdir/RLapiSet
fi
Note that per the bearer token request, I'm using script parameters to pass along the encoded API creds.
2nd step is to create an extension attribute for the last section of the script so that computers can be detected in a smart group as having had the script's policy run:
#! /usr/bin/env zsh
if [[ -f "/usr/local/mylocalorgdir/RLapiSet" ]]; then
Result="Yes"
else
Result="No"
fi
echo "<result>$Result</result>"
Then create two smart groups - one for devices that don't have Recovery Lock enabled:

and one for those that have completed the script policy:

Finally, this will require two policies; one Ongoing policy triggered by Login and a Custom Event and scoped to both Smart Groups above:

And one triggered by check-in which runs the policy trigger for the other policy and is scoped only to the Recovery Lock not enabled group:


Appears to work beautifully - also created another smart group for Macs that might end up members of both of the above groups in order to detect ones that have had the policies run but for whatever reason haven't actually had the set Recovery Lock mgmt cmd go through.
Wow, wonderful!! thanks for sharing.
Finally got a chance to revisit this and wanted to share: I came up with a group/policy workflow that runs automatically if Macs somehow manage to go through PreStage enrollment without Recovery Lock being enabled.
First step is the script (full credit to DerFlounder for his API token sequence):
#! /usr/bin/env zsh
# Set default exit code
exitCode=0
# Explicitly set initial value for the api_token variable to null:
api_token=""
# Explicitly set initial value for the token_expiration variable to null:
token_expiration=""
## generate random 39-digit numerical passcode
recPasscode=$(jot -r -s '' 39 0 9)
## Get the Mac's UUID string
UUID=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}')
# Set basic api access info
jamfpro_url="https://myjssserver.jamfcloud.com"
# Use the encoded credentials with Basic Authorization to request a bearer token
authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/token" --silent --request POST --header "accept: application/json" --header "Authorization: Basic $4")
# Parse the returned output for the bearer token and store the bearer token as a variable.
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
api_token=$(/usr/bin/awk -F \\" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
else
api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "$authToken")
fi
# Verify that API authentication is using a valid token by running an API command
# which displays the authorization details associated with the current API user.
# The API call will only return the HTTP status code.
api_authentication_check=$(/usr/bin/curl --write-out %{http_code} --silent --output /dev/null "${jamfpro_url}/api/v1/auth" --request GET --header "Authorization: Bearer ${api_token}")
## Pull JSSID from "general" subsection
JSS_ID=$(curl -H "Accept: text/xml" -H "Authorization: Bearer ${api_token}" -sf "${jamfpro_url}/JSSResource/computers/udid/${UUID}/subset/general" | xmllint --xpath '/computer/general/id/text()' -)
## get mgmtID
mgmtID=$(curl -s -X GET "${jamfpro_url}/api/v1/computers-inventory-detail/${JSS_ID}" -H "accept: application/json" -H "Authorization: Bearer ${api_token}" | grep -i "managementid" | awk -F '"' '{print $4}')
#checkpoints for troubleshooting
#echo $api_token
#echo $api_authentication_check
#echo $JSS_ID
#echo $mgmtID
## set Recovery Lock password
curl --location --request POST "${jamfpro_url}/api/preview/mdm/commands" \\
--header "Authorization: Bearer ${api_token}" \\
--header 'Content-Type: application/json' \\
--data-raw '{
"clientData": [
{
"managementId": "'$mgmtID'",
"clientType": "COMPUTER"
}
],
"commandData": {
"commandType": "SET_RECOVERY_LOCK",
"newPassword": "'$recPasscode'"
}
}'
## tag Mac as Recovery Lock set by API
if [[ ! -f "/usr/local/mylocalorgdir/RLapiSet" ]] && [[ $? == "0" ]]; then
touch /usr/local/mylocalorgdir/RLapiSet
fi
Note that per the bearer token request, I'm using script parameters to pass along the encoded API creds.
2nd step is to create an extension attribute for the last section of the script so that computers can be detected in a smart group as having had the script's policy run:
#! /usr/bin/env zsh
if [[ -f "/usr/local/mylocalorgdir/RLapiSet" ]]; then
Result="Yes"
else
Result="No"
fi
echo "<result>$Result</result>"
Then create two smart groups - one for devices that don't have Recovery Lock enabled:

and one for those that have completed the script policy:

Finally, this will require two policies; one Ongoing policy triggered by Login and a Custom Event and scoped to both Smart Groups above:

And one triggered by check-in which runs the policy trigger for the other policy and is scoped only to the Recovery Lock not enabled group:


Appears to work beautifully - also created another smart group for Macs that might end up members of both of the above groups in order to detect ones that have had the policies run but for whatever reason haven't actually had the set Recovery Lock mgmt cmd go through.
Hi @mike_prather
I followed your guide but I'm having trouble getting it to work. The policy with the script runs successfully, but the recovery lock isn't active. The log says: "httpStatus" : 401.
I'm not very familiar with the API, so I'm not sure which permissions are necessary for the API role and how to handle the credentials. I've already created an API role with client-id and secret, but I'm not sure if that's the right approach. Could you give me an example of how you do that?
Thanks!