Jamf Pro – Set Computer Management Status

Jamf removed the ability to mass action modify the management account status for computers in 10.49.

Modifying the management status of computers in Jamf Pro can still be useful for various asset management reasons. Expanding on the sentiment of Der Flounder, I created a simple Python tool to set the management status of Jamf Pro computer object(s) via the universal api. Rather than using a pre-defined static list – the tool uses an Advanced Computer Search to iterate over.

Here’s an example of sms-cli modifying the management status of 3 computers.

$ python ./sms-cli.py --url=https://company.jamfcloud.com --username=user --managed --id=156 --password='pass'
Are you sure you want to change the management status for the device(s)? [y/N]: y
[SMS-CLI] Using search ID: Computers - Test Group
[SMS-CLI] Setting management status for host:host1 jssid:2988...
[SMS-CLI]...New management status = True
[SMS-CLI] Setting management status for host:host2 jssid:4277...
[SMS-CLI]...New management status = True
[SMS-CLI] Setting management status for host:host3 jssid:4117...
[SMS-CLI]...New management status = True

Source and setup KB can be found here – https://github.com/1sth1sth1ng0n/smscli#set-management-status-cli-sms-cli

Jamf Pro – Docker Container

Setting up an additional Jamf Pro environment for dev and testing out new features can be time consuming. Docker reduces this overhead and allows for the efficient creation of a ready to use environment.

Full build guide – https://github.com/1sth1sth1ng0n/jamfpro_cont

The following docker base image is used in conjunction with the latest Jamf Pro Java collection (ROOT.war) – https://hub.docker.com/r/jamf/jamfpro.

The required Java collection can be obtained from a licensed Jamf product console – https://www.jamf.com/jamf-nation/my/products.

Once the new image is built – it can be ran directly by docker or called by docker compose. Docker compose allows for a more structured approach where defining the database parameters and networks are more clear.

Additionally Jamf Pro does not support the latest MySQL version 8.x default authentication method. We need to change the default to mysql_native_password using a custom my.cnf conf file which is mapped to the new MySQL container in /etc/mysql/conf.d/my.cnf.

[mysqld] 
default-authentication-plugin=mysql_native_password

The following docker compose yaml file defines all the required components. This includes the Jamf Pro docker image previously created and a base MySQL image with the my.cnf definition. This also creates a bridged network named jamfnet which can be modified to accommodate various requirements. Also defined is our mapped host to container HTTPS and MySQL ports and a depends_on feature which ensures the MySQL container is up before the Jamf Pro container.

version: "3"
services:
  mysql:
    image: "mysql:8.0.31"
    networks:
      - jamfnet
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "jamfsw03"
      MYSQL_DATABASE: "jamfsoftware"
    volumes:
      - ./my.cnf:/etc/mysql/conf.d/my.cnf
  jamfpro:
    image: "jamfpro:10.42.1"
    networks:
      - jamfnet
    ports:
      - "8443:8080"
    environment:
      DATABASE_USERNAME: "root"
      DATABASE_PASSWORD: "jamfsw03"
      DATABASE_HOST: "mysql"
    depends_on:
      - mysql
networks:
  jamfnet:

Full build guide – https://github.com/1sth1sth1ng0n/jamfpro_cont

Unix Password Manager with Multiple Repos + zsh Completion

Pass is a brutally simple and effective CLI password manager for *nix systems. There are multiple front ends if you prefer not to rely on the CLI and also a great iOS app that can sync your pass git repo – passforios.

I prefer to separate various project password stores to multiple repos but pass does not cater for this natively. It will create a single password store here ~/.password-store. To change that behavior and use an alternate location you must modify an environment variable and define your new preferred location.

# define new env variable
$ export PASSWORD_STORE_DIR=~/.password_store_one

Then when we call /usr/local/bin/pass it will use the location we defined as the password store.

To define multiple separate password stores we can create a function for each store you wish to define. These can be stored in your ~/.zshrc file so it will be ready to use in each session.

pass_one() {
  PASSWORD_STORE_DIR=$HOME/.password_store_one pass $@
}

pass_two() {
  PASSWORD_STORE_DIR=$HOME/.password_store_two pass $@
}

If you require auto completion using pass this completion definition will need to be available and defined in $fpath with the definition name of _pass. I modified ~/.zshrc to include my preferred completions functions path and also added the zstyle completion rules for each password store.

