Installing with choices.xml

Some application installers have a lot of options or install additional software you may not use. You can create a choices XML file and customize the installer in many cases. Unfortunately, Jamf Pro does not natively support using a choices.XML file. There are a few ways to go about distributing software using one with Jamf Pro. You can repackage the software and run the install with a post install script assumes that you have cached the installer and a completed choices XML file to a staging location, in this case /Library/Management/ and runs the installer with the options, then deletes the cached files. The variables used are for the package name and the choices XML file name.

installer -applyChoiceChangesXML "/Library/Management/${choicesName}" -pkg "/Library/Management/${pkgName}" -target /

rm -rf "/Library/Management/${choicesName}"
rm -rf "/Library/Management/${pkgName}"

I deploy both Office and Cisco AnyConnect VPN and modify the installation as we do not utilize all the applications included. The reason this works for me is I can create a policy to cache the Office or AnyConnect installer and a separate choices XML to install only the required applications at the time. I may have a group that wants Outlook, while most installs do not, or a group requesting OneNote. Having one package for the installer for Office that is kept up to date and can be installed with the required options lets me create multiple policies that can reference a single installer that I can keep up to date without needing to update multiple policies.

Making Panopto Available in Self Service

As most of the US is now doing remote instruction to practice better social distancing, we’ve found ourselves using new tools more frequently. Panopto is a tool that is for recording classes and being able to distribute later – it lets you record from a camera and microphone and screen share, and it can be uploaded for students to reference.

We’ve been using Panopto for some time, but now we are getting requests to install in en masse. The Panopto installer does some unusual stuff that caused complications when trying to deploy with Jamf’s Self Service. First, it creates a hidden user account to manage uploads to the server. That user needs specific permissions, and in Mojave and Catalina it can cause some issues. If you are following certain security protocols you may need further exceptions for this user account for it to work. It also assumes the installer is being run by the user. We found that when Jamf installed Panopto it was either not creating the account, or it was creating it but failing to give it appropriate permissions.

We saw multiple errors, mostly an error 84 which we couldn’t find documentation for, or an error uploading. It seemed the issues with these were either that the user Panopto creates, panopto_upload, did not have permission to the folder or was not created correctly.

In order to make this work in Self Service I worked out the following steps:

  • Due to failed and incorrectly configured installs the first step is a script I wrote based on the instructions to manually uninstall Panopto. This helps as there were case where Panopto was installed and nonfunctional. The script uses the $3 variable in Jamf for Self Service, but it can be run manually and will use the logged in user if $3 is not specified.
  • Second, we install a profile to ensure the correct hostname is added in settings. Panopto’s installer is supposed to use a hostname in the installer’s filename but I’ve found that to be inconsistent when deployed with Jamf. This is a profile using the Application & Custom Settings payload in Jamf.
  • A script originally written by @macdude22 on the macadmins slack to create the Panopto user correctly before install.
  • Installing the Panopto 7.3 package as provided by Panopto’s support.
  • Running a permissions fix for the user in Self Service.

In testing with Mojave and Catalina, the Self Service installer now runs correctly for the user in Self Service. For now, this covers most of our use case. There will be an additional requirement on shared systems to ensure that the panopto_upload user has permissions for each user who logs in. Tentatively that will run using outset in a login-once script.

Migrating existing mobile AD accounts to Local accounts with NoMAD in Self Service

Moving from mostly on campus and AD bound machines with mobile accounts to working remotely for an unspecified time brings a lot of challenges. one of them is ensuring your password correctly stays in sync when changed, and ensuring your FileVault access and login keychains remain with your login password is important too. We’d had issues with those already, and were testing NoMAD already on campus as a tool to avoid this. NoMAD does not require a bind to AD, but can still sync your password with your AD account and ensure the FileVault and login keychain passwords update correctly.

As part of my test I had a Self Service policy with a script to turn a mobile account into a local account, then install NoMAD and the required profile. This was initially so we could take advantage of the bootstrap token, and use in conjunction with a Self Service policy to enable encryption.

The main part of the process is, based on Rich Trouton’s interactive script but intended for use in Self Service. It uses the #3 variable to pull the user logged in to Self Service and migrate that account if it is in fact a mobile account, and retains admin permissions as it is configured in our environment. The policy is configured to run 3 things; this script to migrate the account, install the NoMAD profile and then the NoMAD application and launchagent.

Alternately I could modify the script to run on the currently logged in account to the OS for Self Service. In our case I don’t believe this will be any more effective. It could be used as part of a policy to migrate systems to NoMAD in the background. Currently in Self Service we can scope to just the machines we are testing with so we have control over the test as we move to a wider scale.

Supporting Java Applets in macOS Catalina

