Sharing a Script to Remove Unwanted macOS Apps

Fluffy
Contributor III

Hello all,

I created this script to get around the not-so-great Safelist and Blocklist profile payload for macOS in Jamf School. This still isn't a great solution, which is why we plan to lock devices down more next school year, but may be found useful for anyone wanting to remove apps remotely. If I had a github account, I probably would have posted it there, but for now I'll walk through it here. I welcome any feedback and advice on how I can improve the script.

Credit to grahampugh for the process killing function from his Remove Application.sh that I tweaked for this.

Please note: Jamf School is not required to use this script. The implementation will be different, but the script itself can easily be used elsewhere on macOS devices.

Script

What the script does is look for the app matching the provided bundle ID. If there is a match, it quits/kills any running processes and deletes the app. Apps outside of the Application folder will still be detected. While doing all of this, it reports parts of the process for the admin to observe afterward.

The script is configurable on how it reports data. The two ways I had in mind is either using echo (as the output shows up in Jamf School) or using the writeLog function, which both uses echo and writes to a log file locally with the date and time. By default, the log is located in /Library/Logs/App Removal/ but can be modified by changing the LOG variable.

It can also email the log if using the writeLog function. By default the subject is 'App Removal', but it can be changed with the SUBJECT variable. I recommend setting up a filter otherwise it can easily be flagged as spam.

Spoiler
#!/bin/bash

#############################################################
#															#
#	This script finds and uninstalls unwanted apps			#
#	using variables provided by admin running script.		#
#	Made for use in Jamf School as a band-aid for			#
#	lack of tools to block apps effectively.				#
#															#
#	Set BUNDLE to the app's Bundle ID. To find Bundle ID,	#
#	use the following command on device with app installed:	#
#	osascript -e 'get id of app "your_app_here"'			#
#															#
#	If process name differs from the app name, modify		#
#	PROCESS as needed.										#
#															#
#	To receive email reports, do the following:				#
#	1. Set EMAIL to 0										#
#	2. Set RECEIVER to where the email is sent to			#
#	3. Set REPORT to "writeLog"								#
#															#
#	To turn off email reports, set EMAIL to 1				#
#															#
#	To keep log, set REPORT to "writeLog".					#
#															#
#	To keep no log, set REPORT to "echo". Emailing reports	#
#	will not work with this option.							#
#															#
#############################################################

#################
### VARIABLES ###
#################

BUNDLE="app_bundle_id"
LOCATION=$(mdfind "kMDItemCFBundleIdentifier == \"$BUNDLE\"")
APP=$(basename "$LOCATION" .app)
PROCESS="$APP"
EMAIL=1
RECEIVER="email_address_to_receive_reports"
SUBJECT="App Removal"
REPORT="writeLog"
LOG="/Library/Logs/App Removal/$APP.log"

#################
### FUNCTIONS ###
#################

## Checks if variables are set by comparing to default.
function flightCheck () {
	if [ "$BUNDLE" == "app_bundle_id" ]; then
		"$REPORT" "App bundle ID not set."
		VARERR=1
	fi
	if ([ "$RECEIVER" == "email_address_to_receive_reports" ] && [ "$EMAIL" -eq 0 ]); then
		"$REPORT" "Emails are set to 0 but the reveiver of the email is not set."
		VARERR=1
	fi
	if ([ "$REPORT" != "writeLog" ] && [ "$EMAIL" -eq 0 ]); then
		"$REPORT" "Emails are set to 0 but REPORT is not set to writeLog."
		VARERR=1
	fi
	if [ "$VARERR" -eq 1 ]; then
		if ([ "$RECEIVER" != "email_address_to_receive_reports" ] && [ "$EMAIL" -eq 0 ]); then
			cat "$LOG" | mail -s "$SUBJECT" "$RECEIVER"
		fi
		echo "Variable error. Please ensure variables set by admin are correct."
		exit 1
	fi
}

## Creates log to show time/date.
function writeLog () {
	mkdir -p "/Library/Logs/App Removal/"
	if [ -n "$1" ]; then
		IN="$1"
	else
		read IN
	fi
    DATE=$(date +%Y-%m-%d\ %H:%M:%S)
    echo "${1}"
    echo "$DATE" " $IN" >> "$LOG"
}

## Silently quits/kills app.
function cleanup () {
    if [[ $(pgrep -ix "$PROCESS") ]]; then
    	"$REPORT" "Closing $APP."
    	/usr/bin/osascript -e "quit app \"$APP\""
    	sleep 1

    	## Double-checks.
    	countUp=0
    	while [[ $countUp -le 10 ]]; do
    		if [[ -z $(pgrep -ix "$PROCESS") ]]; then
    			"$REPORT" "$APP closed."
    			break
    		else
    			let countUp=$countUp+1
    			sleep 1
    		fi
    	done
        if [[ $(pgrep -x "$PROCESS") ]]; then
    	    "$REPORT" "$APP failed to quit - killing."
    	    /usr/bin/pkill "$PROCESS"
        fi
    fi
}

## Deletes app if it exists and checks if successfully removed.
function removeApp () {
    if [ -d "$LOCATION" ]; then
        "$REPORT" "Found app: $LOCATION"
        rm -rf "$LOCATION"
        sleep 1
        if [ -d "$LOCATION" ];then
        	"$REPORT" "Failed to remove $APP."
        else
        	"$REPORT" "Successfully removed $APP."
        fi
    else
        "$REPORT" "Could not find app with bundle ID: $BUNDLE"
    fi
}

############
### BODY ###
############

flightCheck
"$REPORT" "Job start."
cleanup
removeApp
"$REPORT" "Job end."

