Dark mode

Dark mode

There are 0 results matching

article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune Linux Onboarding Tool

Onboard Ubuntu Linux devices to Microsoft Intune using a bash script. Installs prerequisites and starts the user-driven enrollment. ...

See More
article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune macOS Onboarding Tool

Onboard macOS devices to Microsoft Intune using a bash script that initiates the process. Optionally, the script converts mobile accounts, resets the FileVault key, and removes ...

See More
article card image dark article card image light

Published by · Jan 23, 2024 tools · 3 mins read

Introducing: Intune Device Renaming Tool

Rename Intune Devices by setting a Prefix or using a User Attribute as Prefix. Supports Windows, macOS, and Linux ...

See More
article card image dark article card image light

Published by · Dec 8, 2023 intune · 5 mins read

Intune Logs: A Deep Dive into Locations, Interpretation, and Configuration

A Comprehensive Guide to Locations, Interpretation, and Configuration of Intune Logs ...

See More
article card image dark article card image light

Published by · Aug 14, 2023 configmgr · 2 mins read

Configuration Manager Console Extension to show Device Collection Membership with Console Builder

Use the Configuration Manager Console Builder, to add Collection Membership View to the Device Node ...

See More
article card image dark article card image light

Published by · Aug 3, 2023 tools · 3 mins read

Introducing: Configuration Manager SSRS Dashboards

A Configuration Manager Dashboards solution with Reports for Software Updates, Bitlocker and more ...

See More
article card image dark article card image light

Published by · Aug 3, 2023 tools · 2 mins read

Introducing: PowerShell WMI Management Toolkit Module

Streamline your WMI Namespace, Class, and Instance Management with our PowerShell Module ...

See More
article card image dark article card image light

Published by · Jul 14, 2023 configmgr · 1 mins read

Configuration Manager detailed, filterable Port Documentation

Configuration Manager detailed, filterable port documentation as an excel document ...

See More
article card image dark article card image light

Published by · Jul 14, 2023 configmgr · 3 mins read

Configuration Manager PXE TFTP Window Size Bug

Configuration Manager TFTP Block Size and TFTP Window Size Correct Configuration ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 4 mins read

Introducing: Configuration Manager Client Cache Cleanup Tool

Cleaning the Configuration Manager Client Cache the Right Way with PowerShell and Configuration Baselines ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 2 mins read

Introducing: Windows Cache Cleanup Tool

Cleaning Windows and Configuration Manager Caches for Configuration Manager Build and Capture Task Sequence or Standalone Use ...

See More
article card image dark article card image light

Published by · Jun 17, 2023 tools · 1 mins read

Introducing: Windows Update Database Reinitialization Tool

Proactively repair corrupted Windows Update Database with Powershell and Configuration Manager ...

See More
article card image dark article card image light

Published by · Mar 31, 2023 tools · 3 mins read

Introducing: Configuration Manager SQL Products Reporting

A Complete SQL Products reporting solution using Configuration Manager ...

See More
article card image dark article card image light

Published by · Jan 28, 2023 configmgr · 1 mins read

Application Detection Method using the Configuration Manager Application Version

Replace hardcoded application version in scripts, with the Configuration Manager Application Version ...

See More
article card image dark article card image light

Published by · Jan 28, 2023 tools · 3 mins read

Introducing: Certificate Management Toolkit

Managing Certificates with Configuration Manager and PowerShell by using just the Public Key ...

See More
article card image dark article card image light

Published by · Jan 7, 2019 reports · 2 mins read

Configuration Manager Device Boundary and Network Information Report

List Device Boundaries and Network Information with Configuration Manager ...

See More
article card image dark article card image light

Published by · Sep 9, 1980 help · 5 mins read

MEM.Zone Blog Publishing Documentation

Publishing Documentation for MEM.Zone ...

See More

We couldn’t find anything related to

“SCCM”

BLOG / tools zone

Introducing: Intune macOS Onboarding Tool

Published by Popovici Ioan · Apr 11, 2024 · 2 mins read
article card image dark article card image light

Quick Summary

Microsoft Intune provides multiple options when it comes to macOS device enrollment.

We recently encountered a scenario where we needed enroll the devices as BYOD using Company Portal because the customer was reluctant to wipe the devices. This script is a result of that scenario.

To solve this issue, we created a bash script that starts the user driven Intune on-boarding process, converts mobile accounts, removes AD binding, JAMF management, and resets the FileVault key.

Prerequisites

Notes

Company Portal needs to be preinstalled on the device. You can use the provided script to pre-install it.


Recommendations

  • Run the script in a test environment before using it in production.
  • Use multiple test devices to validate your configuration.
Notes

The script logs in the /Library/Logs/${COMPANY_NAME} folder.

Notes

Although the script helped migrating about 5.000 devices, it cannot account for all edge cases. Please test thoroughly before using it in production.


User Defined Variables

COMPANY_NAME

This variable specifies the company name.

DISPLAY_NAME

This variable specifies the script name displayed to the user.

This variable specifies the support link displayed to the user.

This variable specifies the documentation link which will be opened at the end of the installation.

COMPANY_PORTAL_PATH

This variable specifies the path to the Company Portal application.

CONVERT_MOBILE_ACCOUNTS

This variable specifies if the mobile accounts should be converted.
Available options are: YES or NO.

REMOVE_FROM_AD

This variable specifies if the device should be removed from AD.
Available options are: YES or NO.

SET_ADMIN_RIGHTS

This variable specifies if the admin rights for the user account should be set.
Available options are: YES or NO.

JAMF_OFFBOARD

This variable specifies if the device should be off-boarded from JAMF.
Available options are: YES or NO.

SUPPORTED_OS_MAJOR_VERSION

This variable specifies the last supported macOS major version.

JAMF_API_URL

This variable specifies the JAMF API URL. Used for off-boarding from JAMF.

JAMF_API_USER

This variable specifies the JAMF API user. Used for off-boarding from JAMF.

JAMF_API_PASSWORD

This variable specifies the JAMF API password. Used for off-boarding from JAMF.

Notes

Do not modify any other variables unless you know what you are doing.


Manual Steps

The devices need to be tagged as Corporate in Intune in order to allow the FileVault key sync.

Notes

The FileVault key reset is mandatory in order for Intune to be able to manage the FileVault key.


Preview

article card image demo-intune-macos-onboarding-tool
Intune macOS Onboarding Tool Demo

