#!/bin/zsh --no-rcs 
# shellcheck shell=bash

####################################################################################################
#
# EA Download
#
#   Purpose: Command-line script to download Jamf Cloud-hosted Extension Attributes (EAs)
#
####################################################################################################
#
# HISTORY
#
# Version 1.0.0, 30-Sep-2025, Dan K. Snelson (@dan-snelson)
#   - First "official" version (tested with Jamf Pro 11.21.0)
#
####################################################################################################



####################################################################################################
#
# Global Variables
#
####################################################################################################

export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/

# Script Version
scriptVersion="1.0.0"

# Client-side Log
scriptLog="org.churchofjesuschrist.log"

# Log Level [ DEBUG, INFO, WARNING, ERROR ]
logLevel="DEBUG"

# Path of the current script
typeset -gr scriptPath=${(%):-%x}   
typeset -gr scriptName=${scriptPath:t}
typeset -gr scriptBase=${scriptPath:t:r}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

# Script Human-readabale Name
humanReadableScriptName="EA Download"

# Organization's Script Name
organizationScriptName="EAD"

# Date Time Stamp
dateTimeStamp=$( date '+%Y-%m-%d-%H%M%S' )



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

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function log() {
  echo "${organizationScriptName} ($scriptVersion): $( date +%Y-%m-%d\ %H:%M:%S ) - [${1}] ${2}" | tee -a "${workingDirectory}/${scriptLog}"
}

