#!/bin/bash
# set -x

####################################################################################################
# Script Name:  Get-APNSMDMClientHealth.sh
# By:  Zack Thompson / Created:  11/3/2023
# Version:  1.0.0 / Updated:  11/20/2023 / By:  ZT
#
# Description:  This script checks the health of the APNS MDM Client and local MDM Profile.
#
#	Pass [--debug | -d ] as an argument to "test" the script and not write to the local EA files.
#
####################################################################################################

##################################################
# Set variables for your environment

# Jamf Pro Server URL
jamf_pro_server=$( /usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url )

# APNS Cert Topic
# Can be obtained from https://identity.apple.com/pushcert/ 
# 	or https://<jps.url.org>:8443/pushNotificationCertificate.html
if [[ "${jamf_pro_server}" =~ .*production.* ]]; then
	apple_mgmt_cert="com.apple.mgmt.External.<INSERT_APNs_TOPIC_GUID_HERE"
elif [[ "${jamf_pro_server}" =~ .*development.* ]]; then
	apple_mgmt_cert="com.apple.mgmt.External.INSERT_APNs_TOPIC_GUID_HERE"
fi

# MDM Profile Identifier
mdm_profile_identifier="00000000-0000-0000-A000-4A414D460003"

# The number of days before reporting device as "inactive."
last_connected_variance=7

# The date format expected from date fields generated by `apns_stats`.
default_date_format="%b %d, %Y at %H:%M:%S %p"

# The format of the date string that will be used for reporting.
jamfpro_ea_date_format="%Y-%m-%d %H:%M:%S"

# Locally log EA value for historical reference (since Jamf Pro only ever has the last value).
# Supported actions:
#   true - Do locally Log
#   false - Do not log locally
locally_log="true"
local_ea_history="/opt/ManagedFrameworks/EA_History.log"
local_ea_inventory="/opt/ManagedFrameworks/Inventory.plist"
debugging_description="Get-APNSMDMClientHealth.sh:  "

####################
# Compiled Results
local_ea_history_compiled_description="APNS/MDM Management Topics:  "
local_ea_inventory_compiled_key="apns_mdm_mgmt_topics"

# MDM Profile APNS Topic
local_ea_history_mdm_profile_description="MDM Management Topic:  "
local_ea_inventory_mdm_profile_key="mdm_apns_topic"

# APNS Cert and Local Push Topic Comparison
local_ea_history_apns_topic_description="APNS Topics:  "
local_ea_inventory_apns_topic_key="apns_topics"

# APNS Cert and Device Push Topic Comparison
local_ea_history_apns_and_device_topics="APNS Cert and Device Topics:  "
local_ea_inventory_apns_and_device_topics="apns_and_device_topics"

# APNS Cert and User Push Topic Comparison
local_ea_history_apns_and_user_topics="APNS Cert and User Topics:  "
local_ea_inventory_apns_and_user_topics="apns_and_user_topics"

####################
# APNS Device Channel Reporting Variables

# APNS Device Last Push Date
local_ea_history_apns_device_last_push_date="APNS Device Last Push Date:  "
local_ea_inventory_apns_device_last_push_date="apns_device_last_push_date"

# APNS Device Persistent Connection Status
local_ea_history_apns_device_persistent_conn_status="APNS Device Persistent Connection Status:  "
local_ea_inventory_apns_device_persistent_conn_status="apns_device_persistent_conn_status"

# APNS Device Push Topics
local_ea_history_apns_device_push_topics="APNS Device Push Topics:  "
local_ea_inventory_apns_device_push_topics="apns_device_push_topics"

# APNS Device Opportunistic Topic Status
local_ea_history_apns_device_opportunistic_topic_status="APNS Device Opportunistic Topic Status:  "
local_ea_inventory_apns_device_opportunistic_topic_status="apns_device_opportunistic_topic_status"

####################
# APNS User Channel Reporting Variables