In the wake of a sudden pivot to remote instruction we’ve had to look at different ways to support applications used in classes. One specific example is an application that is provided by the US Geological Survey called TopoDrive and ParticleFlow. Available on the website is an executable for Windows machines. We have that loaded to a Windows lab on campus. Unfortunately, many students only have a macOS device to use at home and could not easily run the programs, especially as modern web browsers have deprecated java plugins making it difficult to run the applet version.

So to support these students I ran some testing. I was able to get the applet version to run if I installed a version of OpenJDK 8. I chose Amazon’s Corretto distribution. After installing I was able to run the application using the appletviewer command. Turns out that was pretty simple to get working, once the JDK is installed.

appletviewer ~/Downloads/tdpf1.0web/pflow/pflow.html

Running that line executes the html file with the java applet assuming it was in the default location of the user’s /Downloads folder. To take that one step further I can save it as a bash script to easily execute again. I could use the .command extension and it will run the script using the java JDK appletviewer command. But to make it one step simpler for students to use on personal machines, I used Platypus and created an app out of the one line script.

Platypus is a simple application that can build a few different types of apps out of basic scripts. I simply took my saved script, added a custom icon and built the .app file. by Svein Bjorn

I duplicated this for both applets in the download, TopoDrive and ParticleFlow. Once you click the desktop icon you get the following window:

html page that includes the launch button

And after clicking the button the Applet launches:

the applet launches in its own window

If I was deploying this to managed machines in my own organization I would have signed the app and deployed it to /Applications, installed the correct version of Amazon Corretto and stored the files in a shared location, such as in the /Users/Shared folder. But as I am offering this to students with unknown versions of macOS, and without a management framework to use the easiest way was to email instructions and a .zip file with the .apps. The steps were:

While it is very simple I have the scripts and Platypus created applications on GitHub here.

Running Jamf policy custom triggers via script

At first glance, my script may seem too simple. It just calls a Jamf policy by a custom trigger. But this can be very useful in a few workflows.

if [[ -z $event ]]; then
	read -p "Jamf Custom Trigger:" event

jamf policy -event $event

I use this script to create Self Service policies that are scoped specifically to the required OS or department. They refer back to a policy that actually installs the software that is only triggered by the custom event. I can also create another policy to automatically install on newly DEP enrolled machines during their provisioning workflow. I can have multiple policies that refer back to the primary one. This means I only need to edit one policy if something changes in the installer, package name, or choices XML modifications.

I also use this to kickstart our DEP Provisioning workflow in Self Service because the enrollmentComplete trigger from Jamf can be unreliable. If the network connection is interrupted during the enrollment, it may never see the trigger. So when setting up a machine, if I don’t see the notice that it’s starting the process I can click it from Self Service.

If Jamf changed the way to call a custom event it also would mean only changing the primary script which just makes things slightly easier, as opposed to using the “Execute Command” feature located under Files and Processes in a Jamf Policy.

Add and Remove from Static Groups with the Jamf API

I have 2 scripts I’ve found useful with scoping policies and profiles to a group that isn’t easily configured by other information in a device’s record. A MacBook Pro that is shared for a lab is difficult encrypt, but the same model in the same department is assigned to a specific user and that we do want to encrypt. We can scope a policy to a static group and use a Self Service policy to add a machine to that group, then trigger the policy by a custom event to kick off. grabs the serial number of the device and adds it to a static group by that groups ID number. You can easily locate the ID number of a group by checking the URL in the JSS. In the example below, the number is in the ?id= portion, represented by XX.

Writing the script with these variables allows for reuse, and if an update to the parameters is made it will be easy to update one script instead of every policy that may use it.

curl \
	-s \
	-f \
	-u ${apiUser}:${apiPass} \
	-X PUT \
	-H "Content-Type: text/xml" \
	-d "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>${apiData}" ${jssHost}/JSSResource/computergroups/id/${groupID}

A few usage examples for this would be to add a machine to an exception group to temporarily remove a profile for testing at the help desk, or adding to a group that has an encryption policy scoped to it. is similarly used to remove a machine from a static group. One of the ways I’ve used this is to remove a machine from groups when repurposing a machine with an erase install policy. It could similarly be used in conjunction with the add to static group script for help desk, with a self service policy to add and remove a profile or policy for testing purposes.

curl \
	-s \
	-f \
	-u ${apiUser}:${apiPass} \
	-X PUT \
	-H "Content-Type: text/xml" \
	-d "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>${apiData}" ${jssHost}/JSSResource/computergroups/id/${groupID}

Help Desk Scripts