function logDebug()     { [[ "$logLevel" == "DEBUG" ]] && log DEBUG "$@"; }
function logError()     { log ERROR "$@"; }
function logFatal()     { log FATAL "$@"; exit 1; }
function logInfo()      { log INFO "$@"; }
function logNotice()    { log NOTICE "$@"; }
function logWarn()      { log WARNING "$@"; }



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Help
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function help() {

    local apiURL=$( /usr/bin/defaults read "/Library/Preferences/com.jamfsoftware.jamf.plist" jss_url )
    printf "\nAfter creating a Jamf Pro API Role and Client with the \"Read Computer Extension Attributes\" privilege,\n"
    printf "use the following commands to create entries in Keychain Access:\n\n"
    printf "    security add-generic-password -s \"${organizationScriptName:l}ApiUrl\" -a ${USER} -w \"${apiURL}\"\n"
    printf "    security add-generic-password -s \"${organizationScriptName:l}ApiClientID\" -a ${USER} -w \"API Client ID Goes Here\"\n"
    printf "    security add-generic-password -s \"${organizationScriptName:l}ApiClientSecret\" -a ${USER} -w \"API Client Secret Goes Here\""
    printf "\n\nUse \"zsh ${scriptName} configuration\" with logLevel set to DEBUG to display API credentials.\n\n\n"

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Configuration
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function configuration() {

    logNotice "Configuration:"
    local apiURL=$( security find-generic-password -s "${organizationScriptName:l}ApiUrl" -a "${USER}" -w 2>/dev/null )
    local apiClientID=$( security find-generic-password -s "${organizationScriptName:l}ApiClientID" -a "${USER}" -w 2>/dev/null )
    local apiClientSecret=$( security find-generic-password -s "${organizationScriptName:l}ApiClientSecret" -a "${USER}" -w 2>/dev/null )

    if [ -z ${apiURL} ] || [ -z ${apiClientID} ] || [ -z ${apiClientSecret} ]; then
        logFatal "Unable to read API credentials. Please run \"zsh ${scriptName} help\" to configure API credentials."
    else
        if [[ ${logLevel} == "DEBUG" ]]; then
            logDebug "Displaying API credentials in plain text (logLevel is set to DEBUG)"
            echo "          URL: ${apiURL}"
            echo "    Client ID: ${apiClientID}"
            echo "Client Secret: ${apiClientSecret}"
        else
            logInfo "Displaying masked API credentials (logLevel is not set to DEBUG)"
            maskedApiURL="${apiURL:0:3}$(printf '%*s' $((${#apiURL}-4)) '' | tr ' ' '*')${apiURL: -3}"
            maskedApiClientID="${apiClientID:0:3}$(printf '%*s' $((${#apiClientID}-4)) '' | tr ' ' '*')${apiClientID: -3}"
            maskedApiClientSecret="${apiClientSecret:0:3}$(printf '%*s' $((${#apiClientSecret}-4)) '' | tr ' ' '*')${apiClientSecret: -3}"
            echo "          URL: ${apiURL}"
            echo "    Client ID: ${maskedApiClientID}"
            echo "Client Secret: ${maskedApiClientSecret}"
        fi
    fi

    printf "\n\n\nUse the following commands to update entries in Keychain Access:\n\n"
    printf "    security add-generic-password -U -s \"${organizationScriptName:l}ApiUrl\" -a ${USER} -w \"${apiURL}\"\n"
    printf "    security add-generic-password -U -s \"${organizationScriptName:l}ApiClientID\" -a ${USER} -w \"API Client ID Goes Here\"\n"
    printf "    security add-generic-password -U -s \"${organizationScriptName:l}ApiClientSecret\" -a ${USER} -w \"API Client Secret Goes Here\"\n\n"

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Obtain Bearer Token
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function obtainBearerToken() {
    
    logNotice "Obtain Bearer Token …"

    apiBearerToken=$( curl -s -X POST "${apiURL}/api/oauth/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "client_id=${apiClientID}" \
    --data-urlencode "client_secret=${apiClientSecret}" \
    --data-urlencode "grant_type=client_credentials" \
    | jq -r '.access_token' )

    logDebug "apiBearerToken: ${apiBearerToken}"

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Validate Bearer Token (returns the HTTP status code)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function validateBearerToken() {

    apiBearerTokenCheck=$( curl -s -o /dev/null -w "%{http_code}" "${apiURL}/api/v1/auth" \
        --request GET \
        --header "Authorization: Bearer ${apiBearerToken}")

    logDebug "apiBearerTokenCheck: ${apiBearerTokenCheck}"

    if [[ "${apiBearerTokenCheck}" != 200 ]]; then
        logError "INVALID API CREDENTIALS"
        logError "Please run \"zsh ${scriptName} configuration\" with logLevel set to DEBUG to display API credentials."
        logFatal "Error: ${apiBearerTokenCheck}; exiting."
    else
        logInfo "Bearer Token is valid."
    fi

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Validate and optionally renew the Bearer Token; returns the HTTP status code
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function validateRenewBearerToken() {

    validateBearerToken
    
    if [[ "${apiBearerTokenCheck}" = "200" ]]; then

        apiBearerToken=$( curl -s -X POST "${apiURL}/api/oauth/token" \
        -H "Content-Type: application/x-www-form-urlencoded" \
        --data-urlencode "client_id=${apiClientID}" \
        --data-urlencode "client_secret=${apiClientSecret}" \
        --data-urlencode "grant_type=client_credentials" \
        | jq -r '.access_token' )

    else

        logInfo "Expired Bearer Token; renewing …"
        obtainBearerToken

    fi

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Invalidate the Bearer Token
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function invalidateBearerToken() {

    logNotice "Invalidating bearer token..."

    response=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${apiURL}/api/v1/auth/invalidate-token" \
        -H "accept: application/json" \
        -H "Authorization: Bearer $apiBearerToken")

    if [[ "$response" == "204" ]]; then
        logInfo "Bearer token invalidated successfully."
    else
        logWarn "Failed to invalidate bearer token. HTTP Status: $response"
    fi

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Generate Extension Attribute List
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function generateExtensionAttributeList() {

    logNotice "Generate Extension Attribute List …"
    
    extensionAttributeList=$( curl -s -X GET "${apiURL}/api/v1/computer-extension-attributes" \
        --header "Authorization: Bearer ${apiBearerToken}" \
        -o "${workingDirectory}/extensionAttributeList.json" )

    # Check if the downloaded file is zero bytes
    if [[ ! -s "${workingDirectory}/extensionAttributeList.json" ]]; then
        logFatal "Downloaded ${workingDirectory}/extensionAttributeList.json is empty (zero bytes); exiting."
    else
        extensionAttributeIDs=$(jq -r '.results[].id'  "${workingDirectory}/extensionAttributeList.json")
        extensionAttributeNames=$(jq -r '.results[].name' "${workingDirectory}/extensionAttributeList.json")
    fi

    if [[ "${logLevel}" == "DEBUG" ]]; then
        sortedExtensionAttributeNames=$(echo "${extensionAttributeNames}" | sort)
        logDebug "The following Script Extension Attributes will be downloaded to the ${dateTimeStamp} directory:"
        echo "${sortedExtensionAttributeNames}" | tee -a "${workingDirectory}/${scriptLog}"
    fi

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Extract Script Extension Attributes
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function extractScriptExtensionAttributes() {
    logNotice "Extracting Script Extension Attributes…"
    local json_file="${workingDirectory}/extensionAttributeList.json"
    local output_dir="${workingDirectory}/scripts"

    # Create output directory
    mkdir -p "$output_dir"

    # Extract and write script contents for SCRIPT inputTypes
    while IFS= read -r entry; do
        local id name raw_script filename shebang script_body
        id=$(jq -r '.id' <<< "$entry")
        name=$(jq -r '.name | gsub("[^A-Za-z0-9_]+"; "_")' <<< "$entry")
        raw_script=$(jq -r '.script' <<< "$entry")
        filename="${output_dir}/${id}-${name}.sh"

        # Extract shebang if present
        if [[ "$raw_script" == \#!* ]]; then
            shebang=$(head -n1 <<< "$raw_script")
            script_body=$(tail -n +2 <<< "$raw_script")
        else
            shebang="#!/bin/bash"
            script_body="$raw_script"
        fi

        {
            [[ -n "$shebang" ]] && echo "$shebang"
            echo ""
            echo "###"
            echo "# Name: ${name}"
            echo "#  URL: ${apiURL}/view/settings/computer-management/computer-extension-attributes/${id}"
            echo "#   ID: ${id}"
            echo "#"
            echo "# Extracted on: ${dateTimeStamp} by ${humanReadableScriptName} (${scriptVersion}) from https://snelson.us/audit"
            echo "###"
            echo ""
            echo "$script_body"
        } > "$filename"

        # Convert to Unix line endings
        sed -i '' $'s/\r$//' "$filename"

        chmod +x "$filename"
        echo "Extracted: $filename"
        printf "\n-------------------------\n\n"
    done < <(
        jq -c '
            .results[] |
            select(.inputType == "SCRIPT") |
            {id, name, script: .scriptContents}
        ' "$json_file"
    )
}



####################################################################################################
#
# Main Program
#
####################################################################################################

# Clear the screen
/usr/bin/clear


# Create Working Directory, based on ${dateTimeStamp}
workingDirectory="$(cd "$(dirname "$0")" && pwd)/${dateTimeStamp}"
mkdir -p "${workingDirectory}"
logInfo "Created working directory: ${workingDirectory}"



# Client-side Logging
if [[ ! -f "${workingDirectory}/${scriptLog}" ]]; then
    touch "${workingDirectory}/${scriptLog}"
    if [[ -f "${workingDirectory}/${scriptLog}" ]]; then
        logInfo "Created specified scriptLog: ${scriptLog}"
    else
        logFatal "Unable to create specified scriptLog '${scriptLog}'; exiting.\n\n(Is this script running as 'root' ?)"
    fi
else
    # logInfo "Specified scriptLog '${scriptLog}' exists; writing log entries to it"
fi



# Pre-flight Check: Logging Preamble
logInfo "\n\n###\n# $humanReadableScriptName (${scriptVersion})\n# https://snelson.us/audit\n#\n# Log Level: ${logLevel}\n###\n"



# Check for help command
if [[ "${1}" = "help" ]]; then

    logNotice "Displaying Help …"
    help
    exit 0



# Check for configuration command
elif [[ "${1}" = "configuration" ]]; then

    logNotice "Displaying Configuration …"
    configuration
    exit 0


# Download Extension Attributes
else

    # Retrieve API Credentials from Keychain
    logInfo "Retrieve API Credentials from Keychain …"
    local apiURL=$( security find-generic-password -s "${organizationScriptName:l}ApiUrl" -a "${USER}" -w 2>/dev/null )
    local apiClientID=$( security find-generic-password -s "${organizationScriptName:l}ApiClientID" -a "${USER}" -w 2>/dev/null )
    local apiClientSecret=$( security find-generic-password -s "${organizationScriptName:l}ApiClientSecret" -a "${USER}" -w 2>/dev/null )

    if [ -z ${apiURL} ] || [ -z ${apiClientID} ] || [ -z ${apiClientSecret} ]; then
        logFatal "Unable to read API credentials. Please run \"zsh ${scriptName} help\" to configure API credentials."
    fi

    if [[ ${logLevel} == "DEBUG" ]]; then
        maskedApiClientID="${apiClientID:0:3}$(printf '%*s' $((${#apiClientID}-4)) '' | tr ' ' '*')${apiClientID: -3}"
        maskedApiClientSecret="${apiClientSecret:0:3}$(printf '%*s' $((${#apiClientSecret}-4)) '' | tr ' ' '*')${apiClientSecret: -3}"
        logDebug "API URL: ${apiURL}"
        logDebug "API Client ID: ${maskedApiClientID}"
        logDebug "API Client Secret: ${maskedApiClientSecret}"
    else
        logInfo "API Credentials retrieved successfully."
    fi

    # Get Bearer Token
    obtainBearerToken
    validateBearerToken

    # Download Extension Attributes
    generateExtensionAttributeList
    extractScriptExtensionAttributes

    # Invalidate Bearer Token
    invalidateBearerToken

    # Open Working Directory
    open -R "${workingDirectory}"
    logInfo "Working directory opened in Finder: ${workingDirectory}"

    exit 0

fi