# APNS User Last Push Date
local_ea_history_apns_user_last_push_date="APNS User Last Push Date:  "
local_ea_inventory_apns_user_last_push_date="apns_user_last_push_date"

# APNS User Persistent Connection Status
local_ea_history_apns_user_persistent_conn_status="APNS User Persistent Connection Status:  "
local_ea_inventory_apns_user_persistent_conn_status="apns_user_persistent_conn_status"

# APNS User Push Topics
local_ea_history_apns_user_push_topics="APNS User Push Topics:  "
local_ea_inventory_apns_user_push_topics="apns_user_push_topics"

# APNS User Opportunistic Topic Status
local_ea_history_apns_user_opportunistic_topic_status="APNS User Opportunistic Topic Status:  "
local_ea_inventory_apns_user_opportunistic_topic_status="apns_user_opportunistic_topic_status"

##################################################
# Functions

arg_parse() {
	# Command Line Argument Parser

	while (( "$#" )); do
		# Work through the passed arguments

		case "${1}" in
			-d | --debug )
				debug="true"
				write_to_ea_history "DEBUGGING ENABLED" "${debugging_description}"
			;;
			# * )
			# 	switch="${1}"
			# 	shift
			# 	value="${1}"
			# 	eval "${switch}"="'${value}'"
			# ;;
		esac

		shift
	done
}

write_to_ea_inventory() {

	# Arguments
	# $1 = (str) Plist key that the message value will be assigned too
	# $2 = (str) Message that will be assigned to the key

	local key="${1}"
	local value="${2}"

	if [[ "${locally_log}" == "true" && -z "${debug}" ]]; then

		if [[ ! -e "${local_ea_inventory}" ]]; then

			/bin/mkdir -p "$( /usr/bin/dirname "${local_ea_inventory}" )"
			/usr/bin/touch "${local_ea_inventory}"

		fi

		/usr/bin/defaults write "${local_ea_inventory}" "${key}" "${value}"

	elif [[ -n "${debug}" ]]; then

		write_to_ea_history "key:  ${key} > value: ${value}"

	fi

}

write_to_ea_history() {

	# Arguments
	# $1 = (str) Message that will be written to a log file
	# $2 = (str) Optional description that will be written ahead of message

	local message="${1}"
	local description="${2}"

	time_stamp=$( /bin/date +%Y-%m-%d\ %H:%M:%S )

	if [[ "${locally_log}" == "true" && -z "${debug}" ]]; then

		if [[ ! -e "${local_ea_history}" ]]; then

			/bin/mkdir -p "$( /usr/bin/dirname "${local_ea_history}" )"
			/usr/bin/touch "${local_ea_history}"

		fi

		echo "${time_stamp} | ${description}${message}" >> "${local_ea_history}"

	else

		echo "${time_stamp} | ${description}${message}"

	fi

}

PlistBuddy_Helper() {
	# Helper function to interact with plists.

	# Arguments
	# $1 = (str) action to perform on the plist
		# The "print" action expects to work with text (passed to PlistBuddy
		# via stdin), which can be generated via "print_xml"
	# $2 = (str) Path to plist or generated xml
	# $3 = (str) Key or key path to read
	# $4 = (str) Type that will be used for the value
	# $5 = (str) Value to be set to the passed key

	local action="${1}"
	local plist="${2}"
	local key="${3}"
	local type="${4}"
	local value="${5}"

	if [[ "${action}" == "print_xml"  ]]; then

		# Dump plist file as XML
		/usr/libexec/PlistBuddy -x -c "print" "${plist}" 2> /dev/null

	elif [[ "${action}" == "print"  ]]; then

		# Read plist "str" (as a heredoc) and print the passed key
		/usr/libexec/PlistBuddy -c "Print :${key}" /dev/stdin <<< "${plist}" 2> /dev/null

	elif [[ "${action}" == "add"  ]]; then

		# Configure values
		/usr/libexec/PlistBuddy -c "Add :${key} ${type} ${value}" "${plist}" > /dev/null 2>&1 || \
		/usr/libexec/PlistBuddy -c "Set :${key} ${value}" "${plist}" > /dev/null 2>&1

	elif [[ "${action}" == "delete"  ]]; then

		# Delete a key
		/usr/libexec/PlistBuddy -c "Delete :${key} ${type}" "${plist}" > /dev/null 2>&1

	elif [[ "${action}" == "clear"  ]]; then

		# Clear a key's value
		/usr/libexec/PlistBuddy -c "clear ${type}" "${plist}" > /dev/null 2>&1

	fi

}

