-
Notifications
You must be signed in to change notification settings - Fork 36
Support.app configuration
I make heavy use of the excellent Support.app found here for my users. Several of the items that I use it for are:
In our environment we use Entra / JAMF and no Kerberos. What I was trying to accomplish for the end users is to give them some kind of idea of when their paswswords are about ready to expire. There are couple of ways that you can do this:
- You can use the local login password reset date, but that might not have the same time sync as the Entra server
passwordAge=$(expr $(expr $(date +%s) - $(dscl . read /Users/${LOGGED_IN_USER} | grep -A1 passwordLastSetTime | grep real | awk -F'real>|</real' '{print $2}' | awk -F'.' '{print $1}')) / 86400) - Or you can get the information from the Entra server using the MS Graph API. I have the script for that here
Disclaimer: This may not be the best method to retrieve / store network passwords, but this has been working flawlessly for me for the past year. I welcome any recommendations on a better idea.
- Use this script from my repo found here and have it run Once a Day. You will need to provide your Entra credentials for the script.

- When that runs, it will update and/or create the .plist file and store it in the User's personal library
~/Library/Application Support/<filename.plist>. I do this location as I have multi-user macs in my environment.
Here is the structure of that file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>DriveMappings</key>
<array>
<string>smb://dfs11inf2/clientserver</string>
<string>smb://dfs10INF1/common</string>
</array>
<key>EntraAdminRights</key>
<string>Yes</string>
<key>EntraGroups</key>
<array>
<string>CLIENT TECHNOLOGIES</string>
</array>
<key>PasswordAge</key>
<string>218</string>
<key>PasswordLastChanged</key>
<string>2025-08-13T18:16:13Z</string>
</dict>
</plist>
The key fields that we are going to use are <PasswordAge> and <PasswordLastChanged>.
NOTE: I create the local file so I don't have to constantly log into the server to get password info. Saves time and I believe it offers more flexibility for local scripting on the machine
- If you want to retreieve the PasswordAge field (or any field) use the
defaults readto retrieve the data:
plistFile="<file location>"
PasswordAge=$(defaults read "$plistFile" "PasswordAge")
LastPasswordChange=$(defaults read "$plistFile" "PasswordLastChanged")
-
With this new found information, I do a couple of things with it:
- I have a daily script that will determine if the user's password is about to expire (within two weeks) and show them a dialog message on the screen. Script for that can be found here. A sample of what that looks llie:

- Since I also want to show this information to the end users, I use the Support.app utility and setup custom extensions for this app. Here is what that screen looks like

