#!/bin/zsh
###################################################################################################
# Created by Noah Anderson | se@kandji.io | Kandji, Inc. | Systems Engineering
###################################################################################################
# Created on 02/11/2022
# Updated on 03/10/2022
# Updated on 04/07/2022
###################################################################################################
# Tested macOS Versions
###################################################################################################
#
# macOS 12: 12.1, 12.2, 12.2.1, 12.3b3
# macOS 11: 11.6.1, 11.6.3, 11.6.4
# macOS 10: 10.14.6, 10.15.7
#
###################################################################################################
# Software Information
###################################################################################################
# 
# Audit script for Privileges Checker add-on. Conducts 11 checks to validate the fidelity of the 
# Launch Agent and enforcement script:
# - Execution script exists (1), has proper permissions (2), and has system immutable flag set (3)
# - Launch Agent exists (4), has proper permissions (5), and has system immutable flag set (6)
# - Agent is alive and returns args of script (7), audit/remediation script versions match (8)
# - Timeout value (if set) matches on-disk (9), profile timeout value (if set) matches on-disk (10)
# - User exclusion list (if defined) matches on-disk (11)
# If any of the above fail, return exit 1 to trigger reinstallation of Privileges Checker.
#
###################################################################################################
# License Information
###################################################################################################
# Copyright 2024 Kandji, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
###################################################################################################

###################################################################################################
####################################### VARIABLES TO MODIFY #######################################
###################################################################################################

# If changing the timeout in minutes within the remediation script, also update the value below
# NOTE: This value must match EXACTLY what is defined for MINUTES_TO_WAIT in the remediation script
# Failure to match these values will cause PC to constantly reinstall from the failed audit
# Audit only evaluates this value if defined (e.g. MINUTES_TO_WAIT=20), otherwise skips the check
MINUTES_TO_WAIT=

# If changing the profile timeout option in the remediation script, also update the value below
# NOTE: This value must match EXACTLY what is defined for USE_PROFILE_TIMEOUT in the remediation script
# Failure to match these values will cause PC to constantly reinstall from the failed audit
# Audit only evaluates this value if defined (e.g. USE_PROFILE_TIMEOUT=True), otherwise skips the check
USE_PROFILE_TIMEOUT=

# If changing the users to exclude within the remediation script, also update the value(s) below
# NOTE: This value must match EXACTLY what is defined for USERS_TO_EXCLUDE in the remediation script
# Failure to match these values will cause PC to constantly reinstall from the failed audit
# Audit only evaluates if defined (e.g. USERS_TO_EXCLUDE=("admin")), otherwise skips the check
USERS_TO_EXCLUDE=(
""
""
""
)

###################################################################################################
########################################## DO NOT MODIFY ##########################################
###################################################################################################

#zsh has a built-in operator that can actually do float compares; just gotta load it
autoload is-at-least

#Version for both audit + remediation scripts 
version=1.0.6

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

###############################################
## Logs to stdout as well as Unified Log
## Arguments:
##   Takes one arg, "${1}"
## Outputs:
##   Writes to stdout and Unified log
###############################################
function LOGGING {
    /bin/echo "${1}"
    /usr/bin/logger "Privileges Checker Audit: ${1}"
}

##############################################
## Validates that ${2} is greater than or equal
## to ${1}; used for version validation of
## this audit script and execution code
## Arguments:
##   Takes two args "${1}", "${2}", both floats
## Returns:
##   0 if version is compliant, 1 if not 
###############################################
function vers_check() {
    # is-at-least is a zsh built-in for float math
    # returns exit 0 for true, exit 1 for false
    is-at-least "${1}" "${2}" 
    /bin/echo $?
}

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

#Define paths to Launch Agent and execution script
agent_path="/Library/LaunchAgents/io.kandji.privileges-checker.plist"
script_path="/Library/Scripts/MDMHelpers/privilegeschecker.zsh"

#Derive octal permissions of Launch Agent and execution script
agent_permissions=$(/usr/bin/stat -f %A "${agent_path}")
script_permissions=$(/usr/bin/stat -f %A "${script_path}")

#Ensure the system immutable flag is set on both components
agent_immutable=$(/bin/ls -lO "${agent_path}" | /usr/bin/awk '{print $5}')
script_immutable=$(/bin/ls -lO "${script_path}" | /usr/bin/awk '{print $5}')

#Get execution script version
script_version=$(/bin/cat "${script_path}" 2>/dev/null | /usr/bin/grep "version=" | /usr/bin/cut -d '=' -f2)
#Compare current version against on-disk version; capture exit code for success/failure
version_validation=$(vers_check "${version}" "${script_version}")

