├── .gitignore ├── LICENSE ├── LaunchDaemons └── com.corp.swiftdeploy.plist ├── README.md ├── main script └── swiftdeploy.sh └── pkg install scripts ├── postinstall.sh └── preinstall.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Richard Purves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LaunchDaemons/com.corp.swiftdeploy.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Label 5 | com.corp.swiftdeploy 6 | ProgramArguments 7 | 8 | /usr/local/corp/scripts/swiftdeploy.sh 9 | 10 | KeepAlive 11 | 12 | RunAtLoad 13 | 14 | EnvironmentVariables 15 | 16 | PATH 17 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 18 | 19 | StandardOutPath 20 | /dev/null 21 | StandardErrorPath 22 | /dev/null 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftDeploy 2 | Zero touch deployment method for Jamf Pro and SwiftDialog 3 | 4 | This project is directly inspired by [Setup Your Mac](https://snelson.us/sym) by Dan Snelson, is meant for exclusive use with [Jamf Pro](https://www.jamf.com/products/jamf-pro/) management system and finally makes extensive use of [SwiftDialog](https://github.com/swiftDialog/swiftDialog) by Bart Reardon. 5 | 6 | To those projects and authors, I give my thanks. 7 | 8 | ## What does this do? 9 | 10 | I like [Setup Your Mac](https://snelson.us/sym) as a concept, but unfortunately I have requirements for a more complex and flexible system. As a result, SwiftDeploy now exists. 11 | 12 | The principle difference between the two projects is that I worked out a way to autogenerate the policy lists in SwiftDialog directly from the output of the Jamf binary instead of hard coding everything. So at the expense of code complexity, this system will auto generate and display the correct list every time. 13 | 14 | ## Can I see this in action? 15 | 16 | Sure! 17 | 18 | https://github.com/franton/SwiftDeploy/assets/5807892/f8382ff2-e7f8-43b4-9d73-0e767a8cfe5f 19 | 20 | This shows a very sped up deployment process, where the script initiated the following process: 21 | 22 | - Work out the name of the user that signed in and upload that to Jamf Pro. 23 | - Auto set the name of the computer based on the asset management system data. 24 | - Execute a ```jamf policy -event deploy -verbose``` command. 25 | - Then execute a ```jamf policy -verbose``` command to run check in. 26 | - Re-enables Jamf automatic check in. 27 | - Cleans up and exits. 28 | 29 | For those who remember my JNUC 2022 talk, I am still claiming the prize for fastest Touch ID setup ever :D 30 | 31 | ## What sorcery is this? 32 | 33 | Blog post coming soon that will go into details. 34 | 35 | TL;DR: 36 | 37 | A pipe is set up between the Jamf binary and this script, and we force this to operate in an asychronous mode. The risk otherwise is the pipe could stall and the binary could be prematurely terminated. We invoke the binary using the verbose switch to get extra output. 38 | 39 | A script loop, coded to be as fast as possible processes the output received from the pipe and updates SwiftDialog accordingly 40 | 41 | From all the verbose output we get all the policy names that the binary is to act on. Those names are processed into image files names so we can use appropriately named files. We also get start info, is a policy running a pkg or a script, did it work or did it fail and update accordingly. We also know when we're finished because otherwise async pipes don't terminate. 42 | 43 | The blog post will have more detail. [Eventually.](https://developer.valvesoftware.com/wiki/Valve_Time). 44 | 45 | ## How do I use this? 46 | 47 | - Create a package in your packaging tool of choice. 48 | - Customise the pre and post install scripts to your needs. 49 | - Customise the launch daemon file with your corp details and file locations. 50 | - Customise the main script (see section below) to your needs. 51 | - Add your corporate banner file to the package. (635 × 133 pixel png file is best) 52 | - Add ALL the icon files you're going to require. (We autogenerate the names for those from the policy names.) 53 | - Package, codesign and add to your Jamf Pro prestage. 54 | - Create a policy in Jamf with the custom trigger "isjssup", Ongoing that simply runs ```echo "up"``` . 55 | - TEST! 56 | 57 | Here's a screenshot from my packaging project so you can see where all the files are located. 58 | 59 | swiftdeploypkg 60 | 61 | ## Icon file example. 62 | 63 | A policy with a name such as 64 | ```Deploy BeyondTrust Support Client``` 65 | is translated to 66 | ```beyondtrustsupportclient.png``` 67 | 68 | The code chops off the first word, amalgamates the other text, forces lower case and assumes .png format. 69 | 70 | All files I generated using [SAP's macOS icon generator](https://github.com/SAP/macOS-icon-generator) at 512x512 png. 71 | 72 | By default ALL icon files live in /usr/local/corp/deployimgs but that can be customised. See section below. 73 | 74 | ## Areas of note in the code: 75 | 76 | - L15 and 16: Jamf API Role client id and secret. Used for various API accesses. 77 | 78 | | Permissions | 79 | | ------ | 80 | | Read Buildings | 81 | | Read Computers | 82 | | Read Departments | 83 | | Read Computer Check-In | 84 | | Send Computer Remote Command to Install Package | 85 | 86 | - L20: Log file location. 87 | - L21: Path to working folder. Change this to suit your own needs. 88 | - L22: Name and path of icons folder. Used for banner and auto populating icons. 89 | - L23: Change this to the name and path of your banner image 90 | - L28: Hardcoded URL for Jamf Pro because device wont have enrolled at this stage 91 | - L30: URL of your Jamf Pro server. This runs before any auto detection is possible. 92 | - L31: Your corporate domain. Used for working out email address from current user. 93 | - L319: Detection code to see if enrollment failed and initiate re-enrollment if so. Uses same client id/secret. 94 | - L446: Custom code to run named triggers for adding user details to device record and device naming. (Not provided in this project.) 95 | - L468: Regex for automated device naming checks. Currently set to a dummy of 8 numbers only. 96 | 97 | ## Acknowledgements 98 | 99 | [Mac Admins Slack](https://www.macadmins.org/) 100 | - @dan-snelson For his Setup Your Mac project showing it was all possible 101 | - @bartreardon For his SwiftDialog project that really made the display possible 102 | - @pico For advise on some of the nastier bits of shell pipe handling. (Although neither of us had gone quite this far!) 103 | - The current users of the #zsh , #bash and #swiftdialog channels. (Hi to #jamfnation too!) 104 | - The original authors of cocoaDialog for putting the idea of named async pipes in my head from their original documentation 105 | - @tlark @macmule @rabbitt @bradtchapman @rquigley @marcusransom and other for support and chats during the development of this. 106 | 107 | If i've missed you out, get in touch and i'll fix that mistake. 108 | -------------------------------------------------------------------------------- /main script/swiftdeploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Deploy script for SwiftDialog ZeroTouch 4 | # richard@richard-purves.com 5 | 6 | # Logging output to a file for testing 7 | #time=$( /bin/date "+%d%m%y-%H%M" ) 8 | #set -x 9 | #logfile=/private/tmp/swiftverbose.log 10 | #exec > $logfile 2>&1 11 | 12 | # Set up global variables here. User variables to be set up after login. 13 | scriptversion="1.13 - 25th January 2024" 14 | 15 | clientid="" 16 | clientsecret="" 17 | 18 | ld="/Library/LaunchDaemons/com.jamfsoftware.task.1.plist" 19 | sdld="/Library/LaunchDaemons/com.corp.swiftdeploy.plist" 20 | sdldlabel=$( defaults read $sdld Label ) 21 | scriptloc=${0:A} 22 | 23 | logfile="/private/tmp/swiftdeploy.log" 24 | workfolder="/usr/local/corp" 25 | icons="$workfolder/deployimgs" 26 | bannerimage="$icons/banner.png" 27 | 28 | sdcontrolfile="/private/tmp/sdcontrol.log" 29 | 30 | sd="/usr/local/bin/dialog" 31 | jb="/usr/local/bin/jamf" 32 | 33 | jssurl="https://corp.jamfcloud.com/" 34 | domain="corp.com" 35 | 36 | macosver=$( /usr/bin/sw_vers -productVersion ) 37 | majver=$( echo $macosver | cut -d"." -f1 ) 38 | macosbuild=$( /usr/bin/sw_vers -buildVersion ) 39 | serial=$( /usr/sbin/ioreg -c IOPlatformExpertDevice -d 2 | /usr/bin/awk -F\" '/IOPlatformSerialNumber/{print $(NF-1)}' ) 40 | udid=$( /usr/sbin/ioreg -d2 -c IOPlatformExpertDevice | /usr/bin/awk -F\" '/IOPlatformUUID/{print $(NF-1)}' ) 41 | 42 | # Set up various functions here 43 | function logme() 44 | { 45 | # Check to see if function has been called correctly 46 | if [ -z "$1" ]; 47 | then 48 | echo $( /bin/date )" - ERROR: No text passed to function! Please recheck code!" | /usr/bin/tee -a "${logfile}" 49 | exit 1 50 | fi 51 | 52 | # Log the passed details 53 | echo -e $( /bin/date )" - $1" | /usr/bin/tee -a "${logfile}" 54 | } 55 | 56 | function updatestatus() 57 | { 58 | logme "SD Command issued: $1" 59 | echo "$1" >> "$sdcontrolfile" 60 | } 61 | 62 | function getjamftoken() 63 | { 64 | # Check we have the correct number of parameters passed 65 | if [ $# -lt 3 ]; 66 | then 67 | echo "Usage: $funcstack[1] " 68 | return 1 69 | fi 70 | 71 | # Sort these out into appropriate variables 72 | id="$1" 73 | secret="$2" 74 | jssurl="$3" 75 | 76 | # Use the new oauth system to get a bearer token 77 | jsonresponse=$( /usr/bin/curl --silent --location \ 78 | --request POST "${jssurl}api/oauth/token" \ 79 | --header 'Content-Type: application/x-www-form-urlencoded' \ 80 | --data-urlencode "client_id=${id}" \ 81 | --data-urlencode 'grant_type=client_credentials' \ 82 | --data-urlencode "client_secret=${secret}" ) 83 | 84 | # Return any replies to the original caller 85 | echo "$jsonresponse" 86 | } 87 | 88 | function invalidatejamftoken() 89 | { 90 | if [ $# -lt 2 ]; 91 | then 92 | logme "Usage: $funcstack[1] " 93 | fi 94 | 95 | creds="$1" 96 | jssurl="$2" 97 | 98 | # Send API command to invalidate the token 99 | /usr/bin/curl -s -k "${jssurl}api/v1/auth/invalidate-token" -H "authorization: Bearer ${token}" -X POST 100 | } 101 | 102 | function getcomputerid() 103 | { 104 | if [ $# -lt 3 ]; 105 | then 106 | logme "Usage: $funcstack[1] " 107 | fi 108 | 109 | apitoken="$1" 110 | jssurl="$2" 111 | udid="$3" 112 | 113 | compjson=$( /usr/bin/curl -s "${jssurl}api/v1/computers-inventory?section=GENERAL&filter=udid%3D%3D%22${udid}%22" -H "authorization: Bearer ${apitoken}" ) 114 | 115 | compid=$( /usr/bin/plutil -extract results.0.id raw -o - - <<< "$compjson" ) 116 | 117 | echo "$compid" 118 | } 119 | 120 | function jamfprocess() 121 | { 122 | logme "Calling jamf policy $1" 123 | 124 | # Create a named pipe to feed output from jamf binary into. 125 | # Attach it to file descriptor "3" and delete the created file. 126 | /usr/bin/mkfifo /private/tmp/jamfoutput 127 | exec 3<>"/private/tmp/jamfoutput" 128 | /bin/rm /private/tmp/jamfoutput 129 | 130 | # Run the named policy in the background or check-in if not named, 131 | # and direct the output to the file descriptor. 132 | if [ $# -eq 1 ]; 133 | then 134 | $jb policy -event "$1" -verbose >&3 & 135 | jbpid=$! 136 | else 137 | $jb policy -verbose >&3 & 138 | jbpid=$! 139 | fi 140 | 141 | counter=0 142 | progressbar="1" 143 | 144 | while read output; 145 | do 146 | # Uncomment this for actual jamf verbose to a file 147 | #echo "$output" >> /private/tmp/jamfverbose.log 148 | 149 | # Parse for policy list output. If we get one, add to the list. 150 | if [ $( echo $output | grep -c "verbose: Parsing Policy" ) -gt 0 ]; 151 | then 152 | array+=(${(f)"$( echo $output | /usr/bin/grep "verbose: Parsing Policy" | /usr/bin/awk '{ print substr($0, index($0,$5)) }' | /usr/bin/awk '{NF--; print}' | /usr/bin/sed 's/Enable //g' | /usr/bin/sed 's/Install //g' | /usr/bin/sed 's/Deploy //g' | /usr/bin/sed 's/Configure //g' )"} ) 153 | image="${icons}/$( echo ${array[-1]} | /usr/bin/tr -d " " | /usr/bin/tr '[:upper:]' '[:lower:]' ).png" 154 | updatestatus "listitem: add, title: ${array[-1]}, icon: $image, statustext: Waiting, status: pending" 155 | progressbarstep=$(( 100 / $#array )) 156 | continue 157 | fi 158 | 159 | # Work out what policy we're doing and it's status. 160 | execpolicyname=$( echo $output | grep "Executing Policy" | awk '{ print substr($0, index($1,$5)) }' 2>/dev/null ) 161 | 162 | # Save the name for the next loop round. Also work out the proper name for the dialog updates. 163 | # Also increment the policy counter at the detection of a new policy start. 164 | if [ ! -z "$execpolicyname" ]; 165 | then 166 | policyname="$execpolicyname" 167 | updatename=$( echo $output | awk '{ print substr($0, index($0,$4)) }' ) 168 | updatename=$( echo $updatename | /usr/bin/sed 's/Enable //g' | /usr/bin/sed 's/Install //g' | /usr/bin/sed 's/Deploy //g' | /usr/bin/sed 's/Configure //g' ) 169 | fi 170 | 171 | # If we get a blank line, skip checking for anything. 172 | # Otherwise parse the verbose output for which state we're in. 173 | if [ ! -z "$policyname" ]; 174 | then 175 | downloading=$( echo $output | grep -c "Downloading" ) 176 | installing=$( echo $output | grep -c "Installing" ) # pkgs 177 | executing=$( echo $output | grep -c "Running script" ) # scripts 178 | runcommand=$( echo $output | grep -c "Running command" ) # commands 179 | finished=$( echo $output | grep -c -E 'Successfully|Script exit code: 0|Result of command' ) 180 | failedpkg=$( echo $output | grep -c "Installation failed" ) 181 | failedinstall=$( echo $output | grep "installer:" | grep -c "failed" ) 182 | failedscript=$( echo $output | grep "Script exit code:" | awk '{ print $NF }' ) 183 | fi 184 | 185 | # Depending on state, update swiftDialog list entry 186 | if [ "$downloading" -eq 1 ]; 187 | then 188 | updatestatus "listitem: title: $updatename, statustext: Downloading, status: wait" 189 | updatestatus "progresstext: Downloading $updatename" 190 | fi 191 | 192 | if [ "$installing" -eq 1 ]; 193 | then 194 | updatestatus "listitem: title: $updatename, statustext: Installing, status: wait" 195 | updatestatus "progresstext: Installing $updatename" 196 | fi 197 | 198 | if [ "$executing" -eq 1 ]; 199 | then 200 | updatestatus "listitem: title: $updatename, statustext: Running Script, status: wait" 201 | updatestatus "progresstext: Running Script for $updatename" 202 | fi 203 | 204 | if [ "$runcommand" -eq 1 ]; 205 | then 206 | updatestatus "listitem: title: $updatename, statustext: Running Command, status: wait" 207 | updatestatus "progresstext: Running Command for $updatename" 208 | fi 209 | 210 | if [ "$failedpkg" -eq 1 ] || [ "$failedinstall" -eq 1 ] || [ "$failedscript" -ne 0 ]; 211 | then 212 | progressbar=$(( $progressbar + $progressbarstep )) 213 | counter=$(( $counter + 1 )) 214 | updatestatus "progress: $progressbar" 215 | updatestatus "listitem: title: $updatename, statustext: Failed, status: fail" 216 | updatestatus "progresstext: Installatiion of $updatename FAILED" 217 | unset execpolicyname policyname 218 | fi 219 | 220 | if [ "$finished" = "1" ]; 221 | then 222 | progressbar=$(( $progressbar + $progressbarstep )) 223 | counter=$(( $counter + 1 )) 224 | updatestatus "progress: $progressbar" 225 | updatestatus "listitem: title: $updatename, statustext: Completed, status: success" 226 | updatestatus "progresstext: $updatename Completed" 227 | 228 | # Clean out previous completed task. Only ones left should be the failed ones. 229 | # Then store the current name to be cleaned on the next task 230 | [ ! -z "$previouspolicy" ] && updatestatus "listitem: delete, title: $previouspolicy" 231 | previouspolicy="$updatename" 232 | 233 | unset execpolicyname policyname 234 | fi 235 | 236 | # Clear variables for future loops 237 | unset downloading installing executing finished failedpkg failedscript 238 | 239 | # Loop will never exit on it's own. Break out when the counter 240 | # reaches the same or greater number than the indexes in the deployarray array. 241 | [ "$counter" -ge "$#array" ] && [ "$#array" -gt 0 ] && break 242 | 243 | done <&3 244 | 245 | # Clear the file descriptor we set up earlier 246 | exec 3>&- 247 | 248 | # Ensure the Jamf Binary has terminated before proceeding 249 | wait $jbpid 250 | 251 | # Clear final icon from the list 252 | updatestatus "listitem: delete, title: $previouspolicy" 253 | 254 | # Clear remaining variables used 255 | unset previouspolicy array counter jbpid 256 | } 257 | 258 | function exitscript() 259 | { 260 | if [ $# -lt 2 ] 261 | then 262 | logme "Usage: $funcstack[1] " 263 | exit 1 264 | fi 265 | 266 | logme "Exit code: $1" 267 | logme "Exit message: $2" 268 | 269 | /bin/rm -rf "$icons" 270 | /bin/rm -f "$sdcontrolfile" 271 | /bin/rm -f "$sdld" 272 | /bin/rm "$scriptloc" 273 | /bin/launchctl bootout system/$sdldlabel 274 | sleep 1 275 | 276 | # parse $1 to get exit code. 277 | exit $1 278 | } 279 | 280 | # 281 | ## Start preparation for deployment 282 | # 283 | 284 | # Set error trapping here 285 | trap 'logme "Error at line $LINENO"; exitscript 1 "Script error. Check /private/tmp/swiftdeploy.log"' 286 | 287 | # Caffeinate the mac so it doesn't go to sleep on us. Give it the PID of this script 288 | # so that it auto quits when we're done. 289 | logme "Starting Deployment preparation." 290 | logme "Loading caffeinate so the computer doesn't sleep." 291 | /usr/bin/caffeinate -dimu -w $$ & 292 | 293 | # Loop and wait for enrollment to complete 294 | while [ -f /Library/LaunchDaemons/com.jamf.management.enroll.plist ]; do : ; done 295 | 296 | # Ensure checkin is disabled 297 | while [ ! -f "$ld" ]; do : ; done 298 | /bin/launchctl bootout system "$ld" 299 | 300 | # Enable localadmin SSH access 301 | logme "Enabling SSH access for admin account." 302 | /usr/sbin/systemsetup -f -setremotelogin off 2>&1 >/dev/null 303 | /usr/sbin/dseditgroup -o delete -t group com.apple.access_ssh 2>&1 >/dev/null 304 | /usr/sbin/dseditgroup -o create -q com.apple.access_ssh 2>&1 >/dev/null 305 | /usr/sbin/dseditgroup -o edit -a admin -t user com.apple.access_ssh 2>&1 >/dev/null 306 | /usr/sbin/systemsetup -f -setremotelogin on 2>&1 >/dev/null 307 | 308 | # Wait for the user environment to start before we proceed. Also make sure Jamf check in is disabled. 309 | logme "Waiting for user login before proceeding." 310 | while ! /usr/bin/pgrep -xq Finder; 311 | do 312 | if [ $( /bin/launchctl list | /usr/bin/grep -c "com.jamfsoftware.task.Every 15 Minutes" ) != "0" ]; 313 | then 314 | logme "Disabling Jamf Check-In." 315 | /bin/launchctl bootout "system/com.jamfsoftware.task.Every 15 Minutes" 316 | jamfpid=$( /bin/ps -ax | /usr/bin/grep "jamf policy -randomDelaySeconds" | /usr/bin/grep -v "grep" | /usr/bin/awk '{ print $1 }' ) 317 | [ "$jamfpid" != "" ] && kill -9 "$jamfpid" 318 | unset jamfpid 319 | fi 320 | done 321 | logme "Dock process running. Starting deployment process." 322 | 323 | # Attempt to check if device has properly enrolled into Jamf Pro 324 | logme "Checking if Jamf Enrollment has completed properly" 325 | jsstest=$( /usr/local/bin/jamf policy -event isjssup 2>/dev/null | /usr/bin/grep -c "Script result: up" ) 326 | 327 | # Initiate re-enrollment of computer if the earlier test failed 328 | if [ "$jsstest" = "0" ]; 329 | then 330 | # Write marker file for Jamf auditing 331 | /usr/bin/touch ${workfolder}/enrollretry 332 | 333 | # Jamf API enrollment credentials and URL 334 | jsonresponse=$( getjamftoken "$clientid" "$clientsecret" "$jssurl" ) 335 | 336 | jamftoken=$( /usr/bin/plutil -extract access_token raw -o - - <<< "$jsonresponse" ) 337 | type=$( /usr/bin/plutil -extract token_type raw -o - - <<< "$jsonresponse" ) 338 | expiry=$( /usr/bin/plutil -extract expires_in raw -o - - <<< "$jsonresponse" ) 339 | 340 | # Find and grab computer jamf ID number 341 | jamfid=$( getcomputerid "$jamftoken" "$jssurl" "$udid" ) 342 | 343 | # Send re-enrollment command 344 | /usr/bin/curl -s -X POST "${jssurl}api/v1/jamf-management-framework/redeploy/${jamfid}" -H "accept: application/json" -H "Authorization: ${type} ${jamftoken}" 345 | 346 | # Invalidate the requested API token 347 | invalidatejamftoken "$token" "$jssurl" 348 | 349 | # No computer record, thus we can't re-enroll so we must exit hard here. 350 | if [[ "$computerid" =~ stdin ]]; 351 | then 352 | opts+=(${(f)}"--title \"Computer Deployment Failed\"") 353 | opts+=(${(f)}"--icon ${icons}/jamfuserdetails.png") 354 | opts+=(${(f)}"--message \"Your computer has failed to install correct software. \n\nClick [here to contact IT Support](https://help.corp.com) and request assistance. \n\nThen please click \"Finish\" to exit this message.\n\nThis message will automatically disappear in 30 seconds time.\"") 355 | opts+=(${(f)}"--quitkey o") 356 | opts+=(${(f)}"--position center") 357 | opts+=(${(f)}"--timer 30") 358 | opts+=(${(f)}"--button1text \"Finish\"") 359 | 360 | eval "$sd" --args "${opts[*]}" & 361 | sleep 0.1 362 | 363 | exitscript 1 'Re-enrollment of device failed' 364 | fi 365 | 366 | # Wait for enrollment to complete 367 | while [ "$jsstest" != "0" ]; do jsstest=$( /usr/local/bin/jamf policy -event isjssup 2>/dev/null | /usr/bin/grep -c "Script result: up" ); sleep 0.1; done 368 | fi 369 | 370 | # Kill any check-in in progress 371 | jamfpid=$( /bin/ps -ax | /usr/bin/grep "jamf policy -randomDelaySeconds" | /usr/bin/grep -v "grep" | /usr/bin/awk '{ print $1 }' ) 372 | if [ "$jamfpid" != "" ]; 373 | then 374 | kill -9 "$jamfpid" 375 | fi 376 | 377 | # 378 | ## Start the deployment process now a user has logged in 379 | # 380 | 381 | # Write out deployment start timestamp 382 | timestamp=$( /bin/date '+%Y-%m-%d %H:%M:%S' | /usr/bin/tee -a $workfolder/.deploystart ) 383 | 384 | # Who's the current user and their details? 385 | currentuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' ) 386 | userid=$( /usr/bin/id -u $currentuser ) 387 | userhome=$( /usr/bin/dscl . read /Users/${currentuser} NFSHomeDirectory | /usr/bin/awk '{print $NF}' ) 388 | userpref="${userhome}/Library/Preferences/com.jamf.connect.state.plist" 389 | useremail="${currentuser}@${domain}" 390 | userkeychain="${userhome}/Library/Keychains/login.keychain-db" 391 | jssurl=$( /usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url ) 392 | 393 | # Work out user's first name and capitalise first letter 394 | firstname=$( echo $currentuser | /usr/bin/cut -d"." -f1 ) 395 | firstname=$( /usr/bin/tr '[:lower:]' '[:upper:]' <<< ${firstname:0:1} )${firstname:1} 396 | 397 | # 398 | ## Load the initial swiftDialog screen 399 | # 400 | logme "Start the swiftDialog process with the initial screen." 401 | 402 | # Set initial icon based on whether the Mac is a desktop or laptop 403 | # Use SF symbols for this https://developer.apple.com/sf-symbols/ 404 | if /usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice | /usr/bin/awk -F'["|"]' '/model/{print $4}' | /usr/bin/grep -q "Book"; 405 | then 406 | icon="SF=laptopcomputer.and.arrow.down,weight=semibold,colour1=black" 407 | else 408 | icon="SF=desktopcomputer.and.arrow.down,weight=semibold,colour1=black" 409 | fi 410 | 411 | dialogver=$( /usr/local/bin/dialog --version ) 412 | 413 | title="Hi $firstname, Welcome to your new computer!" 414 | message="Please wait while we configure your computer ..." 415 | helpmessage="If you need assistance, please contact the IT Service Desk: \n- e-mail your line manager. \n\n**Computer Information:** \n- **Operating System:** ${macosver} (${macosbuild}) \n- **Serial Number:** ${serial} \n- **Dialog:** ${dialogver} \n- **Started:** ${timestamp}" 416 | infobox="TRR Device Info\n\n**Username:**\n- Pending\n\n**Computer Name:** \n- Pending\n\n**Department**\n- Pending\n\n**Location**\n- Pending" 417 | 418 | # Set an array of options here. (If you're wondering what all this ${(f)}) stuff is, 419 | # it's a better way of not splitting on spaces without messing with IFS. 420 | opts+=(${(f)}"--bannerimage \"$bannerimage\"") 421 | opts+=(${(f)}"--title \"$title\"") 422 | opts+=(${(f)}"--message \"$message\"") 423 | opts+=(${(f)}"--helpmessage \"$helpmessage\"") 424 | opts+=(${(f)}"--icon \"$icon\"") 425 | opts+=(${(f)}"--infobox \"${infobox}\"") 426 | opts+=(${(f)}"--progress") 427 | opts+=(${(f)}"--progresstext \"Initializing configuration ...\"") 428 | opts+=(${(f)}"--button1text \"Wait\"") 429 | opts+=(${(f)}"--button1disabled") 430 | opts+=(${(f)}"--infotext \"$scriptversion\"") 431 | opts+=(${(f)}"--messagefont 'size=14'") 432 | opts+=(${(f)}"--height '780'") 433 | opts+=(${(f)}"--position centre") 434 | opts+=(${(f)}"--blurscreen") 435 | opts+=(${(f)}"--ontop") 436 | opts+=(${(f)}"--quitkey o") 437 | opts+=(${(f)}"--commandfile \"$sdcontrolfile\"") 438 | 439 | # Now run swiftDialog and get this party started 440 | eval "$sd" "${opts[*]}" & 441 | 442 | # Wait and clear the options array 443 | sleep 1 444 | unset opts 445 | 446 | # 447 | ## Get current user details and put them in Jamf but also ... 448 | # 449 | 450 | # Set up the first two list items by hand 451 | updatestatus "list: show" 452 | sleep 1 453 | updatestatus "listitem: add, title: Update Jamf User Details, icon: ${icons}/jamfuserdetails.png, statustext: Waiting, status pending" 454 | updatestatus "listitem: add, title: Set Computer Name, icon: ${icons}/setcomputername.png, statustext: Waiting, status: pending" 455 | 456 | ## HORRIBLE workaround for Jamf Connect bug 457 | logme "Writing username: $useremail to Jamf Connect plist" 458 | updatestatus "progresstext: Update Jamf User Details" 459 | updatestatus "listitem: title: Update Jamf User Details, statustext: Configuring, status: wait" 460 | /bin/launchctl bootout gui/$userid /Library/LaunchAgents/com.jamf.connect.plist 461 | /usr/bin/defaults write "$userpref" DisplayName -string "$useremail" 462 | /usr/sbin/chown ${currentuser}:staff "$userpref" 463 | /usr/local/bin/jamf policy -event getusername 464 | updatestatus 'infobox: Device Info\\n\\n**Username:** \\n- '"$currentuser"'\\n\\n**Computer Name:** \\n- Pending\\n\\n**Department**\\n- Pending\\n\\n**Location**\\n- Pending' 465 | updatestatus "listitem: title: Update Jamf User Details, statustext: Completed, status: success" 466 | 467 | # 468 | ## Set computer name via auto script in Jamf. 469 | # 470 | logme "Setting computer name via Jamf policy" 471 | updatestatus "listitem: title: Set Computer Name, statustext: Configuring, status: wait" 472 | updatestatus "progresstext: Setting Computer Name" 473 | /usr/local/bin/jamf policy -event autoname 474 | computername=$( /usr/sbin/scutil --get ComputerName ) 475 | 476 | # Now the computer hostname. Do a regex match for 12345678 type format. 477 | logme "Checking if computer name was set correctly" 478 | if [[ $computername =~ ^[0-9]{8}$ ]]; 479 | then 480 | logme "Computer name is in the correct format" 481 | updatestatus "listitem: title: Set Computer Name, statustext: Completed, status: success" 482 | else 483 | logme "Computer name not set correctly" 484 | computername="ERROR" 485 | updatestatus "listitem: title: Set Computer Name, statustext: Failed, status: fail" 486 | fi 487 | updatestatus 'infobox: Device Info\\n\\n**Username:** \\n- '"$currentuser"'\\n\\n**Computer Name:** \\n- '"$computername"'\\n\\n**Department**\\n- Pending\\n\\n**Location**\\n- Pending' 488 | 489 | # 490 | ## Get building and department info 491 | # 492 | 493 | # Obtain an access token for the Jamf API 494 | jsonresponse=$( getjamftoken "$clientid" "$clientsecret" "$jssurl" ) 495 | jamftoken=$( /usr/bin/plutil -extract access_token raw -o - - <<< "$jsonresponse" ) 496 | type=$( /usr/bin/plutil -extract token_type raw -o - - <<< "$jsonresponse" ) 497 | 498 | # Now get the device record from the hardware udid 499 | deviceuserrecord=$( /usr/bin/curl -s -X GET "${jssurl}api/v1/computers-inventory/?section=USER_AND_LOCATION&page=0&page-size=1&sort=id%3Aasc&filter=udid%3D%3D${udid}" -H "accept: application/json" -H "authorization: ${type} ${jamftoken}" ) 500 | 501 | # From that we can extract out the building and department info from the device record 502 | buildingid=$( /usr/bin/plutil -extract results.0.userAndLocation.buildingId raw -o - - <<< "$deviceuserrecord" ) 503 | departmentid=$( /usr/bin/plutil -extract results.0.userAndLocation.departmentId raw -o - - <<< "$deviceuserrecord" ) 504 | 505 | # Problem: They're listed in the record as their Jamf ID numbers. So do more calls to capture the records for those IDs 506 | buildingjson=$( /usr/bin/curl -s -X GET "${jssurl}api/v1/buildings/${buildingid}" -H "accept: application/json" -H "authorization: Bearer ${jamftoken}" ) 507 | departmentjson=$( /usr/bin/curl -s -X GET "${jssurl}api/v1/departments/${departmentid}" -H "accept: application/json" -H "authorization: Bearer ${jamftoken}" ) 508 | 509 | building=$( /usr/bin/plutil -extract name raw -o - - <<< "$buildingjson" 2>/dev/null ) 510 | department=$( /usr/bin/plutil -extract name raw -o - - <<< "$departmentjson" 2>/dev/null ) 511 | 512 | # Finally we have names. Or do we? Fail out if not. Report if we do. 513 | if [ ! -z "$building" ] && [ ! -z "$department" ]; 514 | then 515 | updatestatus 'infobox: Device Info\\n\\n**Username:** \\n- '"$currentuser"'\\n\\n**Computer Name:** \\n- '"$computername"'\\n\\n**Department**\\n- '"$department"'\\n\\n**Location**\\n- '"$building"'' 516 | else 517 | updatestatus 'infobox: Device Info\\n\\n**Username:** \\n- '"$currentuser"'\\n\\n**Computer Name:** \\n- '"$computername"'\\n\\n**Department**\\n- Retrieval FAILED\\n\\n**Location**\\n- Contact I.T.' 518 | fi 519 | 520 | # Invalidate the requested API token 521 | invalidatejamftoken "$jamftoken" "$jssurl" 522 | 523 | # Clean up icons 524 | updatestatus "listitem: delete, title: Update Jamf User Details" 525 | updatestatus "listitem: delete, title: Set Computer Name" 526 | 527 | # 528 | ## Software Deployment Prep Process 529 | # 530 | logme "Getting policy list from Jamf" 531 | updatestatus "progresstext: Starting Deployment Process" 532 | jamfprocess deploy 533 | 534 | # 535 | ## Device Inventory Process 536 | # 537 | updatestatus "listitem: add, title: Update Inventory, icon: ${icons}/updateinventory.png, statustext: In Progress, status: pending" 538 | updatestatus "progresstext: Updating Jamf Inventory Record" 539 | $jb recon 540 | updatestatus "listitem: title: Update Inventory, statustext: Completed, status: success" 541 | sleep 1 542 | 543 | # 544 | ## Check-in Policy Process 545 | # 546 | updatestatus "progresstext: Starting check-in policies" 547 | updatestatus "listitem: delete, title: Update Inventory" 548 | jamfprocess 549 | 550 | # 551 | ## Set up following tasks to be displayed 552 | # 553 | updatestatus "listitem: add, title: Enable Jamf Check-In, icon: ${icons}/jamfcheckin.png, statustext: Waiting, status: pending" 554 | updatestatus "listitem: add, title: Finalising Deployment, icon: ${icons}/finished.png, statustext: Waiting, status: pending" 555 | sleep 1 556 | 557 | # 558 | ## Re-enable Jamf check-in 559 | # 560 | updatestatus "progresstext: Enabling Jamf Check-In" 561 | updatestatus "listitem: title: Enable Jamf Check-In, statustext: Waiting, status: wait" 562 | /bin/launchctl bootstrap system "$ld" 563 | sleep 1 564 | updatestatus "listitem: title: Enable Jamf Check-In, statustext: Completed, status: success" 565 | 566 | # 567 | ## Create deploy finished touch file and final device inventory process 568 | # 569 | updatestatus "listitem: title: Finalising Deployment, statustext: Waiting, status: pending" 570 | updatestatus "progresstext: Finalising the device deployment" 571 | /bin/date '+%Y-%m-%d %H:%M:%S' | /usr/bin/tee -a $workfolder/.deploycomplete 572 | $jb recon 573 | 574 | # 575 | ## Force Jamf Connect menu agent to run 576 | # 577 | /bin/launchctl bootstrap gui/$userid /Library/LaunchAgents/com.jamf.connect.plist 578 | sleep 1 579 | updatestatus "listitem: delete, title: Enable Jamf Check-In" 580 | updatestatus "listitem: title: Finalising Deployment, statustext: Completed, status: success" 581 | 582 | # 583 | ## Quit main swiftDialog window 584 | # 585 | updatestatus "progresstext: Deployment Completed" 586 | sleep 10 587 | updatestatus "quit:" 588 | sleep 0.1 589 | 590 | # 591 | ## Clean up files and script and exit 592 | # 593 | logme "Cleaning up working files and exiting." 594 | exitscript 0 'Success!' 595 | -------------------------------------------------------------------------------- /pkg install scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Preinstallation script for installing swiftDialog plus deploy 4 | # Author - richard@richard-purves.com 5 | 6 | # Version 1.0 - 04-14-2023 7 | 8 | # Check for current logged in user 9 | loggedinuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' ) 10 | 11 | # For some reason best known to Apple, our files occasionally have quarantine attributes set. Fix this. 12 | /usr/bin/xattr -r -d com.apple.quarantine /usr/local/corp 13 | /usr/bin/xattr -r -d com.apple.quarantine /Library/LaunchDaemons/com.corp.swiftdeploy.plist 14 | /usr/bin/xattr -r -d com.apple.quarantine /Library/Application\ Support/Dialog 15 | /usr/bin/xattr -r -d com.apple.quarantine /usr/local/bin/dialog 16 | 17 | # If loginwindow, setup assistant or no user, then we're in a DEP environment. Load the LaunchDaemon. 18 | if [[ "$loggedinuser" = "loginwindow" ]] || [[ "$loggedinuser" = "_mbsetupuser" ]] || [[ -z "$loggedinuser" ]]; 19 | then 20 | /bin/launchctl bootstrap system /Library/LaunchDaemons/com.corp.swiftdeploy.plist 21 | fi 22 | 23 | # User initiated enrollment devices excluded. We'll pick them up after a restart via Jamf. 24 | exit -------------------------------------------------------------------------------- /pkg install scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Preinstallation script for installing swiftDialog plus deploy 4 | # Very much based on the work of Dan Snelson and Bart Reardon 5 | # Author - richard@richard-purves.com 6 | 7 | # Version 1.1 - 01-08-2024 8 | 9 | # Variables here 10 | dialogteamid="PWA5E9TQ59" 11 | 12 | # We're deploying some branding graphics so let's make sure the folders exist for them to go into! 13 | [ ! -d "/usr/local/corp" ] && mkdir /usr/local/corp 14 | 15 | # Now let's make sure the ownership and permissions are correct 16 | chown -R root:wheel /usr/local/corp 17 | chmod -R 755 /usr/local/corp 18 | 19 | # Download swiftDialog for our use if it's not already present 20 | if [ ! -e "/Library/Application Support/Dialog/Dialog.app" ]; 21 | then 22 | for i in {1..5}; 23 | do 24 | echo "Finding swiftDialog latest URL. Attempt $i of 5." 25 | sdurl=$( /usr/bin/curl -s "https://api.github.com/repos/swiftDialog/swiftDialog/releases/latest" | /usr/bin/awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }" ) 26 | [[ "$sdurl" =~ ^https?:\/\/(.*) ]] && break 27 | sleep 0.2 28 | done 29 | 30 | # In case things move or the autodetect fails, you can just hardcode a path to the installer here 31 | #sdurl="https://github.com/swiftDialog/swiftDialog/releases/download/v2.3.3-4734/dialog-2.3.3-4734.pkg" 32 | 33 | [ "$i" -ge 5 ] && { echo "URL detection failed."; exit 1; } 34 | 35 | for i in {1..5}; 36 | do 37 | echo "Download attempt no. $i of 5." 38 | httpstatus=$( /usr/bin/curl -L -s -o "/private/tmp/dialog.pkg" "$sdurl" -w "%{http_code}" ) 39 | [ "$httpstatus" = "200" ] && break 40 | sleep 0.2 41 | done 42 | 43 | [ "$i" -ge 5 ] && { echo "Download failed."; exit 1; } 44 | 45 | echo "Checking swiftDialog download." 46 | testteamid=$( /usr/sbin/spctl -a -vv -t install "/private/tmp/Dialog.pkg" 2>&1 | /usr/bin/awk '/origin=/ {print $NF }' | tr -d '()' ) 47 | 48 | [ "$testteamid" != "$dialogteamid" ] && { echo "Download check failed."; exit 1; } 49 | 50 | echo "Installing SwiftDialog." 51 | /usr/sbin/installer -pkg "/private/tmp/dialog.pkg" -target / 52 | sleep 2 53 | 54 | echo "Checking installed swiftDialog." 55 | if [ -f "/usr/local/bin/dialog" ]; 56 | then 57 | echo "swiftDialog version $( /usr/local/bin/dialog --version ) installed." 58 | else 59 | echo "swiftDialog install failed." 60 | exit 1 61 | fi 62 | 63 | else 64 | echo "SwiftDialog already installed. Skipping download." 65 | fi 66 | 67 | /bin/rm -f /private/tmp/dialog.pkg 68 | 69 | exit 0 70 | --------------------------------------------------------------------------------