and here is the Extension script to produce that output:
```
#!/bin/zsh
# Support App Extension - Show Password Age
#
#
# Support App Extension to show the age of the current user's password and how many days are left until it expires.
#
# get the currently logged in user and their home directory
LOGGED_IN_USER=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
USER_DIR=$( dscl . -read /Users/${LOGGED_IN_USER} NFSHomeDirectory | awk '{ print $2 }' )
extensionID="GetPasswordAge"
passwordLimit=365
plistFile="<yourplistlocation>" #ex. $USER_DIR/Library/Application Support/EntraInfo.plist
# Retrieve password age from the user's .plist file and write it to the Support App preference plist
defaults write /Library/Preferences/nl.root3.support.plist "${extensionID}_loading" -bool true
sleep .5
# Get password age and calculate days left until password expires
PasswordAge=$(defaults read "$plistFile" "PasswordAge")
LastPasswordChange=$(defaults read "$plistFile" "PasswordLastChanged")
dayleft=$((passwordLimit - PasswordAge))
LastPasswordChangeDate=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" $LastPasswordChange +"%x")
# Write output to Support App preference plist
defaults write /Library/Preferences/nl.root3.support.plist "${extensionID}" -string "Changed: ${LastPasswordChangeDate}\n${dayleft} Days Left"
defaults write /Library/Preferences/nl.root3.support.plist "${extensionID}_loading" -bool false
# Trigger an orange warning notification for the user if their password is set to expire within 14 days
if [[ $dayleft -le 14 ]]; then
defaults write /Library/Preferences/nl.root3.support.plist "${extensionID}_alert" -bool true
# Execute the JAMF policy to trigger the notification for the user if their password is set to expire within 14 days
/usr/local/bin/jamf policy -event password_expire
else
defaults write /Library/Preferences/nl.root3.support.plist "${extensionID}_alert" -bool false
fi
>IMPORTANT! Do not use the $HOME variable to determine the user's home drive as this extension runs with elevated privleges, so it will return the wrong home drive if you use the $HOME variable.
I also like the fact that I can set an "alert" symbol" when the user's password is within the 14 day limit, so not only do they see the symbol in their menubar, but they also get a dialog prompt showing what to do to change it as well
## Other Entra Scripts
I also have other MS Entra scripts in my repo that can retrieve the following:
* Last Password Change
* Employee ID Photo
* Groups
* Admin Privleges
These scripts can all be found [here](https://github.com/ScottEKendall/JAMF-Pro-System-Scripts/tree/main)
## Battery Info
For laptops, I have the support.app extensin to show the battery level. I also show an alert if the threshold is below 80% capacity. Here is the code for that:
#!/bin/zsh
#set -x
readonly PREF_FILE="/Library/Preferences/nl.root3.support.plist" readonly EXT_ID="BatteryHealth" readonly COLOR_INDICATORS="true"
typeset -g symbol="battery.100" typeset -g retval="" typeset -g is_alert=false
function get_battery_info() { # 1. Check if it's a laptop if ! "$(system_profiler SPHardwareDataType; then retval="Not A Laptop" symbol="desktopcomputer" return fi
# 2. Get Current Charge Percentage (for the Icon)
local current_charge=$(pmset -g batt | awk -F'[\t%]' '/InternalBattery/ {print $2}')
[[ -z "$current_charge" ]] && current_charge=100
# Map charge to SF Symbols
if (( current_charge > 87 )); then symbol="battery.100"
elif (( current_charge > 62 )); then symbol="battery.75"
elif (( current_charge > 37 )); then symbol="battery.50"
elif (( current_charge > 12 )); then symbol="battery.25"
else symbol="battery.0"; fi
# 3. Get Health/Condition logic
local arch=$(arch)
local health_cond=$(system_profiler SPPowerDataType | awk -F': ' '/Condition/ {print $2}' | xargs)
local max_cap="100"
if [[ "$arch" == "arm64" ]]; then
max_cap=$(system_profiler SPPowerDataType | awk -F': ' '/Maximum Capacity/ {print $2}' | tr -d '% ' )
retval="$health_cond ($max_cap%)"
else
retval="$health_cond"
fi
# 4. Set Alert if Health is bad or Capacity < 80%
if [[ "$health_cond" != "Normal" || "$max_cap" -lt 80 ]]; then
is_alert=true
fi
}
defaults write "$PREF_FILE" "${EXT_ID}_loading" -bool true sleep 0.25
get_battery_info
local indicator="" if "$COLOR_INDICATORS" == "true"; then "$is_alert" == "true" && indicator="🔴 " || indicator="🟢 " fi
defaults write "$PREF_FILE" "${EXT_ID}_alert" -bool "$is_alert" defaults write "$PREF_FILE" "${EXT_ID}" -string "${indicator}${retval}" defaults write "$PREF_FILE" "${EXT_ID}_symbol" -string "${symbol}" defaults write "$PREF_FILE" "${EXT_ID}_loading" -bool false
"$is_alert" == "true" && /usr/local/bin/jamf policy -event battery_condition_fail
## Show Active IP Address
I have developed a small extension script to reflect the IP address of the active adapter. I follow this hierarchy (VPN > Ethernet > Wi-Fi). Which ever active adapter is highest in the hierachy then I display that information. Very handy for remote support calls. Also displays an alert badge if there are no active addresses.
#!/bin/zsh
#set -x
readonly PREF_FILE="/Library/Preferences/nl.root3.support.plist" readonly EXT_ID="NetworkInfo" readonly COLOR_INDICATORS="true"
typeset -g symbol="network" typeset -g retval=""
function get_nic_info() { local ip vpn_bin
# 1. Check VPN (Highest Priority)
# Use -e (exists) and find the first match quickly
for bin in "/opt/cisco/secureclient/bin/vpn" "/opt/cisco/anyconnect/bin/vpn"; do
[[ -f "$bin" ]] && vpn_bin="$bin" && break
done
if [[ -n "$vpn_bin" ]]; then
ip=$($vpn_bin stats 2>/dev/null | awk -F': ' '/Client Address \(IPv4\)/ {print $2}' | xargs)
if [[ -n "$ip" && "$ip" != "Not Available" ]]; then
retval="${ip}\n(VPN)"
symbol="lock.icloud"
return
fi
fi
# 2. Check Ethernet (Prioritize wired)
# Find active services and filter for Ethernet-like names
local eth_dev=$(networksetup -listnetworkserviceorder | awk -F'Device: ' '/Ethernet|LAN/ {print $2}' | tr -d ')')
for dev in ${(f)eth_dev}; do
ip=$(ipconfig getifaddr "$dev" 2>/dev/null)
if [[ -n "$ip" ]]; then
retval="${ip}\n(Ethernet)"
symbol="network"
return
fi
done
# 3. Check Wi-Fi
local wifi_dev=$(networksetup -listallhardwareports | awk '/Wi-Fi/{getline; print $2}')
ip=$(ipconfig getifaddr "$wifi_dev" 2>/dev/null)
if [[ -n "$ip" ]]; then
retval="${ip}\n(Wi-Fi)"
symbol="wifi"
return
fi
retval="No active adapter found"
symbol="network"
}
local circle="" "$COLOR_INDICATORS" == "true" && { green="🟢 "; red="🔴 "; }
defaults write "$PREF_FILE" "${EXT_ID}_loading" -bool true sleep 0.25
get_nic_info
local is_alert=false if "$retval" == "No active"*; then is_alert=true nic_status="${red}${retval}" else nic_status="${green}${retval}" fi
defaults write "$PREF_FILE" "${EXT_ID}_alert" -bool "$is_alert" defaults write "$PREF_FILE" "${EXT_ID}" -string "${nic_status}" defaults write "$PREF_FILE" "${EXT_ID}_symbol" -string "${symbol}" defaults write "$PREF_FILE" "${EXT_ID}_loading" -bool false
Some sample output:

## JAMF Checkin
This one came with the sample scripts from the support.app site, but I modified it slightly to show status icons:
#!/bin/zsh --no-rcs
twenty_four_hour_format="false"
extension_id="last_check_in" color_indicators="true" # Set to "true" to use the color circle emojis, "false" or anything else for no emojis if "$color_indicators" == "true"; then green_circle="🟢 " yellow_circle="🟡 " red_circle="🔴 " else green_circle="" yellow_circle="" red_circle="" fi
preference_file_location="/Library/Preferences/nl.root3.support.plist"
defaults write "${preference_file_location}" "${extension_id}_loading" -bool true
defaults write "${preference_file_location}" "${extension_id}" -string "KeyPlaceholder"
sleep 0.5
last_check_in_time=$(grep "Checking for policies triggered by "recurring check-in"" "/private/var/log/jamf.log" | tail -n 1 | awk '{ print $2,$3,$4 }')
last_check_in_time_epoch=$(date -j -f "%b %d %T" "${last_check_in_time}" +"%s")
if "${twenty_four_hour_format}" == "true"; then
last_check_in_time_human_reable=$(date -r "${last_check_in_time_epoch}" "+%A %H:%M") else
last_check_in_time_human_reable=$(date -r "${last_check_in_time_epoch}" "+%A %I:%M %p") fi
defaults write "${preference_file_location}" "${extension_id}" -string "${last_check_in_time_human_reable}"
now_epoch=$(date +%s) diff_seconds=$(( now_epoch - last_check_in_time_epoch ))
four_hours=14400 eight_hours=28800
if $diff_seconds -ge $eight_hours; then
status_symbol=$red_circle elif $diff_seconds -ge $four_hours; then
status_symbol=$yellow_circle else
status_symbol=$green_circle fi
final_output="${status_symbol} ${last_check_in_time_human_reable}"
defaults write "${preference_file_location}" "${extension_id}" -string "${final_output}"
defaults write "${preference_file_location}" "${extension_id}_loading" -bool false
## Using JAMF Policies
You can take advantage of the JAMF `policy -event <custom event>` to execute a task/script when specific conditions occur. I will detail the example of my battery condition failure, but this can be applied on any support.app Extension that you create:
1. ### Create a JAMF policy
Create your standard JAMF policy, but make sure to put in a custom event keyword

>Take note of the custom event name...this is what you use in the Extensions script
2. ### JAMF Policy Script
I have JAMF run one of my scripts that will notify the user of their battery failure. (Script can be found here: https://github.com/ScottEKendall/JAMF-Pro-Scripts/tree/main/BatteryInfo

3. ### Extension Script Logic
Once you have the JAMF policy created, you need to put the logic in your extensions script to have it call the JAMF policy
```# display notification if alert is true (battery health is not good)
[[ "$is_alert" == "true" ]] && /usr/local/bin/jamf policy -event battery_condition_fail
Note the event name needs to match your policy trigger
With this example above, when the user activates the support.app, the JAMF policy will only run if the battery condition shows failure
