├── Baseline.png ├── Build ├── Scripts │ └── ExampleScript.sh ├── Packages │ ├── .DS_Store │ └── ExamplePKG.pkg ├── Icons │ └── ExampleIcon.png ├── com.secondsonconsulting.baseline.plist ├── Baseline_daemon-preinstall.sh └── Baseline_daemon-postinstall.sh ├── .gitignore ├── ProfileManifest ├── README.md ├── com.secondsonconsulting.baseline.json └── com.secondsonconsulting.baseline.plist ├── LICENSE ├── ExampleConfigurationFiles ├── BaselineConfig.plist ├── Baseline-BasicExample.mobileconfig └── Baseline-ProductionExample.mobileconfig ├── README.md ├── ExampleInitialScripts └── InitialScriptExample-1.sh └── Baseline.sh /Baseline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecondSonConsulting/Baseline/HEAD/Baseline.png -------------------------------------------------------------------------------- /Build/Scripts/ExampleScript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | #set -x 3 | 4 | echo "I am an example script: $0" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | Build/.DS_Store 3 | .DS_Store 4 | .DS_Store 5 | Build/.DS_Store 6 | TestConfig.plist 7 | -------------------------------------------------------------------------------- /Build/Packages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecondSonConsulting/Baseline/HEAD/Build/Packages/.DS_Store -------------------------------------------------------------------------------- /Build/Icons/ExampleIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecondSonConsulting/Baseline/HEAD/Build/Icons/ExampleIcon.png -------------------------------------------------------------------------------- /Build/Packages/ExamplePKG.pkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecondSonConsulting/Baseline/HEAD/Build/Packages/ExamplePKG.pkg -------------------------------------------------------------------------------- /Build/com.secondsonconsulting.baseline.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AbandonProcessGroup 6 | 7 | Label 8 | com.secondsonconsulting.baseline 9 | Program 10 | /usr/local/Baseline/Baseline.sh 11 | RunAtLoad 12 | 13 | StandardErrorPath 14 | /var/log/BaselineOutput.log 15 | StandardOutPath 16 | /var/log/BaselineOutput.log 17 | 18 | 19 | -------------------------------------------------------------------------------- /ProfileManifest/README.md: -------------------------------------------------------------------------------- 1 | # These are for reference and beta only 2 | 3 | These files may not match what's in the official public ProfileManifest repositories. They may be out of date, or they may be ahead of pull requests on the official repos. Check the modified date and use with caution. 4 | 5 | For JSONs or Manifests to use with Beta features, check this folder in the `dev` branch. 6 | 7 | [The office ProfileManifest repository is the proper place to get the latest public release.](https://github.com/ProfileCreator/ProfileManifests/blob/master/Manifests/ManagedPreferencesApplications/com.secondsonconsulting.baseline.plist) 8 | 9 | [Jamf JSON Schema](https://github.com/Jamf-Custom-Profile-Schemas/ProfileManifestsMirror/blob/main/manifests/ManagedPreferencesApplications/com.secondsonconsulting.baseline.json) 10 | -------------------------------------------------------------------------------- /Build/Baseline_daemon-preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | 3 | # Function to exit with an error message 4 | exit_with_error() { 5 | echo "Error: $1" >&2 6 | exit 1 7 | } 8 | 9 | # Set path and name variables 10 | launchDPath="/Library/LaunchDaemons" 11 | launchDName="com.secondsonconsulting.baseline" 12 | 13 | # Check if the launch daemon is running 14 | launchctl list "$launchDName" > /dev/null 2>&1 15 | list_result=$? 16 | 17 | # If daemon is running, attempt to unload and remove it 18 | if [ "$list_result" -eq 0 ]; then 19 | launchctl bootout system/"$launchDName" 20 | bootout_result=$? 21 | 22 | if [ "$bootout_result" -ne 0 ]; then 23 | exit_with_error "Failed to unload the launch daemon." 24 | fi 25 | 26 | # Remove plist file 27 | rm "$launchDPath/$launchDName.plist" || true 28 | fi 29 | 30 | # Successful exit 31 | exit 0 32 | -------------------------------------------------------------------------------- /Build/Baseline_daemon-postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | 3 | # Set variables for path/name 4 | launchDPath="/Library/LaunchDaemons" 5 | launchDName="com.secondsonconsulting.baseline" 6 | 7 | # Function to exit with an error message 8 | exit_with_error() { 9 | echo "Error: $1" >&2 10 | exit 1 11 | } 12 | 13 | # Load the launch daemon 14 | launchctl bootstrap system "$launchDPath/$launchDName.plist" > /dev/null 2>&1 15 | bootstrap_result=$? 16 | 17 | # Test if the bootstrap command failed 18 | if [ "$bootstrap_result" -ne 0 ]; then 19 | exit_with_error "Failed to load the launch daemon." 20 | fi 21 | 22 | # Check if the launch daemon is actually running 23 | launchctl list "$launchDName" > /dev/null 2>&1 24 | list_result=$? 25 | 26 | # Exit fail if the launch daemon isn't loaded 27 | if [ "$list_result" -ne 0 ]; then 28 | exit_with_error "Launch daemon is not running." 29 | fi 30 | 31 | # Successful exit 32 | exit 0 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Second Son Consulting 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 | -------------------------------------------------------------------------------- /ExampleConfigurationFiles/BaselineConfig.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InitialScripts 6 | 7 | 8 | Arguments 9 | --fdsa 10 | DisplayName 11 | Initial Example Script 12 | MD5 13 | e567257026d6032dd232df75fd3ba500 14 | ScriptPath 15 | ExampleScript.sh 16 | 17 | 18 | Installomator 19 | 20 | 21 | DisplayName 22 | Firefox 23 | Label 24 | firefoxpkg 25 | 26 | 27 | DisplayName 28 | Dockutil 29 | Label 30 | dockutil 31 | 32 | 33 | Packages 34 | 35 | 36 | DisplayName 37 | Example Package 38 | PackagePath 39 | ExamplePKG.pkg 40 | TeamID 41 | 7Q6XP5698G 42 | 43 | 44 | Scripts 45 | 46 | 47 | Arguments 48 | --asdf 49 | DisplayName 50 | Example Script 51 | MD5 52 | e567257026d6032dd232df75fd3ba500 53 | ScriptPath 54 | ExampleScript.sh 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![BaselineScreenshot_2024-05-13](https://github.com/SecondSonConsulting/Baseline/assets/106293503/f7508dae-2b29-4fce-83cc-ac591c0e9cfe) 2 | An MDM agnostic zero touch or light touch solution for macOS. 3 | 4 | By leveraging swiftDialog, Installomator, and original code, Baseline provides an automated way to install applications and run scripts. Configure the behavior of Baseline via a mobileconfig or plist file. Baseline will install packages, scripts, and Installomator labels as defined in the configuration file. 5 | 6 | ## Requirements 7 | - macOS 12 or newer* 8 | - An installation PKG for the project. Either the provided PKG or your own. 9 | - A configuration profile defining what Installomator labels, Packages, and/or Scripts you wish to run 10 | 11 | _*This requirement is based on the latest version requirements for swiftDialog. Older versions of swiftDialog supported macOS 11, and Baseline can be used with macOS 11 if you deploy an older swiftDialog prior to running Baseline._ 12 | 13 | ## Visit the Wiki 14 | Detailed documentation on how Baseline works and how to configure it can be found in the [wiki](https://github.com/SecondSonConsulting/Baseline/wiki). 15 | 16 | ## Thank you to the Mac Admins Community 17 | This project wouldn’t be possible without the amazing hard work provided to the Mac Admins community. Bart Reardon, Søren Theilgaard, Armin Briegel, Adam Codega, Dan Snelson, Pico Mitchell, and all of the other amazing people maintaining and testing swiftDialog, Installomator, and other community tools. 18 | We are happy to have the opportunity to give back, and hope other Mac Admins might find this project useful. 19 | 20 | Thank you to Drew Diver and Mykola for feature enhancements on this project. 21 | 22 | [macos](https://icons8.com/icon/80591/apple-logo) icon by [Icons8](https://icons8.com) used for example. 23 | -------------------------------------------------------------------------------- /ExampleConfigurationFiles/Baseline-BasicExample.mobileconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PayloadContent 6 | 7 | 8 | InitialScripts 9 | 10 | 11 | Arguments 12 | --fdsa 13 | DisplayName 14 | Initial Example Script 15 | MD5 16 | e567257026d6032dd232df75fd3ba500 17 | ScriptPath 18 | ExampleScript.sh 19 | 20 | 21 | Installomator 22 | 23 | 24 | DisplayName 25 | Firefox 26 | Label 27 | firefoxpkg 28 | 29 | 30 | DisplayName 31 | Dockutil 32 | Label 33 | dockutil 34 | 35 | 36 | Packages 37 | 38 | 39 | DisplayName 40 | Example Package 41 | PackagePath 42 | ExamplePKG.pkg 43 | TeamID 44 | 7Q6XP5698G 45 | 46 | 47 | PayloadDisplayName 48 | Baseline 49 | PayloadIdentifier 50 | com.secondsonconsulting.baseline.D031FEC3-06B0-497A-BC0A-5D24AE8E91AD 51 | PayloadType 52 | com.secondsonconsulting.baseline 53 | PayloadUUID 54 | F46B02DA-FF67-481D-B39C-485F8CC9E39B 55 | PayloadVersion 56 | 1 57 | Scripts 58 | 59 | 60 | Arguments 61 | --asdf 62 | DisplayName 63 | Example Script 64 | MD5 65 | e567257026d6032dd232df75fd3ba500 66 | ScriptPath 67 | ExampleScript.sh 68 | 69 | 70 | 71 | 72 | PayloadDescription 73 | Configuration file for Baseline.sh 74 | PayloadDisplayName 75 | Baseline Config 76 | PayloadIdentifier 77 | SECONDSON.6F1AD41D-2069-4861-B72C-C5F0D44C9B99 78 | PayloadOrganization 79 | Second Son Consulting Inc. 80 | PayloadScope 81 | System 82 | PayloadType 83 | Configuration 84 | PayloadUUID 85 | 8A016FF5-057E-4F31-9043-9C715F061433 86 | PayloadVersion 87 | 1 88 | 89 | 90 | -------------------------------------------------------------------------------- /ExampleConfigurationFiles/Baseline-ProductionExample.mobileconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PayloadContent 6 | 7 | 8 | Installomator 9 | 10 | 11 | DisplayName 12 | Firefox 13 | Icon 14 | ExampleIcon.png 15 | Label 16 | firefoxpkg 17 | 18 | 19 | DisplayName 20 | Microsoft Office 365 21 | Icon 22 | SF=doc.circle,color=blue 23 | Label 24 | microsoftoffice365 25 | 26 | 27 | DisplayName 28 | Zoom 29 | Icon 30 | SF= video.circle,color=blue 31 | Label 32 | zoom 33 | 34 | 35 | Packages 36 | 37 | 38 | DisplayName 39 | Mist v0.5 40 | Icon 41 | https://github.com/ninxsoft/Mist/blob/main/README%20Resources/App%20Icon.png?raw=true 42 | PackagePath 43 | https://github.com/ninxsoft/Mist/releases/download/v0.5/Mist.0.5.pkg 44 | TeamID 45 | 7K3HVCLV7Z 46 | 47 | 48 | PayloadDisplayName 49 | Baseline 50 | PayloadIdentifier 51 | com.secondsonconsulting.baseline.D031FEC3-06B0-497A-BC0A-5D24AE8E91AD 52 | PayloadType 53 | com.secondsonconsulting.baseline 54 | PayloadUUID 55 | 75A6163E-E581-4E91-88B7-17927BF9667A 56 | PayloadVersion 57 | 1 58 | Scripts 59 | 60 | 61 | DisplayName 62 | Enabling Location Services 63 | Icon 64 | /System/Library/PrivateFrameworks/AOSUI.framework/Versions/A/Resources/findmy.icns 65 | MD5 66 | c32364f1f3f7fc2410e265d82095feee 67 | ScriptPath 68 | https://raw.githubusercontent.com/acodega/macos/main/enable_location-services.sh 69 | 70 | 71 | 72 | 73 | PayloadDescription 74 | Configuration file for Baseline.sh 75 | PayloadDisplayName 76 | Baseline Config 77 | PayloadIdentifier 78 | SecondSon.6F1AD41D-2069-4861-B72C-C5F0D44C9B99 79 | PayloadOrganization 80 | Second Son Consulting Inc. 81 | PayloadScope 82 | System 83 | PayloadType 84 | Configuration 85 | PayloadUUID 86 | BEDFC054-5B20-430F-B388-A51C3B4D30AE 87 | PayloadVersion 88 | 1 89 | 90 | 91 | -------------------------------------------------------------------------------- /ExampleInitialScripts/InitialScriptExample-1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # Uncomment this for verbose debug 3 | #set -x 4 | 5 | ########## 6 | # This is an example script designed to show some of the features of Baseline. 7 | # Written by: Trevor Sysock of Second Son Consulting 8 | # @BigMacAdmin on Github and Slack 9 | # 10 | # In this example, we're making an InitialScript that will prompt the user to choose their 11 | # department and then rename the computer and put a script in place to be processed by Baseline 12 | # according to the user's response. 13 | # 14 | # This script assumes you have the following in place: 15 | # - A Baseline configuration file containing this script in the "InitialScripts" section and "DepartmentSetup.sh" in the "Scripts" section. 16 | # - One or more scripts named to match the abbreviation of the department the user chooses. 17 | # - /usr/local/Baseline/Scripts/ 18 | # - Acct.sh 19 | # - Sales.sh 20 | # - Exec.sh 21 | # - Art.sh 22 | # - Tech.sh 23 | # - A "default" script in the Baseline Scripts directory name "DepartmentSetup.sh" 24 | # 25 | # When the user chooses a department, the script associated with that department is renamed "DepartmentSetup.sh", and will be processed by Baseline. 26 | 27 | ############# 28 | # Functions # 29 | ############# 30 | 31 | # Verify we are running as root 32 | if [[ $(id -u) -ne 0 ]]; then 33 | echo "ERROR: This script must be run as root **EXITING**" 34 | exit 1 35 | fi 36 | 37 | ############# 38 | # Variables # 39 | ############# 40 | 41 | # Company name or abbreviation used for naming the computer 42 | companyName="Acme" 43 | 44 | # Path to SwiftDialog 45 | dialogPath='/usr/local/bin/dialog' 46 | 47 | # Path to Baseline Scripts directory 48 | BaselineScripts="/usr/local/Baseline/Scripts" 49 | 50 | # Get the shortname of the current user 51 | currentUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' ) 52 | # Get the UID of the current user 53 | currentUserUID=$(/usr/bin/id -u "$currentUser") 54 | # Get the Display Name of the current user 55 | currentUserDisplayName=$(id -F "$currentUser") 56 | # Get the First Name of the current user 57 | firstName=$(/usr/bin/id -F "$currentUser"| cut -d ' ' -f 1) 58 | 59 | # Setup your welcome experience 60 | welcomeTitle="Help Us Name Your Mac." 61 | welcomeBody="Hi $currentUserDisplayName, lets get your computer setup.\n\n To start, please choose your department." 62 | departmentChoices="Art,Sales,Accounting,Executive,Technician" 63 | 64 | ###################### 65 | # Script Starts Here # 66 | ###################### 67 | 68 | # Call the dialog command and put the results in a variable, so we can check against them afterwards 69 | departmentSelection=$("$dialogPath" \ 70 | --title "$welcomeTitle" \ 71 | --message "$welcomeBody" \ 72 | --selecttitle "Department" \ 73 | --ontop \ 74 | --blurscreen \ 75 | --position top \ 76 | --selectvalues "$departmentChoices") 77 | 78 | # Set the Department Abbreviation based on user input 79 | case $departmentSelection in 80 | 81 | *'"Department" : "Accounting"'*) 82 | departmentAbbr="Acct" 83 | ;; 84 | *'"Department" : "Sales"'*) 85 | departmentAbbr="Sales" 86 | ;; 87 | *'"Department" : "Executive"'*) 88 | departmentAbbr="Exec" 89 | ;; 90 | *'"Department" : "Art"'*) 91 | departmentAbbr="Art" 92 | ;; 93 | *'"Department" : "Technician"'*) 94 | departmentAbbr="Tech" 95 | ;; 96 | esac 97 | 98 | # Set the device name variable 99 | finalComputerName="$companyName"-"$departmentAbbr"-"$firstName" 100 | 101 | # Name the computer 3 ways 102 | scutil --set ComputerName $finalComputerName 103 | scutil --set HostName "$finalComputerName".shared 104 | scutil --set LocalHostName $finalComputerName 105 | 106 | # Check if there is a department script configured. If there is, rename it so that Baseline processes it. 107 | if [ -e "$BaselineScripts"/"$departmentAbbr".sh ]; then 108 | mv "$BaselineScripts"/"$departmentAbbr".sh "$BaselineScripts"/DepartmentSetup.sh 109 | fi 110 | -------------------------------------------------------------------------------- /ProfileManifest/com.secondsonconsulting.baseline.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Baseline by Second Son Consulting (com.secondsonconsulting.baseline)", 3 | "description": "Baseline by Second Son Consulting Configuration", 4 | "properties": { 5 | "InitialScripts": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "properties": { 10 | "DisplayName": { 11 | "type": "string", 12 | "title": "Display Name", 13 | "description": "The name you want to appear in SwiftDialog and logs. Initial Scripts will only be shown to the user if the item Fails." 14 | }, 15 | "ScriptPath": { 16 | "type": "string", 17 | "title": "Script Path", 18 | "description": "A path to the script you want to run. Can be a local file path or a URL." 19 | }, 20 | "SHA256": { 21 | "type": "string", 22 | "title": "SHA256", 23 | "description": "The expected SHA256 of the script being run." 24 | }, 25 | "MD5": { 26 | "type": "string", 27 | "title": "MD5", 28 | "description": "The expected MD5 of the script being run." 29 | }, 30 | "Arguments": { 31 | "type": "string", 32 | "title": "Arguments", 33 | "description": "Arguments you want to pass to the script when it is run." 34 | }, 35 | "AsUser": { 36 | "type": "boolean", 37 | "title": "Run as User", 38 | "description": "Enable this option to run the script as the logged in user." 39 | } 40 | } 41 | }, 42 | "title": "InitialScripts", 43 | "description": "Define scripts that are run immediately as the user logs in, prior to any Baseline SwiftDialog windows. These scripts are the place to add your custom welcome messages or pre-flight scripts.", 44 | "property_order": 5 45 | }, 46 | "Installomator": { 47 | "type": "array", 48 | "items": { 49 | "type": "object", 50 | "properties": { 51 | "DisplayName": { 52 | "type": "string", 53 | "title": "Display Name", 54 | "description": "The name you want to appear in the SwiftDialog menu as this Installomator label is processed." 55 | }, 56 | "Label": { 57 | "type": "string", 58 | "title": "Label", 59 | "description": "The Installomator label." 60 | }, 61 | "Arguments": { 62 | "type": "string", 63 | "title": "Arguments", 64 | "description": "Arguments you want to pass to the Installomator command for this label." 65 | }, 66 | "Icon": { 67 | "type": "string", 68 | "title": "Icon", 69 | "description": "The icon you want to appear in the SwiftDialog menu as this Installomator label is processed. Can be a URL or local file path." 70 | }, 71 | "Subtitle": { 72 | "type": "string", 73 | "title": "Subtitle", 74 | "description": "The subtitle text to appear on this line item." 75 | } 76 | } 77 | }, 78 | "title": "Installomator Labels", 79 | "description": "Define Installomator labels to be run by Baseline. For information specific to Installomator, see the Installomator Github: https://github.com/Installomator/Installomator", 80 | "property_order": 10 81 | }, 82 | "Packages": { 83 | "type": "array", 84 | "items": { 85 | "type": "object", 86 | "properties": { 87 | "DisplayName": { 88 | "type": "string", 89 | "title": "Display Name", 90 | "description": "The name you want to appear in the SwiftDialog menu as this package is installed." 91 | }, 92 | "PackagePath": { 93 | "type": "string", 94 | "title": "Package Path", 95 | "description": "A path to the package you want to install. Can be a local file path or a URL." 96 | }, 97 | "TeamID": { 98 | "type": "string", 99 | "title": "TeamID", 100 | "description": "The expected TeamID of the package being installed." 101 | }, 102 | "SHA256": { 103 | "type": "string", 104 | "title": "SHA256", 105 | "description": "The expected SHA256 of the package being installed." 106 | }, 107 | "MD5": { 108 | "type": "string", 109 | "title": "MD5", 110 | "description": "The expected MD5 of the package being installed." 111 | }, 112 | "Arguments": { 113 | "type": "string", 114 | "title": "Arguments", 115 | "description": "Arguments you want to pass to the installer command for this package." 116 | }, 117 | "Icon": { 118 | "type": "string", 119 | "title": "Icon", 120 | "description": "The icon you want to appear in the SwiftDialog menu as this package is installed. Can be a URL or local file path." 121 | }, 122 | "Subtitle": { 123 | "type": "string", 124 | "title": "Subtitle", 125 | "description": "The subtitle text to appear on this line item." 126 | } 127 | } 128 | }, 129 | "title": "Packages", 130 | "description": "Define Packages to be run by Baseline", 131 | "property_order": 15 132 | }, 133 | "Scripts": { 134 | "type": "array", 135 | "items": { 136 | "type": "object", 137 | "properties": { 138 | "DisplayName": { 139 | "type": "string", 140 | "title": "Display Name", 141 | "description": "The name you want to appear in the SwiftDialog menu as this script is run." 142 | }, 143 | "ScriptPath": { 144 | "type": "string", 145 | "title": "Script Path", 146 | "description": "A path to the script you want to run. Can be a local file path or a URL." 147 | }, 148 | "SHA256": { 149 | "type": "string", 150 | "title": "SHA256", 151 | "description": "The expected SHA256 of the script being run." 152 | }, 153 | "MD5": { 154 | "type": "string", 155 | "title": "MD5", 156 | "description": "The expected MD5 of the script being run." 157 | }, 158 | "Arguments": { 159 | "type": "string", 160 | "title": "Arguments", 161 | "description": "Arguments you want to pass to the script when it is run." 162 | }, 163 | "AsUser": { 164 | "type": "boolean", 165 | "title": "Run as User", 166 | "description": "Enable this option to run the script as the logged in user." 167 | }, 168 | "Icon": { 169 | "type": "string", 170 | "title": "Icon", 171 | "description": "The icon you want to appear in the SwiftDialog menu as this script is run. Can be a URL or local file path." 172 | }, 173 | "Subtitle": { 174 | "type": "string", 175 | "title": "Subtitle", 176 | "description": "The subtitle text to appear on this line item." 177 | } 178 | } 179 | }, 180 | "title": "Scripts", 181 | "description": "Define Scripts to be run by Baseline", 182 | "property_order": 20 183 | }, 184 | "WaitFor": { 185 | "type": "array", 186 | "items": { 187 | "type": "object", 188 | "properties": { 189 | "DisplayName": { 190 | "type": "string", 191 | "title": "Display Name", 192 | "description": "The name you want to appear in the SwiftDialog menu for this item we are waiting for." 193 | }, 194 | "Path": { 195 | "type": "string", 196 | "title": "Path", 197 | "description": "A path to the file you want Baseline to wait for." 198 | }, 199 | "Icon": { 200 | "type": "string", 201 | "title": "Icon", 202 | "description": "The icon you want to appear in the SwiftDialog menu for this item. Can be a URL or local file path." 203 | }, 204 | "Subtitle": { 205 | "type": "string", 206 | "title": "Subtitle", 207 | "description": "The subtitle text to appear on this line item." 208 | } 209 | } 210 | }, 211 | "title": "WaitFor", 212 | "description": "Define files which you want Baseline to wait for. Use this for items not directly installed by Baseline, like VPP or MDM installed apps.", 213 | "property_order": 25 214 | }, 215 | "FinalScripts": { 216 | "type": "array", 217 | "items": { 218 | "type": "object", 219 | "properties": { 220 | "DisplayName": { 221 | "type": "string", 222 | "title": "Display Name", 223 | "description": "The name you want to appear SwiftDialog and logs. Final Scripts will only be shown to the user if the item Fails." 224 | }, 225 | "ScriptPath": { 226 | "type": "string", 227 | "title": "Script Path", 228 | "description": "A path to the script you want to run. Can be a local file path or a URL." 229 | }, 230 | "SHA256": { 231 | "type": "string", 232 | "title": "SHA256", 233 | "description": "The expected SHA256 of the script being run." 234 | }, 235 | "MD5": { 236 | "type": "string", 237 | "title": "MD5", 238 | "description": "The expected MD5 of the script being run." 239 | }, 240 | "Arguments": { 241 | "type": "string", 242 | "title": "Arguments", 243 | "description": "Arguments you want to pass to the script when it is run." 244 | }, 245 | "AsUser": { 246 | "type": "boolean", 247 | "title": "Run as User", 248 | "description": "Enable this option to run the script as the logged in user." 249 | } 250 | } 251 | }, 252 | "title": "FinalScripts", 253 | "description": "Define scripts that are run just before completion dialogs. For reporting webhooks, cleanup tasks, etc. Similar to InitialScripts, these items do not appear on the List View.", 254 | "property_order": 30 255 | }, 256 | "WaitForTimeout": { 257 | "type": "integer", 258 | "title": "WaitFor Timeout", 259 | "description": "When using WaitFor, enter the number of seconds you wish to wait before Baseline considers remaining items a failure. This timer does not start until all other items are processed. Default if key is omitted is 600.", 260 | "property_order": 35 261 | }, 262 | "Restart": { 263 | "type": "boolean", 264 | "title": "Force Restart", 265 | "default": true, 266 | "description": "This setting controls whether Baseline forces a restart after completion.", 267 | "property_order": 40 268 | }, 269 | "LogOut": { 270 | "type": "boolean", 271 | "title": "Force Log Out", 272 | "default": false, 273 | "description": "This setting controls whether Baseline forces a restart after completion.", 274 | "property_order": 45 275 | }, 276 | "BlurScreen": { 277 | "type": "boolean", 278 | "title": "Blur Screen", 279 | "default": true, 280 | "description": "This setting controls whether Baseline uses the SwiftDialog --blurscreen feature. Default is true. If you set to false, consider using --ontop in your DialogListOptions key", 281 | "property_order": 50 282 | }, 283 | "ProgressBar": { 284 | "type": "boolean", 285 | "title": "Show Progress Bar", 286 | "default": false, 287 | "description": "This setting controls whether Baseline displays a progress bar.", 288 | "property_order": 55 289 | }, 290 | "ProgressBarDisplayNames": { 291 | "type": "boolean", 292 | "title": "Show Display Names on Progress Bar", 293 | "default": false, 294 | "description": "This setting controls whether Baseline displays the current item being processed under the progress bar.", 295 | "property_order": 60 296 | }, 297 | "CleanupAfterUse": { 298 | "type": "boolean", 299 | "title": "Cleanup After Use", 300 | "default": true, 301 | "description": "This setting controls whether Baseline deletes it's install directory after use: /usr/local/Baseline.", 302 | "property_order": 65 303 | }, 304 | "ReinstallDialog": { 305 | "type": "boolean", 306 | "title": "Reinstall SwiftDialog", 307 | "description": "Deprecated: This feature was removed in v.2.3, as it is no longer needed. This setting controls whether Baseline will reinstall SwiftDialog as the final step before completion. Default behavior if this key is omitted will reinstall SwiftDialog if a custom PNG is found in /Library/Application Support/Dialog/Dialog.png. Leave this as default if you use Baseline to deploy a custom SwiftDialog icon. Set to false if you do not want Baseline to ever reinstall SwiftDialog. Set to True if you always want to reinstall SwiftDialog after each Baseline.", 308 | "property_order": 70 309 | }, 310 | "InstallomatorSwiftDialogIntegration": { 311 | "type": "boolean", 312 | "title": "Use Installomator SwiftDialog Integration", 313 | "default": false, 314 | "description": "Tell Installomator to use it's built in SwiftDialog integration for the List View.", 315 | "property_order": 75 316 | }, 317 | "IgnoreDefaultInstallomatorOptions": { 318 | "type": "boolean", 319 | "title": "Ignore Default Installomator Options", 320 | "default": false, 321 | "description": "By default, Baseline uses \"NOTIFY=silent\" and \"BLOCKING_PROCESS_ACTION=kill\" Installomator options. Setting this to True means Baseline will not use those options.", 322 | "property_order": 80 323 | }, 324 | "Button1Enabled": { 325 | "type": "boolean", 326 | "title": "Button 1 Enabled", 327 | "default": false, 328 | "description": "This setting enables SwiftDialog \"Button 1\" (the OK button) on the Baseline List View. This allows users to dismiss the list view and continue working while Baseline runs. By default this button is disabled.", 329 | "property_order": 85 330 | }, 331 | "JamfVerbose": { 332 | "type": "boolean", 333 | "title": "Verbose Jamf Items", 334 | "default": false, 335 | "description": "If set to true, when a Script is processing a jamf policy (/usr/local/bin/jamf), Baseline will watch the Jamf log to provide more verbose output on that line item. Similar to the Installomator integration, text and a progress circle will be used for all jamf items. ", 336 | "property_order": 90 337 | }, 338 | "CloseListBeforeFinalScripts": { 339 | "type": "boolean", 340 | "title": "Close List View Before Final Scripts", 341 | "default": false, 342 | "description": "If set to true, Baseline will close the list view window before running FinalScripts. Use this option if you want to utilize FinalScripts to have the user complete actions prior to the Baseline completion dialog.", 343 | "property_order": 95 344 | }, 345 | "DialogListOptions": { 346 | "type": "string", 347 | "title": "Dialog List Window Options", 348 | "description": "SwiftDialog options for the primary Baseline progress list window.", 349 | "property_order": 100 350 | }, 351 | "DialogSuccessOptions": { 352 | "type": "string", 353 | "title": "Dialog Success Window Options", 354 | "description": "SwiftDialog options for the Success Dialog window.", 355 | "property_order": 105 356 | }, 357 | "DialogFailureOptions": { 358 | "type": "string", 359 | "title": "Dialog Failure Window Options", 360 | "description": "SwiftDialog options for the Failure Dialog window.", 361 | "property_order": 110 362 | }, 363 | "ExitCondition": { 364 | "type": "string", 365 | "title": "Exit Condition", 366 | "description": "Define a file or folder path. When Baseline starts, if this file or folder exists, Baseline will exit quietly without processing any items. Use this option if your management tool may re-send scripts when not desired.", 367 | "property_order": 115 368 | }, 369 | "BailOutFile": { 370 | "type": "string", 371 | "title": "Bail Out File", 372 | "description": "Define a file path. If this file exists at any point while Baseline is running, Baseline will exit and will not process additional items.", 373 | "property_order": 120 374 | } 375 | } 376 | } -------------------------------------------------------------------------------- /ProfileManifest/com.secondsonconsulting.baseline.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pfm_app_url 6 | https://github.com/SecondSonConsulting/Baseline/releases 7 | pfm_description 8 | Baseline by Second Son Consulting Configuration 9 | pfm_documentation_url 10 | https://github.com/SecondSonConsulting/Baseline/wiki 11 | pfm_domain 12 | com.secondsonconsulting.baseline 13 | pfm_format_version 14 | 1 15 | pfm_last_modified 16 | 2025-04-23T00:00:00Z 17 | pfm_platforms 18 | 19 | macOS 20 | 21 | pfm_subkeys 22 | 23 | 24 | pfm_default 25 | Baseline configuration preferences 26 | pfm_description 27 | The human-readable description of this payload. This description appears on the Detail screen. 28 | pfm_description_reference 29 | Optional. A human-readable description of this payload. This description is shown on the Detail screen. 30 | pfm_name 31 | PayloadDescription 32 | pfm_title 33 | Payload Description 34 | pfm_type 35 | string 36 | 37 | 38 | pfm_default 39 | Baseline 40 | pfm_description 41 | The human-readable name for the profile payload. The name appears on the Detail screen and doesn't need to be unique. 42 | pfm_description_reference 43 | A human-readable name for the profile payload. This name is displayed on the Detail screen. It does not have to be unique. 44 | pfm_name 45 | PayloadDisplayName 46 | pfm_require 47 | always 48 | pfm_title 49 | Payload Display Name 50 | pfm_type 51 | string 52 | 53 | 54 | pfm_default 55 | com.secondsonconsulting.baseline 56 | pfm_description 57 | The reverse-DNS-style identifier for the payload. This identifier is usually the same as the TopLevel value, with an additional appended component. This string must be unique within the profile. 58 | During a profile replacement, the system updates payloads with the same 'PayloadIdentifier' and 'PayloadUUID' in the old and new profiles. 59 | pfm_description_reference 60 | A reverse-DNS-style identifier for the specific payload. It is usually the same identifier as the root-level PayloadIdentifier value with an additional component appended. 61 | pfm_name 62 | PayloadIdentifier 63 | pfm_require 64 | always 65 | pfm_title 66 | Payload Identifier 67 | pfm_type 68 | string 69 | 70 | 71 | pfm_default 72 | com.secondsonconsulting.baseline 73 | pfm_description 74 | The payload type, which each payload domain's reference page specifies. 75 | pfm_description_reference 76 | The payload type. 77 | pfm_name 78 | PayloadType 79 | pfm_require 80 | always 81 | pfm_title 82 | Payload Type 83 | pfm_type 84 | string 85 | 86 | 87 | pfm_description 88 | The globally unique identifier for the payload. The actual content is unimportant, but must be globally unique. In macOS, use 'uuidgen' to generate UUIDs. 89 | During a profile replacement, the system updates payloads with the same 'PayloadIdentifier' and 'PayloadUUID' in the old and new profiles. 90 | pfm_description_reference 91 | A globally unique identifier for the payload. The actual content is unimportant, but it must be globally unique. In macOS, you can use uuidgen to generate reasonable UUIDs. 92 | pfm_format 93 | ^[0-9A-Za-z]{8}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{12}$ 94 | pfm_name 95 | PayloadUUID 96 | pfm_require 97 | always 98 | pfm_title 99 | Payload UUID 100 | pfm_type 101 | string 102 | 103 | 104 | pfm_default 105 | 1 106 | pfm_description 107 | The version of this specific payload. 108 | pfm_description_reference 109 | The version number of the individual payload. 110 | A profile can consist of payloads with different version numbers. For example, changes to the VPN software in iOS might introduce a new payload version to support additional features, but Mail payload versions would not necessarily change in the same release. 111 | pfm_name 112 | PayloadVersion 113 | pfm_range_list 114 | 115 | 1 116 | 117 | pfm_require 118 | always 119 | pfm_title 120 | Payload Version 121 | pfm_type 122 | integer 123 | 124 | 125 | pfm_description 126 | The human-readable string containing the name of the organization that provides the profile. This value doesn't need to match the organization payload value in the enclosing dictionary. 127 | pfm_name 128 | PayloadOrganization 129 | pfm_title 130 | Payload Organization 131 | pfm_type 132 | string 133 | 134 | 135 | pfm_description 136 | Define scripts that are run immediately as the user logs in, prior to any Baseline SwiftDialog windows. These scripts are the place to add your custom welcome messages or pre-flight scripts. 137 | pfm_name 138 | InitialScripts 139 | pfm_subkeys 140 | 141 | 142 | pfm_subkeys 143 | 144 | 145 | pfm_description 146 | The name you want to appear in SwiftDialog and logs. Initial Scripts will only be shown to the user if the item Fails. 147 | pfm_name 148 | DisplayName 149 | pfm_title 150 | Display Name 151 | pfm_type 152 | string 153 | 154 | 155 | pfm_description 156 | A path to the script you want to run. Can be a local file path or a URL. 157 | pfm_name 158 | ScriptPath 159 | pfm_title 160 | Script Path 161 | pfm_type 162 | string 163 | 164 | 165 | pfm_description 166 | The expected SHA256 of the script being run. 167 | pfm_name 168 | SHA256 169 | pfm_title 170 | SHA256 171 | pfm_type 172 | string 173 | 174 | 175 | pfm_description 176 | The expected MD5 of the script being run. 177 | pfm_name 178 | MD5 179 | pfm_title 180 | MD5 181 | pfm_type 182 | string 183 | 184 | 185 | pfm_description 186 | Arguments you want to pass to the script when it is run. 187 | pfm_name 188 | Arguments 189 | pfm_title 190 | Arguments 191 | pfm_type 192 | string 193 | 194 | 195 | pfm_description 196 | Enable this option to run the script as the logged in user. 197 | pfm_name 198 | AsUser 199 | pfm_title 200 | Run as User 201 | pfm_type 202 | boolean 203 | 204 | 205 | pfm_type 206 | dictionary 207 | 208 | 209 | pfm_title 210 | InitialScripts 211 | pfm_type 212 | array 213 | 214 | 215 | pfm_description 216 | Define Installomator labels to be run by Baseline. For information specific to Installomator, see the Installomator Github: https://github.com/Installomator/Installomator 217 | pfm_name 218 | Installomator 219 | pfm_subkeys 220 | 221 | 222 | pfm_subkeys 223 | 224 | 225 | pfm_description 226 | The name you want to appear in the SwiftDialog menu as this Installomator label is processed. 227 | pfm_name 228 | DisplayName 229 | pfm_title 230 | Display Name 231 | pfm_type 232 | string 233 | 234 | 235 | pfm_description 236 | The Installomator label. 237 | pfm_name 238 | Label 239 | pfm_title 240 | Label 241 | pfm_type 242 | string 243 | 244 | 245 | pfm_description 246 | Arguments you want to pass to the Installomator command for this label. 247 | pfm_name 248 | Arguments 249 | pfm_title 250 | Arguments 251 | pfm_type 252 | string 253 | 254 | 255 | pfm_description 256 | The icon you want to appear in the SwiftDialog menu as this Installomator label is processed. Can be a URL or local file path. 257 | pfm_name 258 | Icon 259 | pfm_title 260 | Icon 261 | pfm_type 262 | string 263 | 264 | 265 | pfm_app_min 266 | 2.1 267 | pfm_description 268 | The subtitle text to appear on this line item. 269 | pfm_name 270 | Subtitle 271 | pfm_title 272 | Subtitle 273 | pfm_type 274 | string 275 | 276 | 277 | pfm_type 278 | dictionary 279 | 280 | 281 | pfm_title 282 | Installomator Labels 283 | pfm_type 284 | array 285 | 286 | 287 | pfm_description 288 | Define Packages to be run by Baseline 289 | pfm_name 290 | Packages 291 | pfm_subkeys 292 | 293 | 294 | pfm_subkeys 295 | 296 | 297 | pfm_description 298 | The name you want to appear in the SwiftDialog menu as this package is installed. 299 | pfm_name 300 | DisplayName 301 | pfm_title 302 | Display Name 303 | pfm_type 304 | string 305 | 306 | 307 | pfm_description 308 | A path to the package you want to install. Can be a local file path or a URL. 309 | pfm_name 310 | PackagePath 311 | pfm_title 312 | Package Path 313 | pfm_type 314 | string 315 | 316 | 317 | pfm_description 318 | The expected TeamID of the package being installed. 319 | pfm_name 320 | TeamID 321 | pfm_title 322 | TeamID 323 | pfm_type 324 | string 325 | 326 | 327 | pfm_description 328 | The expected SHA256 of the package being installed. 329 | pfm_name 330 | SHA256 331 | pfm_title 332 | SHA256 333 | pfm_type 334 | string 335 | 336 | 337 | pfm_description 338 | The expected MD5 of the package being installed. 339 | pfm_name 340 | MD5 341 | pfm_title 342 | MD5 343 | pfm_type 344 | string 345 | 346 | 347 | pfm_description 348 | Arguments you want to pass to the installer command for this package. 349 | pfm_name 350 | Arguments 351 | pfm_title 352 | Arguments 353 | pfm_type 354 | string 355 | 356 | 357 | pfm_description 358 | The icon you want to appear in the SwiftDialog menu as this package is installed. Can be a URL or local file path. 359 | pfm_name 360 | Icon 361 | pfm_title 362 | Icon 363 | pfm_type 364 | string 365 | 366 | 367 | pfm_app_min 368 | 2.1 369 | pfm_description 370 | The subtitle text to appear on this line item. 371 | pfm_name 372 | Subtitle 373 | pfm_title 374 | Subtitle 375 | pfm_type 376 | string 377 | 378 | 379 | pfm_type 380 | dictionary 381 | 382 | 383 | pfm_title 384 | Packages 385 | pfm_type 386 | array 387 | 388 | 389 | pfm_description 390 | Define Scripts to be run by Baseline 391 | pfm_name 392 | Scripts 393 | pfm_subkeys 394 | 395 | 396 | pfm_subkeys 397 | 398 | 399 | pfm_description 400 | The name you want to appear in the SwiftDialog menu as this script is run. 401 | pfm_name 402 | DisplayName 403 | pfm_title 404 | Display Name 405 | pfm_type 406 | string 407 | 408 | 409 | pfm_description 410 | A path to the script you want to run. Can be a local file path or a URL. 411 | pfm_name 412 | ScriptPath 413 | pfm_title 414 | Script Path 415 | pfm_type 416 | string 417 | 418 | 419 | pfm_description 420 | The expected SHA256 of the script being run. 421 | pfm_name 422 | SHA256 423 | pfm_title 424 | SHA256 425 | pfm_type 426 | string 427 | 428 | 429 | pfm_description 430 | The expected MD5 of the script being run. 431 | pfm_name 432 | MD5 433 | pfm_title 434 | MD5 435 | pfm_type 436 | string 437 | 438 | 439 | pfm_description 440 | Arguments you want to pass to the script when it is run. 441 | pfm_name 442 | Arguments 443 | pfm_title 444 | Arguments 445 | pfm_type 446 | string 447 | 448 | 449 | pfm_description 450 | Enable this option to run the script as the logged in user. 451 | pfm_name 452 | AsUser 453 | pfm_title 454 | Run as User 455 | pfm_type 456 | boolean 457 | 458 | 459 | pfm_description 460 | The icon you want to appear in the SwiftDialog menu as this script is run. Can be a URL or local file path. 461 | pfm_name 462 | Icon 463 | pfm_title 464 | Icon 465 | pfm_type 466 | string 467 | 468 | 469 | pfm_app_min 470 | 2.1 471 | pfm_description 472 | The subtitle text to appear on this line item. 473 | pfm_name 474 | Subtitle 475 | pfm_title 476 | Subtitle 477 | pfm_type 478 | string 479 | 480 | 481 | pfm_type 482 | dictionary 483 | 484 | 485 | pfm_title 486 | Scripts 487 | pfm_type 488 | array 489 | 490 | 491 | pfm_app_min 492 | 2.2 493 | pfm_description 494 | Define files which you want Baseline to wait for. Use this for items not directly installed by Baseline, like VPP or MDM installed apps. 495 | pfm_name 496 | WaitFor 497 | pfm_subkeys 498 | 499 | 500 | pfm_subkeys 501 | 502 | 503 | pfm_description 504 | The name you want to appear in the SwiftDialog menu for this item we are waiting for. 505 | pfm_name 506 | DisplayName 507 | pfm_title 508 | Display Name 509 | pfm_type 510 | string 511 | 512 | 513 | pfm_description 514 | A path to the file you want Baseline to wait for. 515 | pfm_name 516 | Path 517 | pfm_title 518 | Path 519 | pfm_type 520 | string 521 | 522 | 523 | pfm_description 524 | The icon you want to appear in the SwiftDialog menu for this item. Can be a URL or local file path. 525 | pfm_name 526 | Icon 527 | pfm_title 528 | Icon 529 | pfm_type 530 | string 531 | 532 | 533 | pfm_app_min 534 | 2.1 535 | pfm_description 536 | The subtitle text to appear on this line item. 537 | pfm_name 538 | Subtitle 539 | pfm_title 540 | Subtitle 541 | pfm_type 542 | string 543 | 544 | 545 | pfm_type 546 | dictionary 547 | 548 | 549 | pfm_title 550 | WaitFor 551 | pfm_type 552 | array 553 | 554 | 555 | pfm_description 556 | Define scripts that are run just before completion dialogs. For reporting webhooks, cleanup tasks, etc. Similar to InitialScripts, these items do not appear on the List View. 557 | pfm_name 558 | FinalScripts 559 | pfm_subkeys 560 | 561 | 562 | pfm_subkeys 563 | 564 | 565 | pfm_description 566 | The name you want to appear SwiftDialog and logs. Final Scripts will only be shown to the user if the item Fails. 567 | pfm_name 568 | DisplayName 569 | pfm_title 570 | Display Name 571 | pfm_type 572 | string 573 | 574 | 575 | pfm_description 576 | A path to the script you want to run. Can be a local file path or a URL. 577 | pfm_name 578 | ScriptPath 579 | pfm_title 580 | Script Path 581 | pfm_type 582 | string 583 | 584 | 585 | pfm_description 586 | The expected SHA256 of the script being run. 587 | pfm_name 588 | SHA256 589 | pfm_title 590 | SHA256 591 | pfm_type 592 | string 593 | 594 | 595 | pfm_description 596 | The expected MD5 of the script being run. 597 | pfm_name 598 | MD5 599 | pfm_title 600 | MD5 601 | pfm_type 602 | string 603 | 604 | 605 | pfm_description 606 | Arguments you want to pass to the script when it is run. 607 | pfm_name 608 | Arguments 609 | pfm_title 610 | Arguments 611 | pfm_type 612 | string 613 | 614 | 615 | pfm_description 616 | Enable this option to run the script as the logged in user. 617 | pfm_name 618 | AsUser 619 | pfm_title 620 | Run as User 621 | pfm_type 622 | boolean 623 | 624 | 625 | pfm_type 626 | dictionary 627 | 628 | 629 | pfm_title 630 | FinalScripts 631 | pfm_type 632 | array 633 | 634 | 635 | pfm_app_min 636 | 2.2 637 | pfm_description 638 | When using WaitFor, enter the number of seconds you wish to wait before Baseline considers remaining items a failure. This timer does not start until all other items are processed. Default if key is omitted is 600. 639 | pfm_name 640 | WaitForTimeout 641 | pfm_title 642 | WaitFor Timeout 643 | pfm_type 644 | integer 645 | 646 | 647 | pfm_default 648 | 649 | pfm_description 650 | This setting controls whether Baseline forces a restart after completion. 651 | pfm_name 652 | Restart 653 | pfm_title 654 | Force Restart 655 | pfm_type 656 | boolean 657 | 658 | 659 | pfm_default 660 | 661 | pfm_description 662 | This setting controls whether Baseline forces a restart after completion. 663 | pfm_name 664 | LogOut 665 | pfm_title 666 | Force Log Out 667 | pfm_type 668 | boolean 669 | 670 | 671 | pfm_default 672 | 673 | pfm_description 674 | This setting controls whether Baseline uses the SwiftDialog --blurscreen feature. Default is true. If you set to false, consider using --ontop in your DialogListOptions key 675 | pfm_name 676 | BlurScreen 677 | pfm_title 678 | Blur Screen 679 | pfm_type 680 | boolean 681 | 682 | 683 | pfm_default 684 | 685 | pfm_description 686 | This setting controls whether Baseline displays a progress bar. 687 | pfm_name 688 | ProgressBar 689 | pfm_title 690 | Show Progress Bar 691 | pfm_type 692 | boolean 693 | 694 | 695 | pfm_default 696 | 697 | pfm_description 698 | This setting controls whether Baseline displays the current item being processed under the progress bar. 699 | pfm_name 700 | ProgressBarDisplayNames 701 | pfm_title 702 | Show Display Names on Progress Bar 703 | pfm_type 704 | boolean 705 | 706 | 707 | pfm_default 708 | 709 | pfm_description 710 | This setting controls whether Baseline deletes it's install directory after use: /usr/local/Baseline. 711 | pfm_name 712 | CleanupAfterUse 713 | pfm_title 714 | Cleanup After Use 715 | pfm_type 716 | boolean 717 | 718 | 719 | pfm_description 720 | Deprecated: This feature was removed in v.2.3, as it is no longer needed. This setting controls whether Baseline will reinstall SwiftDialog as the final step before completion. Default behavior if this key is omitted will reinstall SwiftDialog if a custom PNG is found in /Library/Application Support/Dialog/Dialog.png. Leave this as default if you use Baseline to deploy a custom SwiftDialog icon. Set to false if you do not want Baseline to ever reinstall SwiftDialog. Set to True if you always want to reinstall SwiftDialog after each Baseline. 721 | pfm_name 722 | ReinstallDialog 723 | pfm_title 724 | Reinstall SwiftDialog 725 | pfm_type 726 | boolean 727 | 728 | 729 | pfm_default 730 | 731 | pfm_description 732 | Tell Installomator to use it's built in SwiftDialog integration for the List View. 733 | pfm_name 734 | InstallomatorSwiftDialogIntegration 735 | pfm_title 736 | Use Installomator SwiftDialog Integration 737 | pfm_type 738 | boolean 739 | 740 | 741 | pfm_default 742 | 743 | pfm_description 744 | By default, Baseline uses "NOTIFY=silent" and "BLOCKING_PROCESS_ACTION=kill" Installomator options. Setting this to True means Baseline will not use those options. 745 | pfm_name 746 | IgnoreDefaultInstallomatorOptions 747 | pfm_title 748 | Ignore Default Installomator Options 749 | pfm_type 750 | boolean 751 | 752 | 753 | pfm_default 754 | 755 | pfm_description 756 | This setting enables SwiftDialog "Button 1" (the OK button) on the Baseline List View. This allows users to dismiss the list view and continue working while Baseline runs. By default this button is disabled. 757 | pfm_name 758 | Button1Enabled 759 | pfm_title 760 | Button 1 Enabled 761 | pfm_type 762 | boolean 763 | 764 | 765 | pfm_app_min 766 | 2.2 767 | pfm_default 768 | 769 | pfm_description 770 | If set to true, when a Script is processing a jamf policy (/usr/local/bin/jamf), Baseline will watch the Jamf log to provide more verbose output on that line item. Similar to the Installomator integration, text and a progress circle will be used for all jamf items. 771 | pfm_name 772 | JamfVerbose 773 | pfm_title 774 | Verbose Jamf Items 775 | pfm_type 776 | boolean 777 | 778 | 779 | pfm_app_min 780 | 2.3 781 | pfm_default 782 | 783 | pfm_description 784 | If set to true, Baseline will close the list view window before running FinalScripts. Use this option if you want to utilize FinalScripts to have the user complete actions prior to the Baseline completion dialog. 785 | pfm_name 786 | CloseListBeforeFinalScripts 787 | pfm_title 788 | Close List View Before Final Scripts 789 | pfm_type 790 | boolean 791 | 792 | 793 | pfm_description 794 | SwiftDialog options for the primary Baseline progress list window. 795 | pfm_name 796 | DialogListOptions 797 | pfm_title 798 | Dialog List Window Options 799 | pfm_type 800 | string 801 | 802 | 803 | pfm_description 804 | SwiftDialog options for the Success Dialog window. 805 | pfm_name 806 | DialogSuccessOptions 807 | pfm_title 808 | Dialog Success Window Options 809 | pfm_type 810 | string 811 | 812 | 813 | pfm_description 814 | SwiftDialog options for the Failure Dialog window. 815 | pfm_name 816 | DialogFailureOptions 817 | pfm_title 818 | Dialog Failure Window Options 819 | pfm_type 820 | string 821 | 822 | 823 | pfm_description 824 | Define a file or folder path. When Baseline starts, if this file or folder exists, Baseline will exit quietly without processing any items. Use this option if your management tool may re-send scripts when not desired. 825 | pfm_name 826 | ExitCondition 827 | pfm_title 828 | Exit Condition 829 | pfm_type 830 | string 831 | 832 | 833 | pfm_description 834 | Define a file path. If this file exists at any point while Baseline is running, Baseline will exit and will not process additional items. 835 | pfm_name 836 | BailOutFile 837 | pfm_title 838 | Bail Out File 839 | pfm_type 840 | string 841 | 842 | 843 | pfm_targets 844 | 845 | system 846 | 847 | pfm_title 848 | Baseline by Second Son Consulting 849 | pfm_unique 850 | 851 | pfm_version 852 | 5 853 | 854 | 855 | -------------------------------------------------------------------------------- /Baseline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | set -x 3 | #dryRun=true 4 | 5 | # Written by Trevor Sysock of Second Son Consulting 6 | # @BigMacAdmin on the MacAdmins Slack 7 | # trevor@secondsonconsulting.com 8 | 9 | scriptVersion="2.3" 10 | 11 | # MIT License 12 | # 13 | # Copyright (c) 2024 Second Son Consulting 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a copy 16 | # of this software and associated documentation files (the "Software"), to deal 17 | # in the Software without restriction, including without limitation the rights 18 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | # copies of the Software, and to permit persons to whom the Software is 20 | # furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included in all 23 | # copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | # SOFTWARE. 32 | # 33 | 34 | ######################################################################################################## 35 | ######################################################################################################## 36 | ## 37 | ## DEFINE INITIAL VARIABLES 38 | ## 39 | ######################################################################################################## 40 | ######################################################################################################## 41 | 42 | if [ "${1}" = '--version' ]; then 43 | echo "$(basename $0) by Second Son Consulting - v. $scriptVersion" 44 | exit 0 45 | fi 46 | 47 | ################################# 48 | # Declare file/folder paths # 49 | ################################# 50 | #Baseline files/folders 51 | BaselineConfig="/Library/Managed Preferences/com.secondsonconsulting.baseline.plist" 52 | BaselineDir="/usr/local/Baseline" 53 | BaselineTempDir="$(mktemp -d /var/tmp/baselineTempDir.XXXXXXX)" 54 | customConfigPlist="$BaselineDir/BaselineConfig.plist" 55 | logFile="/var/log/Baseline.log" 56 | reportFile="/var/log/Baseline-Report.txt" 57 | BaselinePath="$BaselineDir/Baseline.sh" 58 | BaselineScripts="$BaselineDir/Scripts" 59 | BaselinePackages="$BaselineDir/Packages" 60 | BaselineIcons="$BaselineDir/Icons" 61 | BaselineLaunchDaemon="/Library/LaunchDaemons/com.secondsonconsulting.baseline.plist" 62 | BaselineTempIconsDir=$(mktemp -d "${BaselineTempDir}/baselineTmpIcons.XXXX") 63 | ScriptOutputLog="/var/log/Baseline-ScriptsOutput.log" 64 | 65 | #Binaries 66 | pBuddy="/usr/libexec/PlistBuddy" 67 | dialogPath="/usr/local/bin/dialog" 68 | dialogAppPath="/Library/Application Support/Dialog/Dialog.app" 69 | installomatorPath="/usr/local/Installomator/Installomator.sh" 70 | 71 | #Other stuff 72 | dialogCommandFile=$(mktemp "${BaselineTempDir}/baselineDialog.XXXXXX") 73 | dialogJsonFile=$(mktemp "${BaselineTempDir}/baselineJson.XXXX") 74 | expectedDialogTeamID="PWA5E9TQ59" 75 | defaultWaitForTimeout=600 76 | 77 | # Path to the Jamf log file 78 | jamfLogFile="/private/var/log/jamf.log" 79 | 80 | chmod -R 655 "${BaselineTempDir}" 81 | 82 | ######################################################################################################## 83 | ######################################################################################################## 84 | ## 85 | ## DEFINE FUNCTIONS 86 | ## 87 | ######################################################################################################## 88 | ######################################################################################################## 89 | 90 | ################################# 91 | # Logging and Housekeeping # 92 | ################################# 93 | 94 | function check_root(){ 95 | 96 | # check we are running as root 97 | if [[ $(id -u) -ne 0 ]]; then 98 | echo "ERROR: This script must be run as root **EXITING**" 99 | # Delete Baseline Temp Dir 100 | rm_if_exists "${BaselineTempDir}" 101 | exit 1 102 | fi 103 | } 104 | 105 | function make_directory(){ 106 | if [ ! -d "${1}" ]; then 107 | debug_message "Folder does not exist. Making it: ${1}" 108 | mkdir -p "${1}" 109 | fi 110 | } 111 | 112 | #Used only for debugging. Gives feedback into standard out if verboseMode=1, also to $logFile if you set it 113 | function debug_message(){ 114 | if [ "$verboseMode" = 1 ]; then 115 | /bin/echo "DEBUG: $*" 116 | fi 117 | } 118 | 119 | #Publish a message to the log (and also to the debug channel) 120 | function log_message(){ 121 | echo "$(date): $*" | tee >( cat >> "$logFile" ) 122 | debug_message "$*" 123 | } 124 | 125 | function rotate_logs(){ 126 | touch "$logFile" 127 | chmod 655 "$logFile" 128 | if [ "$(wc -l < "$logFile" | xargs)" -ge 30000 ]; then 129 | log_message "Rotating Logs" 130 | mv "$logFile" "$logFile".old 131 | touch "$logFile" 132 | log_message "Logfile Rotated" 133 | fi 134 | } 135 | 136 | #Report messages go to our report, but also pass through log_message (and thus, also to debug_message) 137 | function report_message(){ 138 | /bin/echo "$@" >> "$reportFile" 139 | log_message "$@" 140 | } 141 | 142 | # Initiate logging 143 | function initiate_logging(){ 144 | if ! touch "$logFile" ; then 145 | debug_message "ERROR: Logging fail. Cannot create log file" 146 | # Delete Baseline Temp Dir 147 | rm_if_exists "${BaselineTempDir}" 148 | exit 1 149 | else 150 | log_message "Baseline.sh initiated" 151 | fi 152 | } 153 | 154 | #Only delete something if the variable has a value! 155 | function rm_if_exists(){ 156 | if [ -n "${1}" ] && [ -e "${1}" ];then 157 | /bin/rm -rf "${1}" 158 | fi 159 | } 160 | 161 | function initiate_report(){ 162 | if ! touch "$reportFile" ; then 163 | debug_message "ERROR: Reporting fail. Cannot create report file" 164 | # Delete Baseline Temp Dir 165 | rm_if_exists "${BaselineTempDir}" 166 | exit 1 167 | else 168 | rm_if_exists "$reportFile" 169 | report_message "Report created: $(date)" 170 | fi 171 | } 172 | 173 | #Define our script exit process. Usage: cleanup_and_exit 'exitcode' 'exit message' 174 | function cleanup_and_exit(){ 175 | # Check if we are going to restart 176 | check_restart_option 177 | 178 | # Check if we are leaving the Baseline working directory or deleting 179 | cleanupAfterUse=$($pBuddy -c "Print :CleanupAfterUse" "$BaselineConfig" 2> /dev/null) 180 | 181 | if [[ $cleanupAfterUse == "false" ]]; then 182 | cleanupBaselineDirectory="false" 183 | else 184 | cleanupBaselineDirectory="true" 185 | fi 186 | 187 | # Log message 188 | report_message "$2" 189 | report_message "Baseline exited with error code: $1" 190 | 191 | # Delete the Baseline LaunchDaemon 192 | # Doing this in a loop because I've seen edge cases where it failed unexpectedly and it is high impact. 193 | while [ -e "$BaselineLaunchDaemon" ]; do 194 | rm_if_exists "$BaselineLaunchDaemon" 195 | sleep 1 196 | done 197 | 198 | kill "$caffeinatepid" 199 | dialog_command "quit:" 200 | rm_if_exists "${BaselineTempDir}" 201 | if [ "$dryRun" != true ] && [ "$cleanupBaselineDirectory" = "true" ] ; then 202 | rm_if_exists "$BaselineDir" 203 | fi 204 | # Delete Baseline Temp Dir 205 | rm_if_exists "${BaselineTempDir}" 206 | exit "$1" 207 | } 208 | 209 | # This function doesn't always shut down, but I'm leaving the name in place for now at least. 210 | # Usage: cleanup_and_exit 'exitcode' 'exit message' 211 | function cleanup_and_restart(){ 212 | # Check if we are going to restart 213 | check_restart_option 214 | 215 | # Check if we are leaving the Baseline working directory or deleting 216 | cleanupAfterUse=$($pBuddy -c "Print :CleanupAfterUse" "$BaselineConfig" 2> /dev/null ) 217 | 218 | if [ "$cleanupAfterUse" = "false" ]; then 219 | cleanupBaselineDirectory="false" 220 | else 221 | cleanupBaselineDirectory="true" 222 | fi 223 | 224 | # Log message 225 | report_message "$2" 226 | 227 | # Delete the LaunchDaemon. Saw an edge case where it didn't delete once, so I made it a while loop. 228 | while [ -e "$BaselineLaunchDaemon" ]; do 229 | rm_if_exists "$BaselineLaunchDaemon" 230 | sleep 1 231 | done 232 | # Kill our caffeinate command 233 | kill "$caffeinatepid" 234 | # Close dialog window 235 | dialog_command "quit:" 236 | 237 | 238 | # Check if we are deleting the Baseline working directory and do it 239 | if [ $cleanupBaselineDirectory = "true" ]; then 240 | rm_if_exists "$BaselineDir" 241 | fi 242 | 243 | # Determine exit configuration 244 | # If ForceRestart is set to false, and dry run is off 245 | if $forceRestart && ! $dryRun ; then 246 | report_message "Force Restart is configured. Restarting" 247 | # Delete Baseline Temp Dir 248 | rm_if_exists "${BaselineTempDir}" 249 | log_message "Forcing restart" 250 | shutdown -r now 251 | # If Force Log Out is set to true, and dry run is off 252 | elif $forceLogOut && ! $dryRun; then 253 | report_message "Force Log Out is set to true." 254 | osascript -e "tell application \"/System/Library/CoreServices/loginwindow.app\" to «event aevtrlgo»" 255 | # Delete Baseline Temp Dir 256 | rm_if_exists "${BaselineTempDir}" 257 | exit "$1" 258 | elif ! $forceLogOut && ! $forceRestart && ! $dryRun; then 259 | report_message "Force Log Out and Force Restart are false. Exiting with no action." 260 | # Delete Baseline Temp Dir 261 | rm_if_exists "${BaselineTempDir}" 262 | exit "$1" 263 | # If the script is in DryRun mode 264 | elif $dryRun; then 265 | report_message "Dry Run Enabled, no exit action taken." 266 | report_message "ForceRestart is set to: $forceRestart" 267 | report_message "ForceLogOut is set to: $forceLogOut" 268 | # Delete Baseline Temp Dir 269 | rm_if_exists "${BaselineTempDir}" 270 | exit "$1" 271 | fi 272 | 273 | # Shutting down 274 | log_message "Unknown ExitAction determined. Falling back on default to ForceRestart" 275 | # Delete Baseline Temp Dir 276 | rm_if_exists "${BaselineTempDir}" 277 | shutdown -r now 278 | } 279 | 280 | function no_sleeping(){ 281 | 282 | /usr/bin/caffeinate -d -i -m -u & 283 | caffeinatepid=$! 284 | 285 | } 286 | 287 | # execute a dialog command 288 | function dialog_command(){ 289 | /bin/echo "$@" >> $dialogCommandFile 290 | sleep .1 291 | } 292 | 293 | #This function is modified from the awesome one given to us via Adam Codega. Thanks Adam! 294 | #https://github.com/acodega/dialog-scripts/blob/main/dialogCheckFunction.sh 295 | 296 | function install_dialog(){ 297 | 298 | # Check for Dialog and install if not found. We'll try 10 times before exiting the script with a fail. 299 | dialogInstallAttempts=0 300 | while [ ! -e "$dialogAppPath" ] && [ "$dialogInstallAttempts" -lt 10 ]; do 301 | # If SwiftDialog.pkg exists in the Packages folder check the TeamID is valid, and install it 302 | localDialogTeamID=$(/usr/sbin/spctl -a -vv -t install "$BaselinePackages/SwiftDialog.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()') 303 | if [ "$expectedDialogTeamID" = "$localDialogTeamID" ]; then 304 | /usr/sbin/installer -pkg "$BaselinePackages/SwiftDialog.pkg" -target / > /dev/null 2>&1 305 | # If Installomator is already here use that 306 | elif [ -e "$installomatorPath" ]; then 307 | "$installomatorPath" swiftdialog INSTALL=force NOTIFY=silent BLOCKING_PROCESS_ACTION=ignore > /dev/null 2>&1 308 | dialogInstallAttempts=$((dialogInstallAttempts+1)) 309 | else 310 | # Get the URL of the latest PKG From the Dialog GitHub repo 311 | # Expected Team ID of the downloaded PKG 312 | dialogURL=$(curl -L --silent --fail "https://api.github.com/repos/swiftDialog/swiftDialog/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }") 313 | log_message "Dialog not found. Installing." 314 | # Create temporary working directory 315 | workDirectory=$( /usr/bin/basename "$0" ) 316 | tempDirectory=$( /usr/bin/mktemp -d "/private/tmp/$workDirectory.XXXXXX" ) 317 | # Download the installer package 318 | /usr/bin/curl --location --silent "$dialogURL" -o "$tempDirectory/SwiftDialog.pkg" 319 | # Verify the download 320 | teamID=$(/usr/sbin/spctl -a -vv -t install "$tempDirectory/SwiftDialog.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()') 321 | # Install the package if Team ID validates 322 | if [ "$expectedDialogTeamID" = "$teamID" ]; then 323 | /usr/sbin/installer -pkg "$tempDirectory/SwiftDialog.pkg" -target / > /dev/null 2>&1 324 | fi 325 | #If Dialog wasn't installed, wait 5 seconds and increase the attempt count 326 | if [ ! -e "$dialogAppPath" ]; then 327 | log_message "Dialog installation failed." 328 | sleep 5 329 | dialogInstallAttempts=$((dialogInstallAttempts+1)) 330 | fi 331 | # Remove the temporary working directory when done 332 | rm_if_exists "$tempDirectory" 333 | fi 334 | done 335 | } 336 | 337 | function install_installomator(){ 338 | 339 | # Check for Installomator and install if not found. We'll try 10 times before exiting the script with a fail. 340 | installomatorInstallAttempts=0 341 | while [ ! -e "$installomatorPath" ] && [ "$installomatorInstallAttempts" -lt 10 ]; do 342 | # Check if there is a local Installomator.pkg, and if so run it. 343 | if [ -e "${BaselinePackages}/Installomator.pkg" ]; then 344 | /usr/sbin/installer -pkg "${BaselinePackages}/Installomator.pkg" -target / > /dev/null 2>&1 345 | else 346 | # Get the URL of the latest PKG From the Installomator GitHub repo 347 | # Expected Team ID of the downloaded PKG 348 | installomatorURL=$(curl --silent -L --fail "https://api.github.com/repos/Installomator/Installomator/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }") 349 | expectedTeamID="JME5BW3F3R" 350 | log_message "Installomator not found. Installing." 351 | # Create temporary working directory 352 | workDirectory=$( /usr/bin/basename "$0" ) 353 | tempDirectory=$( /usr/bin/mktemp -d "/private/tmp/$workDirectory.XXXXXX" ) 354 | # Download the installer package 355 | /usr/bin/curl --location --silent "$installomatorURL" -o "$tempDirectory/Installomator.pkg" 356 | # Verify the download 357 | teamID=$(/usr/sbin/spctl -a -vv -t install "$tempDirectory/Installomator.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()') 358 | # Install the package if Team ID validates 359 | if [ "$expectedTeamID" = "$teamID" ]; then 360 | /usr/sbin/installer -pkg "$tempDirectory/Installomator.pkg" -target / > /dev/null 2>&1 361 | installomatorInstallExitCode=$? 362 | fi 363 | if [ ! -e "$installomatorPath" ]; then 364 | log_message "Installomator installation failed." 365 | sleep 5 366 | installomatorInstallAttempts=$((installomatorInstallAttempts+1)) 367 | fi 368 | # Remove the temporary working directory when done 369 | rm_if_exists "$tempDirectory" 370 | fi 371 | done 372 | } 373 | 374 | #Checks if a user is logged in yet, and if not it waits and loops until we can confirm there is a real user 375 | function wait_for_user(){ 376 | #Set our test to false 377 | verifiedUser="false" 378 | 379 | #Loop until user is found 380 | while [ "$verifiedUser" = "false" ]; do 381 | #Get currently logged in user 382 | currentUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' ) 383 | #Verify the current user is not root, loginwindow, or _mbsetupuser 384 | if [ "$currentUser" = "root" ] \ 385 | || [ "$currentUser" = "loginwindow" ] \ 386 | || [ "$currentUser" = "_mbsetupuser" ] \ 387 | || [ -z "$currentUser" ] 388 | then 389 | #If we aren't verified yet, wait 1 second and try again 390 | sleep 1 391 | else 392 | #Logged in user found, but continue the loop until Dock and Finder processes are running 393 | if pgrep -q "dock" && pgrep -q "Finder"; then 394 | uid=$(id -u "$currentUser") 395 | log_message "Verified User is logged in: $currentUser UID: $uid" 396 | verifiedUser="true" 397 | fi 398 | fi 399 | debug_message "Disabling verbose output to prevent logspam while waiting for user at timestamp: $(date +%s)" 400 | set +x 401 | done 402 | debug_message "Re-enabling verbose output after finding user at timestamp: $(date +%s)" 403 | 404 | } 405 | 406 | #Check for custom config. We prioritize this even over a mobileconfig file. 407 | function check_for_custom_plist(){ 408 | if [ -e $customConfigPlist ] && ! $configFromArgument; then 409 | BaselineConfig="$customConfigPlist" 410 | fi 411 | } 412 | 413 | #Verify configuration file 414 | function verify_configuration_file(){ 415 | #We need to make sure our configuration file is in place. By the time the user logs in, this should have happened. 416 | debug_message "Verifying configuration file. Failure here probably means an MDM profile hasn't been properly scoped, or there's a problem with the MDM delivering the profile." 417 | 418 | #Set timeout variables 419 | configFileTimeout=600 420 | configFileWaiting=0 421 | 422 | # Look for a custom plist 423 | check_for_custom_plist 424 | 425 | # While the plist or configuration file doesn't exist, wait and timeout at 10 minutes if never found. 426 | while [ ! -e $BaselineConfig ]; do 427 | check_for_custom_plist 428 | #wait 2 seconds 429 | sleep 2 430 | debug_message "Configuration file not found" 431 | configFileWaiting=$((configFileWaiting+2)) 432 | if [ $configFileWaiting -gt $configFileTimeout ]; then 433 | cleanup_and_exit 1 "ERROR: Configuration file not found within $configFileTimeout seconds. Exiting." 434 | fi 435 | done 436 | debug_message "Configuration file found successfully: $BaselineConfig " 437 | 438 | # If we're working off an MDM configuration profile, copy it to our temp location and go off the copy. 439 | # Have seen edge cases where an MDM removes or re-applies profiles, this will prevent that from causing issues. 440 | if [[ "$BaselineConfig" == "/Library/Managed Preferences/com.secondsonconsulting.baseline.plist" ]]; then 441 | cp "$BaselineConfig" "$BaselineTempDir/BaselineConfig.plist" 442 | BaselineConfig="$BaselineTempDir/BaselineConfig.plist" 443 | fi 444 | } 445 | 446 | function build_installomator_array(){ 447 | #Set an index internal to this function 448 | index=0 449 | #Loop through and test if there is a value in the slot of this index for the given array 450 | #If this command fails it means we've reached the end of the array in the config file and we exit our loop 451 | 452 | while $pBuddy -c "Print :Installomator:${index}" "$BaselineConfig" > /dev/null 2>&1; do 453 | #Get the Display Name of the current item 454 | currentDisplayName=$($pBuddy -c "Print :Installomator:${index}:DisplayName" "$BaselineConfig") 455 | dialogList+="$currentDisplayName" 456 | #Done looping. Increase our array value and loop again. 457 | index=$((index+1)) 458 | done 459 | } 460 | 461 | function process_installomator_labels(){ 462 | #Set an index internal to this function 463 | currentIndex=0 464 | #Loop through and test if there is a value in the slot of this index for the given array 465 | #If this command fails it means we've reached the end of the array in the config file (or there are none) and we exit our loop 466 | while $pBuddy -c "Print :Installomator:${currentIndex}" "$BaselineConfig" > /dev/null 2>&1; do 467 | check_for_bail_out 468 | if [ ! -e "$installomatorPath" ]; then 469 | cleanup_and_exit 1 "ERROR: Installomator failed to install after numerous attempts. Exiting." 470 | fi 471 | #Set the current label name 472 | currentLabel=$($pBuddy -c "Print :Installomator:${currentIndex}:Label" "$BaselineConfig") 473 | #Check if there are Options defined, and set the variable accordingly 474 | if $pBuddy -c "Print :Installomator:${currentIndex}:Arguments" "$BaselineConfig" > /dev/null 2>&1; then 475 | #This label has options defined 476 | currentArguments=$($pBuddy -c "Print :Installomator:${currentIndex}:Arguments" "$BaselineConfig") 477 | else 478 | #This label does not have options defined 479 | currentArguments="" 480 | fi 481 | #Now we have to do a trick in case there are multiple arguments, some of which are quoted together 482 | #Consider: /path/to/script.sh --font "Times New Roman" 483 | #Used the eval trick outlined here: https://superuser.com/questions/1066455/how-to-split-a-string-with-quotes-like-command-arguments-in-bash 484 | currentArgumentArray=() 485 | if [ -n "$currentArguments" ]; then 486 | eval 'for argument in '$currentArguments'; do currentArgumentArray+=$argument; done' 487 | fi 488 | #Get the display name of the label we're installing. We need this to update the dialog list 489 | currentDisplayName=$($pBuddy -c "Print :Installomator:${currentIndex}:DisplayName" "$BaselineConfig") 490 | 491 | # Configure Installomator SwiftDialog Integration 492 | useInstallomatorSwiftDialogIntegration=$($pBuddy -c "Print :InstallomatorSwiftDialogIntegration" "$BaselineConfig" 2> /dev/null) 493 | 494 | # If we're using the integrated SwiftDialog, then 495 | if [[ $useInstallomatorSwiftDialogIntegration == "true" ]]; then 496 | currentArgumentArray+="DIALOG_CMD_FILE=\"$dialogCommandFile\"" 497 | currentArgumentArray+=DIALOG_LIST_ITEM_NAME=\"$currentDisplayName\" 498 | else 499 | #Update the dialog window so that this item shows as "pending" 500 | dialog_command "listitem: title: $currentDisplayName, status: wait" 501 | fi 502 | 503 | set_progressbar_text "$currentDisplayName" 504 | #Call installomator with our desired options. Default options first, so that they can be overriden by "currentArguments" 505 | $installomatorPath $currentLabel ${defaultInstallomatorOptions[@]} ${currentArgumentArray[@]} > /dev/null 2>&1 506 | installomatorExitCode=$? 507 | if [ $installomatorExitCode != 0 ]; then 508 | report_message "Failed Item - Installomator: $currentLabel - Exit Code: $installomatorExitCode" 509 | failList+=("$currentDisplayName") 510 | # If we're NOT using the integrated SwiftDialog, then 511 | if [[ $useInstallomatorSwiftDialogIntegration != "true" ]]; then 512 | dialog_command "listitem: title: $currentDisplayName, status: fail" 513 | fi 514 | else 515 | report_message "Successful Item - Installomator: $currentLabel" 516 | successList+=("$currentDisplayName") 517 | if [[ $useInstallomatorSwiftDialogIntegration != "true" ]]; then 518 | dialog_command "listitem: title: $currentDisplayName, status: success" 519 | fi 520 | fi 521 | update_tracker "$currentDisplayName" $installomatorExitCode 522 | currentIndex=$((currentIndex+1)) 523 | # This gets set for use with the BailOut feature 524 | previousDisplayName="$currentDisplayName" 525 | check_for_bail_out 526 | increment_progress_bar 527 | done 528 | } 529 | 530 | # Our main list builder for the Dialog window 531 | function build_dialog_array(){ 532 | ## Usage: Build the dialog array for the given profile configuration key. $1 is the name of the key 533 | ## Example: build_dialog_array Scripts | InitialScripts | Packages | Installomator 534 | 535 | # Set the MDM key to the given argument 536 | configKey="${1}" 537 | 538 | #Set an index internal to this function 539 | index=0 540 | #Loop through and test if there is a value in the slot of this index for the given array 541 | #If this command fails it means we've reached the end of the array in the config file and we exit our loop 542 | 543 | while $pBuddy -c "Print :$configKey:${index}" "$BaselineConfig" > /dev/null 2>&1; do 544 | #Get the Display Name of the current item 545 | currentDisplayName=$($pBuddy -c "Print :$configKey:${index}:DisplayName" "$BaselineConfig") 546 | dialogList+="$currentDisplayName" 547 | 548 | #Get the icon path if populated in the configuration profile 549 | if $pBuddy -c "Print :$configKey:${index}:Icon" "$BaselineConfig" > /dev/null 2>&1; then 550 | currentIconPath=$($pBuddy -c "Print :$configKey:${index}:Icon" "$BaselineConfig") 551 | # Check if Icon is remotely hosted URL 552 | if [[ ${currentIconPath:0:4} == "http" ]]; then 553 | log_message "Icon set to URL: $currentIconPath" 554 | # Check if Icon is an SF Symbol 555 | elif [[ ${currentIconPath:0:3} == 'SF=' ]]; then 556 | log_message "Icon set to SF Symbol: $currentIconPath" 557 | #Check of the given icon path exists on disk 558 | elif [ -e "$currentIconPath" ]; then 559 | log_message "Icon found: $currentIconPath" 560 | elif [ -e "$BaselineTempIconsDir/$currentIconPath" ]; then 561 | log_message "Icon found: $currentIconPath" 562 | currentIconPath="$BaselineTempIconsDir/$currentIconPath" 563 | chmod 655 "${currentIconPath}" 564 | else 565 | #If we can't find the local file, report and leave blank 566 | log_message "ERROR: Icon key cannot be located: $currentIconPath" 567 | currentIconPath="" 568 | fi 569 | else 570 | #If no icon key is set, ensure it's blank 571 | currentIconPath="" 572 | fi 573 | 574 | #Get the desired subtitle if populated in the configuration profile 575 | if $pBuddy -c "Print :$configKey:${index}:Subtitle" "$BaselineConfig" > /dev/null 2>&1; then 576 | currentSubtitle=$($pBuddy -c "Print :$configKey:${index}:Subtitle" "$BaselineConfig") 577 | else 578 | #If no icon key is set, ensure it's blank 579 | currentSubtitle="" 580 | fi 581 | 582 | #Generate JSON entry for item 583 | #NOTE: We will strip out the final comma later to ensure a valid JSON 584 | dialogListJson+="{\"title\" : \"$currentDisplayName\",\"subtitle\" : \"$currentSubtitle\", \"icon\" : \"$currentIconPath\", \"status\" : \"\"}," 585 | 586 | #Done looping. Increase our array value and loop again. 587 | index=$((index+1)) 588 | progressBarTotal=$((progressBarTotal+1)) 589 | done 590 | } 591 | 592 | function process_scripts(){ 593 | # Usage: process_scripts ProfileKey 594 | # Actual use: process_scripts [ InitialScripts | Scripts ] 595 | #Set an index internal to this function 596 | currentIndex=0 597 | #Loop through and test if there is a value in the slot of this index for the given array 598 | #If this command fails it means we've reached the end of the array in the config file (or there are none) and we exit our loop 599 | while $pBuddy -c "Print :${1}:${currentIndex}" "$BaselineConfig" > /dev/null 2>&1; do 600 | check_for_bail_out 601 | #Unset variables for next loop 602 | unset useVerboseJamf 603 | unset jamfVerbosePID 604 | unset expectedMD5 605 | unset actualMD5 606 | unset expectedSHA256 607 | unset actualSHA256 608 | unset currentArguments 609 | unset currentArgumentArray 610 | unset currentScript 611 | unset currentScriptPath 612 | unset currentDisplayName 613 | unset scriptDownloadExitCode 614 | #Get the display name of the label we're installing. We need this to update the dialog list 615 | currentDisplayName=$($pBuddy -c "Print :${1}:${currentIndex}:DisplayName" "$BaselineConfig") 616 | #Set the current script name 617 | currentScriptPath=$($pBuddy -c "Print :${1}:${currentIndex}:ScriptPath" "$BaselineConfig") 618 | #Set where we are running in the user context or root 619 | asUser=$($pBuddy -c "Print :${1}:${currentIndex}:AsUser" "$BaselineConfig" 2> /dev/null) 620 | #Check if the defined script is a remote path 621 | if [[ ${currentScriptPath:0:4} == "http" ]]; then 622 | #Set variable to the base file name to be downloaded 623 | currentScript="$BaselineScripts/"$(basename "$currentScriptPath") 624 | #Download the remote script, and put it in the Baseline Scripts directory 625 | curl -s --fail-with-body "${currentScriptPath}" -o "$currentScript" 626 | #Capture the exit code of our curl command 627 | scriptDownloadExitCode=$? 628 | #Check if curl exited cleanly 629 | if [ "$scriptDownloadExitCode" != 0 ];then 630 | #Report a failed download 631 | report_message "Failed Item - Script download error: $currentScriptPath" 632 | #Rm the output of our curl command. This will result in it being processed as a failure 633 | rm_if_exists "$currentScript" 634 | else 635 | log_message "Script downloaded successfully: $currentScriptPath" 636 | #Make our downloaded script executable 637 | chmod +x "$currentScript" 638 | fi 639 | #Check if the given script exists on disk 640 | elif [ -e "$currentScriptPath" ]; then 641 | # The path to the script is a local file path which exists 642 | currentScript="$currentScriptPath" 643 | elif [ -e "$BaselineScripts/$currentScriptPath" ]; then 644 | currentScript="$BaselineScripts/$currentScriptPath" 645 | fi 646 | #If the currentScript variable still isn't set to an existing file we need to bail.. 647 | if [ ! -e "$currentScript" ]; then 648 | report_message "Failed Item - Script does not exist: $currentScript" 649 | # Iterate the index up one 650 | currentIndex=$((currentIndex+1)) 651 | increment_progress_bar 652 | # Report the fail 653 | dialog_command "listitem: title: $currentDisplayName, status: fail" 654 | failList+=("$currentDisplayName") 655 | update_tracker $currentDisplayName 99 656 | # Bail this pass through the while loop and continue processing next item 657 | continue 658 | fi 659 | #Check if this is a Jamf binary call and if we're using verbose jamf output 660 | if [[ ${currentScriptPath} == "/usr/local/bin/jamf" ]] && $showVerboseJamf ; then 661 | jamf_verbose_dialog "$currentDisplayName" & jamfVerbosePID=$! 662 | fi 663 | ##Check for SHA256 validation 664 | if $pBuddy -c "Print :${1}:${currentIndex}:SHA256" "$BaselineConfig" > /dev/null 2>&1; then 665 | #This script has SHA256 validation provided 666 | #Read the expected SHA256 value from the profile 667 | expectedSHA256=$($pBuddy -c "Print :${1}:${currentIndex}:SHA256" "$BaselineConfig") 668 | #Calculate the actual SHA256 of the script 669 | actualSHA256=$(shasum -a 256 "$currentScript" | awk '{ print $1 }') 670 | #Evaluate whether the expected and actual SHA256 do not match 671 | if [ "$actualSHA256" != "$expectedSHA256" ]; then 672 | report_message "Failed Item - Script SHA256 error: $currentScriptPath - Expected $expectedSHA256 - Actual $actualSHA256" 673 | # Iterate the index up one 674 | currentIndex=$((currentIndex+1)) 675 | # Only increment the progress bar if we're processing Scripts, not InitialScripts since users won't see those 676 | if [ "$1" = "Scripts" ]; then 677 | increment_progress_bar 678 | fi 679 | # Report the fail 680 | dialog_command "listitem: title: $currentDisplayName, status: fail" 681 | failList+=("$currentDisplayName") 682 | # Bail this pass through the while loop and continue processing next item 683 | continue 684 | fi 685 | fi 686 | ##Check for MD5 validation 687 | if $pBuddy -c "Print :${1}:${currentIndex}:MD5" "$BaselineConfig" > /dev/null 2>&1; then 688 | #This script has MD5 validation provided 689 | #Read the expected MD5 value from the profile 690 | expectedMD5=$($pBuddy -c "Print :${1}:${currentIndex}:MD5" "$BaselineConfig") 691 | #Calculate the actual MD5 of the script 692 | actualMD5=$(md5 -q "$currentScript") 693 | #Evaluate whether the expected and actual MD5 do not match 694 | if [ "$actualMD5" != "$expectedMD5" ]; then 695 | report_message "Failed Item - Script MD5 error: $currentScriptPath - Expected $expectedMD5 - Actual $actualMD5" 696 | # Iterate the index up one 697 | currentIndex=$((currentIndex+1)) 698 | # Only increment the progress bar if we're processing Scripts, not InitialScripts since users won't see those 699 | if [ "$1" = "Scripts" ]; then 700 | increment_progress_bar 701 | fi 702 | # Report the fail 703 | dialog_command "listitem: title: $currentDisplayName, status: fail" 704 | failList+=("$currentDisplayName") 705 | # Bail this pass through the while loop and continue processing next item 706 | continue 707 | fi 708 | fi 709 | #Check if there are Arguments defined, and set the variable accordingly 710 | if $pBuddy -c "Print :${1}:${currentIndex}:Arguments" "$BaselineConfig" > /dev/null 2>&1; then 711 | #This script has arguments defined 712 | currentArguments=$($pBuddy -c "Print :${1}:${currentIndex}:Arguments" "$BaselineConfig") 713 | else 714 | #This script does not have arguments defined 715 | currentArguments="" 716 | fi 717 | #Now we have to do a trick in case there are multiple arguments, some of which are quoted together 718 | #Consider: /path/to/script.sh --font "Times New Roman" 719 | #Used the eval trick outlined here: https://superuser.com/questions/1066455/how-to-split-a-string-with-quotes-like-command-arguments-in-bash 720 | currentArgumentArray=() 721 | if [ -n "$currentArguments" ]; then 722 | eval 'for argument in '$currentArguments'; do currentArgumentArray+=$argument; done' 723 | fi 724 | 725 | #Update the dialog window so that this item shows as "pending" 726 | dialog_command "listitem: title: $currentDisplayName, status: wait" 727 | 728 | #Only set the progress label if we're processing Scripts, not InitialScripts since users won't see those 729 | if [ "$1" = "Scripts" ]; then 730 | set_progressbar_text "$currentDisplayName" 731 | fi 732 | 733 | #Call our script with our desired options. Default options first, so that they can be overriden by "currentArguments" 734 | if [[ $asUser == "true" ]]; then 735 | if [[ -z "$currentUser" ]]; then 736 | # If we don't have a user, we don't want to run this script 737 | log_message "Script set to run asUser but no user logged in: $currentScript" 738 | /usr/bin/false 739 | else 740 | log_message "Running Script: $currentScript as $currentUser with arguments: ${currentArgumentArray[@]}" 741 | /bin/launchctl asuser "$currentUserUID" sudo -u "$currentUser" "$currentScript" ${currentArgumentArray[@]} >> "$ScriptOutputLog" 2>&1 742 | fi 743 | else 744 | log_message "Running Script: $currentScript as root with arguments: ${currentArgumentArray[@]}" 745 | "$currentScript" ${currentArgumentArray[@]} >> "$ScriptOutputLog" 2>&1 746 | fi 747 | scriptExitCode=$? 748 | if [ $scriptExitCode != 0 ]; then 749 | report_message "Failed Item - Script runtime error: $currentScript - Exit Code: $scriptExitCode" 750 | dialog_command "listitem: title: $currentDisplayName, status: fail" 751 | failList+=("$currentDisplayName") 752 | else 753 | report_message "Successful Item - Script: $currentScript" 754 | dialog_command "listitem: title: $currentDisplayName, status: success" 755 | successList+=("$currentDisplayName") 756 | fi 757 | update_tracker $currentDisplayName $scriptExitCode 758 | 759 | #Iterate index for next loop 760 | currentIndex=$((currentIndex+1)) 761 | 762 | # This gets set for use with the BailOut feature 763 | previousDisplayName="$currentDisplayName" 764 | 765 | #Stuff in this section only happens if we're processing Scripts and not InitialScripts 766 | if [ "$1" = "Scripts" ]; then 767 | increment_progress_bar 768 | #If we're using jamf, and jamf verbose is configured 769 | if [[ ${currentScriptPath} == "/usr/local/bin/jamf" ]] && $showVerboseJamf; then 770 | # If the PID is still running, kill it 771 | if ps -x "$jamfVerbosePID" > /dev/null 2>&1; then 772 | kill "$jamfVerbosePID" 773 | fi 774 | # Now clear the jamf verbose status text 775 | dialog_command "listitem: title: ${currentDisplayName}, statustext: " 776 | fi 777 | fi 778 | done 779 | } 780 | 781 | function process_pkgs(){ 782 | #Set an index internal to this function 783 | currentIndex=0 784 | #Loop through and test if there is a value in the slot of this index for the given array 785 | #If this command fails it means we've reached the end of the array in the config file (or there are none) and we exit our loop 786 | while $pBuddy -c "Print :Packages:${currentIndex}" "$BaselineConfig" > /dev/null 2>&1; do 787 | check_for_bail_out 788 | # Unset variables for next loop 789 | unset currentPKG 790 | unset currentPKGPath 791 | unset expectedTeamID 792 | unset expectedMD5 793 | unset actualMD5 794 | unset expectedSHA256 795 | unset actualSHA256 796 | unset actualTeamID 797 | unset currentArguments 798 | unset currentArgumentArray 799 | unset currentDisplayName 800 | unset pkgBasename 801 | unset downloadResult 802 | 803 | #Get the display name of the label we're installing. We need this to update the dialog list 804 | currentDisplayName=$($pBuddy -c "Print :Packages:${currentIndex}:DisplayName" "$BaselineConfig") 805 | #Set the current package path 806 | currentPKGPath=$($pBuddy -c "Print :Packages:${currentIndex}:PackagePath" "$BaselineConfig") 807 | 808 | ##Here is where we begin checking what kind of PKG was defined, and how to process it 809 | ##The end result of this chunk of code, is that we have a valid path to a PKG on the file system 810 | ##Else we bail and continue looping to install the next item 811 | 812 | #Check if the package path is a web URL 813 | if [[ ${currentPKGPath:0:4} == "http" ]]; then 814 | # The path to the PKG appears to be a URL. 815 | #Get the basename of the .pkg we're downloading 816 | pkgBasename=$(basename "$currentPKGPath") 817 | #Set the "currentPKG" variable, this gets used as the download path as well as processed later 818 | currentPKG="$BaselinePackages"/"$pkgBasename" 819 | #Check for conflict. If there's already a PKG in the directory we're downloading to, delete it 820 | rm_if_exists "$currentPKG" 821 | #Perform the download of the remote pkg 822 | curl -LJs "$currentPKGPath" -o "$currentPKG" 823 | #Capture the output of our curl command 824 | downloadResult=$? 825 | #Verify curl exited with 0 826 | if [ "$downloadResult" != 0 ]; then 827 | report_message "Failed Item - Package download error: $currentPKGPath" 828 | # Iterate the index up one 829 | currentIndex=$((currentIndex+1)) 830 | increment_progress_bar 831 | # Report the fail 832 | dialog_command "listitem: title: $currentDisplayName, status: fail" 833 | # Bail this pass through the while loop and continue processing next item 834 | continue 835 | else 836 | debug_message "PKG downloaded successfully: $currentPKGPath" 837 | fi 838 | fi 839 | 840 | # Check if the pkg exists 841 | if [ -e "$currentPKG" ]; then 842 | debug_message "PKG found: $currentPKG" 843 | elif [ -e "$currentPKGPath" ]; then 844 | # The path to the PKG appears to exist on the local file system 845 | currentPKG="$currentPKGPath" 846 | elif [ -e "$BaselinePackages/$currentPKGPath" ]; then 847 | # The path to the PKG appears to exist within Baseline directory 848 | currentPKG="$BaselinePackages/$currentPKGPath" 849 | else 850 | report_message "Failed Item - Package does not exist: $currentPKGPath" 851 | dialog_command "listitem: title: $currentDisplayName, status: fail" 852 | failList+=("$currentDisplayName") 853 | currentIndex=$((currentIndex+1)) 854 | increment_progress_bar 855 | update_tracker $currentDisplayName 99 856 | continue 857 | fi 858 | 859 | ##At this point, the pkg exists on the file system, or we've bailed on this loop. 860 | 861 | #Check if there are Arguments defined, and set the variable accordingly 862 | if $pBuddy -c "Print :Packages:${currentIndex}:Arguments" "$BaselineConfig" > /dev/null 2>&1; then 863 | #This pkg has arguments defined 864 | currentArguments=$($pBuddy -c "Print :Packages:${currentIndex}:Arguments" "$BaselineConfig") 865 | else 866 | #This pkg does not have arguments defined 867 | currentArguments="" 868 | fi 869 | #Now we have to do a trick in case there are multiple arguments, some of which are quoted together 870 | #Consider: /path/to/script.sh --font "Times New Roman" 871 | #Used the eval trick outlined here: https://superuser.com/questions/1066455/how-to-split-a-string-with-quotes-like-command-arguments-in-bash 872 | currentArgumentArray=() 873 | eval 'for argument in '$currentArguments'; do currentArgumentArray+=$argument; done' 874 | 875 | if $pBuddy -c "Print :Packages:${currentIndex}:TeamID" "$BaselineConfig" > /dev/null 2>&1; then 876 | #This pkg has TeamID defined 877 | expectedTeamID=$($pBuddy -c "Print :Packages:${currentIndex}:TeamID" "$BaselineConfig") 878 | else 879 | #This pkg does not have TeamID Validation defined 880 | expectedTeamID="" 881 | fi 882 | if $pBuddy -c "Print :Packages:${currentIndex}:SHA256" "$BaselineConfig" > /dev/null 2>&1; then 883 | #This script has SHA256 defined 884 | expectedSHA256=$($pBuddy -c "Print :Packages:${currentIndex}:SHA256" "$BaselineConfig") 885 | else 886 | #This script does not have SHA256 defined 887 | expectedSHA256="" 888 | fi 889 | if $pBuddy -c "Print :Packages:${currentIndex}:MD5" "$BaselineConfig" > /dev/null 2>&1; then 890 | #This script has MD5 defined 891 | expectedMD5=$($pBuddy -c "Print :Packages:${currentIndex}:MD5" "$BaselineConfig") 892 | else 893 | #This script does not have MD5 defined 894 | expectedMD5="" 895 | fi 896 | #Update the dialog window so that this item shows as "pending" 897 | dialog_command "listitem: title: $currentDisplayName, status: wait" 898 | set_progressbar_text "$currentDisplayName" 899 | 900 | ## Package validation happens here 901 | # Check TeamID, if a value has been provided 902 | if [ -n "$expectedTeamID" ]; then 903 | #Get the TeamID for the current PKG 904 | actualTeamID=$(spctl -a -vv -t install "$currentPKG" 2>&1 | awk -F '(' '/origin=/ {print $2 }' | tr -d ')' ) 905 | # Check if actual does not match expected 906 | if [ "$expectedTeamID" != "$actualTeamID" ]; then 907 | report_message "Failed Item - Package TeamID error: $currentPKG - Expected - $expectedTeamID Actual - $actualTeamID" 908 | dialog_command "listitem: title: $currentDisplayName, status: fail" 909 | failList+=("$currentDisplayName") 910 | # Iterate the index up one 911 | currentIndex=$((currentIndex+1)) 912 | increment_progress_bar 913 | # Report the fail 914 | dialog_command "listitem: title: $currentDisplayName, status: fail" 915 | # Bail this pass through the while loop and continue processing next item 916 | continue 917 | else 918 | log_message "TeamID of PKG validated: $currentPKG $expectedTeamID" 919 | fi 920 | fi 921 | 922 | # Check SHA256, if a value has been provided 923 | if [ -n "$expectedSHA256" ]; then 924 | #Get SHA256 for the current PKG 925 | actualSHA256=$(shasum -a 256 "$currentPKG" | awk '{ print $1 }') 926 | # Check if actual does not match expected 927 | if [ "$expectedSHA256" != "$actualSHA256" ]; then 928 | report_message "Failed Item - Package SHA256 error: $currentPKG - Expected - $expectedSHA256 Actual - $actualSHA256" 929 | dialog_command "listitem: title: $currentDisplayName, status: fail" 930 | failList+=("$currentDisplayName") 931 | # Iterate the index up one 932 | currentIndex=$((currentIndex+1)) 933 | increment_progress_bar 934 | # Report the fail 935 | dialog_command "listitem: title: $currentDisplayName, status: fail" 936 | update_tracker $currentDisplayName 99 937 | # Bail this pass through the while loop and continue processing next item 938 | continue 939 | else 940 | log_message "SHA256 of PKG validated: $currentPKG $expectedSHA256" 941 | fi 942 | fi 943 | 944 | # Check MD5, if a value has been provided 945 | if [ -n "$expectedMD5" ]; then 946 | #Get MD5 for the current PKG 947 | actualMD5=$(md5 -q "$currentPKG") 948 | # Check if actual does not match expected 949 | if [ "$expectedMD5" != "$actualMD5" ]; then 950 | report_message "Failed Item - Package MD5 error: $currentPKG - Expected - $expectedMD5 Actual - $actualMD5" 951 | dialog_command "listitem: title: $currentDisplayName, status: fail" 952 | failList+=("$currentDisplayName") 953 | # Iterate the index up one 954 | currentIndex=$((currentIndex+1)) 955 | increment_progress_bar 956 | # Report the fail 957 | dialog_command "listitem: title: $currentDisplayName, status: fail" 958 | update_tracker $currentDisplayName 99 959 | # Bail this pass through the while loop and continue processing next item 960 | continue 961 | else 962 | log_message "MD5 of PKG validated: $currentPKG $expectedMD5" 963 | fi 964 | fi 965 | 966 | ## The package installation happens here. We do this in a variable so we can capture the output and report it for debugging 967 | pkgInstallerOutput=$(installer -allowUntrusted -pkg "$currentPKG" -target / ${currentArgumentArray[@]} ) 968 | # Capture the installer exit code 969 | pkgExitCode=$? 970 | # Verify the install completed successfully 971 | if [ $pkgExitCode != 0 ]; then 972 | report_message "Failed Item - Package installation error: $currentPKG - Exit Code: $pkgExitCode" 973 | dialog_command "listitem: title: $currentDisplayName, status: fail" 974 | failList+=("$currentDisplayName") 975 | else 976 | report_message "Successful Item - Package: $currentPKG" 977 | dialog_command "listitem: title: $currentDisplayName, status: success" 978 | successList+=("$currentDisplayName") 979 | fi 980 | update_tracker $currentDisplayName $pkgExitCode 981 | debug_message "Output of the install package command: $pkgInstallerOutput" 982 | # Iterate to the next index item, and continue our loop 983 | currentIndex=$((currentIndex+1)) 984 | increment_progress_bar 985 | # This gets set for use with the BailOut feature 986 | previousDisplayName="$currentDisplayName" 987 | check_for_bail_out 988 | done 989 | } 990 | 991 | function copy_icons_dir(){ 992 | if [ -d "${BaselineIcons}" ]; then 993 | cp -r "${BaselineIcons}/"* "${BaselineTempIconsDir}/" 994 | chmod -R 655 "${BaselineTempIconsDir}" 995 | fi 996 | } 997 | 998 | function build_dialog_json_file(){ 999 | # Initiate Json file 1000 | /bin/echo "{\"listitem\" : [" >> $dialogJsonFile 1001 | # For each item in our list, add the Json line 1002 | for jsonItem in $dialogListJson; do 1003 | /bin/echo "$jsonItem" >> $dialogJsonFile 1004 | done 1005 | # This trick removes the final character from the file, to ensure a valid Json 1006 | cat "$dialogJsonFile" | sed '$ s/.$//' > "${BaselineTempDir}/tempJson1" 1007 | mv "${BaselineTempDir}/tempJson1" "$dialogJsonFile" 1008 | # Finish Json file 1009 | /bin/echo "]}" >> "$dialogJsonFile" 1010 | 1011 | # Set global read permissions for Json file 1012 | chmod 644 "$dialogJsonFile" 1013 | 1014 | } 1015 | 1016 | function build_dialog_list_options(){ 1017 | # This function populates an array with all of the items Baseline iterates through 1018 | for i in $dialogList; do 1019 | dialogListItems+=(--listitem $i) 1020 | done 1021 | } 1022 | 1023 | function check_exit_condition(){ 1024 | exitConditionPath="" 1025 | # If `ExitCondition` key is passed in the configuration profile, then set a variable 1026 | if $pBuddy -c "Print :ExitCondition" "$BaselineConfig" > /dev/null 2>&1; then 1027 | exitConditionPath=$($pBuddy -c "Print :ExitCondition" "$BaselineConfig" | sed 's/"//g' ) 1028 | fi 1029 | # If our variable is set, and if the file exists, cleanup and exit quietly 1030 | if [ -n "$exitConditionPath" ] && [ -e "$exitConditionPath" ]; then 1031 | cleanup_and_exit 0 "Exit Condition exists. Exiting: "$exitConditionPath"" 1032 | fi 1033 | 1034 | } 1035 | 1036 | function check_bail_out_configuration(){ 1037 | # If BailOutFile key has a value, set it for the filepath we'll check 1038 | if $pBuddy -c "Print :BailOutFile" "$BaselineConfig" > /dev/null 2>&1; then 1039 | bailOutFilePath=$($pBuddy -c "Print :BailOutFile" "$BaselineConfig") 1040 | else 1041 | bailOutFilePath="" 1042 | fi 1043 | } 1044 | 1045 | function check_for_bail_out(){ 1046 | # If our BailOutFilePath has a value 1047 | if [ ! -z $bailOutFilePath ]; then 1048 | # Check if the file exists 1049 | if [ -f "$bailOutFilePath" ]; then 1050 | # Add the previousDisplayName to our failure list 1051 | failList+=("$previousDisplayName") 1052 | # Delete the bail out file 1053 | rm_if_exists "$bailOutFilePath" 1054 | #Close our running dialog window 1055 | dialog_command "quit:" 1056 | # Do the Failure window 1057 | present_failure_window 1058 | # Exit with code 99 1059 | cleanup_and_restart 99 "Bail out file identified: $bailOutFilePath" 1060 | fi 1061 | fi 1062 | } 1063 | 1064 | function check_restart_option(){ 1065 | restartSetting=$($pBuddy -c "Print :Restart" "$BaselineConfig" 2> /dev/null ) 1066 | logOutSetting=$($pBuddy -c "Print :LogOut" "$BaselineConfig" 2> /dev/null ) 1067 | 1068 | 1069 | if [ -z $logOutSetting ]; then 1070 | log_message "No LogOut key in configuration file" 1071 | forceLogOut="unset" 1072 | elif [[ "$logOutSetting" == "true" ]]; then 1073 | log_message "LogOut set to true from configuration file" 1074 | forceLogOut=true 1075 | elif [[ "$logOutSetting" == "false" ]]; then 1076 | log_message "LogOut set to false from configuration file" 1077 | forceLogOut="false" 1078 | else 1079 | log_message "Invalid value for LogOut key. Setting to default. Invalid Key Value: $logOutSetting" 1080 | forceLogOut="unset" 1081 | fi 1082 | 1083 | if [ -z $restartSetting ]; then 1084 | log_message "No Restart key in configuration file" 1085 | forceRestart="unset" 1086 | elif [[ "$restartSetting" == "false" ]]; then 1087 | log_message "Force Restart set to false from configuration file." 1088 | forceRestart="false" 1089 | elif [[ "$restartSetting" == "true" ]]; then 1090 | log_message "Force Restart set to true from configuration file." 1091 | forceRestart="true" 1092 | else 1093 | log_message "Force Restart setting invalid. Setting default. Invalid Key Value: $restartSetting" 1094 | forceRestart="unset" 1095 | fi 1096 | 1097 | debug_message "Checking exit action variables" 1098 | 1099 | if [[ "$forceRestart" == "unset" ]] && [[ "$forceLogOut" == "unset" ]]; then 1100 | log_message "No Restart or LogOut key in configuration file. Setting default behavior to Restart" 1101 | forceRestart=true 1102 | forceLogOut=false 1103 | elif [[ "$forceRestart" == "true" ]]; then 1104 | log_message "Restart key set to true. Device will be restarted." 1105 | forceRestart=true 1106 | forceLogOut=false 1107 | elif [[ "$forceLogOut" == "true" ]]; then 1108 | log_message "LogOut key set to true. User will be logged out." 1109 | forceRestart=false 1110 | forceLogOut=true 1111 | elif [[ "$forceRestart" == "false" ]]; then 1112 | log_message "Restart key set to false. LogOut key not set to true. No restart and No LogOut will occur." 1113 | forceRestart=false 1114 | forceLogOut=false 1115 | elif [[ "$forceRestart" == "unset" ]] ; then 1116 | log_message "Restart key not in configuration file. LogOut key set to false. Device will be restarted." 1117 | forceRestart=true 1118 | forceLogOut=false 1119 | else 1120 | log_message "Unknown combination of LogOut and Restart values. Open an issue on GitHub and provide logs." 1121 | forceRestart=false 1122 | forceLogOut=false 1123 | fi 1124 | 1125 | } 1126 | 1127 | function check_progress_options(){ 1128 | # Set variable for whether or not we'll display a progress bar. Defaults to 'false' 1129 | showProgressBarSetting=$($pBuddy -c "Print :ProgressBar" "$BaselineConfig" 2> /dev/null ) 1130 | 1131 | if [[ $showProgressBarSetting == "true" ]]; then 1132 | showProgressBar="true" 1133 | else 1134 | showProgressBar="false" 1135 | fi 1136 | 1137 | # Set variable for whether or not we'll display a progress bar label. Defaults to 'false' 1138 | showProgressBarDisplayNameSetting=$($pBuddy -c "Print :ProgressBarDisplayNames" "$BaselineConfig" 2> /dev/null ) 1139 | 1140 | if [[ $showProgressBarDisplayNameSetting == "true" ]]; then 1141 | progressBarDisplayNames="true" 1142 | else 1143 | progressBarDisplayNames="false" 1144 | fi 1145 | } 1146 | 1147 | function increment_progress_bar(){ 1148 | # If we're not displaying the progress bar, skip 1149 | if [ "$showProgressBar" != "true" ]; then 1150 | return 1151 | fi 1152 | 1153 | # Increment progress bar 1154 | progressBarValue=$((progressBarValue+1)) 1155 | # Do the math to determine total progress bar size for real increment 1156 | progressBarPercentage=$((progressBarValue*100/progressBarTotal)) 1157 | 1158 | dialog_command "progress: $progressBarPercentage" 1159 | } 1160 | 1161 | function set_progressbar_text(){ 1162 | # If we're not displaying the progress bar, skip 1163 | if [ "$progressBarDisplayNames" != "true" ]; then 1164 | return 1165 | fi 1166 | 1167 | dialog_command "progresstext: $1" 1168 | } 1169 | 1170 | function present_failure_window(){ 1171 | #There was at least one failed item. Build fail list 1172 | failListItems=() 1173 | for i in ${failList[@]}; do 1174 | failListItems+=(--listitem $i) 1175 | done 1176 | #Create our Failure Dialog Window. We use a "while" loop and a nested if/then in order to bail if there's a configuration file problem. 1177 | #Set a timer for our attempts 1178 | dialogAttemptCount=1 1179 | #Set our exit variable for the while loop 1180 | dialogCompletionWindow="incomplete" 1181 | #Set our exit condition for the while loop 1182 | while [ $dialogCompletionWindow = "incomplete" ]; do 1183 | #If we haven't tried 10 times yet, then try to call Dialog 1184 | if [ "$dialogAttemptCount" -le 10 ]; then 1185 | #If dialog exits 0, then exit our loop 1186 | ${finalFailureCommand[@]} ${failListItems[@]} 1187 | dialogExitCode=$? 1188 | if [ $dialogExitCode = 0 ] || [ $dialogExitCode = 4 ] || [ $dialogExitCode = 10 ]; then 1189 | dialogCompletionWindow="complete" 1190 | fi 1191 | #Increment our dialog attempt count 1192 | sleep 1 1193 | dialogAttemptCount=$(( dialogAttemptCount +1 )) 1194 | else 1195 | #If we got here, dialog tried 10 times and never opened properly. Exit with a message to the log file. 1196 | cleanup_and_exit 1 "**WARNING** SwiftDialog failed to launch after 10 attempts. This likely indicates an issue with the options in the configuration file. Check your file paths." 1197 | fi 1198 | done 1199 | } 1200 | 1201 | function present_success_window(){ 1202 | #Create our Success Dialog Window. We use a "while" loop and a nested if/then in order to bail if there's a configuration file problem. 1203 | #Set a timer for our attempts 1204 | dialogAttemptCount=1 1205 | #Set our exit variable for the while loop 1206 | dialogCompletionWindow="incomplete" 1207 | #Set our exit condition for the while loop 1208 | while [ $dialogCompletionWindow = "incomplete" ]; do 1209 | #If we haven't tried 10 times yet, then try to call Dialog 1210 | if [ "$dialogAttemptCount" -le 10 ]; then 1211 | #If dialog exits 0, then exit our loop 1212 | ${finalSuccessCommand[@]} 1213 | dialogExitCode=$? 1214 | if [ $dialogExitCode = 0 ] || [ $dialogExitCode = 4 ] || [ $dialogExitCode = 10 ]; then 1215 | dialogCompletionWindow="complete" 1216 | fi 1217 | #Increment our dialog attempt count 1218 | sleep 1 1219 | dialogAttemptCount=$(( dialogAttemptCount +1 )) 1220 | else 1221 | #If we got here, dialog tried 10 times and never opened properly. Exit with a message to the log file. 1222 | cleanup_and_exit 1 "**WARNING** SwiftDialog failed to launch after 10 attempts. This likely indicates an issue with the options in the configuration file. Check your file paths." 1223 | fi 1224 | done 1225 | } 1226 | 1227 | function initiate_tracker_file(){ 1228 | if "$useTracker"; then 1229 | # Set tracker file path 1230 | trackerFilePath=/var/log/Baseline-$(basename "$BaselineConfig")-tracker.plist 1231 | 1232 | # If tracker file already exists, verify its a valid plist 1233 | if [ -e "$trackerFilePath" ]; then 1234 | if ! $pBuddy -c Print "$trackerFilePath" > /dev/null 2>&1; then 1235 | report_message "Invalid tracker file. Cannot not use tracker feature." 1236 | useTracker=false 1237 | else 1238 | report_message "Tracker file will be used: $trackerFilePath" 1239 | fi 1240 | # Else, create the file. If we can't create it, turn off useTracker 1241 | else 1242 | if ! $pBuddy -c "Add :TrackerCreationDate integer $(date +%s)" "$trackerFilePath" > /dev/null 2>&1; then 1243 | report_message "Cannot write to tracker file. Cannot not use tracker feature." 1244 | useTracker=false 1245 | else 1246 | $pBuddy -c "Add :TrackerCreationDateReadable string $(date)" "$trackerFilePath" 1247 | report_message "Tracker file created: $trackerFilePath" 1248 | fi 1249 | fi 1250 | fi 1251 | 1252 | } 1253 | 1254 | function update_tracker(){ 1255 | # If we're using a tracker 1256 | if $useTracker; then 1257 | currentTrackerName=$(echo "${1}" | tr -d '[:space:]' | sed "s:\'::g" | sed "s:\"::g") 1258 | # If we can't Add the value, it must already exist so we instead set the value. 1259 | if ! $pBuddy -c "Add :$currentTrackerName:LastExitCode integer $2" "$trackerFilePath" > /dev/null 2>&1; then 1260 | $pBuddy -c "Set :$currentTrackerName:LastExitCode $2" "$trackerFilePath" 1261 | fi 1262 | if ! $pBuddy -c "Add :$currentTrackerName:LastRun integer $(date +%s)" "$trackerFilePath" > /dev/null 2>&1; then 1263 | $pBuddy -c "Set :$currentTrackerName:LastRun $(date +%s)" "$trackerFilePath" 1264 | fi 1265 | # If the item was successful, update the tracker to reflect that 1266 | if [[ $2 = 0 ]]; then 1267 | if ! $pBuddy -c "Add :$currentTrackerName:LastSuccessfulCompletion integer $(date +%s)" "$trackerFilePath" > /dev/null 2>&1; then 1268 | $pBuddy -c "Set :$currentTrackerName:LastSuccessfulCompletion $(date +%s)" "$trackerFilePath" 1269 | fi 1270 | if ! $pBuddy -c "Add :$currentTrackerName:LastSuccessfulCompletionReadable string $(date)" "$trackerFilePath" > /dev/null 2>&1; then 1271 | $pBuddy -c "Set :$currentTrackerName:LastSuccessfulCompletionReadable $(date)" "$trackerFilePath" 1272 | fi 1273 | else 1274 | # Update the tracker to show 0/never if the item has never completed successfully 1275 | if ! $pBuddy -c "Print :$currentTrackerName:LastSuccessfulCompletion" "$trackerFilePath" > /dev/null 2>&1; then 1276 | $pBuddy -c "Add :$currentTrackerName:LastSuccessfulCompletion integer 0" "$trackerFilePath" 1277 | fi 1278 | if ! $pBuddy -c "Print :$currentTrackerName:LastSuccessfulCompletionReadable" "$trackerFilePath" > /dev/null 2>&1; then 1279 | $pBuddy -c "Add :$currentTrackerName:LastSuccessfulCompletionReadable string never" "$trackerFilePath" 1280 | fi 1281 | fi 1282 | 1283 | fi 1284 | } 1285 | 1286 | function check_silent_option(){ 1287 | # If silentMode hasn't been set yet, check if its in our configuration file. 1288 | if [ -z "$silentModeEnabled" ]; then 1289 | # This will exit empty if the key does not exist. 1290 | silentModeEnabled=$($pBuddy -c "Print :SilentMode" "$BaselineConfig" 2> /dev/null ) 1291 | fi 1292 | 1293 | if [ -z $silentModeEnabled ]; then 1294 | log_message "No SilentMode key in configuration file" 1295 | silentModeEnabled="false" 1296 | elif [[ "$silentModeEnabled" == "true" ]]; then 1297 | log_message "SilentMode set to true" 1298 | silentModeEnabled=true 1299 | elif [[ "$silentModeEnabled" == "false" ]]; then 1300 | log_message "SilentMode set to false from configuration file" 1301 | silentModeEnabled="false" 1302 | else 1303 | log_message "Invalid value for SilentMode key. Setting to default. Invalid Key Value: $silentModeEnabled" 1304 | silentModeEnabled="false" 1305 | fi 1306 | 1307 | configure_silent_mode 1308 | } 1309 | 1310 | function configure_silent_mode(){ 1311 | # If silentMode is enabled, rewrite all functions which use SwiftDialog to `true` 1312 | # This effectively takes SwiftDialog entirely out of use. 1313 | if $silentModeEnabled; then 1314 | 1315 | dialogPath=true 1316 | dialogAppPath="/System/Applications" 1317 | 1318 | function dialog_command(){ 1319 | true 1320 | } 1321 | 1322 | function install_dialog(){ 1323 | true 1324 | } 1325 | 1326 | function wait_for_user(){ 1327 | true 1328 | } 1329 | 1330 | function build_dialog_json_file(){ 1331 | true 1332 | } 1333 | 1334 | function build_dialog_list_options(){ 1335 | true 1336 | } 1337 | 1338 | function increment_progress_bar(){ 1339 | true 1340 | } 1341 | 1342 | function set_progressbar_text(){ 1343 | true 1344 | } 1345 | 1346 | function present_failure_window(){ 1347 | true 1348 | } 1349 | 1350 | function present_success_window(){ 1351 | true 1352 | } 1353 | 1354 | fi 1355 | } 1356 | 1357 | function jamf_verbose_dialog(){ 1358 | # This function reads the jamf log output and updates the associated item in swiftDialog with verbose details like Installomator uses 1359 | # $1 is the DisplayName of the item we want to update 1360 | # Tail the Jamf log file 1361 | tail -Fn0 "$jamfLogFile" | while read line ; do 1362 | # Check for "Executing" status 1363 | if echo "$line" | grep -qi "executing"; then 1364 | dialog_command "listitem: title: ${1}, statustext: Executing..., progress: 25" 1365 | fi 1366 | 1367 | # Check for "Verifying" status 1368 | if echo "$line" | grep -qi "verifying"; then 1369 | dialog_command "listitem: title: ${1}, statustext: Verifying..., progress: 50" 1370 | fi 1371 | 1372 | # Check for "Installing" status 1373 | if echo "$line" | grep -qi "installing"; then 1374 | dialog_command "listitem: title: ${1}, statustext: Installing..., progress: 75" 1375 | fi 1376 | 1377 | # Check for "Successfully installed" status 1378 | if echo "$line" | grep -qi "successfully installed"; then 1379 | dialog_command "listitem: title: ${1}, statustext: Installation Finished" 1380 | break # Exit after successful installation 1381 | fi 1382 | 1383 | # Check for "Failed" status 1384 | if echo "$line" | grep -qi "failed"; then 1385 | dialog_command "listitem: title: ${1}, statustext: Installation Failed" 1386 | break # Exit on failure 1387 | fi 1388 | done 1389 | sleep 1 1390 | 1391 | } 1392 | 1393 | function check_jamf_verbose_option(){ 1394 | # Set variable for whether or not we'll use Jamf Verbosle Options. Defaults to 'false' 1395 | showVerboseJamfSetting=$($pBuddy -c "Print :JamfVerbose" "$BaselineConfig" 2> /dev/null ) 1396 | 1397 | if [[ $showVerboseJamfSetting == "true" ]]; then 1398 | showVerboseJamf="true" 1399 | else 1400 | showVerboseJamf="false" 1401 | fi 1402 | 1403 | } 1404 | 1405 | function process_wait_for_items(){ 1406 | # See if any `WaitFor` items are in our config, if not end this function 1407 | if ! $pBuddy -c "Print :WaitFor:0" "$BaselineConfig" > /dev/null 2>&1; then 1408 | debug_message "No WaitFor items found" 1409 | return 0 1410 | else 1411 | debug_message "WaitFor values found. Initiating WaitFor" 1412 | fi 1413 | 1414 | # Initiate empty arrays 1415 | waitForPaths=() 1416 | waitForDisplayNames=() 1417 | 1418 | # This is our index as we build our arrays 1419 | waitCount=0 1420 | 1421 | # Put all Paths/DisplayNames into our arrays 1422 | while "$pBuddy" -c "Print WaitFor:${waitCount}:Path" "$BaselineConfig" > /dev/null 2>&1; do 1423 | waitForPaths+=$("$pBuddy" -c "Print WaitFor:${waitCount}:Path" "$BaselineConfig") 1424 | waitForDisplayNames+=$("$pBuddy" -c "Print WaitFor:${waitCount}:DisplayName" "$BaselineConfig") 1425 | waitCount=$(( waitCount + 1 )) 1426 | done 1427 | 1428 | # Put all of our WaitFor items into spinny wait mode 1429 | #for waitForDisplayName in "${waitForDisplayNames[@]}"; do 1430 | # dialog_command "listitem: title: $waitForDisplayName, status: wait" 1431 | #done 1432 | 1433 | # While we still have paths we're waiting for 1434 | while [ -n "$waitForPaths" ] ; do 1435 | # Check for each path in our Paths array, and see if it exists yet 1436 | for waitPath in "${waitForPaths[@]}"; do 1437 | # If our item exists 1438 | if [ -e "$waitPath" ]; then 1439 | debug_message "$waitPath exists" 1440 | # Find what index in our array the current item belongs to 1441 | indexItemToRemove="${waitForPaths[(Ie)${waitPath}]}" 1442 | # As long as that index is not zero 1443 | if [ "$indexItemToRemove" != 0 ]; then 1444 | # Remove the Path and the DisplayName from the list of items we're waiting to complete 1445 | debug_message "Removing from waitForPaths: $waitForPaths[${indexItemToRemove}] index: ${indexItemToRemove}" 1446 | debug_message "Removing from waitForDisplayNames: $waitForDisplayNames[${indexItemToRemove}] index: ${indexItemToRemove}" 1447 | # Mark the item as complete 1448 | dialog_command "listitem: title: $waitForDisplayNames[$indexItemToRemove], status: success" 1449 | report_message "Successful Item - WaitFor: $waitForDisplayNames[$indexItemToRemove]" 1450 | successList+=("$waitForDisplayNames[$indexItemToRemove]") 1451 | increment_progress_bar 1452 | sleep 1 1453 | waitForPaths[${indexItemToRemove}]=() 1454 | waitForDisplayNames[${indexItemToRemove}]=() 1455 | fi 1456 | fi 1457 | done 1458 | # Sleep 2 seconds between checking for all items 1459 | sleep 2 1460 | done 1461 | 1462 | 1463 | } 1464 | 1465 | ######################################################################################################## 1466 | ######################################################################################################## 1467 | ## 1468 | ## SCRIPT STARTS HERE 1469 | ## 1470 | ######################################################################################################## 1471 | ######################################################################################################## 1472 | 1473 | debug_message "Starting script actions" 1474 | 1475 | #Verify we're running as root 1476 | check_root 1477 | 1478 | #Check if exit condition has been defined 1479 | check_exit_condition 1480 | 1481 | #No falling asleep on the job, bud 1482 | no_sleeping 1483 | 1484 | #Set trap so that things always exit cleanly 1485 | trap cleanup_and_exit 1 2 3 6 1486 | 1487 | #Check if directories for Packages and Scripts exist already. 1488 | #This is useful for testing, or if running the script directly (not the pkg) 1489 | make_directory "$BaselineScripts" 1490 | make_directory "$BaselinePackages" 1491 | make_directory "$BaselineIcons" 1492 | 1493 | #Initiate Logging 1494 | initiate_logging 1495 | 1496 | #Setup report 1497 | initiate_report 1498 | 1499 | ################################# 1500 | # Process Script Arguments # 1501 | ################################# 1502 | 1503 | configFromArgument=false 1504 | useTracker=false 1505 | 1506 | if [ -z $dryRun ]; then 1507 | dryRun=false 1508 | fi 1509 | 1510 | while [ ! -z "$1" ]; do 1511 | case $1 in; 1512 | "/") 1513 | log_message "Shifting arguments for Jamf" 1514 | shift 2 1515 | ;; 1516 | -c|--config|--configuration) 1517 | shift 1518 | if [ -e "$1" ] && $pBuddy -c "Print" "${1}" > /dev/null 2>&1; then 1519 | log_message "Using configuration profile from argument: $1" 1520 | BaselineConfig="$1" 1521 | function verify_configuration_file(){ 1522 | true 1523 | } 1524 | configFromArgument=true 1525 | elif [ ! -e "$1" ]; then 1526 | cleanup_and_exit 80 "ERROR: Configuration not found: $1" 1527 | else 1528 | cleanup_and_exit 81 "ERROR: Invalid configuration file: $1" 1529 | fi 1530 | ;; 1531 | -s|--silent|--silent-mode) 1532 | log_message "Setting Silent Mode from Command-Line" 1533 | silentModeEnabled=true 1534 | ;; 1535 | -t|--tracker) 1536 | useTracker=true 1537 | ;; 1538 | *) 1539 | cleanup_and_exit 82 "Unknown argument: $1" 1540 | ;; 1541 | esac 1542 | shift 1543 | done 1544 | 1545 | ############################################################ 1546 | # De-Configure Functions and Variables for Silent Mode # 1547 | ############################################################ 1548 | 1549 | 1550 | 1551 | ############################################# 1552 | # Verify a Configuration File is in Place # 1553 | ############################################# 1554 | verify_configuration_file 1555 | check_silent_option 1556 | initiate_tracker_file 1557 | 1558 | ############################################# 1559 | # Configure Default Installomator Options # 1560 | ############################################# 1561 | 1562 | # Set variable for whether or not we'll use Baseline default Installomator options 1563 | ignoreInstallomatorOptionsSetting=$($pBuddy -c "Print :IgnoreDefaultInstallomatorOptions" "$BaselineConfig" 2> /dev/null ) 1564 | 1565 | if [[ $ignoreInstallomatorOptionsSetting == "true" ]]; then 1566 | defaultInstallomatorOptions=() 1567 | else 1568 | defaultInstallomatorOptions=( 1569 | BLOCKING_PROCESS_ACTION=kill 1570 | NOTIFY=silent 1571 | ) 1572 | fi 1573 | 1574 | if [ "$dryRun" = true ]; then 1575 | defaultInstallomatorOptions+="DEBUG=2" 1576 | fi 1577 | 1578 | ########################### 1579 | # Install Installomator # 1580 | ########################### 1581 | #If Installomator is going to be used, install it now 1582 | if $pBuddy -c "Print :Installomator:0" "$BaselineConfig" > /dev/null 2>&1; then 1583 | install_installomator 1584 | fi 1585 | 1586 | ######################### 1587 | # Install SwiftDialog # 1588 | ######################### 1589 | install_dialog 1590 | #If swiftDialog still isn't installed, exit with an error 1591 | if [ ! -e "$dialogAppPath" ]; then 1592 | cleanup_and_exit 1 "ERROR: SwiftDialog failed to install after numerous attempts. Exiting." 1593 | fi 1594 | 1595 | ############################################# 1596 | # Wait until a user is verified logged in # 1597 | ############################################# 1598 | wait_for_user 1599 | 1600 | # Get the currently logged in user home folder and UID 1601 | currentUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' ) 1602 | currentUserUID=$(/usr/bin/id -u "$currentUser") 1603 | userHomeFolder=$(dscl . -read /users/${currentUser} NFSHomeDirectory | cut -d " " -f 2) 1604 | 1605 | ############# 1606 | # Arrays # 1607 | ############# 1608 | 1609 | # Initiate arrays 1610 | dialogList=() 1611 | dialogListItems=() 1612 | dialogListJson=() 1613 | failList=() 1614 | successList=() 1615 | 1616 | installomatorLabels=() 1617 | installomatorOptions=() 1618 | 1619 | scriptsToProcess=() 1620 | scriptArguments=() 1621 | 1622 | pkgsToInstall=() 1623 | pkgValidations=() 1624 | 1625 | ###################### 1626 | # Integers and Bools # 1627 | ###################### 1628 | 1629 | # Initiate integers 1630 | progressBarValue=0 1631 | progressBarTotal=0 1632 | 1633 | # Initiate bools 1634 | showProgressBar="false" 1635 | progressBarDisplayNames="false" 1636 | 1637 | ############################## 1638 | # Process Initial Scripts # 1639 | ############################## 1640 | 1641 | # Check Bail Out configuration 1642 | check_bail_out_configuration 1643 | 1644 | process_scripts InitialScripts 1645 | 1646 | #Check if a custom plist was delivered/altered during InitialScripts 1647 | check_for_custom_plist 1648 | 1649 | #We check for Installomator again, to support custom plist swapping 1650 | if $pBuddy -c "Print :Installomator:0" "$BaselineConfig" > /dev/null 2>&1; then 1651 | install_installomator 1652 | fi 1653 | 1654 | ####################### 1655 | # Customizations # 1656 | ####################### 1657 | # Check if we are going to restart. This has to be here, because the Dialog customizations depend on it 1658 | check_restart_option 1659 | 1660 | # Check if we should display a progress bar under the UI 1661 | check_progress_options 1662 | 1663 | # Check if there is an Icons directory, and if so make a temporary copy of it 1664 | copy_icons_dir 1665 | 1666 | ##################################### 1667 | # Initiate Dialog Option Arays # 1668 | ##################################### 1669 | 1670 | finalListCommand=() 1671 | finalSuccessCommand=() 1672 | finalFailureCommand=() 1673 | 1674 | finalListCommand+="$dialogPath" 1675 | finalSuccessCommand+="$dialogPath" 1676 | finalFailureCommand+="$dialogPath" 1677 | 1678 | 1679 | ###################################### 1680 | # Configure List Customizations # 1681 | ###################################### 1682 | 1683 | # Configure Installomator SwiftDialog Integration 1684 | useInstallomatorSwiftDialogIntegration=$($pBuddy -c "Print :InstallomatorSwiftDialogIntegration" "$BaselineConfig" 2> /dev/null) 1685 | 1686 | if [[ $useInstallomatorSwiftDialogIntegration == "true" ]]; then 1687 | true 1688 | fi 1689 | 1690 | # Configure Blur Screen options 1691 | blurScreen=$($pBuddy -c "Print :BlurScreen" "$BaselineConfig" 2> /dev/null) 1692 | 1693 | if [[ $blurScreen == "false" ]]; then 1694 | true 1695 | else 1696 | finalListCommand+="--blurscreen" 1697 | finalSuccessCommand+="--blurscreen" 1698 | finalFailureCommand+="--blurscreen" 1699 | fi 1700 | 1701 | # Configure Blur Screen options 1702 | button1enabled=$($pBuddy -c "Print :Button1Enabled" "$BaselineConfig" 2> /dev/null) 1703 | 1704 | if [[ $button1enabled == "true" ]]; then 1705 | true 1706 | else 1707 | finalListCommand+="--button1disabled" 1708 | fi 1709 | 1710 | # Read the Dialog List `Arguments` customizations, if there are any 1711 | if $pBuddy -c "Print DialogListOptions" "$BaselineConfig" > /dev/null 2>&1; then 1712 | dialogListArguments=$($pBuddy -c "Print DialogListOptions" "$BaselineConfig") 1713 | fi 1714 | 1715 | # If we found customizations, read them into our final list command array 1716 | if [ -n "$dialogListArguments" ]; then 1717 | eval 'for customization in '$dialogListArguments'; do finalListCommand+=$customization; done' 1718 | fi 1719 | 1720 | # This function is not with the rest, but it makes the most sense as I add on this feature. 1721 | # I also don't know how to set the variable by passing the name of that variable as an argument to the function. 1722 | # So I'll have to define this function several times. 1723 | # I grow old … I grow old …I shall wear the bottoms of my trousers rolled.. 1724 | function configure_dialog_list_arguments(){ 1725 | # $1 is the SwiftDialog option to change, $2 is the default value for that option if its not included in the profile 1726 | if (($dialogListArguments[(Ie)$1])); then 1727 | # $1 was included in the customization, so we report it and move along 1728 | log_message "Dialog List Customization Found: $1" 1729 | else 1730 | # $1 wasn't included in the customization options, so we'll set the default value 1731 | finalListCommand+="$1" 1732 | finalListCommand+="$2" 1733 | fi 1734 | } 1735 | 1736 | # Adjust language of our list view window depending on whether or not the device will restart/logout 1737 | defaultListMessage="Feel free to step away, this could take 30 minutes or more." 1738 | if $forceLogOut; then 1739 | # Add a line break and a sentence about logging out 1740 | defaultListMessage+="\n\nYou will be logged out when it's ready for use." 1741 | elif $forceRestart; then 1742 | # Add a line break and a sentence about restarting. 1743 | defaultListMessage+="\n\nYour computer will restart when it's ready for use." 1744 | fi 1745 | 1746 | configure_dialog_list_arguments "--title" "Your computer setup is underway" 1747 | configure_dialog_list_arguments "--message" "$defaultListMessage" 1748 | configure_dialog_list_arguments "--icon" "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns" 1749 | configure_dialog_list_arguments "--width" 900 1750 | configure_dialog_list_arguments "--height" 550 1751 | configure_dialog_list_arguments "--quitkey" ']' 1752 | 1753 | if [ "$showProgressBar" = "true" ]; then 1754 | configure_dialog_list_arguments "--progress" 1755 | fi 1756 | 1757 | if [ "$progressBarDisplayNames" = "true" ]; then 1758 | configure_dialog_list_arguments "--progresstext" ' ' 1759 | fi 1760 | 1761 | ######################################### 1762 | # Configure Success Customizations # 1763 | ######################################### 1764 | 1765 | # Read the Dialog Success Arguments customizations, if there are any 1766 | if $pBuddy -c "Print DialogSuccessOptions" "$BaselineConfig" > /dev/null 2>&1; then 1767 | dialogSuccessArguments=$($pBuddy -c "Print DialogSuccessOptions" "$BaselineConfig") 1768 | fi 1769 | 1770 | # If we found customizations, read them into our final list command array 1771 | if [ -n "$dialogSuccessArguments" ]; then 1772 | eval 'for customization in '$dialogSuccessArguments'; do finalSuccessCommand+=$customization; done' 1773 | fi 1774 | 1775 | function configure_dialog_success_arguments(){ 1776 | # $1 is the SwiftDialog option to change, $2 is the default value for that option if its not included in the profile 1777 | if (($dialogSuccessArguments[(Ie)$1])); then 1778 | # $1 was included in the customization, so we report it and move along 1779 | log_message "Dialog Success Customization Found: $1" 1780 | else 1781 | # $1 wasn't included in the customization options, so we'll set the default value 1782 | finalSuccessCommand+="$1" 1783 | finalSuccessCommand+="$2" 1784 | fi 1785 | } 1786 | 1787 | configure_dialog_success_arguments "--title" "Your computer setup is complete" 1788 | configure_dialog_success_arguments "--icon" "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns" 1789 | configure_dialog_success_arguments "--quitkey" ']' 1790 | # Different values for --message and --button1text if we're forcing log out or restart 1791 | if $forceLogOut; then 1792 | configure_dialog_success_arguments "--message" "You must log out before you can begin using your computer." 1793 | configure_dialog_success_arguments "--button1text" "Log Out Now" 1794 | configure_dialog_success_arguments "--timer" "120" 1795 | fi 1796 | 1797 | if $forceRestart; then 1798 | configure_dialog_success_arguments "--message" "Your device needs to restart before you can begin use." 1799 | configure_dialog_success_arguments "--button1text" "Restart Now" 1800 | configure_dialog_success_arguments "--timer" "120" 1801 | else 1802 | configure_dialog_success_arguments "--message" "Your device is ready for you." 1803 | fi 1804 | 1805 | 1806 | ######################################### 1807 | # Configure Failure Customizations # 1808 | ######################################### 1809 | 1810 | # Read the Dialog Failure Arguments customizations, if there are any 1811 | if $pBuddy -c "Print DialogFailureOptions" "$BaselineConfig" > /dev/null 2>&1; then 1812 | dialogFailureArguments=$($pBuddy -c "Print DialogFailureOptions" "$BaselineConfig") 1813 | fi 1814 | 1815 | # If we found customizations, read them into our final list command array 1816 | if [ -n "$dialogFailureArguments" ]; then 1817 | eval 'for customization in '$dialogFailureArguments'; do finalFailureCommand+=$customization; done' 1818 | fi 1819 | 1820 | function configure_dialog_failure_arguments(){ 1821 | # $1 is the SwiftDialog option to change, $2 is the default value for that option if its not included in the profile 1822 | if (($dialogFailureArguments[(Ie)$1])); then 1823 | # $1 was included in the customization, so we report it and move along 1824 | log_message "Dialog Failure Customization Found: $1" 1825 | else 1826 | # $1 wasn't included in the customization options, so we'll set the default value 1827 | finalFailureCommand+="$1" 1828 | finalFailureCommand+="$2" 1829 | fi 1830 | } 1831 | 1832 | configure_dialog_failure_arguments "--title" "Your computer setup is complete" 1833 | configure_dialog_failure_arguments "--icon" "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns" 1834 | configure_dialog_failure_arguments "--message" "Your computer setup is complete, however not everything was installed as expected. Review the list below, and contact IT if you need assistance." 1835 | configure_dialog_failure_arguments "--quitkey" ']' 1836 | 1837 | # Different values for --message and --button1text if we're forcing log out or restart 1838 | if $forceLogOut; then 1839 | configure_dialog_failure_arguments "--button1text" "Log Out Now" 1840 | configure_dialog_failure_arguments "--timer" "120" 1841 | fi 1842 | 1843 | if $forceRestart; then 1844 | configure_dialog_failure_arguments "--button1text" "Restart Now" 1845 | configure_dialog_failure_arguments "--timer" "120" 1846 | fi 1847 | 1848 | 1849 | ################### 1850 | # Build Arrays # 1851 | ################### 1852 | # Build dialogList array by reading our configuration and looping through things 1853 | 1854 | build_dialog_array Installomator 1855 | build_dialog_array Packages 1856 | build_dialog_array Scripts 1857 | build_dialog_array WaitFor 1858 | build_dialog_json_file 1859 | build_dialog_list_options 1860 | 1861 | 1862 | ################################## 1863 | # Draw our dialog list window # 1864 | ################################## 1865 | 1866 | #Create our initial Dialog Window. Do this in an "until" loop, and attempts 10 times before exiting in case it fails to launch for some reason 1867 | dialogAttemptCount=1 1868 | if ! $silentModeEnabled; then 1869 | until pgrep -q -f "$dialogAppPath.* --commandfile $dialogCommandFile --jsonfile $dialogJsonFile"; do 1870 | if [ "$dialogAttemptCount" -le 10 ]; then 1871 | ${finalListCommand[@]} \ 1872 | --commandfile "$dialogCommandFile" \ 1873 | --jsonfile "$dialogJsonFile" \ 1874 | & sleep 1 1875 | dialogAttemptCount=$(( dialogAttemptCount +1 )) 1876 | else 1877 | cleanup_and_exit 1 "**WARNING** SwiftDialog failed to launch after 10 attempts. This likely indicates an issue with the options in the configuration file. Check your file paths." 1878 | fi 1879 | done 1880 | fi 1881 | 1882 | ######################### 1883 | # Install the things # 1884 | ######################### 1885 | 1886 | # Progress Bar will be pulsing until a value is set 1887 | if [ "$showProgressBar" = "true" ]; then 1888 | dialog_command "progress: 1" 1889 | fi 1890 | 1891 | # Check Bail Out configuration 1892 | check_bail_out_configuration 1893 | 1894 | ## Setup WaitFor loop 1895 | # Check for a custom "WaitForTimeout" value 1896 | waitForTimeoutSetting=$($pBuddy -c "Print :WaitForTimeout" "$BaselineConfig" 2> /dev/null ) 1897 | # If the "WaitForTimeout" value is an integer, set our timeout to that. Otherwise, set to default. 1898 | if [[ "${waitForTimeoutSetting}" =~ '^[0-9]+$' ]] ; then 1899 | waitForTimeout="${waitForTimeoutSetting}" 1900 | else 1901 | waitForTimeout="${defaultWaitForTimeout}" 1902 | fi 1903 | # Set the time at which we'll stop waiting for items by getting the date now and adding the seconds for our deadline 1904 | waitForDateNow=$(date +%s) 1905 | waitForDeadline=$(( waitForDateNow + waitForTimeout )) 1906 | 1907 | process_wait_for_items & waitForPID=$! 1908 | 1909 | # Process Items 1910 | process_installomator_labels 1911 | 1912 | process_pkgs 1913 | 1914 | process_scripts Scripts 1915 | 1916 | # Clear any text off the progress bar 1917 | set_progressbar_text " " 1918 | 1919 | # Starting WaitFor mode 1920 | # Initiate empty arrays 1921 | waitForPaths=() 1922 | waitForDisplayNames=() 1923 | failedWaitDisplayNames=() 1924 | 1925 | # This is our index as we build our arrays 1926 | waitCount=0 1927 | 1928 | # Put all Paths/DisplayNames into our arrays 1929 | while "$pBuddy" -c "Print WaitFor:${waitCount}:Path" "$BaselineConfig" > /dev/null 2>&1; do 1930 | waitForPaths+=$("$pBuddy" -c "Print WaitFor:${waitCount}:Path" "$BaselineConfig") 1931 | waitForDisplayNames+=$("$pBuddy" -c "Print WaitFor:${waitCount}:DisplayName" "$BaselineConfig") 1932 | waitCount=$(( waitCount + 1 )) 1933 | done 1934 | 1935 | # If the wait_for_items function is still running OR we haven't reached our deadline, then sleep a bit 1936 | until ! ps -p $waitForPID > /dev/null || [[ $(date +%s) -gt "${waitForDeadline}" ]]; do 1937 | # Wait for the process to finish 1938 | sleep 2 1939 | done 1940 | 1941 | if ps -p $waitForPID > /dev/null; then 1942 | # If we're here, we've reached our deadline and the WaitFor items are still running 1943 | # Kill the process 1944 | kill $waitForPID 1945 | report_message "WaitFor items timed out with items remaining after $waitForTimeout seconds" 1946 | fi 1947 | 1948 | # If we have WaitFor paths 1949 | if [ -n "$waitForPaths" ] ; then 1950 | # Check for each path in our Paths array, and see if it exists yet 1951 | for waitPath in "${waitForPaths[@]}"; do 1952 | # If our item does not exists 1953 | if [ ! -e "$waitPath" ]; then 1954 | # Find what index in our array the current item belongs to 1955 | indexItemOfFail="${waitForPaths[(Ie)${waitPath}]}" 1956 | # As long as that index is not zero 1957 | if [ "$indexItemOfFail" != 0 ]; then 1958 | # Mark the item as failed 1959 | failedWaitDisplayName="${waitForDisplayNames[${indexItemOfFail}]}" 1960 | report_message "Failed Item - WaitFor: $failedWaitDisplayName" 1961 | failList+=("$failedWaitDisplayName") 1962 | dialog_command "listitem: title: $failedWaitDisplayName, status: fail" 1963 | fi 1964 | fi 1965 | done 1966 | fi 1967 | 1968 | if [ -n "$failedWaitDisplayNames" ]; then 1969 | report_message "One or more failed WaitFor items: $failedWaitDisplayNames" 1970 | fi 1971 | 1972 | # Set custom Dialog icon if it exists 1973 | if [ -e "/Library/Application Support/Dialog/Dialog.png" ]; then 1974 | log_message "Custom Dialog icon found. Applying it." 1975 | "/Library/Application Support/Dialog/Dialog.app/Contents/MacOS/Dialog" --seticon "/Library/Application Support/Dialog/Dialog.png" 1976 | fi 1977 | 1978 | if [ "$dryRun" = true ]; then 1979 | sleep 5 1980 | fi 1981 | 1982 | # Check if we are closing the list window before running Final Scripts 1983 | if $pBuddy -c "Print :CloseListBeforeFinalScripts" "$BaselineConfig" > /dev/null 2>&1; then 1984 | closeListBeforeFinalScripts=$($pBuddy -c "Print :CloseListBeforeFinalScripts" "$BaselineConfig") 1985 | else 1986 | closeListBeforeFinalScripts="false" 1987 | fi 1988 | 1989 | log_message "CloseListBeforeFinalScripts: $closeListBeforeFinalScripts" 1990 | 1991 | # If we are closing the List window before running Final Scripts 1992 | if [[ "$closeListBeforeFinalScripts" == "true" ]]; then 1993 | dialog_command "quit:" 1994 | process_scripts FinalScripts 1995 | else 1996 | process_scripts FinalScripts 1997 | dialog_command "quit:" 1998 | fi 1999 | 2000 | #Do final script swiftDialog stuff 2001 | #If the failList is empty, this means success 2002 | if [ -z "$failList" ]; then 2003 | present_success_window 2004 | update_tracker "Baseline" 0 2005 | # We are done! 2006 | cleanup_and_restart 0 "Baseline completed - All items successful." 2007 | else 2008 | present_failure_window 2009 | update_tracker "Baseline" 1 2010 | # We are done! 2011 | cleanup_and_restart 0 "Baseline completed - Some items failed." 2012 | fi 2013 | --------------------------------------------------------------------------------