check_last_connection() {

	# Check if the last connected date is older than date variance.
	# Arguments
	# $1 = (str) a date in string format
	# $2 = (str) the expected format of the date string
	# $3 = (int) date variance; in number of days
	# $4 = (str) Additional message to prefix the output

	fixed_date="$( fixup_space_in_string "${1}" )"

	if [[
		$( convert_date "${fixed_date}" "${2}" "%s" ) \
		-lt $( /bin/date -j -v-"${3}"d +"%s" )
	]]; then

		echo "${4}${fixed_date}  ([WARNING] Last connection was over ${3} days)"

	else

		echo "${4}${fixed_date}  (Last connection was less than ${3} days)"

	fi

}

convert_date(){

	# Convert a formatted date string into another format
	# Arguments
	# $1 = (str) a date in string format
	# $2 = (str) the expected format of the date string
	# $3 = (str) the desired format of the string

	/bin/date -j -f "${2}" "$( fixup_space_in_string "${1}" )" +"${3}"

}

fixup_space_in_string(){

	# Convert unicode spaces in a string into a basic space
	# Arguments
	# $1 = (str) a string

	/usr/bin/sed 's/[[:space:]]/ /g' <<< "${1}"

}

verify_array_items_match() {

	# Verify all items in a passed array match
	# Each item has leading and trailing whitespace removed
	# Arguments
	# $@ = (array) an array

	local array=("$@")

	result="True"

	for (( i=0; i<((${#array[@]}-1)); i++ )); do
		if [[ $( echo "${array[$i]}" | xargs ) != $( echo "${array[(($i+1))]}" | xargs ) ]]; then
			result="False"
			break
		fi
	done

	echo "${result}"

}

##################################################
# Command Line Argument Parser

parameters=( "$@" )
arg_parse "${parameters[@]}"

##################################################
# Check MDM Profile APNS Topic

# Dump installed Profiles to stdout in XML format
all_profiles=$( /usr/bin/profiles show -cached --output "stdout-xml" )

# Get the number of installed profiles
number_of_profiles=$(
	PlistBuddy_Helper "print" "${all_profiles}" "_computerlevel" | \
	/usr/bin/grep -a -E "^    Dict {$" | /usr/bin/wc -l | /usr/bin/xargs
)

mdm_profile_matches="Unable to validate MDM Profile"

# Loop through the Profile, searching for the one we're looking for.
for (( count=0; count < number_of_profiles; ++count )); do

	if [[ "${break_loop}" == "true" ]]; then
		break
	fi

	identifier=$(
		PlistBuddy_Helper "print" "${all_profiles}" "_computerlevel:${count}:ProfileIdentifier"
	)

	if [[ "${identifier}" == "${mdm_profile_identifier}" ]]; then

		number_of_profile_items=$(
			PlistBuddy_Helper "print" "${all_profiles}" "_computerlevel:${count}:ProfileItems" | \
			/usr/bin/grep -a -E "^    Dict {$" | /usr/bin/wc -l | /usr/bin/xargs
		)

		for ((
			profile_items_count=0;
			profile_items_count < number_of_profile_items;
			++profile_items_count
		)); do

			if [[ "${break_loop}" == "true" ]]; then
				break
			fi

			payload_type=$(
				PlistBuddy_Helper "print" "${all_profiles}" \
				"_computerlevel:${count}:ProfileItems:${profile_items_count}:PayloadType"
			)

			if [[ "${payload_type}" == "com.apple.mdm" ]]; then

				payload_content_topic=$(
					PlistBuddy_Helper "print" "${all_profiles}" \
					"_computerlevel:${count}:ProfileItems:${profile_items_count}:PayloadContent:Topic"
				)

				if [[ "${payload_content_topic}" == "${apple_mgmt_cert}" ]]; then

					write_to_ea_history "MDM Profile's APNS Topic is valid" "${local_ea_history_mdm_profile_description}"
					write_to_ea_inventory "${local_ea_inventory_mdm_profile_key}" "Valid"
					mdm_profile_matches="true"
					break_loop="true"

				else

					write_to_ea_history "[ERROR] MDM Profile's APNS Topic is invalid!" "${local_ea_history_mdm_profile_description}"
					write_to_ea_history "MDM Profile APNS Topic:  ${payload_content_topic}" "${local_ea_history_mdm_profile_description}"
					write_to_ea_inventory "${local_ea_inventory_mdm_profile_key}" "Invalid"
					break_loop="true"
					mdm_profile_matches="false"

				fi

			fi

		done

	fi

done

##################################################
# Check MDM Profile APNS Topic

# Check APNS status for the MDM Client service
apns_stats=$( /System/Library/PrivateFrameworks/ApplePushService.framework/apsctl status )

if [[ -z "${apns_stats}" ]]; then

	apns_topic_matches="Unable to validate APNS Topic"

else

	########################################
	# Device Channel Stats
device_apns_stats=$( /usr/bin/osascript -l JavaScript << EndOfScript

	var apns_stats=\`$apns_stats\`

	apns_stats.match(
		/(^\s+application port name:\s+)com.apple.aps.mdmclient.daemon.push.production(.|\n)+?(?=\1)/gm
	)

EndOfScript
)

	##############################
	# Device Last Push Notification Date
	IFS=$'\n' read -r -d '' -a device_last_push_date <<< "$(
		echo "${device_apns_stats}" | \
		/usr/bin/awk -F 'last full wake incoming push:' '{print $2}' | \
		/usr/bin/awk -F ' \\(' '{print $1}'
	)"

	declare -a device_last_push_date_fixup
	for item in "${device_last_push_date[@]}"; do
		item=$( echo "${item}" | xargs )
		device_last_push_date_fixup+=("${item}")
	done

	# Check if the more recent date is older than seven days
	write_to_ea_history "$(
		check_last_connection "${device_last_push_date_fixup[0]}" \
			"${default_date_format}" $last_connected_variance "${local_ea_history_apns_device_last_push_date}"
	)"
	write_to_ea_inventory "${local_ea_inventory_apns_device_last_push_date}" "$(
		convert_date "${device_last_push_date_fixup[0]}" "${default_date_format}" "${jamfpro_ea_date_format}"
	)"

	if [[ $( verify_array_items_match "${device_last_push_date_fixup[@]}" ) != "True" ]]; then
		# Check if the more recent date is older than seven days
		write_to_ea_history "$(
			check_last_connection "${device_last_push_date_fixup[1]}" \
				"${default_date_format}" $last_connected_variance "${local_ea_history_apns_device_last_push_date}"
		)"
	fi

	##############################
	# Device Persistent Connection Status (Should be 'Ok')
	device_persistent_conn_status=$(
		echo "${device_apns_stats}" | \
		/usr/bin/awk -F 'persistent connection status:' '{print $2}' | \
		/usr/bin/xargs
	)

	write_to_ea_history "${device_persistent_conn_status}" "${local_ea_history_apns_device_persistent_conn_status}"
	write_to_ea_inventory "${local_ea_inventory_apns_device_persistent_conn_status}" "${device_persistent_conn_status}"

	##############################
	# Device APNS Topics
	IFS=$'\n' read -r -d '' -a device_push_topics <<< "$(
		echo "${device_apns_stats}" | \
		/usr/bin/awk -F 'topic:' '{print $2}'
	)"

	declare -a device_push_topics_fixup
	for item in "${device_push_topics[@]}"; do
		item=$( echo "${item}" | xargs )
		device_push_topics_fixup+=("${item}")
	done

	if [[ $( verify_array_items_match "${device_push_topics_fixup[@]}" ) == "True" ]]; then
		write_to_ea_history "Match" "${local_ea_history_apns_device_push_topics}"
		write_to_ea_inventory "${local_ea_inventory_apns_device_push_topics}" "Match"
	else
		write_to_ea_history "${device_push_topics_fixup[0]}" "${local_ea_history_apns_device_push_topics}"
		write_to_ea_history "${device_push_topics_fixup[1]}" "${local_ea_history_apns_device_push_topics}"
		write_to_ea_inventory "${local_ea_inventory_apns_device_push_topics}" "Mismatch"
	fi

	##############################
	# Device APNS Opportunistic Topic Status (Should be 'Ok')
	device_opportunistic_topic_status=$(
		echo "${device_apns_stats}" | \
		/usr/bin/grep -Ezo '\s\s+status:\s+[A-Za-z]+' | \
		/usr/bin/awk -F ':' '{print $2}' | \
		/usr/bin/xargs
	)

	write_to_ea_history "${device_opportunistic_topic_status}" "${local_ea_history_apns_device_opportunistic_topic_status}"
	write_to_ea_inventory "${local_ea_inventory_apns_device_opportunistic_topic_status}" "${device_opportunistic_topic_status}"

	########################################
	# User Channel Stats
	user_apns_stats=$( /usr/bin/osascript -l JavaScript << EndOfScript

		var apns_stats=\`$apns_stats\`

		apns_stats.match(
			/(^\s+application port name:\s+)com.apple.aps.mdmclient.agent.push.production(.|\n)+?(?=\1)/gm
		)

EndOfScript
)

	##############################
	# User Last Push Notification Date
	IFS=$'\n' read -r -d '' -a user_last_push_date <<< "$(
		echo "${user_apns_stats}" | \
		/usr/bin/awk -F 'last incoming push(:| notification:)' '{print $2}' | \
		/usr/bin/awk -F ' \\(' '{print $1}'
	)"

	if [[ -z "${user_last_push_date}" ]]; then 

		write_to_ea_history "Last Push Not Reported" "${local_ea_history_apns_user_last_push_date}"
		write_to_ea_inventory "${local_ea_inventory_apns_user_last_push_date}" ""

	else

		declare -a user_last_push_date_fixup
		for item in "${user_last_push_date[@]}"; do
			item=$( echo "${item}" | xargs )
			user_last_push_date_fixup+=("${item}")
		done

		# Check if the more recent date is older than seven days
		write_to_ea_history "$(
			check_last_connection "${user_last_push_date_fixup[0]}" \
				"${default_date_format}" $last_connected_variance "${local_ea_history_apns_user_last_push_date}"
		)"
		write_to_ea_inventory "${local_ea_inventory_apns_user_last_push_date}" "$(
			convert_date "${user_last_push_date_fixup[0]}" "${default_date_format}" "${jamfpro_ea_date_format}"
		)"

		if [[ $( verify_array_items_match "${user_last_push_date_fixup[@]}" ) != "True" ]]; then
			# Check if the more recent date is older than seven days
			write_to_ea_history "$(
				check_last_connection "${user_last_push_date_fixup[1]}" \
					"${default_date_format}" $last_connected_variance "${local_ea_history_apns_user_last_push_date}"
			)"
		fi

	fi

	##############################
	# User Persistent Connection Status (Should be 'Ok')
	user_persistent_conn_status=$(
		echo "${user_apns_stats}" | \
		/usr/bin/awk -F 'persistent connection status:' '{print $2}' | \
		/usr/bin/xargs
	)

	write_to_ea_history "${user_persistent_conn_status}" "${local_ea_history_apns_user_persistent_conn_status}"
	write_to_ea_inventory "${local_ea_inventory_apns_user_persistent_conn_status}" "${user_persistent_conn_status}"

	##############################
	# User APNS Topics
	IFS=$'\n' read -r -d '' -a user_push_topics <<< "$(
		echo "${user_apns_stats}" | \
		/usr/bin/awk -F 'topic:' '{print $2}'
	)"

	declare -a user_push_topics_fixup
	for item in "${user_push_topics[@]}"; do
		item=$( echo "${item}" | xargs )
		user_push_topics_fixup+=("${item}")
	done

	if [[ $( verify_array_items_match "${user_push_topics_fixup[@]}" ) == "True" ]]; then
		write_to_ea_history "Match" "${local_ea_history_apns_user_push_topics}"
		write_to_ea_inventory "${local_ea_inventory_apns_user_push_topics}" "Match"
	else
		write_to_ea_history "${user_push_topics_fixup[0]}" "${local_ea_history_apns_user_push_topics}"
		write_to_ea_history "${user_push_topics_fixup[1]}" "${local_ea_history_apns_user_push_topics}"
		write_to_ea_inventory "${local_ea_inventory_apns_user_push_topics}" "Mismatch"
	fi

	##############################
	# Device APNS Opportunistic Topic Status (Should be 'Ok')
	user_opportunistic_topic_status=$(
		echo "${user_apns_stats}" | \
		/usr/bin/grep -Ezo '\s\s+status:\s+[A-Za-z]+' | \
		/usr/bin/awk -F ':' '{print $2}' | \
		/usr/bin/xargs
	)

	write_to_ea_history "${user_opportunistic_topic_status}" "${local_ea_history_apns_user_opportunistic_topic_status}"
	write_to_ea_inventory "${local_ea_inventory_apns_user_opportunistic_topic_status}" "${user_opportunistic_topic_status}"

	########################################
	# Compiled Device and User Comparison Results
	if [[
		"${apple_mgmt_cert}" == "${device_push_topics_fixup[0]}" &&
		"${apple_mgmt_cert}" == "${user_push_topics_fixup[0]}"
	]]; then
		write_to_ea_history "Valid" "${local_ea_history_apns_topic_description}"
		write_to_ea_inventory "${local_ea_inventory_apns_topic_key}" "Valid"
		apns_topic_matches="true"
	else

		write_to_ea_history "[ERROR] Mismatch!" "${local_ea_history_apns_topic_description}"
		write_to_ea_inventory "${local_ea_inventory_apns_topic_key}" "Invalid"

		if [[ "${apple_mgmt_cert}" == "${device_push_topics_fixup[0]}" ]]; then
			write_to_ea_history "Valid" "${local_ea_history_apns_and_device_topics}"
			write_to_ea_inventory "${local_ea_inventory_apns_and_device_topics}" "Valid"
		else
			write_to_ea_history "[ERROR] Mismatch!" "${local_ea_history_apns_and_device_topics}"
			write_to_ea_inventory "${local_ea_inventory_apns_and_device_topics}" "Invalid"
		fi

		if [[ "${apple_mgmt_cert}" == "${user_push_topics_fixup[0]}" ]]; then
			write_to_ea_history "Valid" "${local_ea_history_apns_and_user_topics}"
			write_to_ea_inventory "${local_ea_inventory_apns_and_user_topics}" "Valid"
		else
			write_to_ea_history "[ERROR] Mismatch!" "${local_ea_history_apns_and_user_topics}"
			write_to_ea_inventory "${local_ea_inventory_apns_and_user_topics}" "Invalid"
		fi

	fi

fi

if [[ "${mdm_profile_matches}" == "true" && "${apns_topic_matches}" == "true" ]]; then
	write_to_ea_history "Valid" "${local_ea_history_compiled_description}"
	write_to_ea_inventory "${local_ea_inventory_compiled_key}" "Valid"
else
	write_to_ea_history "Invalid" "${local_ea_history_compiled_description}"
	write_to_ea_inventory "${local_ea_inventory_compiled_key}" "Invalid"
fi

exit 0