# define completion definition functions
fpath=( ~/completions $fpath )

# required to activate autocomplete in zsh
autoload -Uz compinit && compinit


compdef _pass pass_one
zstyle ':completion::complete:pass_one::' prefix "$HOME/.password_store_one"
pass_one() {
  PASSWORD_STORE_DIR=$HOME/.password_store_one pass $@
}

compdef _pass pass_two
zstyle ':completion::complete:pass_two::' prefix "$HOME/.password_store_two"
pass_two() {
  PASSWORD_STORE_DIR=$HOME/.password_store_two pass $@
}

Now we can initialize our pass repos using the gpg ID and start creating encrypted data.

$ pass_one init "Password Storage Key"
Password store initialized for Password Storage Key
$ pass_one generate secrets/blob 15                     
/Users/user/.password_store_one/secrets
The generated password for secrets/blob is:
^E5^^Em16AykW9R
$ pass_one 
Password Store
└── secrets
    └── blob
$ pass_two init "Password Storage Key"
Password store initialized for Password Storage Key
$ pass_two generate secrets/blob 15                     
/Users/user/.password_store_two/secrets
The generated password for secrets/blob is:
TA`[{1sp{E6f-|q
$ pass_two 
Password Store
└── secrets
    └── blob

Jamf Pro – MakeMeAdmin On Demand

I wanted to provide an option for standard macOS users to request local admin access on demand. The process needed to be robust and efficient. No reboots etc. My preference was to not use /usr/sbin/dseditgroup or /usr/bin/dscl as these tools may not produce the efficient workflow I am looking for.

I had experimented with SAP’s Privileges app in the past and found it was a very clean way for end users to flop over to admin-land then back to standard-user-town with the click of a button. I now however needed a way to kick them out of admin-land after a set time period had passed.

Privileges does offer a ‘DockToggleTimeout‘ feature which revokes admin access after a set time. But this key is only honored by the ‘Toggle Privileges’ function which is enabled by right clicking Privileges from the Dock – so this is not really of use to us, it’s not efficient and users will not adhere to using that method. More on that here https://derflounder.wordpress.com/2022/07/22/privileges-app-and-time-limited-admin/

The approach I settled on – and have been using in production for over a year – is to simply use the Privileges CLI triggered from a Jamf Pro Self Service policy. There are similar projects out there, however my concept was to have the tool clean up after itself, and I did not want the entire Privileges app bundle to be available to the user at any point. We also use an automated admin access request PowerApp which logs all the relevant details and then, once approved, triggers the addition of the computer to a Jamf Pro static computer group via the Jamf Pro API. This then makes the MakeMeAdmin policy available in Self Service.

This similar project uses WatchPaths and a signal file to demote a user’s access https://github.com/sgmills/PrivilegesDemoter

Workflow

After isolating just the Privileges CLI tool, and installing it to /usr/local/bin, we then load the Privileges helper tool daemon prior to calling /usr/local/bin/PrivilegesCLI. Privileges CLI cannot be run as root, so calling it from a Jamf Pro script requires us to call it in the context of the logged in user.

# elevate privs now
loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
userID=$(id -u "$loggedInUser")
/bin/launchctl bootstrap system /Library/LaunchDaemons/corp.sap.privileges.helper.plist
launchctl asuser $userID sudo -u $loggedInUser /usr/local/bin/PrivilegesCLI --add

The LaunchDaemon , which is created by the initial Self Service script run, revokes admin access after a predefined time has elapsed. The StartInterval key has a value in seconds. This ensures the removeAdminRights.sh script is called after exactly N seconds of run time, regardless of if the machine reboots or is offline etc.

{
    Label = "com.company.removeAdmin";
    ProgramArguments =     (
        "/bin/sh",
        "/Library/Application Support/JAMF/bin/removeAdminRights.sh"
    );
    RunAtLoad = 1;
    StandardErrorPath = "/private/var/userToRemove/err.err";
    StandardOutPath = "/private/var/userToRemove/out.out";
    StartInterval = 300;
}

The removeAdminRights.sh script is called by my LaunchDaemon. We previously captured the logged in user as $userToRemove and stored the value locally. This script also removes the computer from the initial static computer group, ensuring the user cannot run the tool again from Self Service, and also removes all components of the Privileges app.

# revoke privs now
userToRemove=$(cat /private/var/userToRemove/user)
sudo -u $userToRemove /usr/local/bin/PrivilegesCLI --remove
/bin/sleep 2
/bin/launchctl bootout system /Library/LaunchDaemons/corp.sap.privileges.helper.plist
# initiating self destruct sequence
/bin/rm -f /private/var/userToRemove/user
/bin/rm -f /Library/LaunchDaemons/com.rga.removeAdmin.plist
# remove all sap stuff
/bin/rm -rf /usr/local/bin/PrivilegesCLI
/bin/rm -f /Library/LaunchDaemons/corp.sap.privileges.helper.plist
/bin/rm -f /Library/PrivilegedHelperTools/corp.sap.privileges.helper
# recon to update user admin status is jamf pro
/usr/local/bin/jamf recon &
# remove this script
/bin/rm -f "$0"
# we cannot unload the daemon as the path no longer exists - just remove it from launchd
/bin/launchctl remove com.rga.removeAdmin

End Result

After running the Self Service MakeMeAdmin policy the end user is alerted once admin privileges are elevated and revoked. This also includes a JamfHelper countdown which (roughly) corresponds to the LaunchDaemon StartInterval. I will soon abandon JamfHelper in favor of a more reliable dialog tool as I have done with other projects – https://github.com/bartreardon/swiftDialog.

Git repo – https://github.com/1sth1sth1ng0n/jamfpro_makemeadmin

Jamf Pro – Application Usage Reports using Grafana and MySQL

Application usage data on macOS can easily be misinterpreted. Even the native Screen Time service does not do a great job to determine which applications were truly ‘in use’ and for how long. More on this here: https://www.r-bloggers.com/2019/10/spelunking-macos-screentime-app-usage-with-r/

Please see my other blog post regarding the JamfDaemon and application usage collection.

Jamf Pro does have the ability to display application usage reports using the web app, however there is no option to export usage reports and manipulating and visualizing the data in a meaningful way is not really possible. Speed of generating reports is slow as the database is queried heavily to populate the resulting data.

The Jamf Pro classic api does include a computerapplicationusage endpoint resource that can be utilized. This would mean iterating through each device record in order to collect application usage and it’s not the most efficient method, albeit it is the easiest, but who needs easy?…right?! Splunk and Power BI are both options to consider to make use of the Jamf Pro api but as our organization was already using Grafana – it was worth investigating what could be done.

Grafana works best with time series data but can also be used to evaluate table based data and display a wide variety of visualizations.

The accuracy of the data collected by the JamfDaemon is close enough for what we need. If you are collecting data from devices with multiple full screen applications, multiple logged in users, external displays and remote sessions active then your results may vary. For our organization’s needs we really just required a holistic overview of app usage so we could compare for example; Slack vs Microsoft Teams, and determine app popularity and trending over time.

The following MySQL query joins the ‘application_usage_logs’ table with the ‘application_details’ table. This gives us a resulting table that includes ‘minutes_forground’ data and corresponding application name (e.g.Google Chrome.app). I also used a Grafana variable to select the application I was interested in ‘$browser_applications’.

SELECT application_usage_logs.usage_date_epoch as time,
avg(application_usage_logs.minutes_foreground) as chrome
FROM application_details
inner join application_usage_logs
on application_details.id=application_usage_logs.application_details_id
where name = 'Google Chrome.app'
and minutes_foreground > 0 and minutes_foreground < 1440
and "${browser_applications}" like '%Google Chrome.app%'
group by time
order by time
Browser Usage Details – All Devices (30 days)

It was necessary to use one query per application in order to display the time data all on one graph – so four browsers = four separate queries. The ‘$browser_applications’ variable took care of only displaying usage of the browser selected in the variable drop down selector.

Browser Usage Details – All Devices (Chrome, 30 days)

Visualizing application usage data on a per device basis is also possible. Here is the query I developed for that.

SELECT usage_date_epoch as time_sec,
minutes_foreground,
computer_name
FROM computers_denormalized
inner join application_usage_logs
on computers_denormalized.computer_id=application_usage_logs.computer_id
inner join application_details
on application_usage_logs.application_details_id=application_details.id
where name = "${application}"
and computer_name = ${device}
and minutes_foreground > 0 and minutes_foreground < 1440

Again using Grafana variables I was able to select the application name and then a device name. This then displays the time series data for that specific device.

Browser Usage Details – Single Device (Chrome, 30 days)

Jamf Pro – Shallow Diving macOS Application Usage Logs

Application usage data on macOS can easily be misinterpreted. Even the native Screen Time service does not do a great job to determine which applications were truly ‘in use’ and for how long. More on this here: https://www.r-bloggers.com/2019/10/spelunking-macos-screentime-app-usage-with-r/

With that said – let’s take a quick review on how the macOS Jamf Pro framework collects application usage logs.

Log Collection:

One of the JamfDaemon‘s primary responsibilities is monitoring application usage events and capturing statistics.

# location of launchdaemon
/Library/LaunchDaemons/com.jamf.management.daemon.plist
# jamf.app application bundle incl. jamfdaemon executable
/Library/Application\ Support/JAMF/Jamf.app/Contents/MacOS/JamfDaemon.app/Contents/MacOS/JamfDaemon

Previously application usage data was logged to /Library/Application Support/JAMF/Usage/[capture-date]/[current-user].plist. The basic syntax structure for each tracked application contained key value pairs for elapsed mins/secs for foremost apps and apps that were loaded and running.

> defaults read /Library/Application\ Support/JAMF/Usage/[capture-date]/[current-user].plist
"/Applications/Google Chrome.app" =     {
    foremost = 13;
    open = 2460;
    secondsforemost = 832;
    secondsopen = 147643;
    version = "91.0.4472.106";
};

Sometime around Jamf Pro version 10.30.1 the application usage data was moved to /Library/Application Support/JAMF/usage_reports. The schema was changed to JSON and now only contains a secondsActive key which seems to be collecting the same data as the secondsforemost key from the older .plist structure.

> cat /Library/Application\ Support/JAMF/usage_reports/2021-09-06 | jq  
{
  "appName": "Google Chrome",
  "appPath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
  "secondsActive": 2259
},

It seems the JamfDaemon is monitoring application lifecycle using an NSWorkspace object which watches Launch Services (launchservicesd) events. Running strings on the JamfDaemon app bundle shows some plain text references.

> strings /Library/Application\ Support/JAMF/Jamf.app/Contents/MacOS/JamfDaemon.app/Contents/MacOS/JamfDaemon | grep -i NSWorkspace
stopping listening for NSWorkspace events
starting to listen for NSWorkspace events
NSWorkspaceApplicationKey
Unable to set currently active process. NSWorkspace reports no frontmostApplication or menuBarOwningApplication
failed to update icon - NSWorkspace unable to set icon
NSWorkspace

Frontmost/foreground app state can be viewed, along with the app’s unique ASN number, by calling lsappinfo while interactively switching frontmost apps in the macOS GUI. This demonstrates the type of events being watched, such as kLSNotifyBecameFrontmost and LSMenuBarOwnerASN.

> lsappinfo listen +becameFrontmost forever
Notification: kLSNotifyBecameFrontmost time=09/06/2021 19:23:46.696 dataRef={ "LSOtherASN"=ASN:0x0-0x11011:, "LSMenuBarOwnerASN"=ASN:0x0-0xa40a4:, "LSMenuBarOwnerApplicationSeed"=455, "ApplicationType"="Foreground", "LSASN"=ASN:0x0-0xa40a4:, "LSFrontApplicationSeed"=495, "CFBundleIdentifier"="com.apple.TextEdit" } affectedASN="TextEdit" ASN:0x0-0xa40a4:  context=0x0 sessionID=186a8 notificationID=0x7f86c2704d20

lsappinfo can also give a great insight into the launch time of a specific app.

> lsappinfo info -only kLSLaunchTimeKey TextEdit                                                                             [15:46:43]
"LSLaunchTime"=2021/09/06 14:38:37

Log Submission:

Once the macOS device completes an inventory update the captured JSON data will be sent to the Jamf Pro server and stored in the following MySQL database table – application_usage_logs.

Log Review:

Checking the device’s inventory record in Jamf Pro will show all captured usage data. See the Jamf Pro Administrator’s guide.

Application Usage Logs – Jamf Pro

Usage data can also be returned by the Jamf Pro classic API on a per-device basis using the computerapplicationusage endpoint resource.

Log retention in Jamf Pro is set under System Settings > Log Flushing. Depending on database restrictions and performance it may not be possible to extended the log retention too much.