## Emails contents of log.
if [ "$EMAIL" -eq 0 ]; then
	cat "$LOG" | mail -s "$SUBJECT" "$RECEIVER"
fi

exit 0

In the case that the bundle ID cannot be gathered or some other reason, I have also made a variation of the script that uses a provided app name. This is less reliable, as it will not work if the app has been renamed, while the script above doesn't care. Otherwise, configuration is the same.

Spoiler
#!/bin/bash

#############################################################
#															#
#	This script finds and uninstalls unwanted apps			#
#	using variables provided by admin running script.		#
#	Made for use in Jamf School as a band-aid for			#
#	lack of tools to block apps effectively.				#
#															#
#	Set APP to the name of the app without the extension.	#
#															#
#	If process name differs from the app name, modify		#
#	PROCESS as needed.										#
#															#
#	To receive email reports, do the following:				#
#	1. Set EMAIL to 0										#
#	2. Set RECEIVER to where the email is sent to			#
#	3. Set REPORT to "writeLog"								#
#															#
#	To turn off email reports, set EMAIL to 1				#
#															#
#	To keep log, set REPORT to "writeLog".					#
#															#
#	To keep no log, set REPORT to "echo". Emailing reports	#
#	will not work with this option.							#
#															#
#############################################################

#################
### VARIABLES ###
#################

APP="name_of_app"
PROCESS="$APP"
BUNDLE=$(osascript -e "get id of app \"$APP\"")
LOCATION=$(mdfind "kMDItemCFBundleIdentifier == \"$BUNDLE\"")
EMAIL=1
RECEIVER="email_address_to_receive_reports"
SUBJECT="App Removal"
REPORT="writeLog"
LOG="/Library/Logs/App Removal/$APP.log"

#################
### FUNCTIONS ###
#################

## Checks if variables are set by comparing to default.
function flightCheck () {
	if [ "$APP" == "name_of_app" ]; then
		"$REPORT" "App name not set."
		VARERR=1
	fi
	if ([ "$RECEIVER" == "email address to receive reports" ] && [ "$EMAIL" -eq 0 ]); then
		"$REPORT" "Emails are set to \"0\" but the reveiver of the email is not set."
		VARERR=1
	fi
	if [ "$VARERR" -eq 1 ]; then
		if ([ "$RECEIVER" != "email_address_to_receive_reports" ] && [ "$EMAIL" -eq 0 ]); then
			cat "$LOG" | mail -s "$SUBJECT" "$RECEIVER"
		fi
		echo "Variable error. Please ensure variables set by admin are correct."
		exit 1
	fi
}

## Creates log to show time/date.
function writeLog () {
	mkdir -p "/Library/Logs/App Removal/"
	if [ -n "$1" ]; then
		IN="$1"
	else
		read IN
	fi
    DATE=$(date +%Y-%m-%d\ %H:%M:%S)
    echo "${1}"
    echo "$DATE" " $IN" >> "$LOG"
}

## Silently quits/kills app.
function cleanup () {
    if [[ $(pgrep -ix "$PROCESS") ]]; then
    	"$REPORT" "Closing $APP."
    	/usr/bin/osascript -e "quit app \"$APP\""
    	sleep 1

    	## Double-checks.
    	countUp=0
    	while [[ $countUp -le 10 ]]; do
    		if [[ -z $(pgrep -ix "$PROCESS") ]]; then
    			"$REPORT" "$APP closed."
    			break
    		else
    			let countUp=$countUp+1
    			sleep 1
    		fi
    	done
        if [[ $(pgrep -x "$PROCESS") ]]; then
    	    "$REPORT" "$APP failed to quit - killing."
    	    /usr/bin/pkill "$PROCESS"
        fi
    fi
}

## Deletes app if it exists and checks if successfully removed.
function removeApp () {
    if [ -d "$LOCATION" ]; then
        "$REPORT" "Found app: $LOCATION"
        rm -rf "$LOCATION"
        sleep 1
        if [ -d "$LOCATION" ];then
        	"$REPORT" "Failed to remove $APP."
        else
        	"$REPORT" "Successfully removed $APP."
        fi
    else
        "$REPORT" "$APP not found."
    fi
}

############
### BODY ###
############

flightCheck
"$REPORT" "Job start."
cleanup
removeApp
"$REPORT" "Job end."

## Emails contents of log.
if [ "$EMAIL" -eq 0 ]; then
	cat "$LOG" | mail -s "$SUBJECT" "$RECEIVER"
fi

exit 0

Workflow in Jamf School

Currently, I am using this script for removing apps that are not allowed by the school, primarily VPN apps. The process I use is as follows:

  1. Create a Smart Group for devices with a filter for 'User installed app' and select bundle ID of unwanted app.
  2. Using the Script Module, create a new script and paste my script with the bundle ID of the app, taken from the Smart Group.
  3. Set the 'When to run' to a daily schedule and the scope to the Smart Group.
  4. Check emailed reports every so often for anything that was failed to be removed and 'repeat offenders'.

Once this is done for an app, devices will be automatically added to the Smart Group when Jamf School detects it has been installed. Once the apps have been removed, the devices in the Smart Group will eventually be taken out once Jamf School no longer detects it as installed. The Smart Group will keep the filter even if no devices are detected to have an app with that bundle ID installed.

The main problem for doing it this way is that the apps have to be detected by Jamf School (cannot enter a bundle ID into the filter), which means it is not preventative. Alternatively, if you already know the bundle ID or rely on using the app name, you could create a script scoped to a large group of devices to not require you to check if the app has been installed and detected by Jamf School.

0 REPLIES 0