Code

   1#!/bin/bash
   2#set -x
   3
   4#.SYNOPSIS
   5#    Starts Intune onboarding.
   6#.DESCRIPTION
   7#    Starts Intune onboarding, converting mobile accounts, removing AD binding and JAMF management
   8#    and setting admin rights.
   9#.EXAMPLE
  10#    start-intune-onboarding.sh
  11#.NOTES
  12#    Created by Ioan Popovici
  13#    Company Portal needs to be installed as a pre-requisite.
  14#    Return Codes:
  15#    0   - Success
  16#    10  - OS version not supported
  17#    11  - Company Portal application not installed
  18#    120 - Failed to display notification
  19#    130 - Failed to display dialog
  20#    131 - User cancelled dialog
  21#    140 - Failed to display alert
  22#    141 - User cancelled alert
  23#    150 - OS version not supported
  24#    160 - Invalid FileVault action
  25#    161 - Unauthorized FileVault user
  26#    162 - FileVault is already enabled
  27#    163 - FileVault is not enabled
  28#    164 - User cancelled FileVault action
  29#    165 - Failed to perform FileVault action
  30#    190 - Failed to convert mobile account
  31#    200 - Failed to get JAMF API token
  32#    201 - Failed to invalidate JAMF API token
  33#    202 - Invalid JAMF API token action
  34#    210 - Failed to perform JAMF send command action
  35#    211 - Invalid JAMF device id
  36#    220 - Failed to remove JAMF management profile
  37#.LINK
  38#    https://MEM.Zone
  39#.LINK
  40#    https://MEMZ.one/macOS-Intune-Onboarding-Tool
  41#.LINK
  42#    https://MEMZ.one/macOS-Intune-Onboarding-Tool-CHANGELOG
  43#.LINK
  44#    https://MEMZ.one/macOS-Intune-Onboarding-Tool-GIT
  45#.LINK
  46#    https://MEM.Zone/ISSUES
  47
  48##*=============================================
  49##* VARIABLE DECLARATION
  50##*=============================================
  51#region VariableDeclaration
  52
  53## User Defined variables
  54COMPANY_NAME='MEM.Zone IT'
  55DISPLAY_NAME='Intune Onboarding Tool'
  56SUPPORT_LINK='https://google.com'
  57DOCUMENTATION_LINK='https://google.com'
  58COMPANY_PORTAL_PATH='/Applications/Company Portal.app/'
  59CONVERT_MOBILE_ACCOUNTS='YES'
  60REMOVE_FROM_AD='YES'
  61SET_ADMIN_RIGHTS='YES'
  62JAMF_OFFBOARD='YES'
  63#  Specify last supported OS major version
  64SUPPORTED_OS_MAJOR_VERSION=12
  65#  JAMF API MDM Removal
  66JAMF_API_URL=''
  67JAMF_API_USER=''
  68JAMF_API_PASSWORD=''
  69
  70## Script variables
  71#  Version
  72SCRIPT_VERSION=5.0.1
  73OS_VERSION=$(sw_vers -productVersion)
  74#  Author
  75AUTHOR='Ioan Popovici'
  76#  Script Name
  77SCRIPT_NAME=$(/usr/bin/basename "$0")
  78FULL_SCRIPT_NAME="$(realpath "$(dirname "${BASH_SOURCE[0]}")")/${SCRIPT_NAME}"
  79SCRIPT_NAME_WITHOUT_EXTENSION=$(basename "$0" | sed 's/\(.*\)\..*/\1/')
  80#  Set JAMF variables
  81if [[ -n "$JAMF_API_USER" ]] && [[ -n "$JAMF_API_PASSWORD" ]] && [[ -n "$JAMF_API_URL" ]] ; then
  82    JAMF_API_ENABLED='YES'
  83fi
  84BEARER_TOKEN=''
  85TOKEN_EXPIRATION_EPOCH=0
  86#  Messages
  87MESSAGE_TITLE=$COMPANY_NAME
  88MESSAGE_SUBTITLE=$DISPLAY_NAME
  89#  Logging
  90LOG_NAME=$SCRIPT_NAME_WITHOUT_EXTENSION
  91LOG_DIR="/Library/Logs/${COMPANY_NAME}/${DISPLAY_NAME}"
  92LOG_HEADER="Script Version: $SCRIPT_VERSION \n# Author: $AUTHOR \n# OS Version: $OS_VERSION \n"
  93
  94#endregion
  95##*=============================================
  96##* END VARIABLE DECLARATION
  97##*=============================================
  98
  99##*=============================================
 100##* FUNCTION LISTINGS
 101##*=============================================
 102#region FunctionListings
 103
 104#region Function runAsRoot
 105#Assigned Error Codes: 100 - 109
 106function runAsRoot() {
 107#.SYNOPSIS
 108#    Checks for root privileges.
 109#.DESCRIPTION
 110#    Checks for root privileges and asks for elevation.
 111#.EXAMPLE
 112#    runAsRoot
 113#.NOTES
 114#    This is an internal script function and should typically not be called directly.
 115#.LINK
 116#    https://MEM.Zone
 117#.LINK
 118#    https://MEM.Zone/ISSUES
 119
 120    ## Set human readable parameters
 121    local scriptPath="$1"
 122
 123    ## Check if the script is run as root
 124    if [[ $EUID -ne 0 ]]; then
 125        displayNotification 'This application must be run as root. Please authenticate!'
 126        if [[ -t 1 ]]; then
 127            sudo "$scriptPath"
 128        else
 129            gksu "$scriptPath"
 130        fi
 131        exit 0
 132    fi
 133}
 134#endregion
 135
 136#region Function startLogging
 137#Assigned Error Codes: 110 - 119
 138function startLogging() {
 139#.SYNOPSIS
 140#    Starts logging.
 141#.DESCRIPTION
 142#    Starts loggign to to log file and STDOUT.
 143#.PARAMETER logName
 144#    Specifies the name of the log file.
 145#.PARAMETER logDir
 146#    Specifies the folder of the log file.
 147#.PARAMETER logHeader
 148#    Specifies additional header information to be added to the log file.
 149#.EXAMPLE
 150#    startLogging "logName" "logDir" "logHeader"
 151#.NOTES
 152#    This is an internal script function and should typically not be called directly.
 153#.LINK
 154#    https://MEM.Zone
 155#.LINK
 156#    https://MEM.Zone/ISSUES
 157
 158    ## Set human readable parameters
 159    local logName="$1"
 160    local logDir="$2"
 161    local logHeader="$3"
 162
 163    ## Set log file path
 164    logFullName="${logDir}/${logName}.log"
 165
 166    ## Creating log directory
 167    if [[ ! -d "$logDir" ]]; then
 168        echo "$(date) | Creating '$logDir' to store logs"
 169        sudo mkdir -p "$logDir"
 170    fi
 171
 172    ## Start logging to log file
 173    exec &> >(sudo tee -a "$logFullName")
 174
 175    ## Write log header
 176    echo   ""
 177    echo   "##*====================================================================================="
 178    echo   "# $(date) | Logging run of '$logName' to log file"
 179    echo   "# Log Path: '$logFullName'"
 180    printf "# ${logHeader}"
 181    echo   "##*====================================================================================="
 182    echo   ""
 183}
 184#endregion
 185
 186#region Function displayNotification
 187#Assigned Error Codes: 120 - 129
 188function displayNotification() {
 189#.SYNOPSIS
 190#    Displays a notification.
 191#.DESCRIPTION
 192#    Displays a notification to the user.
 193#.PARAMETER messageText
 194#    Specifies the message of the notification.
 195#.PARAMETER messageTitle
 196#    Specifies the title of the notification. Defaults to $MESSAGE_TITLE.
 197#.PARAMETER messageSubtitle
 198#    Specifies the subtitle of the notification. Defaults to $$MESSAGE_SUBTITLE.
 199#.PARAMETER notificationDelay
 200#    Specifies the minimum delay between the notifications in seconds. Defaults to 2.
 201#.PARAMETER supressNotification
 202#    Suppresses the notification. Defaults to false.
 203#.PARAMETER supressTerminal
 204#    Suppresses the notification in the terminal. Defaults to false.
 205#.EXAMPLE
 206#    displayNotification 'message' 'title' 'subtitle' 'duration'
 207#.EXAMPLE
 208#    displayNotification 'message' 'title' 'subtitle' '' '' 'suppressTerminal'
 209#.EXAMPLE
 210#    displayNotification 'message' 'title' 'subtitle' '' 'suppressNotification' ''
 211#.EXAMPLE
 212#    displayNotification 'message'
 213#.NOTES
 214#    This is an internal script function and should typically not be called directly.
 215#.LINK
 216#    https://MEM.Zone
 217#.LINK
 218#    https://MEM.Zone/ISSUES
 219
 220    ## Set human readable parameters
 221    local messageText
 222    local messageTitle
 223    local messageSubtitle
 224    local notificationDelay
 225    local supressTerminal
 226    local supressNotification
 227    local executionStatus=0
 228    #  Message
 229    messageText="${1}"
 230    #  Title
 231    if [[ -z "${2}" ]]; then
 232        messageTitle="${MESSAGE_TITLE}"
 233    else messageTitle="${2}"
 234    fi
 235    #  Subtitle
 236    if [[ -z "${3}" ]]; then
 237        messageSubtitle="${MESSAGE_SUBTITLE}"
 238    else messageSubtitle="${3}"
 239    fi
 240    #  Duration
 241    if [[ -z "${4}" ]]; then
 242        notificationDelay=2
 243    else notificationDelay="${4}"
 244    fi
 245    #  Supress notification
 246    if [[ -z "${5}" ]]; then
 247        supressNotification='false'
 248    else supressNotification="${5}"
 249    fi
 250    #  Supress terminal
 251    if [[ -z "${6}" ]]; then
 252        supressTerminal='false'
 253    else supressTerminal="${6}"
 254    fi
 255
 256    ## Debug variables
 257    #echo "messageText: $messageText; messageTitle: $messageTitle; messageSubtitle: $messageSubtitle; notificationDelay: $notificationDelay ; supressNotification: $supressNotification ; supressTerminal: $supressTerminal"
 258
 259    ## Display notification
 260    if [[ "$supressNotification" = 'false' ]]; then
 261        osascript -e "display notification \"${messageText}\" with title \"${messageTitle}\" subtitle \"${messageSubtitle}\""
 262        executionStatus=$?
 263        sleep "$notificationDelay"
 264    fi
 265
 266    ## Display notification in terminal
 267    if [[ "$supressTerminal" = 'false' ]]; then echo "$(date) | $messageText" ; fi
 268
 269    ## Return execution status
 270    if [[ "$executionStatus" -ne 0 ]]; then
 271        echo "$(date) | Failed to display notification. Error: '$executionStatus'"
 272        return 120
 273    fi
 274}
 275#endregion
 276
 277#region Function displayDialog
 278#Assigned Error Codes: 130 - 139
 279function displayDialog() {
 280#.SYNOPSIS
 281#    Displays a dialog box.
 282#.DESCRIPTION
 283#    Displays a dialog box with customizable buttons and optional password prompt.
 284#.PARAMETER messageTitle
 285#    Specifies the title of the dialog. Defaults to $MESSAGE_TITLE.
 286#.PARAMETER messageText
 287#    Specifies the message of the dialog.
 288#.PARAMETER messageSubtitle
 289#    Specifies the subtitle of the notification. Defaults to $MESAGE_SUBTITLE.
 290#.PARAMETER buttonNames
 291#    Specifies the names of the buttons. Defaults to '{Cancel, Ok}'.
 292#.PARAMETER defaultButton
 293#    Specifies the default button. Defaults to '1'.
 294#.PARAMETER cancelButton
 295#    Specifies the button to exit on. Defaults to ''.
 296#.PARAMETER messageIcon
 297#    Specifies the dialog icon as:
 298#       * 'stop', 'note', 'caution'
 299#       * the name of one of the system icons
 300#       * the resource name or ID of the icon
 301#       * the icon POSIX file path
 302#   Defaults to ''.
 303#.PARAMETER promptType
 304#    Specifies the type of prompt.
 305#    Avaliable options:
 306#        'buttonPrompt'   - Button prompt.
 307#        'textPrompt'     - Text prompt.
 308#        'passwordPrompt' - Password prompt.
 309#    Defaults to 'buttonPrompt'.
 310#.EXAMPLE
 311#    displayDialog 'messageTitle' 'messageSubtitle' 'messageText' '{"Ok", "Agree"}' '1' '' '' 'buttonPrompt' 'stop'
 312#.EXAMPLE
 313#    displayDialog 'messageTitle' 'messageSubtitle' 'messageText' '{"Ok", "Stop"}' '1' 'Stop' '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FinderIcon.icns' 'textPrompt'
 314#.EXAMPLE
 315#    displayDialog 'messageTitle' 'messageSubtitle' 'messageText' "{\"Ok\", \"Don't Continue\"}" '1' "Don't Continue" '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FinderIcon.icns' 'passwordPrompt'
 316#.NOTES
 317#    This is an internal script function and should typically not be called directly.
 318#.LINK
 319#    https://MEM.Zone
 320#.LINK
 321#    https://MEM.Zone/ISSUES
 322
 323    ## Set human readable parameters
 324    local messageTitle
 325    local messageSubtitle
 326    local messageText
 327    local buttonNames
 328    local defaultButton
 329    local cancelButton
 330    local messageIcon
 331    local promptType
 332    local commandOutput
 333    local executionStatus=0
 334
 335    ## Set parameter values
 336    #  Title
 337    if [[ -z "${1}" ]] ; then
 338        messageTitle="${MESSAGE_TITLE}"
 339    else messageTitle="${1}"
 340    fi
 341    #  Subtitle
 342    if [[ -z "${2}" ]] ; then
 343        messageSubtitle="${MESSAGE_SUBTITLE}"
 344    else messageSubtitle="${2}"
 345    fi
 346    #  Message
 347    messageText="${3}"
 348    #  Button names
 349    if [[ -z "${4}" ]] ; then
 350        buttonNames='{"Cancel", "Ok"}'
 351    else buttonNames="${4}"
 352    fi
 353    #  Default button
 354    if [[ -z "${5}" ]] ; then
 355        defaultButton='1'
 356    else defaultButton="${5}"
 357    fi
 358    #  Cancel button
 359    if [[ -z "${6}" ]] ; then
 360        cancelButton=''
 361    else cancelButton="cancel button \"${6}\""
 362    fi
 363    #  Icon
 364    if [[ -z "${7}" ]] ; then
 365        messageIcon=''
 366    elif [[ "${7}" = *'/'* ]] ; then
 367        messageIcon="with icon POSIX file \"${7}\""
 368    else messageIcon="with icon ${7}"
 369    fi
 370    #  Prompt type
 371    case "${8}" in
 372        'buttonPrompt')
 373            promptType='buttonPrompt'
 374        ;;
 375        'textPrompt')
 376            promptType='textPrompt'
 377        ;;
 378        'passwordPrompt')
 379            promptType='passwordPrompt'
 380        ;;
 381        *)
 382            promptType='buttonPrompt'
 383        ;;
 384    esac
 385
 386    ## Debug variables
 387    #echo "messageTitle: $messageTitle; messageSubtitle: $messageSubtitle; messageText: $messageText; buttonNames: $buttonNames; defaultButton: $defaultButton; cancelButton: $cancelButton; messageIcon: $messageIcon; promptType: $promptType"
 388
 389    ## Display dialog box
 390    case "$promptType" in
 391        'buttonPrompt')
 392            #  Display dialog with no input. Returns button pressed.
 393            commandOutput=$(osascript -e "
 394                on run
 395                    display dialog \"${messageSubtitle}\n${messageText}\" with title \"${messageTitle}\" buttons ${buttonNames} default button ${defaultButton} ${cancelButton} ${messageIcon}
 396                    set commandOutput to button returned of the result
 397                    return commandOutput
 398                end run
 399            ")
 400            executionStatus=$?
 401        ;;
 402        'textPrompt')
 403            #  Display dialog with text input. Returns text.
 404            commandOutput=$(osascript -e "
 405                on run
 406                    display dialog \"${messageSubtitle}\n${messageText}\" default answer \"\" with title \"${messageTitle}\" with text and answer buttons ${buttonNames} default button ${defaultButton} ${cancelButton} ${messageIcon}
 407                    set commandOutput to text returned of the result
 408                    return commandOutput
 409                end run
 410            ")
 411            executionStatus=$?
 412        ;;
 413        'passwordPrompt')
 414            #  Display dialog with hidden password input. Returns text.
 415            commandOutput=$(osascript -e "
 416                on run
 417                    display dialog \"${messageSubtitle}\n${messageText}\" default answer \"\" with title \"${messageTitle}\" with text and hidden answer buttons ${buttonNames} default button ${defaultButton} ${cancelButton} ${messageIcon}
 418                    set commandOutput to text returned of the result
 419                    return commandOutput
 420                end run
 421            ")
 422            executionStatus=$?
 423        ;;
 424    esac
 425
 426    ## Exit on error
 427    if [[ $commandOutput = *"Error"* ]] ; then
 428        displayNotification "Failed to display alert. Error: '$commandOutput'" '' '' '' 'suppressNotification'
 429        return 130
 430    fi
 431
 432    ## Return cancel if pressed
 433    if [[ $executionStatus != 0 ]] ; then
 434        displayNotification "User cancelled dialog." '' '' '' 'suppressNotification'
 435        return 131
 436    fi
 437
 438    ## Return commandOutput. Remember to assign the result to a variable, if you print it to the terminal, it will be logged.
 439    echo "$commandOutput"
 440}
 441#endregion
 442
 443#region Function displayAlert
 444#Assigned Error Codes: 140 - 149
 445function displayAlert() {
 446#.SYNOPSIS
 447#    Displays a alert box.
 448#.DESCRIPTION
 449#    Displays a alert box with customizable buttons and icon.
 450#.PARAMETER alertText
 451#    Specifies the alert text.
 452#.PARAMETER messageText
 453#    Specifies the message text.
 454#.PARAMETER alertCriticality
 455#    Specifies the alert criticality.
 456#    Avaliable options:
 457#        'informational' - Informational alert.
 458#        'critical'      - Critical alert.
 459#        'warning'       - Warning alert.
 460#    Defaults to 'informational'.
 461#.PARAMETER buttonNames
 462#    Specifies the names of the buttons. Defaults to '{Cancel, Ok}'.
 463#.PARAMETER defaultButton
 464#    Specifies the default button. Defaults to '1'.
 465#.PARAMETER cancelButton
 466#    Specifies the button to exit on. Defaults to ''.
 467#.PARAMETER givingUpAfter
 468#    Specifies the number of seconds to wait before dismissing the alert. Defaults to ''.
 469#.EXAMPLE
 470#   displayAlert 'alertText' 'messageText' 'critical' "{\"Don't Continue\", \"Dismiss Alert\"}" '1' "Don't Continue" '5'
 471#.NOTES
 472#    This is an internal script function and should typically not be called directly.
 473#.LINK
 474#    https://MEM.Zone
 475#.LINK
 476#    https://MEM.Zone/ISSUES
 477
 478    ## Set human readable parameters
 479    local alertText
 480    local messageText
 481    local alertCriticality
 482    local buttonNames
 483    local defaultButton
 484    local cancelButton
 485    local givingUpAfter=''
 486    local commandOutput
 487    local executionStatus=0
 488
 489    #  Alert text
 490    alertText="${1}"
 491    #  Message text
 492    messageText="${2}"
 493    #  Alert criticality
 494    case "${3}" in
 495        'informational')
 496            alertCriticality='as informational'
 497        ;;
 498        'critical')
 499            alertCriticality='as critical'
 500        ;;
 501        'warning')
 502            alertCriticality='as warning'
 503        ;;
 504        *)
 505            alertCriticality='informational'
 506        ;;
 507    esac
 508    #  Button names
 509    if [[ -z "${4}" ]] ; then
 510        buttonNames="{'Cance', 'Ok'}"
 511    else buttonNames="${4}"
 512    fi
 513    #  Default button
 514    if [[ -z "${5}" ]] ; then
 515        defaultButton='1'
 516    else defaultButton="${5}"
 517    fi
 518    #  Cancel button
 519    if [[ -z "${6}" ]] ; then
 520        cancelButton=''
 521    else cancelButton="cancel button \"${6}\""
 522    fi
 523    #  Giving up after
 524    if [[ -z "${7}" ]] ; then
 525        givingUpAfter=''
 526    else givingUpAfter="giving up after ${7}"
 527    fi
 528
 529    ## Debug variables
 530    #echo "alertText: $alertText; messageText: $messageText; alertCriticality: $alertCriticality; buttonNames: $buttonNames; defaultButton: $defaultButton; cancelButton: $cancelButton; givingUpAfter: $givingUpAfter"
 531
 532    ## Display the alert.
 533    commandOutput=$(osascript -e "
 534        on run
 535            display alert \"${alertText}\" message \"${messageText}\" ${alertCriticality} buttons ${buttonNames} default button ${defaultButton} ${cancelButton} ${givingUpAfter}
 536            set commandOutput to alert reply of the result
 537            return commandOutput
 538        end run
 539    ")
 540    executionStatus=$?
 541
 542    ## Exit on error
 543    if [[ $commandOutput = *"Error"* ]] ; then
 544        displayNotification "Failed to display alert. Error: '$commandOutput'" '' '' '' 'suppressNotification'
 545        return 140
 546    fi
 547
 548    ## Return cancel if pressed
 549    if [[ $executionStatus != 0 ]] ; then
 550        displayNotification "User cancelled alert." '' '' '' 'suppressNotification'
 551        return 141
 552    fi
 553
 554    ## Return commandOutput. Remember to assign the result to a variable, if you print it to the terminal, it will be logged.
 555    echo "$commandOutput"
 556}
 557#endregion
 558
 559#region Function checkSupportedOS
 560#Assigned Error Codes: 150 - 159
 561function checkSupportedOS() {
 562#.SYNOPSIS
 563#    Checks if the OS is supported.
 564#.DESCRIPTION
 565#    Checks if the OS is supported and exits if it is not.
 566#.PARAMETER supportedOSMajorVersion
 567#    Specify the major version of the OS to check.
 568#.EXAMPLE
 569#    checkSupportedOS '13'
 570#.NOTES
 571#    This is an internal script function and should typically not be called directly.
 572#.LINK
 573#    https://MEM.Zone
 574#.LINK
 575#    https://MEM.Zone/ISSUES
 576
 577    ## Set human readable parameters
 578    local supportedOSMajorVersion="$1"
 579
 580    ## Variable declaration
 581    local macOSVersion
 582    local macOSMajorVersion
 583    local macOSAllLatestVersions
 584    local macOSSupportedName
 585    local macOSName
 586
 587    ## Set variables
 588    macOSVersion=$(sw_vers -productVersion)
 589    macOSMajorVersion=$(echo "$macOSVersion" | cut -d'.' -f1)
 590
 591    ## Set display notification and alert variables
 592    #  Get all supported OS versions
 593    macOSAllLatestVersions=$( (echo "<table>" ; curl -sfLS "https://support.apple.com/en-us/HT201260" \
 594        | tidy --tidy-mark no --char-encoding utf8 --wrap 0 --show-errors 0 --show-warnings no --clean yes --force-output yes --output-xhtml yes --quiet yes \
 595        | sed -e '1,/<table/d; /<\/table>/,$d' -e 's#<br />##g' ; echo "</table>" ) \
 596        | xmllint --html --xpath "//table/tbody/tr/td/text()" - 2>/dev/null
 597    )
 598    #  Get supported OS display name
 599    macOSSupportedName=$(echo "$macOSAllLatestVersions" | awk "/^${supportedOSMajorVersion}/{getline; print}")
 600    #  Get current installed OS display name
 601    macOSName=$(echo "$macOSAllLatestVersions" | awk "/^${macOSMajorVersion}/{getline; print}")
 602
 603    ## Check if OS is supported
 604    if [[ "$macOSMajorVersion" -lt "$supportedOSMajorVersion" ]] ; then
 605
 606        #  Display notification and alert
 607        displayNotification "Unsupported OS '$macOSName ($macOSVersion)', please upgrade. Terminating execution!"
 608        displayAlert "OS needs to be at least '$macOSSupportedName ($supportedOSMajorVersion)'" 'Please upgrade and try again!' 'critical' '{"Upgrade macOS"}'
 609
 610        #  Forcefully install latest OS update
 611        sudo softwareupdate -i -a
 612        exit 150
 613    else
 614        displayNotification "Supported OS version '$macOSName ($macOSVersion)', continuing..."
 615        return 0
 616    fi
 617}
 618#endregion
 619
 620#region Function invokeFileVaultAction
 621#Assigned Error Codes: 160 - 169
 622function invokeFileVaultAction() {
 623#.SYNOPSIS
 624#    Invokes a FileVault action.
 625#.DESCRIPTION
 626#    Invokes a FileVault action for the current user by prompting for the password, and populating answers for the fdesetup prompts.
 627#.PARAMETER action
 628#    Specify the action to invoke. Valid values are 'enable', 'disable', and 'reissueKey'.
 629#.EXAMPLE
 630#    invokeFileVaultAction 'enable'
 631#.EXAMPLE
 632#    invokeFileVaultAction 'disable'
 633#.EXAMPLE
 634#    invokeFileVaultAction 'reissueKey'
 635#.NOTES
 636#    This is an internal script function and should typically not be called directly.
 637#.LINK
 638#    https://github.com/jamf/FileVault2_Scripts/blob/master/reissueKey.sh (Original script and copyright notice)
 639#.LINK
 640#    https://MEM.Zone
 641#.LINK
 642#    https://MEM.Zone/ISSUES
 643
 644    ## Variable declaration
 645    local fileVaultIcon
 646    local userName
 647    local userNameUUID
 648    local isFileVaultUser
 649    local isFileVaultOn
 650    local loopCounter=1
 651    local action
 652    local actionMessage
 653    local actionTitle
 654    local actionSubtitle
 655    local actionButtons
 656    local checkFileVaultStatus
 657
 658    ## Set action
 659    case "$1" in
 660        'enable')
 661            action="$1"
 662            actionTitle='Enable FileVault'
 663            actionSubtitle='FileVault needs to be enabled!'
 664            actionButtons='{"Cancel", "Enable FileVault")'
 665            checkFileVaultStatus='On'
 666        ;;
 667        'disable')
 668            action="$1"
 669            actionTitle='Disable FileVault'
 670            actionSubtitle='FileVault needs to be disabled!'
 671            actionButtons='{"Cancel", "Disable FileVault"}'
 672            checkFileVaultStatus='Off'
 673
 674        ;;
 675        'reissueKey')
 676            action='changerecovery -personal'
 677            actionTitle='Reissue FileVault Key'
 678            actionSubtitle='FileVault needs to reissue the key!'
 679            actionButtons='{"Cancel", "Reissue Key"}'
 680            checkFileVaultStatus='NotNeeded'
 681        ;;
 682        *)
 683            displayNotification "Invalid FileVault action '$1'. Skipping '$actionTitle'..." '' '' '' 'suppressNotification'
 684            return 160
 685        ;;
 686    esac
 687
 688    ## Set filevault icon
 689    fileVaultIcon='/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FileVaultIcon.icns'
 690
 691    ## Get the logged in user's name
 692    userName=$(/usr/bin/stat -f%Su /dev/console)
 693
 694    ## Get the user's UUID
 695    userNameUUID=$(dscl . -read /Users/"$userName"/ GeneratedUID | awk '{print $2}')
 696
 697    ## Check if user is an authorized FileVault user
 698    isFileVaultUser=$(fdesetup list | awk -v usrN="$userNameUUID" -F, 'match($0, usrN) {print $1}')
 699    if [ "${isFileVaultUser}" != "${userName}" ]; then
 700        displayNotification "${userName} is not a FileVault authorized user. Skipping '$actionTitle'..."
 701        return 161
 702    fi
 703
 704    ## Check to see if the encryption has finished
 705    isFileVaultOn=$(fdesetup status | grep "FileVault is On.")
 706
 707    ## Check FileVault status
 708    if [ "$checkFileVaultStatus" = 'On' ]; then
 709        if [ -n "$isFileVaultOn" ]; then
 710            displayNotification "FileVault is already enabled. Skipping '$actionTitle'..."
 711            return 162
 712        fi
 713    else
 714        if [ -z "$isFileVaultOn" ]; then
 715            displayNotification "FileVault is not enabled. Skipping '$actionTitle'..."
 716            return 163
 717        fi
 718    fi
 719
 720    ## Disable FileVault
 721    while true; do
 722
 723        ## Get the logged in user's password via a prompt
 724        actionMessage="Enter $userName's password:"
 725        userPassword=$(displayDialog "$actionTitle" "$actionSubtitle" "$actionMessage" "$actionButtons" '2' 'Cancel' "$fileVaultIcon" 'passwordPrompt')
 726
 727        ## Check if the user cancelled the prompt (return code 131)
 728        if [ $? = 131 ]; then
 729            displayNotification "User cancelled '$actionTitle' action!"
 730            return 164
 731        fi
 732
 733        displayNotification "Attempting FileVault action '$actionTitle'..." '' '' '1'
 734
 735        ## Automatically populate answers for the fdesetup prompts
 736        output=$(
 737            expect -c "
 738            log_user 0
 739            spawn fdesetup $action
 740            expect \"Enter the user name:\"
 741            send {${userName}}
 742            send \r
 743            expect \"Enter a password for '/', or the recovery key:\"
 744            send {${userPassword}}
 745            send \r
 746            log_user 1
 747            expect eof
 748        ")
 749
 750        if [[ $output = *'Error'* ]] || [[ $output = *'FileVault was not disabled'* ]] ; then
 751            displayNotification "Error performing FileVault action '$actionTitle' Attempt (${loopCounter}/3). $output"
 752            if [ $loopCounter -ge 3 ] ; then
 753                displayNotification "A maximum of 3 retries has been reached.\nContinuing without performing FileVault action '$action'..."
 754                return 165
 755            fi
 756            ((loopCounter++))
 757        else
 758            displayNotification "Sucessfully performed FileVault action '$actionTitle'!"
 759            return 0
 760        fi
 761    done
 762}
 763#endregion
 764
 765#region Function unbindFromAD
 766#Assigned Error Codes: 170 - 179
 767function unbindFromAD() {
 768#.SYNOPSIS
 769#    Unbinds device from AD.
 770#.DESCRIPTION
 771#     Unbinds device from AD and removes search paths.
 772#.EXAMPLE
 773#    unbindFromAD
 774#.NOTES
 775#    This is an internal script function and should typically not be called directly.
 776#.LINK
 777#    https://MEM.Zone
 778#.LINK
 779#    https://MEM.Zone/ISSUES
 780
 781    ## Variable declaration
 782    local searchPath
 783    local isAdJoined
 784    local executionStatus
 785
 786    ## Check for AD binding and unbind if found.
 787    isAdJoined=$(/usr/bin/dscl localhost -list . | grep 'Active Directory')
 788    if [[ -z "$isAdJoined" ]]; then
 789        displayNotification 'Not bound to Active Directory. Skipping unbind...'
 790        #  Return to the pipeline
 791        return 0
 792    fi
 793
 794    ## Display notification
 795    displayNotification 'Unbinding from Active Directory...'
 796
 797    ## Set search path
 798    searchPath=$(/usr/bin/dscl /Search -read . CSPSearchPath | grep Active\ Directory | sed 's/^ //')
 799
 800    ## Force unbind from Active Directory
 801    /usr/sbin/dsconfigad -remove -force -u none -p none
 802    executionStatus=$?
 803
 804    ## Delete the Active Directory domain from the custom /Search and /Search/Contacts paths
 805    /usr/bin/dscl /Search/Contacts -delete . CSPSearchPath "$searchPath"
 806    /usr/bin/dscl /Search -delete . CSPSearchPath "$searchPath"
 807
 808    ## Change the /Search and /Search/Contacts path type from Custom to Automatic
 809    /usr/bin/dscl /Search -change . SearchPolicy dsAttrTypeStandard:CSPSearchPath dsAttrTypeStandard:NSPSearchPath
 810    /usr/bin/dscl /Search/Contacts -change . SearchPolicy dsAttrTypeStandard:CSPSearchPath dsAttrTypeStandard:NSPSearchPath
 811
 812    ## Return execution status
 813    if [[ $executionStatus != 0 ]] ; then
 814        displayNotification "Failed to unbind from Active Directory. Error: '$executionStatus'" '' '' '' 'suppressNotification'
 815        return 170
 816    fi
 817}
 818#endregion
 819
 820#region Function migrateUserPassword
 821#Assigned Error Codes: 180 - 189
 822function migrateUserPassword() {
 823#.SYNOPSIS
 824#    Migrates the user password to the local account.
 825#.DESCRIPTION
 826#    Migrates the user password to the local account by removing the Kerberos and LocalCachedUser user values from the AuthenticationAuthority array.
 827#.PARAMETER userName
 828#    Specifies the name of the user.
 829#.EXAMPLE
 830#    migrateUserPassword "username"
 831#.NOTES
 832#    This is an internal script function and should typically not be called directly.
 833#.LINK
 834#    https://MEM.Zone
 835#.LINK
 836#    https://MEM.Zone/ISSUES
 837
 838    ## Set human readable variables
 839    local userName="${1}"
 840
 841    # Variable declaration
 842    local AuthenticationAuthority
 843    local Kerberosv5
 844    local localCachedUser
 845
 846    ## Display notification
 847    displayNotification "Migrating '$userName' password..."
 848
 849    # macOS 10.14.4 will remove the the actual ShadowHashData key immediately if the AuthenticationAuthority array value which references the ShadowHash is removed from the AuthenticationAuthority array.
 850    # To address this, the existing AuthenticationAuthority array will be modified to remove the Kerberos and LocalCachedUser user values.
 851
 852    ## Get AuthenticationAuthority
 853    AuthenticationAuthority=$(/usr/bin/dscl -plist . -read /Users/"$userName" AuthenticationAuthority)
 854
 855    ## Get Kerberosv5 and LocalCachedUser
 856    Kerberosv5=$(echo "${AuthenticationAuthority}" | xmllint --xpath 'string(//string[contains(text(),"Kerberosv5")])' -)
 857    localCachedUser=$(echo "${AuthenticationAuthority}" | xmllint --xpath 'string(//string[contains(text(),"LocalCachedUser")])' -)
 858
 859    ## Remove Kerberosv5 value
 860    if [[ -n "${Kerberosv5}" ]]; then
 861        /usr/bin/dscl -plist . -delete /Users/"$userName" AuthenticationAuthority "${Kerberosv5}"
 862    fi
 863
 864    ## Remove LocalCachedUser value
 865    if [[ -n "${localCachedUser}" ]]; then
 866        /usr/bin/dscl -plist . -delete /Users/"$userName" AuthenticationAuthority "${localCachedUser}"
 867    fi
 868}
 869#endregion
 870
 871#region Function convertMobileAccount
 872#Assigned Error Codes: 190 - 199
 873function convertMobileAccount() {
 874#.SYNOPSIS
 875#    Converts mobile account to local account.
 876#.DESCRIPTION
 877#    Converts mobile account to local account, by removing mobile account properties and  migrating the user password to the local account.
 878#.PARAMETER userName
 879#    Specifies the name of the user.
 880#.PARAMETER makeAdmin
 881#    Specifies whether the user should be made a local admin.
 882#.EXAMPLE
 883#    convertMobileAccount "username" "YES"
 884#.NOTES
 885#    This is an internal script function and should typically not be called directly.
 886#.LINK
 887#    https://MEM.Zone
 888#.LINK
 889#    https://MEM.Zone/ISSUES
 890
 891    ## Set human readable parameters
 892    local userName="${1}"
 893    local makeAdmin="${2}"
 894
 895    ## Variable declaration
 896    local accountType
 897    local isMobileUser
 898    local attributesToRemove
 899    local attributeToRemove
 900    local homeDirectory
 901
 902    ## Set variable values
 903    attributesToRemove=(
 904        cached_groups
 905        cached_auth_policy
 906        CopyTimestamp
 907        AltSecurityIdentities
 908        SMBPrimaryGroupSID
 909        OriginalAuthenticationAuthority
 910        OriginalNodeName
 911        SMBSID
 912        SMBScriptPath
 913        SMBPasswordLastSet
 914        SMBGroupRID
 915        PrimaryNTDomain
 916        AppleMetaRecordName
 917        MCXSettings
 918        MCXFlags
 919    )
 920
 921    ## Get account type
 922    accountType=$(/usr/bin/dscl . -read /Users/"$userName" AuthenticationAuthority | head -2 | awk -F'/' '{print $2}' | tr -d '\n')
 923
 924    ## Check if account is a mobile account
 925    if [[ "$accountType" = "Active Directory" ]]; then
 926        isMobileUser=$(/usr/bin/dscl . -read /Users/"$userName" AuthenticationAuthority | head -2 | awk -F'/' '{print $1}' | tr -d '\n' | sed 's/^[^:]*: //' | sed s/\;/""/g)
 927        if [[ "$isMobileUser" = "LocalCachedUser" ]]; then
 928            displayNotification "Converting $userName to a local account..."
 929        fi
 930    else
 931        displayNotification "The $userName is not a mobile account. Skipping conversion..."
 932        return
 933    fi
 934
 935    ## Remove the account attributes that identify it as an Active Directory mobile account
 936    for attributeToRemove in "${attributesToRemove[@]}"; do
 937        if [[ ! $(/usr/bin/dscl . -delete /users/"$userName" "$attributeToRemove") ]]; then
 938            displayNotification "Failed to remove account attribute '$attributeToRemove'!"
 939        fi
 940    done
 941
 942    ## Migrate password
 943    migrateUserPassword "$userName"
 944
 945    ## Refresh Directory Services
 946    /usr/bin/killall opendirectoryd
 947    sleep 20
 948
 949    ## Check if account is a mobile account
 950    accountType=$(/usr/bin/dscl . -read /Users/"$userName" AuthenticationAuthority | head -2 | awk -F'/' '{print $2}' | tr -d '\n')
 951    if [[ "$accountType" = "Active Directory" ]]; then
 952        displayNotification "Error converting the $userName account! Terminating execution..."
 953        exit 190
 954    else
 955        displayNotification "$userName was successfully converted to a local account."
 956    fi
 957
 958    ## Update home folder and permissions for the account. This could take a while.
 959    homeDirectory=$(/usr/bin/dscl . -read /Users/"$userName" NFSHomeDirectory | awk '{print $2}')
 960    if [[ "$homeDirectory" != "" ]]; then
 961        displayNotification "Updating $homeDirectory permissions for the $userName account, this could take a while..."
 962        /usr/sbin/chown -R "$1" "$homeDirectory"
 963    fi
 964
 965    ## Add user to the staff group on the Mac
 966    displayNotification "Adding $userName to the staff group..."
 967    /usr/sbin/dseditgroup -o edit -a "$userName" -t user staff
 968
 969    ## Add user to the admin group on the Mac
 970    if [[ "$makeAdmin" = "YES" ]]; then
 971        displayNotification "Granting admin rights to $userName..."
 972        /usr/sbin/dseditgroup -o edit -a "$userName" -t user admin
 973    fi
 974}
 975#endregion
 976
 977#region Function invokeJamfApiTokenAction
 978#Assigned Error Codes: 200 - 209
 979function invokeJamfApiTokenAction() {
 980#.SYNOPSIS
 981#    Performs a JAMF API token action.
 982#.DESCRIPTION
 983#    Performs a JAMF API token action, such as getting, checking validity or invalidating a token.
 984#.PARAMETER apiUrl
 985#    Specifies the JAMF API server url.
 986#.PARAMETER apiUser
 987#    Specifies the JAMF API username.
 988#.PARAMETER apiPassword
 989#    Specifies the JAMF API password.
 990#.PARAMETER tokenAction
 991#    Specifies the action to perform.
 992#    Possible values: get, check, invalidate
 993#.EXAMPLE
 994#    invokeJamfApiTokenAction '[email protected]' 'jamf-api-user' 'strongpassword' 'get'
 995#.NOTES
 996#    Returns the token and the token expiration epoch in the global variables BEARER_TOKEN and TOKEN_EXPIRATION_EPOCH.
 997#    This is an internal script function and should typically not be called directly.
 998#.LINK
 999#    https://MEM.Zone
1000#.LINK
1001#    https://MEM.Zone/ISSUES
1002#.LINK
1003#    https://developer.jamf.com/reference/jamf-pro/
1004
1005    ## Variable declarations
1006    local apiUrl
1007    local apiUser
1008    local apiPassword
1009    local tokenAction
1010    local response
1011    local responseCode
1012    local nowEpochUTC
1013
1014    ## Set variable values
1015    if [[ -z "${1}" ]] || [[ -z "${2}" ]] || [[ -z "${3}" ]] ; then
1016        apiUrl="$JAMF_API_URL"
1017        apiUser="$JAMF_API_USER"
1018        apiPassword="$JAMF_API_PASSWORD"
1019    else
1020        apiUrl="${1}"
1021        apiUser="${2}"
1022        apiPassword="${3}"
1023    fi
1024    tokenAction="${4}"
1025
1026    #region Inline Functions
1027    getBearerToken() {
1028        response=$(curl -s -u "$apiUser":"$apiPassword" "$apiUrl"/api/v1/auth/token -X POST)
1029        BEARER_TOKEN=$(echo "$response" | plutil -extract token raw -)
1030        tokenExpiration=$(echo "$response" | plutil -extract expires raw - | awk -F . '{print $1}')
1031        TOKEN_EXPIRATION_EPOCH=$(date -j -f "%Y-%m-%dT%T" "$tokenExpiration" +"%s")
1032        if [[ -z "$BEARER_TOKEN" ]] ; then
1033            displayNotification "Failed to get a valid API token!" '' '' '' 'suppressNotification'
1034            return 200
1035        else
1036            displayNotification "API token successfully retrieved!" '' '' '' 'suppressNotification'
1037        fi
1038    }
1039
1040    checkTokenExpiration() {
1041        nowEpochUTC=$(date -j -f "%Y-%m-%dT%T" "$(date -u +"%Y-%m-%dT%T")" +"%s")
1042        if [[ TOKEN_EXPIRATION_EPOCH -gt nowEpochUTC ]] ; then
1043            displayNotification "API token valid until the following epoch time: $TOKEN_EXPIRATION_EPOCH" '' '' '' 'suppressNotification'
1044        else
1045            displayNotification "No valid API token available..." '' '' '' 'suppressNotification'
1046            getBearerToken
1047        fi
1048    }
1049
1050    invalidateToken() {
1051        responseCode=$(curl -w "%{http_code}" -H "Authorization: Bearer ${BEARER_TOKEN}" "$apiUrl"/api/v1/auth/invalidate-token -X POST -s -o /dev/null)
1052        if [[ ${responseCode} == 204 ]] ; then
1053            displayNotification "Token successfully invalidated!" '' '' '' 'suppressNotification'
1054            BEARER_TOKEN=''
1055            TOKEN_EXPIRATION_EPOCH=0
1056        elif [[ ${responseCode} == 401 ]] ; then
1057            displayNotification "Token already invalid!" '' '' '' 'suppressNotification'
1058        else
1059            displayNotification "An unknown error occurred invalidating the token!" '' '' '' 'suppressNotification'
1060            return 201
1061        fi
1062    }
1063    #endregion
1064
1065    ## Perform token action
1066    case "$tokenAction" in
1067        get)
1068            displayNotification "Getting new token..." '' '' '' 'suppressNotification'
1069            getBearerToken
1070            ;;
1071        check)
1072            displayNotification "Checking token validity..." '' '' '' 'suppressNotification'
1073            checkTokenExpiration
1074            ;;
1075        invalidate)
1076            displayNotification "Invalidating token..." '' '' '' 'suppressNotification'
1077            invalidateToken
1078            ;;
1079        *)
1080            displayNotification "Invalid token action '$tokenAction' specified! Terminating execution..."
1081            exit 202
1082            ;;
1083    esac
1084}
1085#endregion
1086
1087#region Function invokeSendJamfCommand
1088#Assigned Error Codes: 210 - 219
1089function invokeSendJamfCommand() {
1090#.SYNOPSIS
1091#    Performs a JAMF API send command.
1092#.DESCRIPTION
1093#    Performs a JAMF API send command, with the specified command and device serial number.
1094#.PARAMETER apiUrl
1095#    Specifies the JAMF API server url.
1096#.PARAMETER apiUser
1097#    Specifies the JAMF API username.
1098#.PARAMETER apiPassword
1099#    Specifies the JAMF API password.
1100#.PARAMETER serialNumber
1101#    Specifies the device serial number.
1102#.PARAMETER command
1103#    Specifies the command to perform, keep in mind that you need to specify the command and the parameters in one string.
1104#.EXAMPLE
1105#    invokeSendJamfCommand '[email protected]' 'jamf-api-user' 'strongpassword' 'FVFHX12QQ6LY' 'UnmanageDevice'
1106#.EXAMPLE
1107#    invokeSendJamfCommand '[email protected]' 'jamf-api-user' 'strongpassword' 'FVFHX12QQ6LY' 'EraseDevice/passcode/123456'
1108#.NOTES
1109#    This is an internal script function and should typically not be called directly.
1110#.LINK
1111#    https://MEM.Zone
1112#.LINK
1113#    https://MEM.Zone/ISSUES
1114#.LINK
1115#    https://developer.jamf.com/reference/jamf-pro/
1116
1117    ## Variable declarations
1118    local apiUrl
1119    local apiUser
1120    local apiPassword
1121    local command
1122    local serialNumber
1123    local deviceId
1124    local result
1125
1126    ## Set variable values
1127    if [[ -z "${1}" ]] || [[ -z "${2}" ]] || [[ -z "${3}" ]] ; then
1128        apiUrl="$JAMF_API_URL"
1129        apiUser="$JAMF_API_USER"
1130        apiPassword="$JAMF_API_PASSWORD"
1131    else
1132        apiUrl="${1}"
1133        apiUser="${2}"
1134        apiPassword="${3}"
1135    fi
1136    serialNumber="${4}"
1137    command="${5}"
1138
1139    ## Get API token
1140    invokeJamfApiTokenAction "$apiUrl" "$apiUser" "$apiPassword" 'get'
1141
1142    ## Get JAMF device ID
1143    deviceId=$(curl --request GET \
1144        --url "${apiUrl}"/JSSResource/computers/serialnumber/"${serialNumber}"/subset/general \
1145        --header 'Accept: application/xml' \
1146        --header "Authorization: Bearer ${BEARER_TOKEN}" \
1147        --silent --show-error --fail | xmllint --xpath '//computer/general/id/text()' -
1148    )
1149
1150    ## Perform action
1151    if [[ $deviceId -gt 0 ]]; then
1152       result=$(curl -s -o /dev/null -I -w "%{http_code}" \
1153            --request POST \
1154            --url "${apiUrl}"/JSSResource/computercommands/command/"${command}"/id/"${deviceId}" \
1155            --header 'Content-Type: application/xml' \
1156            --header "Authorization: Bearer ${BEARER_TOKEN}" \
1157        )
1158        ## Check result (201 = Created/Success)
1159        if [[ $result -eq 201 ]]; then
1160            displayNotification "Successfully performed command '${command}' on device '${serialNumber} [${deviceId}]'!" '' '' '' 'suppressNotification'
1161            return 0
1162        else
1163            displayNotification "Failed to perform command '${command}' on device '${serialNumber} [${deviceId}]'!" '' '' '' 'suppressNotification'
1164            return 210
1165        fi
1166    else
1167        displayNotification "Invalid device id '${deviceId} [${serialNumber}]'. Skipping '${command}'..." '' '' '' 'suppressNotification'
1168        return 211
1169    fi
1170}
1171#endregion
1172
1173#region Function startJamfOffboarding
1174#Assigned Error Codes: 220 - 229
1175function startJamfOffboarding() {
1176#.SYNOPSIS
1177#    Starts JAMF offboarding.
1178#.DESCRIPTION
1179#    Starts JAMF offboarding, removing certificates, profiles and binaries.
1180#.EXAMPLE
1181#    startJamfOffboarding
1182#.EXAMPLE
1183#    startJamfOffboarding
1184#.NOTES
1185#    This is an internal script function and should typically not be called directly.
1186#.LINK
1187#    https://MEM.Zone
1188#.LINK
1189#    https://MEM.Zone/ISSUES
1190
1191    ## Variable declaration
1192    local hasJamfBinaries
1193    local isJamfManaged
1194    local currentUser
1195    local serialNumber
1196    local loopCounter=0
1197
1198    ## Check if JAMF managed
1199    isJamfManaged=$(/usr/bin/profiles -C | /usr/bin/grep '00000000-0000-0000-A000-4A414D460003')
1200    hasJamfBinaries=$(which jamf)
1201    if [[ -z "$isJamfManaged" ]] && [[ -z "$hasJamfBinaries" ]]; then
1202        displayNotification 'Not JAMF managed. Skipping JAMF offboarding...'
1203        return 0
1204    fi
1205
1206    ## Display notification
1207    displayNotification 'JAMF offboarding has started...'
1208
1209    ## Get current user
1210    currentUser=$(stat -f '%Su' /dev/console)
1211
1212    ## Quit Self-Service.
1213    displayNotification 'Stopping self Service Process...'
1214    killall "Self Service"
1215
1216    ## Remove JAMF management
1217    if [[ -n "$isJamfManaged" ]] ; then
1218        if [[ "$JAMF_API_ENABLED" = 'YES' ]]; then
1219            displayNotification 'Removing JAMF management trough API...'
1220            #  Get serial number
1221            serialNumber=$(system_profiler SPHardwareDataType | grep Serial | awk '{print $NF}')
1222            #  Remove JAMF management
1223            invokeSendJamfCommand '' '' '' "$serialNumber" 'UnmanageDevice'
1224        else
1225            #  Remove JAMF management without API
1226            displayNotification 'Removing JAMF management profile...'
1227            sudo jamf removeMdmProfile
1228        fi
1229        #  Check if management profile was removed
1230        isJamfManaged=$(/usr/bin/profiles -C | /usr/bin/grep '00000000-0000-0000-A000-4A414D460003')
1231    else
1232        displayNotification 'Not JAMF managed. Skipping JAMF management removal...'
1233    fi
1234
1235    ## Check if management profile was removed
1236    while [ -n "$isJamfManaged" ]; do
1237        #  Wait 15 seconds
1238        sleep 15
1239        #  Check if management profile is still present
1240        isJamfManaged=$(/usr/bin/profiles -C | /usr/bin/grep '00000000-0000-0000-A000-4A414D460003')
1241        #  Try to remove profile without API
1242        sudo jamf removeMdmProfile
1243        #  Terminate execution after 3 retries
1244        if [ $loopCounter -ge 4 ] ; then
1245            displayNotification "JAMF management profile could not be removed! Terminating execution..."
1246            exit 220
1247        fi
1248        #  Increment loop counter
1249        ((loopCounter++))
1250    done
1251
1252    ## Remove JAMF Framework
1253    if [[ -n "$hasJamfBinaries" ]]; then
1254        displayNotification 'Removing JAMF Framework...'
1255        sudo jamf removeFramework
1256    else
1257        displayNotification 'JAMF Framework not installed. Skipping JAMF Framework removal...'
1258    fi
1259
1260    ## Remove all system profiles
1261    displayNotification 'Removing System Profiles...'
1262    for identifier in $(/usr/bin/profiles -L | awk '/attribute/' | awk '{print $4}'); do
1263        displayNotification "Attempting to remove System Profile '$identifier'..."  '' '' '' 'suppressNotification'
1264        sudo -u "$currentUser" profiles -R -p "$identifier" >/dev/null 2>&1
1265    done
1266    if [[ ! $identifier ]]; then
1267        displayNotification 'No System Profiles to remove...'
1268    fi
1269
1270    ## Remove Configuration Profiles
1271    displayNotification 'Removing all Configuration Profiles...'
1272    sudo -u "$currentUser" profiles remove -forced -all -v
1273
1274    ## Display notification
1275    displayNotification 'JAMF Offboarding is complete!'
1276}
1277#endregion
1278
1279#endregion
1280##*=============================================
1281##* END FUNCTION LISTINGS
1282##*=============================================
1283
1284##*=============================================
1285##* SCRIPT BODY
1286##*=============================================
1287#region ScriptBody
1288
1289## Check if script is running as root
1290runAsRoot "$FULL_SCRIPT_NAME"
1291
1292## Start logging
1293startLogging "$LOG_NAME" "$LOG_DIR" "$LOG_HEADER"
1294
1295## Show script version and suppress terminal output
1296displayNotification "Running $SCRIPT_NAME version $SCRIPT_VERSION" '' '' '' '' 'suppressTerminal'
1297
1298## Check if OS is supported
1299checkSupportedOS "$SUPPORTED_OS_MAJOR_VERSION"
1300
1301## If Company Portal is installed, continue, otherwise quit
1302if open -Ra 'Company Portal'; then
1303    displayNotification 'Company Portal application is installed, continuing...'
1304else
1305    displayNotification 'Company Portal application not installed, contact support!'
1306    displayAlert "Company Portal app is not installed" 'In order to continue, please contact support!' 'critical' '{"Contact Support"}'
1307    open "$SUPPORT_LINK"
1308    exit 11
1309fi
1310
1311## Unbind from AD
1312if [[ "$REMOVE_FROM_AD" = 'YES' ]] ; then unbindFromAD ; fi
1313
1314## Convert mobile accounts to local accounts
1315if [[ "$CONVERT_MOBILE_ACCOUNTS" = 'YES' ]] ; then
1316    localUsers=$(/usr/bin/dscl . list /Users UniqueID | awk '$2 > 500 {print $1}')
1317    for localUser in $localUsers; do
1318        convertMobileAccount "$localUser" "$SET_ADMIN_RIGHTS"
1319    done
1320fi
1321
1322## Offboard JAMF
1323if [[ "$JAMF_OFFBOARD" = 'YES' ]] ; then
1324    startJamfOffboarding
1325fi
1326
1327## Disable FileVault
1328invokeFileVaultAction 'disable'
1329
1330## Start Company Portal
1331displayNotification 'Starting Company Portal...'
1332open -a "$COMPANY_PORTAL_PATH"
1333
1334## Display documentation
1335displayNotification 'Displaying documentation...'
1336open -gj "$DOCUMENTATION_LINK"
1337
1338#endregion
1339##*=============================================
1340##* END SCRIPT BODY
1341##*=============================================

SHARE

article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune Linux Onboarding Tool

Onboard Ubuntu Linux devices to Microsoft Intune using a bash script. Installs prerequisites and starts the user-driven enrollment. ...

See More
article card image dark article card image light

Published by · Jan 23, 2024 tools · 3 mins read

Introducing: Intune Device Renaming Tool

Rename Intune Devices by setting a Prefix or using a User Attribute as Prefix. Supports Windows, macOS, and Linux ...

See More