I have added a couple items to Jamf Self Service that can be useful for Help Desk. They are simple items but can empower users and help desk staff to help when there are security obstacles, like a firmware password or out of sync user password. does some minor clearing of caches and performs an NVRAM reset. This is useful on macOS systems with firmware passwords; the method to hold down CMD+Option+P+R will not traditionally work when there is a firmware password set. Clicking a Self Service policy to run this script and rebooting can potentially alleviate some issues.

update_dyld_shared_cache -root / -force -debug
/usr/libexec/xpchelper --rebuild-cache
nvram -c will simply remove an old keychain for a user with a keychain they can no longer access. This occasionally happens with AD mobile accounts when the password is changed on another system and then authenticated to AD updating the password. In macOS if you bypass the password change mechanism some items cannot be updated, like the Keychain password or Filevault password. This script will get rid of the issue when attempting to remember the old password is not working.

rm -rf "$home"/Library/Keychains/*

I’ve also added an installer for Recovery Selector from Two Canoes. Again firmware passwords can be a hindrance for some items, like rebooting to run diagnostics. When we have macOS devices that need diagnostics run I’ve added a Self Service policy to install this application to the /Applications/Utilities folder. The system still requires the firmware password, but you do not need to remove it just for diagnostics. In some cases you will still need to remove it to complete a repair, but this can make things quicker.

edit 2021-03-17: updated script links

Jamf API Scripts on Github

I’ve been maintaining a Github with useful Jamf Pro scripts I’ve found helpful, and some I featured in a presentation at the 2019 Penn State MacAdmins Conference.

I specifically featured a few scripts that I use in our DEP provisioning workflow. This workflow relies heavily on using the Inventory Preload feature in Jamf Pro. It allows me to assign a machine to a user, assign it an asset tag, and put it in a department and building. The first time a machine checks in, Jamf will assign these items to the computer’s record in Jamf. These are all ways I can sort computers in Jamf Pro, but I can communicate with the Jamf API to pull some of these items and use them while preparing a new system.

For security purposes I have the username and password sent as a variable to the script. I also made the account with limited permissions to only make the call to pull computer data to mitigate concerns. is a simple script that grabs the user assigned in Jamf Pro and adds that user as an administrator. It first gets the serial number, and uses that to perform a lookup to find the assigned user via API call. It then uses a dscl command to set that user as an admin. This script would primarily be useful if you have machines prepped in advance by IT instead of initial configuration by a user.

username=$(/usr/bin/curl -H "Accept: text/xml" -sfku "${apiUser}:${apiPass}" "${jssHost}/JSSResource/computers/serialnumber/${serialNumber}/subset/location" | xmllint --format - 2>/dev/null | awk -F'>|<' '/<username>/{print $3}')
dscl . -append /Groups/admin GroupMembership $username could easily be customized, but this allows for a unique firmware password to be set per machine, utilizing the Asset Tag field in this case. As the firmwarepasswd command is interactive it uses the expect shell to respond. Again we get the serial number and use that to pull the asset tag from the computer’s inventory record. We use the expect shell to interact with a command line and send a unique password with a password scheme, assuming one isn’t already set. There are potential security concerns with setting a firmware password with a script. It will run in plain text and be cached to the Jamf Waiting Room, but in the case of IT preparing machines it is unlikely that any information would still be cached when the user gets the system.

doesexist=`firmwarepasswd -check`
barcode=$(/usr/bin/curl -H "Accept: text/xml" -sfku "${apiUser}:${apiPass}" "${jssHost}/JSSResource/computers/serialnumber/${serialNumber}/subset/general" | xmllint --format - 2>/dev/null | awk -F'>|<' '/<asset_tag>/{print $3}')

if [ "$doesexist" = "Password Enabled: No" ]; then
	/usr/bin/expect <<- DONE
		spawn firmwarepasswd -setpasswd
		expect "Enter new password:"
		send "$firmware\r";
		expect "Re-enter new password:"
		send "$firmware\r";
		expect EOF
	echo "Firmware Password Already Exists"
fi similarly pulls the Asset Tag from the computer’s record in Jamf, again by using the serial number to perform a lookup. This sets each of the names to the same name. In this case we use the asset tag proceeded by the type of device , Mac-##### in the script. This can be coupled with a Jamf policy to keep computer name if you want to ensure you can find a machine on the network.

barcode=$(/usr/bin/curl -H "Accept: text/xml" -sfku "${apiUser}:${apiPass}" "${jssHost}/JSSResource/computers/serialnumber/${serialNumber}/subset/general" | xmllint --format - 2>/dev/null | awk -F'>|<' '/<asset_tag>/{print $3}')
scutil --set ComputerName $hostname
scutil --set HostName $hostname
scutil --set LocalHostName $hostname