#Validate console user
console_user=$(/usr/bin/stat -f%Su /dev/console)
#If no user logged in, we can throw away stderr
uid=$(/usr/bin/id -u "${console_user}" 2>/dev/null)

if [[ "${uid}" -lt 501 ]]; then
    #No user logged in, so skip our agent validation
    agent_args="SKIP"
else
    #If we have a console user, validate our script path is present in the args for our launch agent
    agent_args=$(/bin/launchctl print "gui/${uid}/io.kandji.privileges-checker.plist" 2>&1 | sed -e '1,/arguments = {/d' -e '/}/,$d' | grep -o "${script_path}")
fi

#Check if our MINUTES_TO_WAIT var is populated above
if [[ -n ${MINUTES_TO_WAIT} ]]; then
    #If so, grab the current timeout set on-disk
    on_disk_minutes=$(/bin/cat "${script_path}" 2>/dev/null | /usr/bin/grep "MINUTES_TO_WAIT=" | /usr/bin/grep -v "mdm_minutes" | /usr/bin/cut -d '=' -f2)
    #Confirm the values match
    if [[ "${on_disk_minutes}" == "${MINUTES_TO_WAIT}" ]]; then
        timeout_matches=true
    else
        #If they don't, set this to false
        timeout_matches=false
    fi
else
    timeout_matches=true
fi

#Check if our USE_PROFILE_TIMEOUT var is populated above
if [[ -n ${USE_PROFILE_TIMEOUT} ]]; then
    #If so, grab the current profile value set on-disk
    on_disk_profile=$(/bin/cat "${script_path}" 2>/dev/null | /usr/bin/grep "USE_PROFILE_TIMEOUT=" | /usr/bin/cut -d '=' -f2 | /usr/bin/sed 's/"//g')
    #Confirm the values match
    if [[ "${on_disk_profile}" == "${USE_PROFILE_TIMEOUT}" ]]; then
        profile_matches=true
    else
        #If they don't, set this to false
        profile_matches=false
    fi
else
    profile_matches=true
fi

#Collapse our USERS_TO_EXCLUDE array to see if any user exclusion values are defined to check against
if [[ -n $(/bin/echo "${USERS_TO_EXCLUDE}" | xargs) ]]; then
    #If so, grab list of exclusion users defined on-disk
    on_disk_exclusions=($(/bin/cat "${script_path}" 2>/dev/null | /usr/bin/grep "USERS_TO_EXCLUDE" | /usr/bin/head -1 | /usr/bin/sed -e 's/.*(\(.*\)).*/\1/;s/,//g'))
    #Echo the array elements together, remove extraneous spaces, convert remaining spaces into newlines, sort them, and then report on any unmatched values (case-insensitive)
    exclusion_diff=$(/bin/echo "${USERS_TO_EXCLUDE[@]}" "${on_disk_exclusions[@]}" | /usr/bin/xargs | /usr/bin/tr ' ' '\n' | /usr/bin/sort -f | /usr/bin/uniq -ui)
fi

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

#These 11 checks will validate audit + enforcement of script/agent health, as well as validating the latest version is installed, the timeout value, profile setting, + user exclusions (if defined) match across audit + remediation scripts 
if [[ -f "${agent_path}" && -f "${script_path}" && -n "${agent_args}" && "${agent_permissions}" -eq 644 && "${script_permissions}" -eq 444 && "${agent_immutable}" == "schg" && "${script_immutable}" == "schg" && "${version_validation}" -eq 0 && "${timeout_matches}" == true && "${profile_matches}" == true && -z "${exclusion_diff}" ]]; then
    LOGGING "All checks pass"
    exit 0
else
    LOGGING "One or more failures occurred:"
    LOGGING "Agent Path: $(/bin/ls ${agent_path} 2>&1)"
    LOGGING "Agent Permissions: ${agent_permissions} (expected 644)"
    LOGGING "Agent Immutable: ${agent_immutable} (expected schg)"
    LOGGING "Script Path: $(/bin/ls ${script_path} 2>&1)"
    LOGGING "Script Permissions: ${script_permissions} (expected 444)"
    LOGGING "Script Immutable: ${script_immutable} (expected schg)"
    LOGGING "Agent Arguments: ${agent_args} (expected /path/to/privilegeschecker.zsh)"
    LOGGING "On-disk version: ${script_version} (expected ${version})"
    LOGGING "On-disk timeout: ${on_disk_minutes} (expected ${MINUTES_TO_WAIT})"
    LOGGING "On-disk use profile: ${on_disk_profile} (expected ${USE_PROFILE_TIMEOUT})"
    LOGGING "Excluded users: ${on_disk_exclusions} (expected ${USERS_TO_EXCLUDE})"
    exit 1
fi
