├── .gitignore ├── Bom.txt ├── LICENSE ├── README.md ├── build-info.json ├── build_python_framework ├── images ├── ss_dep.png ├── ss_manual.png ├── ss_uamdm.png └── umad_diagram.png ├── payload └── Library │ ├── LaunchAgents │ └── com.erikng.umad.plist │ ├── LaunchDaemons │ ├── com.erikng.umad.check_dep_record.plist │ └── com.erikng.umad.trigger_nag.plist │ └── umad │ ├── Logs │ └── .gitignore │ └── Resources │ ├── company_logo.png │ ├── nag_ss.png │ ├── nibbler.py │ ├── uamdm_ss.png │ ├── umad │ ├── umad.nib │ ├── designable.nib │ └── keyedobjects.nib │ ├── umad_check_dep_record │ └── umad_trigger_nag ├── py3_requirements.txt └── scripts └── postinstall /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | *.pkg 4 | -------------------------------------------------------------------------------- /Bom.txt: -------------------------------------------------------------------------------- 1 | . 40755 0/0 2 | ./Library 40755 0/0 3 | ./Library/LaunchAgents 40755 0/0 4 | ./Library/LaunchAgents/com.erikng.umad.plist 100755 0/0 4397 3667671753 5 | ./Library/LaunchDaemons 40755 0/0 6 | ./Library/LaunchDaemons/com.erikng.umad.check_dep_record.plist 100755 0/0 559 2672307172 7 | ./Library/LaunchDaemons/com.erikng.umad.trigger_nag.plist 100755 0/0 544 2929504266 8 | ./Library/umad 40755 0/0 9 | ./Library/umad/Logs 40755 0/0 10 | ./Library/umad/Logs/.gitignore 100755 0/0 14 838015408 11 | ./Library/umad/Resources 40755 0/0 12 | ./Library/umad/Resources/._company_logo.png 100755 0/0 0 0 13 | ./Library/umad/Resources/._nag_ss.png 100755 0/0 0 0 14 | ./Library/umad/Resources/._uamdm_ss.png 100755 0/0 0 0 15 | ./Library/umad/Resources/._umad 100755 0/0 0 0 16 | ./Library/umad/Resources/._umad.nib 40755 0/0 0 0 17 | ./Library/umad/Resources/._umad_check_dep_record 100755 0/0 0 0 18 | ./Library/umad/Resources/._umad_trigger_nag 100755 0/0 0 0 19 | ./Library/umad/Resources/company_logo.png 100755 0/0 16622 2197199182 20 | ./Library/umad/Resources/nag_ss.png 100755 0/0 31794 3177474883 21 | ./Library/umad/Resources/nibbler.py 100755 0/0 4552 2490287542 22 | ./Library/umad/Resources/uamdm_ss.png 100755 0/0 54940 1985954285 23 | ./Library/umad/Resources/umad 100755 0/0 37855 980732209 24 | ./Library/umad/Resources/umad.nib 40755 0/0 25 | ./Library/umad/Resources/umad.nib/designable.nib 100644 0/0 24452 4060103585 26 | ./Library/umad/Resources/umad.nib/keyedobjects.nib 100644 0/0 18267 410556532 27 | ./Library/umad/Resources/umad_check_dep_record 100755 0/0 4254 3041188598 28 | ./Library/umad/Resources/umad_trigger_nag 100755 0/0 2958 1897768855 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UMAD (macadmin's Slack #umad) 2 | [U]niversal 3 | [M]DM 4 | [A]pproval 5 | [D]ialog 6 | 7 | ## Embedded Python 8 | As of v2.0, UMAD now uses its own embedded python (currently v3.8). This is due to Apple's upcoming removal of Python2. 9 | 10 | `FoundationPlist` has been replaced by Python 3's version of `plistlib` 11 | 12 | Nibbler has been updated to support python 3. 13 | 14 | ### Building embedded python framework 15 | 16 | To reduce the size of the git repository, you **must** create your own Python. To do this, simply run the `./build_python_framework` script within the repository. 17 | 18 | This process was tested on Catalina only. 19 | 20 | ``` 21 | ./build_python_framework 22 | 23 | Cloning relocatable-python tool from github... 24 | Cloning into '/tmp/relocatable-python-git'... 25 | remote: Enumerating objects: 28, done. 26 | remote: Counting objects: 100% (28/28), done. 27 | remote: Compressing objects: 100% (19/19), done. 28 | remote: Total 78 (delta 12), reused 19 (delta 9), pack-reused 50 29 | Unpacking objects: 100% (78/78), done. 30 | Downloading https://www.python.org/ftp/python/3.8.0/python-3.8.0-macosx10.9.pkg... 31 | 32 | ... 33 | 34 | Done! 35 | Customized, relocatable framework is at /Library/umad/Python.framework 36 | Moving Python.framework to umad munki-pkg payload folder 37 | Taking ownership of the file to not break git 38 | ``` 39 | 40 | ## Purpose 41 | A Professional Tool to help users with getting pre-existing devices enrolled into MDM. 42 | 43 | ## Screenshots 44 | 45 | ### DEP 46 | ![Screenshot DEP](/images/ss_dep.png?raw=true) 47 | 48 | ### Manual 49 | ![Screenshot Manual](/images/ss_manual.png?raw=true) 50 | 51 | ### UAMDM 52 | ![Screenshot UAMDM](/images/ss_uamdm.png?raw=true) 53 | 54 | ### Simplified Diagram 55 | ![Simplified Diagram](/images/umad_diagram.png?raw=true) 56 | 57 | ### Notes 58 | You will need to use [munki-pkg](https://github.com/munki/munki-pkg) to build this package. 59 | 60 | Because of the way git works, umad will not contain the `Logs` folder required for the postinstall to complete. 61 | In order to create a properly working package, you will need to run the following command: 62 | `munkipkg --sync /path/to/cloned_repo/mdm/umad` 63 | 64 | ## OS Support v1 65 | The following operating system and versions have been tested. 66 | - 10.10.0 [Note 1](https://github.com/AnotherToolAppleShouldHaveProvided/umad/issues/11), 10.10.5 - [Note 2](https://github.com/AnotherToolAppleShouldHaveProvided/umad/issues/10) 67 | - 10.11.0, 10.11.6 68 | - 10.12.0, 10.12.6 (10.12 is very unreliable with DEP nagging) 69 | - 10.13.0 10.13.3, 10.13.6 70 | - 10.14.0 71 | - 10.15 72 | 73 | ## OS Support v2 (embedded python) 74 | The following operating system and versions have been tested with the embedded python. 75 | - 10.14 76 | - 10.15 77 | 78 | ## Getting started 79 | To start, you can use the default settings in `/Library/LaunchAgent/com.anothertoolappleshouldhaveprovided.umad.plist` 80 | 81 | Essentially every component of the UI is customizable, using the above LaunchAgent. 82 | * Create your .pkg with munki-pkg and install on your target workstation. 83 | * Open terminal. 84 | example 85 | 86 | `/Library/Application Support/umad/Resources/umad --cutoffdate 2018-9-7-17:00 87 | ` 88 | sets the cutoff date to September 7th at 5pm 89 | 90 | ### Cutoff date 91 | Cut off date in UTC. 92 | 93 | ```xml 94 | --cutoffdate 95 | 2018-12-31-17:00 96 | ``` 97 | 98 | ### Cut off date warning 99 | This is the number, in days, of when to start the initial UI warning. When this set of days passes, the user will be required to hit an "I Understand" button, followed by the "Close" button to exit out of the UI. 100 | 101 | ```xml 102 | --cutoffdatewarning 103 | 14 104 | ``` 105 | 106 | ### Due date text 107 | This is the bolded portion of the UI towards the top under the ["titletext".](#title-text) 108 | 109 | ```xml 110 | --duedatetext 111 | MDM Enrollment is required by 12/31/2018 (No Restart Required) 112 | ``` 113 | 114 | ### DEP failure text 115 | If a user has a DEP capable device, but they are past the enrollment window, they will have an option to manually enroll. 116 | 117 | This is the first set of text above the enrollment button. 118 | 119 | ```xml 120 | --depfailuretext 121 | Not getting this notification? 122 | ``` 123 | 124 | ### DEP failure subtext 125 | If a user has a DEP capable device, but they are past the enrollment window, they will have an option to manually enroll. 126 | 127 | This is the second set of text above the enrollment button. 128 | 129 | ```xml 130 | --depfailuresubtext 131 | You can also enroll manually below: 132 | ``` 133 | 134 | ### Enable enrollment button 135 | Always show the manual enrollment button, DEP or not. 136 | 137 | ```xml 138 | --enableenrollmentbutton 139 | ``` 140 | 141 | ### Honor DND settings 142 | If a device is DEP capable, umad will not honor DoNotDisturb settings so the nag can actually appear. 143 | 144 | If the admin wants to honor DoNotDisturb for DEP devices, use this feature. 145 | 146 | Non-DEP devices will honor the users DND settings 147 | 148 | ```xml 149 | --honordndsettings 150 | ``` 151 | 152 | ### Logo path 153 | You can replace the included company_logo.png with your own company_logo.png or you can configure a custom Path 154 | with the following string: 155 | 156 | ```xml 157 | --logopath 158 | /Some/Custom/Path/company_logo.png 159 | ``` 160 | 161 | ### Manual enrollment text 162 | If a user does not have a DEP capable device, they will have the option to manually enroll. 163 | Authentication may be required for manual enrollment. 164 | 165 | This is the bolded text that takes place of the DEP or UAMDM screenshot. 166 | 167 | ```xml 168 | --manualenrollmenttext 169 | Manual Enrollment Required 170 | ``` 171 | 172 | ### Manual enrollment h1 text 173 | If a user does not have a DEP capable device, they will have the option to manually enroll. 174 | Authentication may be required for manual enrollment. 175 | 176 | This is the first set of text above the enrollment button. 177 | 178 | ```xml 179 | --manualenrollh1text 180 | Want this box to go away? 181 | ``` 182 | 183 | ### Manual enrollment h2 text 184 | If a user does not have a DEP capable device, they will have the option to manually enroll. 185 | Authentication may be required for manual enrollment. 186 | 187 | This is the second set of text above the enrollment button. 188 | 189 | ```xml 190 | --manualenrollh2text 191 | Click on the Manual Enrollment button below. 192 | ``` 193 | 194 | ### Manual enrollment URL 195 | Configure the Manual Enrollment button with a custom URL. 196 | ```xml 197 | --manualenrollmenturl 198 | https://apple.com 199 | ``` 200 | 201 | ### More info URL 202 | When you see the Manual Enrollment button, you can customize a URL directing the users to more information. 203 | ```xml 204 | --moreinfourl 205 | https://google.com 206 | ``` 207 | 208 | ### Nag screenshot path 209 | You can modify the LaunchAgent adding your custom path or just replace the included nag_ss.png with your own .png. 210 | (remember to name the file nag_ss.png if you are not using a custom path) 211 | ```xml 212 | --nagsspath 213 | /Some/Custom/Path/nag_ss.png 214 | ``` 215 | 216 | ### No timer 217 | Use this setting if you DO NOT want to restore the umad GUI to the front of a user's window. 218 | 219 | ```xml 220 | --notimer 221 | ``` 222 | 223 | ### Paragraph 1 text 224 | This is the text for the first paragraph. 160 character limit. 225 | ```xml 226 | --paragraph1 227 | If you do not enroll into MDM you will lose the ability to connect to Wi-Fi, VPN and Managed Software Center. 228 | ``` 229 | 230 | ### Paragraph 2 text 231 | This is the text for the second paragraph. 160 character limit. 232 | ```xml 233 | --paragraph2 234 | To enroll, just look for the below notification, and click Details. Once prompted, log in with your username and password. 235 | ``` 236 | 237 | ### Paragraph 2 text 238 | This is the text for the third paragraph. 160 character limit. 239 | ```xml 240 | --paragraph3 241 | To enroll, just look for the below notification, and click Details. Once prompted, log in with your OneLogin username and password. 242 | ``` 243 | 244 | ### Profile identifier 245 | This is the profile identifier for < 10.13 machines to check for enrollment. Should you not set this value, umad will attempt to look for a profile installed on the machine with the _PayloadType_ of `com.apple.mdm` 246 | 247 | ```xml 248 | --profileidentifier 249 | B68ABF1E-70E2-43B0-8300-AE65F9AFA330 250 | ``` 251 | 252 | To get this value, run the following command on a computer with your MDM profile installed: `profiles -C -o stdout-xml` 253 | 254 | Look for the MDM profile and notate the identifier. Some MDMs may use a UUID for this value. 255 | 256 | Some examples: 257 | ```xml 258 | 259 | ProfileDescription 260 | MDM profile 261 | ProfileDisplayName 262 | MDM Profile 263 | ProfileIdentifier 264 | 220cad8d-c273-422f-afcb-9740857b38a0 265 | 266 | ``` 267 | 268 | ```xml 269 | 270 | ProfileDescription 271 | MDM profile 272 | ProfileDisplayName 273 | MDM Profile 274 | ProfileIdentifier 275 | com.awesome.mdm.profile 276 | 277 | ``` 278 | 279 | ### Sub-title text 280 | This is the text right under the main title. 281 | ```xml 282 | --subtitletext 283 | A friendly reminder from your local IT team 284 | ``` 285 | 286 | ### System Preferences H1 text 287 | Should the user have a 10.13.4+ device that is not User Approved MDM, they will be notified that they need to approve the MDM. 288 | 289 | This is the first set of text above the system preferences button. 290 | ```xml 291 | --sysprefsh1text 292 | Want this box to go away? 293 | ``` 294 | 295 | ### System Preferences H2 text 296 | Should the user have a 10.13.4+ device that is not User Approved MDM, they will be notified that they need to approve the MDM. 297 | 298 | This is the second set of text above the system preferences button. 299 | ```xml 300 | --sysprefsh2text 301 | Open System Preferences and approve Device Management. 302 | ``` 303 | 304 | ### Title text 305 | This is the main, bolded text at the very top. 306 | ```xml 307 | --titletext 308 | MDM Enrollment 309 | ``` 310 | 311 | ### Timer Day 1 312 | The time, in seconds, to restore the umad GUI to the front of a user's window. This will occur indefinitely until the UI is closed or MDM is enrolled. 313 | 314 | When the MDM cutoff date is one day or less, this timer becomes active. 315 | ```xml 316 | --timerday1 317 | 600 318 | ``` 319 | 320 | ### Timer Day 3 321 | The time, in seconds, to restore the umad GUI to the front of a user's window. This will occur indefinitely until the UI is closed or MDM is enrolled. 322 | 323 | When the MDM cutoff date is three days or less from current date. 324 | ```xml 325 | --timerday3 326 | 7200 327 | ``` 328 | 329 | ### Timer Elapsed 330 | After the user interacts with umad GUI, (such as clicking the "I understand" button) timer elapsed controls when the UI 331 | will display again. 332 | 333 | This will occur indefinitely until the MDM is enrolled. 334 | ```xml 335 | --timerelapsed 336 | 10 337 | ``` 338 | 339 | ### Timer Final 340 | The time, in seconds, to restore the umad GUI to the front of a user's window. This will occur indefinitely until the UI is closed or MDM is enrolled. 341 | 342 | This is when the MDM cutoff date is one hour or less 343 | ```xml 344 | --timerfinal 345 | 60 346 | ``` 347 | 348 | ### Timer Initial 349 | The time, in seconds, to restore the umad GUI to the front of a user's window. This will occur indefinitely until the UI is closed or MDM is enrolled. 350 | 351 | When the MDM cutoff date is over three days. 352 | ```xml 353 | --timerinital 354 | 14400 355 | ``` 356 | 357 | ### Timer MDM 358 | The time, in seconds, to check if the device is enrolled into MDM. 359 | 360 | ```xml 361 | --timermdm 362 | 5 363 | ``` 364 | 365 | ### User Approved MDM paragraph 1 text 366 | This is the text for the first paragraph on the user Approved MDM UI. 367 | ```xml 368 | --uamdmparagraph1 369 | Thank you for enrolling your device into MDM. We sincerely appreciate you doing this in a timely manner. 370 | ``` 371 | 372 | ### User Approved MDM paragraph 2 text 373 | This is the text for the second paragraph on the user Approved MDM UI. 374 | ```xml 375 | --uamdmparagraph2 376 | Unfortunately, your device has been detected as only partially enrolled into our system. 377 | ``` 378 | 379 | ### User Approved MDM paragraph 3 text 380 | This is the text for the third paragraph on the user Approved MDM UI. 381 | ```xml 382 | --uamdmparagraph3 383 | Please go to System Preferences -> Profiles, click on the Device Enrollment profile and click on the approve button. 384 | ``` 385 | 386 | ### User Approved MDM screenshot path 387 | You can customize the uamdm screenshot path. Option 2, just replace the included uamdm_ss.png with your own .png. Make sure you name the .png the same as the original and place it back into `umad/Resources/` . 388 | ```xml 389 | --uasspath 390 | /Some/Custom/Path/uamdm_ss.png 391 | ``` 392 | 393 | ## Tips, Tricks, and Troubleshooting 394 | 395 | * I made changes to the default LaunchAgent and now the UI isn't appearing? 396 | 397 | Make sure you unload, and reload the LaunchAgent after making changes. 398 | 399 | * Where is the logging located? 400 | 401 | `/Library/Application Support/umad/umad.log` 402 | 403 | * Why isn't the log file there? 404 | 405 | Remember to unload and reload the LaunchAgent. 406 | 407 | 408 | ## Credits 409 | This tool would not be possible without [nibbler](https://github.com/pudquick/nibbler), written by [Michael Lynn](https://twitter.com/mikeymikey) 410 | -------------------------------------------------------------------------------- /build-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "distribution_style": true, 3 | "identifier": "com.erikng.umad", 4 | "install_location": "/", 5 | "name": "umad-${version}.pkg", 6 | "ownership": "recommended", 7 | "postinstall_action": "none", 8 | "suppress_bundle_relocation": true, 9 | "version": "2.0" 10 | } 11 | -------------------------------------------------------------------------------- /build_python_framework: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # 3 | # Build script for Python 3 framework for UMAD 4 | # Taken from https://github.com/munki/munki/blob/Munki3dev/code/tools/build_python_framework.sh 5 | 6 | # IMPORTANT 7 | # Run this with your current directory being the path where this script is located 8 | 9 | TOOLSDIR=$(dirname $0) 10 | REQUIREMENTS="${TOOLSDIR}/py3_requirements.txt" 11 | PYTHON_VERSION=3.8.0 12 | PYTHONTOOLDIR="/tmp/relocatable-python-git" 13 | CONSOLEUSER=$(/usr/bin/stat -f "%Su" /dev/console) 14 | FRAMEWORKDIR="/Library/umad" 15 | 16 | # Sanity checks. 17 | GIT=$(which git) 18 | WHICH_GIT_RESULT="$?" 19 | if [ "${WHICH_GIT_RESULT}" != "0" ]; then 20 | echo "Could not find git in command path. Maybe it's not installed?" 1>&2 21 | echo "You can get a Git package here:" 1>&2 22 | echo " https://git-scm.com/download/mac" 23 | exit 1 24 | fi 25 | if [ ! -f "${REQUIREMENTS}" ]; then 26 | echo "Missing requirements file at ${REQUIREMENTS}." 1>&2 27 | exit 1 28 | fi 29 | 30 | # Create CPE framework path if not present 31 | if [ ! -d "${FRAMEWORKDIR}" ]; then 32 | /usr/bin/sudo /bin/mkdir -p "${FRAMEWORKDIR}" 33 | fi 34 | 35 | # remove existing library Python.framework if present 36 | if [ -d "${FRAMEWORKDIR}/Python.framework" ]; then 37 | /usr/bin/sudo /bin/rm -rf "${FRAMEWORKDIR}/Python.framework" 38 | fi 39 | 40 | # clone our relocatable-python tool 41 | if [ -d "${PYTHONTOOLDIR}" ]; then 42 | /usr/bin/sudo /bin/rm -rf "${PYTHONTOOLDIR}" 43 | fi 44 | echo "Cloning relocatable-python tool from github..." 45 | git clone https://github.com/gregneagle/relocatable-python.git "${PYTHONTOOLDIR}" 46 | CLONE_RESULT="$?" 47 | if [ "${CLONE_RESULT}" != "0" ]; then 48 | echo "Error cloning relocatable-python tool repo: ${CLONE_RESULT}" 1>&2 49 | exit 1 50 | fi 51 | 52 | # remove existing munki-pkg Python.framework if present 53 | if [ -d "$TOOLSDIR/payload/${FRAMEWORKDIR}/Python.framework" ]; then 54 | /bin/rm -rf "$TOOLSDIR/payload/${FRAMEWORKDIR}/Python.framework" 55 | fi 56 | 57 | # build the framework 58 | /usr/bin/sudo "${PYTHONTOOLDIR}/make_relocatable_python_framework.py" \ 59 | --python-version "${PYTHON_VERSION}" \ 60 | --pip-requirements "${REQUIREMENTS}" \ 61 | --destination "${FRAMEWORKDIR}" 62 | 63 | # move the framework 64 | echo "Moving Python.framework to umad munki-pkg payload folder" 65 | /usr/bin/sudo /bin/mv "${FRAMEWORKDIR}/Python.framework" "$TOOLSDIR/payload/${FRAMEWORKDIR}" 66 | 67 | # take ownership of the payload folder 68 | echo "Taking ownership of the file to not break git" 69 | /usr/bin/sudo /usr/sbin/chown -R ${CONSOLEUSER}:wheel "$TOOLSDIR/payload/${FRAMEWORKDIR}/Python.framework" 70 | -------------------------------------------------------------------------------- /images/ss_dep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/images/ss_dep.png -------------------------------------------------------------------------------- /images/ss_manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/images/ss_manual.png -------------------------------------------------------------------------------- /images/ss_uamdm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/images/ss_uamdm.png -------------------------------------------------------------------------------- /images/umad_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/images/umad_diagram.png -------------------------------------------------------------------------------- /payload/Library/LaunchAgents/com.erikng.umad.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.erikng.umad 7 | LimitLoadToSessionType 8 | 9 | Aqua 10 | 11 | ProgramArguments 12 | 13 | /Library/umad/Resources/umad 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | RunAtLoad 90 | 91 | StandardOutPath 92 | /Library/umad/Logs/umad.log 93 | StandardErrorPath 94 | /Library/umad/Logs/umad.log 95 | StartCalendarInterval 96 | 97 | 98 | Minute 99 | 0 100 | 101 | 102 | Minute 103 | 30 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /payload/Library/LaunchDaemons/com.erikng.umad.check_dep_record.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.erikng.umad.check_dep_record 7 | ProgramArguments 8 | 9 | /Library/umad/Resources/umad_check_dep_record 10 | 11 | KeepAlive 12 | 13 | PathState 14 | 15 | /var/tmp/umad/.check_dep_record 16 | 17 | 18 | 19 | OnDemand 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /payload/Library/LaunchDaemons/com.erikng.umad.trigger_nag.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.erikng.umad.trigger_nag 7 | ProgramArguments 8 | 9 | /Library/umad/Resources/umad_trigger_nag 10 | 11 | KeepAlive 12 | 13 | PathState 14 | 15 | /var/tmp/umad/.trigger_nag 16 | 17 | 18 | 19 | OnDemand 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /payload/Library/umad/Logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /payload/Library/umad/Resources/company_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/payload/Library/umad/Resources/company_logo.png -------------------------------------------------------------------------------- /payload/Library/umad/Resources/nag_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/payload/Library/umad/Resources/nag_ss.png -------------------------------------------------------------------------------- /payload/Library/umad/Resources/nibbler.py: -------------------------------------------------------------------------------- 1 | # Use the "Identifier" property of your control in Interface Builder and give 2 | # your controls a name. Then use the 'attach' method on your Nibbler to link 3 | # the control to a python function 4 | 5 | from Foundation import NSObject, NSBundle 6 | from AppKit import NSNib, NSApp, NSApplication 7 | import objc 8 | import os 9 | import os.path 10 | import types 11 | 12 | from ctypes import CDLL, Structure, POINTER, c_uint32, byref 13 | from ctypes.util import find_library 14 | 15 | 16 | class ProcessSerialNumber(Structure): 17 | _fields_ = [('highLongOfPSN', c_uint32), ('lowLongOfPSN', c_uint32)] 18 | 19 | 20 | kCurrentProcess = 2 21 | kProcessTransformToForegroundApplication = 1 22 | kProcessTransformToUIElementAppication = 4 23 | ApplicationServices = CDLL(find_library('ApplicationServices')) 24 | TransformProcessType = ApplicationServices.TransformProcessType 25 | TransformProcessType.argtypes = [POINTER(ProcessSerialNumber), c_uint32] 26 | 27 | 28 | def views_recursive(view_obj): 29 | yield view_obj 30 | for x in view_obj.subviews(): 31 | for y in views_recursive(x): 32 | yield y 33 | 34 | 35 | def views_dict(nib_obj): 36 | # Find the NSWindow instance at the top level 37 | all_windows = [x for x in nib_obj if x.className() == 'NSWindow'] 38 | win = all_windows[0] 39 | # Now find all the views within the window where the identifier is defined 40 | top_view = win.contentView() 41 | v_dict = dict() 42 | for v in views_recursive(top_view): 43 | ident = v.identifier() 44 | if ident is not None: 45 | if not ident.startswith('_'): 46 | # Someone has customized it, remember it 47 | v_dict[ident] = v 48 | return v_dict 49 | 50 | 51 | def quit_app(): 52 | NSApplication.sharedApplication().terminate_(None) 53 | 54 | 55 | class genericController(NSObject): 56 | def setTheThing_(self, f_obj): 57 | self.f = f_obj 58 | 59 | def doTheThing_(self, sender): 60 | if hasattr(self, 'f'): 61 | self.f() 62 | 63 | 64 | def func_to_controller_selector(f_obj): 65 | o = genericController.alloc().init() 66 | o.setTheThing_(f_obj) 67 | return o 68 | 69 | 70 | class Nibbler(object): 71 | def __init__(self, path): 72 | bundle = NSBundle.mainBundle() 73 | info = bundle.localizedInfoDictionary() or bundle.infoDictionary() 74 | # Did you know you can override parts of infoDictionary (Info.plist, 75 | # after loading) even though Apple says it's read-only? 76 | info['LSUIElement'] = '1' 77 | # Initialize our shared application instance 78 | NSApplication.sharedApplication() 79 | # Two possibilities here 80 | # Either the path is a directory and we really want the file inside it 81 | # or the path is just a real .nib file 82 | if os.path.isdir(path): 83 | # Ok, so they just saved it from Xcode, not their fault 84 | # let's fix the path 85 | path = os.path.join(path, 'keyedobjects.nib') 86 | with open(path, 'rb') as f: 87 | # get nib bytes 88 | buffer = memoryview 89 | d = buffer(f.read()) 90 | n_obj = NSNib.alloc().initWithNibData_bundle_(d, None) 91 | placeholder_obj = NSObject.alloc().init() 92 | result, n = n_obj.instantiateWithOwner_topLevelObjects_( 93 | placeholder_obj, None) 94 | self.hidden = True 95 | self.nib_contents = n 96 | self.win = [ 97 | x for x in self.nib_contents if x.className() == 'NSWindow'][0] 98 | self.views = views_dict(self.nib_contents) 99 | self._attached = [] 100 | 101 | def attach(self, func, identifier_label): 102 | # look up the object with the identifer provided 103 | o = self.views[identifier_label] 104 | # get the classname of the object and handle appropriately 105 | o_class = o.className() 106 | if o_class == 'NSButton': 107 | # Wow, we actually know how to do this one 108 | temp = func_to_controller_selector(func) 109 | # hold onto it 110 | self._attached.append(temp) 111 | o.setTarget_(temp) 112 | # button.setAction_(objc.selector(controller.buttonClicked_, 113 | # signature='v@:')) 114 | o.setAction_(temp.doTheThing_) 115 | 116 | def run(self): 117 | if self.hidden: 118 | psn = ProcessSerialNumber(0, kCurrentProcess) 119 | ApplicationServices.TransformProcessType( 120 | psn, kProcessTransformToUIElementAppication) 121 | else: 122 | psn = ProcessSerialNumber(0, kCurrentProcess) 123 | ApplicationServices.TransformProcessType( 124 | psn, kProcessTransformToForegroundApplication) 125 | self.win.makeKeyAndOrderFront_(None) 126 | self.win.display() 127 | NSApp.activateIgnoringOtherApps_(True) 128 | NSApp.run() 129 | 130 | def quit(self): 131 | NSApplication.sharedApplication().terminate_(None) 132 | -------------------------------------------------------------------------------- /payload/Library/umad/Resources/uamdm_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/payload/Library/umad/Resources/uamdm_ss.png -------------------------------------------------------------------------------- /payload/Library/umad/Resources/umad: -------------------------------------------------------------------------------- 1 | #!/Library/umad/Python.framework/Versions/3.8/bin/python3 2 | # -*- coding: utf-8 -*- 3 | '''umad - python wrapper for UAMDM/DEP enrollment and annoying the users to 4 | actually enroll.''' 5 | import optparse 6 | import os 7 | import platform 8 | import plistlib 9 | import sqlite3 10 | import subprocess 11 | import time 12 | import webbrowser 13 | from datetime import datetime 14 | from distutils.version import LooseVersion 15 | import Foundation 16 | import objc 17 | from AppKit import NSApplication, NSImage 18 | from Foundation import (CFPreferencesAppSynchronize, CFPreferencesCopyAppValue, 19 | CFPreferencesCopyValue, CFPreferencesSetValue, 20 | CFPreferencesSynchronize, NSBundle, 21 | NSUserNotificationCenter, kCFPreferencesCurrentHost, 22 | kCFPreferencesCurrentUser) 23 | from SystemConfiguration import SCDynamicStoreCopyConsoleUser 24 | 25 | from nibbler import * 26 | 27 | 28 | class timerController(Foundation.NSObject): 29 | '''Thanks to frogor for help in figuring this part out''' 30 | def activateWindow_(self, timer_obj): 31 | umadlog('Re-activating .nib to the foreground') 32 | # Move the application to the front 33 | NSApplication.sharedApplication().activateIgnoringOtherApps_(True) 34 | # Move the main window to the front 35 | # Nibbler objects have a .win property (...should probably be .window) 36 | # that contains a reference to the first NSWindow it finds 37 | umad.win.makeKeyAndOrderFront_(None) 38 | 39 | 40 | class mdmTimerController(Foundation.NSObject): 41 | '''The MDM timer controller''' 42 | def checkMDMStatus_(self, timer_obj): 43 | '''check mdm status in a timer''' 44 | check_mdm_status(True) 45 | 46 | 47 | def button_moreinfo(): 48 | '''Open browser more info button''' 49 | webbrowser.open_new_tab(moreinfourl) 50 | 51 | 52 | def button_manualenrollment(): 53 | '''Open browser manual enrollment button''' 54 | webbrowser.open_new_tab(manualenrollmenturl) 55 | 56 | 57 | def button_ok(): 58 | '''Quit out of umad if user hits the ok button''' 59 | umad.quit() 60 | 61 | 62 | def button_sysprefs(): 63 | '''Open System Preferences''' 64 | cmd = [ 65 | '/usr/bin/open', '/System/Library/PreferencePanes/Profiles.prefPane'] 66 | subprocess.Popen(cmd) 67 | 68 | 69 | def button_understand(): 70 | '''Add an extra button to force the user to read the dialog, prior to being 71 | able to exit the UI.''' 72 | umad.views['button.understand'].setHidden_(True) 73 | umad.views['button.ok'].setHidden_(False) 74 | umad.views['button.ok'].setEnabled_(True) 75 | 76 | 77 | def check_mdm_status(umadupdate): 78 | '''Check MDM Status''' 79 | uamdm_enrolled = False 80 | mdm_enrolled = False 81 | # Check the OS and run our dep checks based on OS version 82 | if get_os_version() >= LooseVersion('10.13.4'): 83 | umadlog('Checking mdm status - modern') 84 | if check_mdm_status_modern()[2]: 85 | umadlog('MDM enrolled device %s' % get_os_version()) 86 | uamdm_enrolled = True 87 | if umadupdate: 88 | umad.quit() 89 | else: 90 | # Check if MDM is installed. 91 | if check_mdm_status_modern()[1]: 92 | umadlog('Non-UAMDM enrolled device, trigger UAMDM UI') 93 | mdm_enrolled = True 94 | uamdm_enrolled = False 95 | if umadupdate: 96 | update_umad_ui_uamdm(uamdmparagraph1, uamdmparagraph2, 97 | uamdmparagraph3) 98 | else: 99 | # Anything lower than 10.13.4, we just check if the profile is 100 | # installed 101 | umadlog('Checking mdm status - legacy') 102 | if check_mdm_legacy(mdm_profile_identifier): 103 | mdm_enrolled = True 104 | umadlog('MDM enrolled device %s' % get_os_version()) 105 | if umadupdate: 106 | umad.quit() 107 | return uamdm_enrolled, mdm_enrolled 108 | 109 | 110 | def check_mdm_legacy(mdm_profile_identifier): 111 | '''Check MDM enrollment for older machines''' 112 | check_payload_type = False 113 | if mdm_profile_identifier == 'B68ABF1E-70E2-43B0-8300-AE65F9AFA330': 114 | umadlog('WARN - Did not set mdm profile identifier!') 115 | check_payload_type = True 116 | cmd = ['/usr/bin/profiles', '-C', '-o', 'stdout-xml'] 117 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 118 | output, err = run.communicate() 119 | if check_payload_type: 120 | try: 121 | plist = plistlib.loads(output) 122 | except: # noqa 123 | plist = {'_computerlevel': []} 124 | try: 125 | for possible_plist in plist['_computerlevel']: 126 | for item_content in possible_plist['ProfileItems']: 127 | try: 128 | profile_type = item_content['PayloadType'] 129 | except KeyError: 130 | profile_type = '' 131 | if profile_type == 'com.apple.mdm': 132 | return True 133 | return False 134 | except KeyError: 135 | return False 136 | else: 137 | try: 138 | plist = plistlib.loads(output) 139 | except: # noqa 140 | plist = {'_computerlevel': []} 141 | try: 142 | for possible_plist in plist['_computerlevel']: 143 | try: 144 | profile_uuid = possible_plist['ProfileIdentifier'] 145 | except KeyError: 146 | profile_uuid = '' 147 | if profile_uuid == mdm_profile_identifier: 148 | return True 149 | return False 150 | except KeyError: 151 | return False 152 | 153 | 154 | def check_mdm_high_sierra_legacy(): 155 | '''Only for 10.13.0 -> 10.13.3''' 156 | enrolled = 'An enrollment profile is currently installed on this system' 157 | not_enrolled = 'There is no enrollment profile installed on this system' 158 | cmd = ['/usr/bin/profiles', 'status', '-type', 'enrollment'] 159 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 160 | output, err = run.communicate() 161 | status = output.split(b'\n')[0] 162 | if enrolled in status: 163 | return True 164 | elif not_enrolled in status: 165 | return False 166 | return False 167 | 168 | 169 | def check_mdm_status_modern(): 170 | '''Only for 10.13.4 and higher''' 171 | dep_enrolled = b'Enrolled via DEP: Yes' 172 | # dep_not_enrolled = 'Enrolled via DEP: No' 173 | uamdm_enrolled = b'MDM enrollment: Yes (User Approved)' 174 | # uamdm_not_enroll = 'MDM enrollment: Yes' 175 | mdm_not_enrolled = b'MDM enrollment: No' 176 | cmd = ['/usr/bin/profiles', 'status', '-type', 'enrollment'] 177 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 178 | output, err = run.communicate() 179 | dep_status = output.split(b'\n')[0] 180 | mdm_status = output.split(b'\n')[1] 181 | dep_enrollment_status = bool(dep_enrolled == dep_status) 182 | mdm_enrollment_status = bool(mdm_not_enrolled != mdm_status) 183 | uamdm_enrollment_status = bool(uamdm_enrolled == mdm_status) 184 | return (dep_enrollment_status, mdm_enrollment_status, 185 | uamdm_enrollment_status) 186 | 187 | 188 | def do_not_disturb_isset(): 189 | '''Check if DND is set''' 190 | bundle_id = 'com.apple.notificationcenterui' 191 | do_not_disturb = CFPreferencesCopyAppValue('doNotDisturb', bundle_id) 192 | do_not_disturb_by_host = CFPreferencesCopyValue('doNotDisturb', bundle_id, 193 | kCFPreferencesCurrentUser, 194 | kCFPreferencesCurrentHost) 195 | return bool(do_not_disturb or do_not_disturb_by_host) 196 | 197 | 198 | def do_not_disturb_set_value(dnd_on_or_off): 199 | '''Enable or Disable DND''' 200 | bundle_id = 'com.apple.notificationcenterui' 201 | CFPreferencesSetValue('doNotDisturb', dnd_on_or_off, bundle_id, 202 | kCFPreferencesCurrentUser, 203 | kCFPreferencesCurrentHost) 204 | CFPreferencesSynchronize(bundle_id, 205 | kCFPreferencesCurrentUser, 206 | kCFPreferencesCurrentHost) 207 | CFPreferencesSetValue('doNotDisturb', dnd_on_or_off, bundle_id, 208 | kCFPreferencesCurrentUser, 209 | kCFPreferencesCurrentHost) 210 | CFPreferencesAppSynchronize(bundle_id) 211 | 212 | 213 | def get_all_notifications_legacy(): 214 | '''Parse old notification db 215 | Largely inspired from https://github.com/ydkhatri/MacForensics''' 216 | try: 217 | db_path = b'com.apple.notificationcenter/db/db' 218 | input_path = os.path.join(get_user_temp_dir(), db_path) 219 | if os.path.exists(input_path): 220 | conn = sqlite3.connect(input_path) 221 | db_items = [] 222 | conn.row_factory = sqlite3.Row 223 | cursor = conn.execute( 224 | 'SELECT date_presented as time, '\ 225 | '(SELECT bundleid from app_info WHERE app_info.app_id = '\ 226 | 'presented_notifications.app_id) AS bundle, (SELECT encoded_data '\ 227 | 'from notifications WHERE notifications.note_id = '\ 228 | 'presented_notifications.note_id) AS data from '\ 229 | 'presented_notifications ') 230 | for row in cursor: 231 | data = {} 232 | if get_os_version() >= LooseVersion('10.11'): 233 | plist = plistlib.loads(row['data']) 234 | title = plist['$objects'][2] 235 | message = plist['$objects'][3] 236 | data['message'] = message 237 | data['title'] = title 238 | date = row['time'] 239 | date_time = datetime.utcfromtimestamp(date + 978307200) 240 | app = row['bundle'] 241 | data['app'] = app 242 | data['date'] = date_time 243 | db_items.append(data) 244 | conn.close() 245 | return db_items 246 | except (sqlite3.OperationalError, IndexError, TypeError, KeyError): 247 | return [] 248 | return [] 249 | 250 | 251 | def get_all_notifications_modern(): 252 | '''Parse High Sierra's notification db 253 | Largely inspired from https://github.com/ydkhatri/MacForensics''' 254 | try: 255 | db_path = b'com.apple.notificationcenter/db2/db' 256 | input_path = os.path.join(get_user_temp_dir(), db_path) 257 | if os.path.exists(input_path): 258 | conn = sqlite3.connect(input_path) 259 | db_items = [] 260 | conn.row_factory = sqlite3.Row 261 | cursor = conn.execute( 262 | 'SELECT (SELECT identifier from app where '\ 263 | 'app.app_id=record.app_id) as app, data, presented, '\ 264 | 'delivered_date FROM record') 265 | for row in cursor: 266 | plist = plistlib.loads(row['data']) 267 | plist_data = plist['req'] 268 | date = row['delivered_date'] 269 | date_time = datetime.utcfromtimestamp(date + 978307200) 270 | if date == None: 271 | # This avoids a type error for apps that put entries with 272 | # blank delivery dates in the notification DB for syncing purposes. 273 | # We don't care about those anyway. 274 | continue 275 | app = row['app'] 276 | title = plist_data.get('titl', '') 277 | message = plist_data.get('body', '') 278 | data = {} 279 | data['app'] = app 280 | data['date'] = date_time 281 | data['message'] = message 282 | data['title'] = title 283 | db_items.append(data) 284 | conn.close() 285 | return db_items 286 | except (sqlite3.OperationalError, IndexError, TypeError, KeyError): 287 | return [] 288 | return [] 289 | 290 | 291 | def get_console_username_info(): 292 | '''Uses Apple's SystemConfiguration framework to get the current 293 | console username''' 294 | return SCDynamicStoreCopyConsoleUser(None, None, None) 295 | 296 | 297 | def get_os_version(): 298 | '''Return OS version.''' 299 | return LooseVersion(platform.mac_ver()[0]) 300 | 301 | 302 | def umadlog(text): 303 | '''logger for umad''' 304 | Foundation.NSLog('[UMAD] ' + text) 305 | 306 | 307 | def get_parsed_options(): 308 | '''Return the parsed options and args for this application.''' 309 | # Options 310 | usage = '%prog [options]' 311 | o = optparse.OptionParser(usage=usage) 312 | o.add_option('--cutoffdate', 313 | help=('Required: UTC cutoff date 2018-12-31-17:00.')) 314 | o.add_option('--cutoffdatewarning', 315 | default=3, 316 | help=('Optional: Days from cutoff date to start warning.')) 317 | o.add_option('--depfailuretext', 318 | default='Not getting this notification?', 319 | help=('Optional: DEP failure text.')) 320 | o.add_option('--depfailuresubtext', 321 | default='You can also enroll manually below:', 322 | help=('Optional: DEP failure sub text.')) 323 | o.add_option('--duedatetext', 324 | default='MDM Enrollment is required (No Restart Required)', 325 | help=('Required: Due date text.')) 326 | o.add_option('--enableenrollmentbutton', default=False, 327 | help='Optional: Enable enrollment button for device', 328 | action='store_true') 329 | o.add_option('--honordndsettings', default=False, 330 | help='Optional: Honor user DND settings (dont do this!)', 331 | action='store_true') 332 | o.add_option('--logopath', 333 | default='company_logo.png', 334 | help=('Optional: Path to company logo.')) 335 | o.add_option('--disablemanualenrollmentfordep', default=False, 336 | help='Optional: Disable the manual enrollment button for DEP devices', 337 | action='store_true') 338 | o.add_option('--manualenrollmenturl', 339 | default='https://apple.com', 340 | help=('Required: Manual Enrollment URL.')) 341 | o.add_option('--manualenrollmenttext', 342 | default='Manual Enrollment Required', 343 | help=('Optional: Manual enrollment text.')) 344 | o.add_option('--manualenrollh1text', 345 | default='Want this box to go away?', 346 | help=('Optional: Manual enrollment text.')) 347 | o.add_option('--manualenrollh2text', 348 | default='Click on the Manual Enrollment button below.', 349 | help=('Optional: Manual enrollment text.')) 350 | o.add_option('--moreinfourl', 351 | default='https://google.com', 352 | help=('Required: More info URL.')) 353 | o.add_option('--nagsspath', 354 | default='nag_ss.png', 355 | help=('Optional: Path to nag screenshot.')) 356 | o.add_option('--notimer', default=False, 357 | help=('Optional: Do not use umad timer functionality.'), 358 | action='store_true') 359 | o.add_option('--paragraph1', 360 | default='Enrollment into MDM is required to ensure that IT ' 361 | 'can protect your computer with basic security necessities ' 362 | 'like encryption and threat detection.', 363 | help=('Required: Paragraph 1 text.')) 364 | o.add_option('--paragraph2', 365 | default='If you do not enroll into MDM you may lose access ' 366 | 'to some items necessary for your day-to-day tasks.', 367 | help=('Required: Paragraph 2 text.')) 368 | o.add_option('--paragraph3', 369 | default='To enroll, just look for the below notification, ' 370 | 'and click Details. Once prompted, log in with your ' 371 | 'username and password.', 372 | help=('Required: Paragraph 3 text.')) 373 | o.add_option('--profileidentifier', 374 | default='B68ABF1E-70E2-43B0-8300-AE65F9AFA330', 375 | help=('Required: MDM profile identifier.')) 376 | o.add_option('--subtitletext', 377 | default='A friendly reminder from your local IT team', 378 | help=('Required: Sub-title text.')) 379 | o.add_option('--sysprefsh1text', 380 | default='Want this box to go away?', 381 | help=('Required: Sys Prefs header 1 text.')) 382 | o.add_option('--sysprefsh2text', 383 | default='Open System Preferences and approve Device ' 384 | 'Management.', 385 | help=('Required: Sys Prefs header 2 text.')) 386 | o.add_option('--timerday1', 387 | default=600, 388 | help=('Optional: Time in seconds for 24-hour umad timer.')) 389 | o.add_option('--timerday3', 390 | default=7200, 391 | help=('Optional: Time in seconds for 72-hour umad timer.')) 392 | o.add_option('--timerelapsed', 393 | default=10, 394 | help=('Optional: Time in seconds for elapsed umad timer.')) 395 | o.add_option('--timerfinal', 396 | default=60, 397 | help=('Optional: Time in seconds for 1-hour umad timer.')) 398 | o.add_option('--timerinital', 399 | default=14400, 400 | help=('Optional: Time in seconds for initial umad timer.')) 401 | o.add_option('--timermdm', 402 | default=5, 403 | help=('Optional: Time in seconds for mdm check timer.')) 404 | o.add_option('--titletext', 405 | default='MDM Enrollment', 406 | help=('Optional: Title Text.')) 407 | o.add_option('--uamdmparagraph1', 408 | default='Thank you for enrolling your device into MDM. We ' 409 | 'sincerely appreciate you doing this in a timely manner.', 410 | help=('Required: UAMDM paragraph 1 text.')) 411 | o.add_option('--uamdmparagraph2', 412 | default='Unfortunately, your device has been detected as ' 413 | 'only partially enrolled into our system.', 414 | help=('Required: UAMDM paragraph 2 text.')) 415 | o.add_option('--uamdmparagraph3', 416 | default='Please go to System Preferences -> Profiles, click ' 417 | 'on the Device Enrollment profile and click on the approve ' 418 | 'button.', 419 | help=('Required: UAMDM paragraph 3 text.')) 420 | o.add_option('--uasspath', 421 | default='uamdm_ss.png', 422 | help=('Optional: Path to User Accepted MDM screenshot.')) 423 | 424 | return o.parse_args() 425 | 426 | 427 | def get_serial(): 428 | '''Get system serial number''' 429 | # Credit to Michael Lynn 430 | IOKit_bundle = Foundation.NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit') 431 | 432 | functions = [("IOServiceGetMatchingService", b"II@"), 433 | ("IOServiceMatching", b"@*"), 434 | ("IORegistryEntryCreateCFProperty", b"@I@@I"), 435 | ] 436 | 437 | objc.loadBundleFunctions(IOKit_bundle, globals(), functions) 438 | # pylint: disable=undefined-variable 439 | serial = IORegistryEntryCreateCFProperty( 440 | IOServiceGetMatchingService( 441 | 0, 442 | IOServiceMatching( 443 | "IOPlatformExpertDevice".encode("utf-8") 444 | )), 445 | Foundation.NSString.stringWithString_("IOPlatformSerialNumber"), 446 | None, 447 | 0) 448 | # pylint: enable=undefined-variable 449 | return serial 450 | 451 | 452 | def get_user_temp_dir(): 453 | '''get user's temp dir''' 454 | darwin_user_dir = subprocess.check_output( 455 | ['/usr/bin/getconf', 'DARWIN_USER_DIR']).rstrip() 456 | return darwin_user_dir 457 | 458 | 459 | def has_dep_activation_record(plist_path): 460 | '''Check if we have a dep activation record''' 461 | try: 462 | with open(plist_path, "rb") as file: 463 | plist = plistlib.load(file) 464 | except: # noqa 465 | plist = {} 466 | if not plist: 467 | return False 468 | else: 469 | return True 470 | 471 | 472 | def restart_notification_center(): 473 | '''Restart NotificationCenter by killing, letting LaunchD respawn.''' 474 | subprocess.call(['/usr/bin/killall', 'NotificationCenter']) 475 | 476 | 477 | def load_umad_globals(): 478 | '''Try to figure out the path of umad.nib and load it.''' 479 | try: 480 | # Figure out the local path of umad 481 | global umad_path 482 | umad_path = os.path.dirname(os.path.realpath(__file__)) 483 | # Setup our global umad variable to inject into our nib file 484 | global umad 485 | umad = Nibbler(os.path.join(umad_path, 'umad.nib')) 486 | except IOError: 487 | umadlog('Unable to load umad nib file!') 488 | exit(20) 489 | 490 | 491 | def umad_already_loaded(): 492 | '''Check if umad is already loaded''' 493 | umad_string = '/Library/umad/Resources/umad' 494 | cmd = ['/bin/ps', '-o', 'pid', '-o', 'command'] 495 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 496 | output, err = run.communicate() 497 | status = output.split(b'\n') 498 | current_pid = str(os.getpid()) 499 | for line in status: 500 | if bytes(umad_string, 'utf-8') in line: 501 | if bytes(current_pid, 'utf-8') in line: 502 | pass 503 | else: 504 | return True 505 | return False 506 | 507 | 508 | def touch(path): 509 | '''Touch a file''' 510 | try: 511 | touchfile = ['/usr/bin/touch', path] 512 | proc = subprocess.Popen(touchfile, stdout=subprocess.PIPE, 513 | stderr=subprocess.PIPE) 514 | touchfileoutput, err = proc.communicate() 515 | os.chmod(path, 0o777) 516 | return touchfileoutput 517 | except Exception: 518 | return None 519 | 520 | 521 | def update_umad_ui_uamdm(uamdm_p1, uamdm_p2, uamdm_p3): 522 | '''Update the umad UI for UAMDM''' 523 | umad.views['field.paragraph1'].setStringValue_(uamdm_p1) 524 | umad.views['field.paragraph2'].setStringValue_(uamdm_p2) 525 | umad.views['field.paragraph3'].setStringValue_(uamdm_p3) 526 | umad.views['image.nagscreen'].setImage_(uamdm_ss_nsimage) 527 | umad.views['button.sysprefs'].setHidden_(False) 528 | umad.views['field.manualenrollmenttext'].setHidden_(True) 529 | umad.views['image.nagscreen'].setHidden_(False) 530 | umad.views['field.depfailuretext'].setHidden_(False) 531 | umad.views['field.depfailuresubtext'].setHidden_(False) 532 | umad.views['field.depfailuretext'].setStringValue_(sysprefs_h1_text) 533 | umad.views['field.depfailuresubtext'].setStringValue_(sysprefs_h2_text) 534 | 535 | 536 | def main(): 537 | '''Main thread''' 538 | opts, _ = get_parsed_options() 539 | global mdm_profile_identifier 540 | mdm_profile_identifier = opts.profileidentifier 541 | main_umad_path = '/Library/umad' 542 | 543 | mdm_profile_set = bool( 544 | mdm_profile_identifier != 'B68ABF1E-70E2-43B0-8300-AE65F9AFA330') 545 | manual_enrollment_url_set = bool( 546 | opts.manualenrollmenturl != 'https://apple.com') 547 | more_info_url_set = bool(opts.moreinfourl != 'https://google.com') 548 | if not mdm_profile_set: 549 | umadlog('WARN - Did not set mdm profile identifier!') 550 | 551 | # Check the OS and run our dep checks based on OS version 552 | mdm_status = check_mdm_status(False) 553 | uamdm_enrolled = mdm_status[0] 554 | mdm_enrolled = mdm_status[1] 555 | 556 | if uamdm_enrolled: 557 | exit(0) 558 | elif get_os_version() < LooseVersion('10.13.4'): 559 | if mdm_enrolled: 560 | exit(0) 561 | 562 | # If we get here, device is not MDM enrolled or device is 10.13.4+ 563 | # without UAMDM - check if umad is already running 564 | if umad_already_loaded(): 565 | umadlog('umad already loaded!') 566 | exit(0) 567 | 568 | # We don't want to nag if device is UAMDM and DEP capable at the same time 569 | # - at the very least, bad things happen with jamf Pro. If the admin does 570 | # not provide the profile identifier though, we don't know if the user 571 | # is on the wrong MDM. 572 | if mdm_profile_set: 573 | skip_nag_check = bool( 574 | get_os_version() >= LooseVersion('10.13.4') and mdm_enrolled and mdm_profile_set) 575 | else: 576 | umadlog('WARN - cannot validate enrolled MDM is correct!') 577 | skip_nag_check = bool( 578 | get_os_version() >= LooseVersion('10.13.4') and mdm_enrolled) 579 | 580 | # Attempt to load our umad globals 581 | load_umad_globals() 582 | global moreinfourl 583 | moreinfourl = opts.moreinfourl 584 | global manualenrollmenturl 585 | manualenrollmenturl = opts.manualenrollmenturl 586 | global manualenrollmenttext 587 | manualenrollmenttext = opts.manualenrollmenttext 588 | global uamdmparagraph1 589 | uamdmparagraph1 = opts.uamdmparagraph1 590 | global uamdmparagraph2 591 | uamdmparagraph2 = opts.uamdmparagraph2 592 | global uamdmparagraph3 593 | uamdmparagraph3 = opts.uamdmparagraph3 594 | global sysprefs_h1_text 595 | sysprefs_h1_text = opts.sysprefsh1text 596 | global sysprefs_h2_text 597 | sysprefs_h2_text = opts.sysprefsh2text 598 | global manualenroll_h1_text 599 | manualenroll_h1_text = opts.manualenrollh1text 600 | global manualenroll_h2_text 601 | manualenroll_h2_text = opts.manualenrollh2text 602 | 603 | # Get the current username 604 | user_name, current_user_uid, _ = get_console_username_info() 605 | 606 | # Bail if we are not in a user session. 607 | if user_name in (None, 'loginwindow', '_mbsetupuser'): 608 | exit(0) 609 | 610 | umad_tmp_dir = '/private/var/tmp/umad' 611 | if not os.path.exists(umad_tmp_dir): 612 | os.makedirs(umad_tmp_dir) 613 | 614 | nag_triggered = False 615 | 616 | if skip_nag_check: 617 | # Trick logic into thinking device isn't DEP capable 618 | dep_capable = False 619 | else: 620 | # Check DND status 621 | original_dnd_status = do_not_disturb_isset() 622 | dep_plist = os.path.join(main_umad_path, 'Resources/dep_record.plist') 623 | # Because of a wonderful bug, when we check the dep activation record, 624 | # if the user hasn't enrolled, this will actually nag them, resulting 625 | # in two nags when our actual tool runs - work around this by turning 626 | # on DND even if it's not on 627 | if not original_dnd_status: 628 | do_not_disturb_set_value(True) 629 | umadlog('Temporarily enabling DND for DEP activation record check') 630 | # Restart Notification Center to have CFPreferences take effect 631 | restart_notification_center() 632 | 633 | # Either we need to nag and show the umad DEP UI or we need show manual 634 | # enrollment UI - this unfortunately requires root access 635 | trigger_path = os.path.join(umad_tmp_dir, '.check_dep_record') 636 | touch(trigger_path) 637 | while os.path.exists(trigger_path): 638 | umadlog('Waiting for DEP record check...') 639 | time.sleep(1) 640 | 641 | if has_dep_activation_record(dep_plist): 642 | dep_capable = True 643 | umadlog('Device DEP capable - True') 644 | # We need to disable DND so the nag can show up 645 | do_not_disturb_set_value(False) 646 | # get the default User Notification Center 647 | user_nc = NSUserNotificationCenter.defaultUserNotificationCenter() 648 | # remove any delivered notifications 649 | user_nc.removeAllDeliveredNotifications() 650 | else: 651 | dep_capable = False 652 | umadlog('Device DEP capable - False') 653 | 654 | if dep_capable: 655 | # Trigger Nag event 656 | # Force Notification center to refresh itself again 657 | nag_trigger_path = os.path.join(umad_tmp_dir, '.trigger_nag') 658 | touch(nag_trigger_path) 659 | while os.path.exists(nag_trigger_path): 660 | umadlog('Waiting for nag event...') 661 | time.sleep(1) 662 | restart_notification_center() 663 | # There is a bug in versions of macOS, where the nag will not show 664 | # up, even with DND turned on. If detected, show the manual 665 | # enrollment info 666 | valid_notifications = [ 667 | '_system_center_:com.apple.mdmclient', 668 | '_SYSTEM_CENTER_:com.apple.mdmclient', 669 | '_SYSTEM_CENTER_:com.apple.mdmclient.cloudconfig', 670 | 'com.apple.mdmclient.usernotifications.v2' 671 | ] 672 | if get_os_version() >= LooseVersion('10.10'): 673 | # Check for nag event every 10th second for 10 seconds 674 | retries = 100 675 | while not nag_triggered: 676 | if get_os_version() >= LooseVersion('10.13'): 677 | notifications = get_all_notifications_modern() 678 | else: 679 | notifications = get_all_notifications_legacy() 680 | if notifications: 681 | last_notification = notifications[-1]['app'] 682 | nag_triggered = bool( 683 | last_notification in valid_notifications) 684 | if nag_triggered: 685 | umadlog('Triggered nag event - True') 686 | break 687 | time.sleep(0.1) 688 | retries -= 1 689 | if retries == 0: 690 | break 691 | if not nag_triggered: 692 | umadlog('Triggered nag event - False') 693 | 694 | if opts.honordndsettings or not dep_capable: 695 | # We will honor DND settings for non-DEP devices, unless the admin 696 | # explicitly wants this- Otherwise the nag will actually disappear 697 | # immediately 698 | if original_dnd_status: 699 | umadlog('Re-enabling DND for user as it was previously set') 700 | do_not_disturb_set_value(True) 701 | # Restart Notification Center to have CFPreferences take effect 702 | restart_notification_center() 703 | 704 | # Use the paths defined, or default to pngs in the same local path of 705 | # umad 706 | for index, path in enumerate([opts.logopath, opts.nagsspath, opts.uasspath]): 707 | if path in ('company_logo.png', 'nag_ss.png', 'uamdm_ss.png'): 708 | local_png_path = os.path.join( 709 | umad_path, path).replace(' ', '%20') 710 | else: 711 | local_png_path = os.path.join(path).replace(' ', '%20') 712 | foundation_nsurl_path = Foundation.NSURL.URLWithString_( 713 | 'file:' + local_png_path) 714 | foundation_nsdata = Foundation.NSData.dataWithContentsOfURL_( 715 | foundation_nsurl_path) 716 | foundation_nsimage = NSImage.alloc().initWithData_( 717 | foundation_nsdata) 718 | if index == 0: 719 | umad.views['image.companylogo'].setImage_(foundation_nsimage) 720 | elif index == 1: 721 | umad.views['image.nagscreen'].setImage_(foundation_nsimage) 722 | elif index == 2: 723 | global uamdm_ss_nsimage 724 | uamdm_ss_nsimage = foundation_nsimage 725 | 726 | # Attach all the nib buttons to functions 727 | umad.attach(button_manualenrollment, 'button.manualenrollment') 728 | umad.attach(button_moreinfo, 'button.moreinfo') 729 | umad.attach(button_ok, 'button.ok') 730 | umad.attach(button_understand, 'button.understand') 731 | umad.attach(button_sysprefs, 'button.sysprefs') 732 | 733 | # Setup More Info button visibility 734 | if not more_info_url_set: 735 | umad.views['button.moreinfo'].setHidden_(True) 736 | 737 | # Setup the UI fields 738 | umad.views['field.titletext'].setStringValue_(opts.titletext) 739 | umad.views['field.subtitletext'].setStringValue_(opts.subtitletext) 740 | umad.views['field.duedatetext'].setStringValue_(opts.duedatetext) 741 | umad.views['field.paragraph1'].setStringValue_(opts.paragraph1) 742 | umad.views['field.paragraph2'].setStringValue_(opts.paragraph2) 743 | umad.views['field.paragraph3'].setStringValue_(opts.paragraph3) 744 | umad.views['field.manualenrollmenttext'].setStringValue_( 745 | opts.manualenrollmenttext) 746 | umad.views['field.depfailuretext'].setStringValue_( 747 | opts.depfailuretext) 748 | umad.views['field.depfailuresubtext'].setStringValue_( 749 | opts.depfailuresubtext) 750 | 751 | # Dynamically set username and serialnumber 752 | umad.views['field.username'].setStringValue_(str(user_name)) 753 | umad.views['field.serialnumber'].setStringValue_(str(get_serial())) 754 | umad.views['field.mdmenrolled'].setStringValue_('No') 755 | if opts.cutoffdate: 756 | todays_date = datetime.utcnow() 757 | cutoff_date = datetime.strptime(opts.cutoffdate, '%Y-%m-%d-%H:%M') 758 | date_diff_seconds = (cutoff_date - todays_date).total_seconds() 759 | date_diff_days = int(round(date_diff_seconds / 86400)) 760 | 761 | if date_diff_seconds >= 0: 762 | umad.views['field.daysremaining'].setStringValue_( 763 | date_diff_days) 764 | else: 765 | umad.views['field.daysremaining'].setStringValue_( 766 | 'Past date!') 767 | 768 | cut_off_warn = bool(date_diff_seconds < int( 769 | opts.cutoffdatewarning) * 86400) 770 | 771 | # Setup our timer controller 772 | umad.timer_controller = timerController.alloc().init() 773 | 774 | if date_diff_seconds <= 0: 775 | # If the cutoff date is over, get stupidly aggressive 776 | 777 | # Disable all buttons so the user cannot exit out of the 778 | # application, and have the manualenrollment button appear 779 | umad.views['button.ok'].setHidden_(True) 780 | umad.views['button.understand'].setHidden_(True) 781 | 782 | # Show the manual enrollment UI for emergency purposes 783 | umad.views['button.manualenrollment'].setHidden_(False) 784 | umad.views['field.manualenrollmenttext'].setHidden_(False) 785 | umad.views['image.nagscreen'].setHidden_(True) 786 | umad.views['field.depfailuretext'].setHidden_(False) 787 | umad.views['field.depfailuretext'].setStringValue_( 788 | manualenroll_h1_text) 789 | umad.views['field.depfailuresubtext'].setHidden_(False) 790 | umad.views['field.depfailuresubtext'].setStringValue_( 791 | manualenroll_h2_text) 792 | 793 | # Bring back umad to the foreground, every 10 seconds 794 | timer = float(opts.timerelapsed) 795 | elif date_diff_seconds <= 3600: 796 | # If the cutoff date is within one hour, get very agressive 797 | 798 | # Disable all buttons so the user cannot exit out of the 799 | # application 800 | umad.views['button.ok'].setHidden_(True) 801 | umad.views['button.understand'].setHidden_(True) 802 | 803 | # Bring back umad to the foreground, every 60 seconds 804 | # (1 minute) 805 | timer = float(opts.timerfinal) 806 | elif date_diff_seconds <= 86400: 807 | # If the cutoff date is within 86,400 seconds (24 hours), start 808 | # getting more agressive 809 | 810 | # Disable the ok button and require users to press understand 811 | # button first 812 | umad.views['button.ok'].setHidden_(True) 813 | 814 | # If the user doesn't close out of umad, we want it to 815 | # reappear - bring back umad to the foreground, every 816 | # 600 seconds (10 minutes) 817 | timer = float(opts.timerday1) 818 | elif cut_off_warn: 819 | # If the cutoff date is within 259,200 seconds (72 hours) or 820 | # whatever the admin set, start getting a bit more agressive 821 | 822 | # Disable the ok button and require users to press understand 823 | # button first 824 | umad.views['button.ok'].setHidden_(True) 825 | 826 | # If the user doesn't close out of umad, we want it to 827 | # reappear - bring back umad to the foreground, every 828 | # 7,200 seconds (2 hours) 829 | timer = float(opts.timerday3) 830 | else: 831 | # If the cutoff date is over 259,200 seconds (72 hours), 832 | # don't be that aggressive 833 | 834 | # Only require the ok button to exit out of umad 835 | umad.views['button.ok'].setHidden_(False) 836 | umad.views['button.understand'].setHidden_(True) 837 | 838 | # If the user doesn't close out of umad, we want it to 839 | # reappear - bring back umad to the foreground, every 840 | # 14,400 seconds (4 hours) 841 | timer = float(opts.timerinital) 842 | 843 | umad.timer = ( 844 | Foundation 845 | .NSTimer 846 | .scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 847 | timer, umad.timer_controller, 'activateWindow:', None, True)) 848 | else: 849 | # If you elect not to use a cutoff date, then the UI will only 850 | # appear one time per run, and only use the ok button 851 | 852 | # Hide the fields used for the cutoff date 853 | umad.views['field.daysremainingtext'].setHidden_(True) 854 | umad.views['field.daysremaining'].setHidden_(True) 855 | 856 | # Only require the ok button to exit out of umad 857 | umad.views['button.ok'].setHidden_(False) 858 | umad.views['button.understand'].setHidden_(True) 859 | 860 | # Enable manual enrollment too 861 | if mdm_profile_set and manual_enrollment_url_set: 862 | umad.views['button.manualenrollment'].setHidden_(False) 863 | else: 864 | umad.views['field.depfailuresubtext'].setStringValue_( 865 | 'Please contact your system administrator.') 866 | 867 | umad.views['field.depfailuretext'].setHidden_(False) 868 | umad.views['field.depfailuresubtext'].setHidden_(False) 869 | 870 | # If you didn't specify any defaults and the device is not DEP capable 871 | # it's really hard to have good data points 872 | if not dep_capable: 873 | umadlog('Did not specify any defaults and device is not DEP capable!') 874 | 875 | timer = float(opts.timerday3) 876 | date_diff_seconds = 1000000 877 | 878 | # Use cut off dates, but don't use the timer functionality 879 | if opts.notimer: 880 | umad.timer.invalidate() 881 | umadlog('Timer invalidated!') 882 | else: 883 | umadlog('Timer is set to %s' % str(timer)) 884 | 885 | # Setup our mdm timer controller 886 | umad.mdm_timer_controller = mdmTimerController.alloc().init() 887 | mdm_timer = float(opts.timermdm) 888 | umadlog('MDM Timer is set to %s' % str(mdm_timer)) 889 | umad.mdm_timer = ( 890 | Foundation 891 | .NSTimer 892 | .scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 893 | mdm_timer, umad.mdm_timer_controller, 'checkMDMStatus:', None, True)) 894 | 895 | # Set up our window controller and delegate 896 | umad.hidden = True 897 | 898 | # If the device isn't dep capable, enable the manual enrollment button 899 | # Also if admin always wants the break glass option 900 | # Also enable if dep nag didn't actually pop-up 901 | if (not dep_capable and mdm_profile_set) or opts.enableenrollmentbutton or not nag_triggered: 902 | umad.views['button.manualenrollment'].setHidden_(False) 903 | umad.views['field.manualenrollmenttext'].setHidden_(False) 904 | umad.views['image.nagscreen'].setHidden_(True) 905 | umad.views['field.depfailuretext'].setHidden_(False) 906 | umad.views['field.depfailuretext'].setStringValue_( 907 | manualenroll_h1_text) 908 | umad.views['field.depfailuresubtext'].setHidden_(False) 909 | umad.views['field.depfailuresubtext'].setStringValue_( 910 | manualenroll_h2_text) 911 | elif date_diff_seconds <= 0: 912 | umad.views['field.manualenrollmenttext'].setHidden_(True) 913 | umad.views['image.nagscreen'].setHidden_(False) 914 | umad.views['field.depfailuretext'].setHidden_(False) 915 | umad.views['field.depfailuretext'].setStringValue_( 916 | opts.depfailuretext) 917 | umad.views['field.depfailuresubtext'].setHidden_(False) 918 | umad.views['field.depfailuresubtext'].setStringValue_( 919 | opts.depfailuresubtext) 920 | 921 | # If the disablemanualenrollmentfordep option is set, never offer 922 | # manual enrollment as an option for DEP capable devices. 923 | if dep_capable and opts.disablemanualenrollmentfordep: 924 | umad.views['button.manualenrollment'].setHidden_(True) 925 | umad.views['field.manualenrollmenttext'].setHidden_(True) 926 | umad.views['field.depfailuretext'].setHidden_(True) 927 | umad.views['field.depfailuresubtext'].setHidden_(True) 928 | 929 | # Do one final MDM check to instantly update the UI for UAMDM 930 | check_mdm_status(True) 931 | 932 | umad.run() 933 | 934 | 935 | if __name__ == '__main__': 936 | main() 937 | -------------------------------------------------------------------------------- /payload/Library/umad/Resources/umad.nib/designable.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Enrollment into MDM is required to ensure that IT can protect your computer with basic security necessities like encryption and threat detection. 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 100 | 108 | 116 | 124 | 132 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /payload/Library/umad/Resources/umad.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/umad/0842a84713700854db3571eb574812e0cf5b9927/payload/Library/umad/Resources/umad.nib/keyedobjects.nib -------------------------------------------------------------------------------- /payload/Library/umad/Resources/umad_check_dep_record: -------------------------------------------------------------------------------- 1 | #!/Library/umad/Python.framework/Versions/3.8/bin/python3 2 | import os 3 | import platform 4 | import plistlib 5 | import subprocess 6 | import time 7 | import Foundation 8 | from distutils.version import LooseVersion 9 | from shutil import copyfile 10 | 11 | 12 | def get_os_version(): 13 | '''Return OS version.''' 14 | return LooseVersion(platform.mac_ver()[0]) 15 | 16 | 17 | def umadlog(text): 18 | '''logger for umad''' 19 | Foundation.NSLog('[UMAD] ' + text) 20 | 21 | 22 | def has_dep_activation_record(plist_path): 23 | # We can't use -o stdout-xml due to another Apple bug :) 24 | good_record = '/private/var/db/ConfigurationProfiles/.cloudConfigRecordFound' 25 | bad_record = '/private/var/db/ConfigurationProfiles/.cloudConfigRecordNotFound' 26 | if os.path.exists(plist_path): 27 | os.remove(plist_path) 28 | if get_os_version() >= LooseVersion('10.12'): 29 | cmd = ['/usr/bin/profiles', '-e', '-o', plist_path] 30 | elif get_os_version() >= LooseVersion('10.11') and get_os_version() < LooseVersion('10.12'): 31 | # This is not supported by Apple, but it works 32 | cmd = ['/usr/libexec/mdmclient', 'dep', 'nag'] 33 | else: 34 | # This is really not supported by Apple, but I discovered it 35 | # Tested on 10.10.5 36 | # /usr/libexec/mdmclient cloudconfig 37 | cmd = ['/usr/libexec/mdmclient', 'cloudconfig'] 38 | if get_os_version() >= LooseVersion('10.11'): 39 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 40 | output, err = run.communicate() 41 | else: 42 | # Delete records in case it is already there. 43 | if os.path.isfile(good_record): 44 | os.remove(good_record) 45 | if os.path.isfile(bad_record): 46 | os.remove(bad_record) 47 | run = subprocess.Popen(cmd, preexec_fn=os.setpgrp) 48 | if get_os_version() >= LooseVersion('10.12'): 49 | if err: 50 | return False 51 | try: 52 | with open(plist_path, "rb") as file: 53 | plist = plistlib.load(file) 54 | except: # noqa 55 | return False 56 | if not plist: 57 | return False 58 | else: 59 | return True 60 | elif get_os_version() >= LooseVersion('10.11') and get_os_version() < LooseVersion('10.12'): 61 | # On 10.11 and lower, /usr/libexec/mdmclient dep nag returns the DEP 62 | # config data on stderr! 63 | if b'ConfigurationURL' in err: 64 | # Make a fake plist with data :( 65 | plist = {} 66 | if get_os_version() >= LooseVersion('10.11'): 67 | try: 68 | for line in err.split(b'\n'): 69 | if b'ConfigurationURL' in line: 70 | # Since this is some stupid output, strip it all 71 | strip_line = line.strip(b' ').strip(b';').strip(b'"') 72 | strip_line_value = strip_line.split(b' = "')[-1] 73 | plist['ConfigurationURL'] = strip_line_value 74 | plist_file = open(plist_path, 'wb') 75 | plistlib.dump(pl, plist_file) 76 | plist_file.close() 77 | return True 78 | except: 79 | return False 80 | return True 81 | else: 82 | return False 83 | else: 84 | # 10.10 and lower 85 | cloud_config_exists = False 86 | retries = 30 87 | while not cloud_config_exists: 88 | cloud_config_exists = bool(os.path.isfile(good_record)) 89 | if cloud_config_exists: 90 | copyfile(good_record, plist_path) 91 | time.sleep(0.1) 92 | retries -= 1 93 | if retries == 0: 94 | break 95 | return cloud_config_exists 96 | return False 97 | 98 | 99 | def main(): 100 | dot_path = '/private/var/tmp/umad/.check_dep_record' 101 | main_umad_path = '/Library/umad' 102 | plist_path = os.path.join(main_umad_path, 'Resources/dep_record.plist') 103 | if has_dep_activation_record(plist_path): 104 | umadlog('Has DEP activation record - True') 105 | else: 106 | umadlog('Has DEP activation record - False') 107 | 108 | # Stop this from happening all the time 109 | if os.path.exists(dot_path): 110 | os.remove(dot_path) 111 | 112 | # Because of what we do with mdmclient on 10.11 we need to force exit 0 113 | if get_os_version() < LooseVersion('10.11'): 114 | exit(0) 115 | 116 | if __name__ == '__main__': 117 | main() 118 | -------------------------------------------------------------------------------- /payload/Library/umad/Resources/umad_trigger_nag: -------------------------------------------------------------------------------- 1 | #!/Library/umad/Python.framework/Versions/3.8/bin/python3 2 | # encoding: utf-8 3 | import os 4 | import platform 5 | import subprocess 6 | import time 7 | import Foundation 8 | from distutils.version import LooseVersion 9 | 10 | 11 | def get_os_version(): 12 | '''Return OS version.''' 13 | return LooseVersion(platform.mac_ver()[0]) 14 | 15 | 16 | def umadlog(text): 17 | '''logger for umad''' 18 | Foundation.NSLog('[UMAD] ' + text) 19 | 20 | 21 | def trigger_nag(): 22 | '''trigger the nag''' 23 | os_version = get_os_version() 24 | if os_version >= LooseVersion('10.13'): 25 | cmd = ['/usr/bin/profiles', 'renew', '-type', 'enrollment'] 26 | elif os_version < LooseVersion('10.13') and os_version >= LooseVersion('10.12.4'): 27 | cmd = ['/usr/bin/profiles', '-N'] 28 | elif os_version < LooseVersion('10.12.4') and os_version >= LooseVersion('10.12'): 29 | # This is not supported by Apple, but it works 30 | cmd = ['/usr/libexec/mdmclient', 'dep', 'nag'] 31 | else: 32 | # This is really not supported by Apple, but I discovered it 33 | # Tested on 10.10.5 34 | # /usr/libexec/mdmclient cloudconfig 35 | cmd = ['/usr/libexec/mdmclient', 'cloudconfig'] 36 | if os_version >= LooseVersion('10.11'): 37 | run = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 38 | output, err = run.communicate() 39 | if err: 40 | return False 41 | else: 42 | return True 43 | else: 44 | run = subprocess.Popen(cmd, preexec_fn=os.setpgrp) 45 | return True 46 | 47 | 48 | def main(): 49 | '''main''' 50 | dot_path = '/private/var/tmp/umad/.trigger_nag' 51 | if trigger_nag(): 52 | umadlog('Triggered nag - True') 53 | else: 54 | if get_os_version() <= LooseVersion('10.13'): 55 | umadlog('Triggered nag - False') 56 | # You cannot do two nag events within 10 seconds of each other on 57 | # at least 10.12 - wait 10 seconds and try one more time. 58 | # If you attempt to do this you will get the following error: 59 | # On 10.12 60 | # [ERROR] Unable to get activation record: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.ManagedClient.cloudconfigurationd" UserInfo={NSDebugDescription=connection to service named com.apple.ManagedClient.cloudconfigurationd} 61 | # On 10.11 62 | # mdmclient[899:21947] Did NOT fetch configuration from Device Enrollment server: 34002 (Unable to communicate with the local Device Enrollment service. Please try again later.) 63 | time.sleep(10) 64 | if trigger_nag(): 65 | umadlog('Triggered nag backup - True') 66 | else: 67 | umadlog('Triggered nag backup - False') 68 | else: 69 | umadlog('Triggered nag - False') 70 | 71 | # Stop this from happening all the time 72 | if os.path.exists(dot_path): 73 | os.remove(dot_path) 74 | 75 | # Because of what we do with mdmclient on 10.11 we need to force exit 0 76 | if get_os_version() < LooseVersion('10.11'): 77 | exit(0) 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /py3_requirements.txt: -------------------------------------------------------------------------------- 1 | xattr==0.9.6 2 | six==1.13.0 3 | certifi==2019.9.11 4 | cffi==1.13.2 5 | chardet==3.0.4 6 | idna==2.8 7 | pycparser==2.19 8 | urllib3==1.26.5 9 | requests==2.22.0 10 | pyobjc-framework-AVFoundation==6.1 11 | pyobjc-framework-AVKit==6.1 12 | pyobjc-framework-Accounts==6.1 13 | pyobjc-framework-AdSupport==6.1 14 | pyobjc-framework-AddressBook==6.1 15 | pyobjc-framework-AppleScriptKit==6.1 16 | pyobjc-framework-AppleScriptObjC==6.1 17 | pyobjc-framework-ApplicationServices==6.1 18 | pyobjc-framework-AuthenticationServices==6.1 19 | pyobjc-framework-Automator==6.1 20 | pyobjc-framework-BusinessChat==6.1 21 | pyobjc-framework-CFNetwork==6.1 22 | pyobjc-framework-CalendarStore==6.1 23 | pyobjc-framework-CloudKit==6.1 24 | pyobjc-framework-Cocoa==6.1 25 | pyobjc-framework-Collaboration==6.1 26 | pyobjc-framework-ColorSync==6.1 27 | pyobjc-framework-Contacts==6.1 28 | pyobjc-framework-ContactsUI==6.1 29 | pyobjc-framework-CoreAudio==6.1 30 | pyobjc-framework-CoreAudioKit==6.1 31 | pyobjc-framework-CoreBluetooth==6.1 32 | pyobjc-framework-CoreData==6.1 33 | pyobjc-framework-CoreHaptics==6.1 34 | pyobjc-framework-CoreLocation==6.1 35 | pyobjc-framework-CoreML==6.1 36 | pyobjc-framework-CoreMedia==6.1 37 | pyobjc-framework-CoreMediaIO==6.1 38 | pyobjc-framework-CoreMotion==6.1 39 | pyobjc-framework-CoreServices==6.1 40 | pyobjc-framework-CoreSpotlight==6.1 41 | pyobjc-framework-CoreText==6.1 42 | pyobjc-framework-CoreWLAN==6.1 43 | pyobjc-framework-CryptoTokenKit==6.1 44 | pyobjc-framework-DVDPlayback==6.1 45 | pyobjc-framework-DeviceCheck==6.1 46 | pyobjc-framework-DictionaryServices==6.1 47 | pyobjc-framework-DiscRecording==6.1 48 | pyobjc-framework-DiscRecordingUI==6.1 49 | pyobjc-framework-DiskArbitration==6.1 50 | pyobjc-framework-EventKit==6.1 51 | pyobjc-framework-ExceptionHandling==6.1 52 | pyobjc-framework-ExecutionPolicy==6.1 53 | pyobjc-framework-ExternalAccessory==6.1 54 | pyobjc-framework-FSEvents==6.1 55 | pyobjc-framework-FileProvider==6.1 56 | pyobjc-framework-FileProviderUI==6.1 57 | pyobjc-framework-FinderSync==6.1 58 | pyobjc-framework-GameCenter==6.1 59 | pyobjc-framework-GameController==6.1 60 | pyobjc-framework-GameKit==6.1 61 | pyobjc-framework-GameplayKit==6.1 62 | pyobjc-framework-IMServicePlugIn==6.1 63 | pyobjc-framework-IOSurface==6.1 64 | pyobjc-framework-ImageCaptureCore==6.1 65 | pyobjc-framework-InputMethodKit==6.1 66 | pyobjc-framework-InstallerPlugins==6.1 67 | pyobjc-framework-InstantMessage==6.1 68 | pyobjc-framework-Intents==6.1 69 | pyobjc-framework-LatentSemanticMapping==6.1 70 | pyobjc-framework-LaunchServices==6.1 71 | pyobjc-framework-LinkPresentation==6.1 72 | pyobjc-framework-LocalAuthentication==6.1 73 | pyobjc-framework-MapKit==6.1 74 | pyobjc-framework-MediaAccessibility==6.1 75 | pyobjc-framework-MediaLibrary==6.1 76 | pyobjc-framework-MediaPlayer==6.1 77 | pyobjc-framework-MediaToolbox==6.1 78 | pyobjc-framework-MetalKit==6.1 79 | pyobjc-framework-ModelIO==6.1 80 | pyobjc-framework-MultipeerConnectivity==6.1 81 | pyobjc-framework-NaturalLanguage==6.1 82 | pyobjc-framework-NetFS==6.1 83 | pyobjc-framework-Network==6.1 84 | pyobjc-framework-NetworkExtension==6.1 85 | pyobjc-framework-NotificationCenter==6.1 86 | pyobjc-framework-OSAKit==6.1 87 | pyobjc-framework-OSLog==6.1 88 | pyobjc-framework-OpenDirectory==6.1 89 | pyobjc-framework-PencilKit==6.1 90 | pyobjc-framework-Photos==6.1 91 | pyobjc-framework-PhotosUI==6.1 92 | pyobjc-framework-PreferencePanes==6.1 93 | pyobjc-framework-PubSub==6.1 94 | pyobjc-framework-PushKit==6.1 95 | pyobjc-framework-Quartz==6.1 96 | pyobjc-framework-QuickLookThumbnailing==6.1 97 | pyobjc-framework-SafariServices==6.1 98 | pyobjc-framework-SceneKit==6.1 99 | pyobjc-framework-ScreenSaver==6.1 100 | pyobjc-framework-ScriptingBridge==6.1 101 | pyobjc-framework-SearchKit==6.1 102 | pyobjc-framework-Security==6.1 103 | pyobjc-framework-SecurityFoundation==6.1 104 | pyobjc-framework-SecurityInterface==6.1 105 | pyobjc-framework-ServiceManagement==6.1 106 | pyobjc-framework-Social==6.1 107 | pyobjc-framework-SoundAnalysis==6.1 108 | pyobjc-framework-Speech==6.1 109 | pyobjc-framework-SpriteKit==6.1 110 | pyobjc-framework-StoreKit==6.1 111 | pyobjc-framework-SyncServices==6.1 112 | pyobjc-framework-SystemConfiguration==6.1 113 | pyobjc-framework-SystemExtensions==6.1 114 | pyobjc-framework-UserNotifications==6.1 115 | pyobjc-framework-VideoSubscriberAccount==6.1 116 | pyobjc-framework-VideoToolbox==6.1 117 | pyobjc-framework-Vision==6.1 118 | pyobjc-framework-WebKit==6.1 119 | pyobjc-framework-iTunesLibrary==6.1 120 | pyobjc-framework-libdispatch==6.1 121 | pyobjc-core==6.1 122 | pyobjc==6.1 123 | -------------------------------------------------------------------------------- /scripts/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # 3 | # Copyright 2019-Present Erik Gomez. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # If you change your daemon and agent file names, update the following two lines 18 | launch_agent_plist_name='com.erikng.umad' 19 | launch_daemon_dep_plist_name='com.erikng.umad.check_dep_record' 20 | launch_daemon_nag_plist_name='com.erikng.umad.trigger_nag' 21 | 22 | # Base paths 23 | launch_agent_base_path='Library/LaunchAgents/' 24 | launch_daemon_base_path='Library/LaunchDaemons/' 25 | base_umad_path='Library/umad/' 26 | 27 | # Load agent if installing to a running system 28 | if [[ $3 == "/" ]] ; then 29 | # Fail the install if the admin forgets to change their paths and they don't exist. 30 | if [ ! -e "$3${launch_daemon_base_path}${launch_daemon_dep_plist_name}.plist" ] || [ ! -e "$3${launch_daemon_base_path}${launch_daemon_nag_plist_name}.plist" ] || [ ! -e "$3${launch_agent_base_path}${launch_agent_plist_name}.plist" ]; then 31 | echo "LaunchAgent or Daemons missing, exiting" 32 | exit 1 33 | fi 34 | 35 | # Make the Log path 777 to cheat - do this before loading LaunchAgent 36 | /bin/mkdir -p "$3${base_umad_path}Logs" 37 | /bin/chmod -R 777 "$3${base_umad_path}Logs" 38 | 39 | # Attempt to unload the DEP daemon if it's already loaded 40 | /bin/launchctl list | /usr/bin/grep $launch_daemon_dep_plist_name 41 | if [[ $? -eq 0 ]]; then 42 | /bin/launchctl unload "$3${launch_daemon_base_path}${launch_daemon_dep_plist_name}.plist" 43 | fi 44 | 45 | # Attempt to unload the nag daemon if it's already loaded 46 | /bin/launchctl list | /usr/bin/grep $launch_daemon_nag_plist_name 47 | if [[ $? -eq 0 ]]; then 48 | /bin/launchctl unload "$3${launch_daemon_base_path}${launch_daemon_nag_plist_name}.plist" 49 | fi 50 | 51 | # Enable the LaunchDaemons 52 | /bin/launchctl load "$3${launch_daemon_base_path}${launch_daemon_dep_plist_name}.plist" 53 | /bin/launchctl load "$3${launch_daemon_base_path}${launch_daemon_nag_plist_name}.plist" 54 | 55 | # Current console user information 56 | console_user=$(/usr/bin/stat -f "%Su" /dev/console) 57 | console_user_uid=$(/usr/bin/id -u "$console_user") 58 | 59 | # Only enable the LaunchAgent if there is a user logged in, otherwise rely on built in LaunchAgent behavior 60 | if [[ -z "$console_user" ]]; then 61 | echo "Did not detect user" 62 | elif [[ "$console_user" == "loginwindow" ]]; then 63 | echo "Detected Loginwindow Environment" 64 | elif [[ "$console_user" == "_mbsetupuser" ]]; then 65 | echo "Detect SetupAssistant Environment" 66 | else 67 | # This is a deprecated command, but until Apple kills it, it is going to be used 68 | /bin/launchctl asuser "${console_user_uid}" /bin/launchctl list | /usr/bin/grep 'umad' 69 | # Unload the agent so it can be triggered on re-install 70 | if [[ $? -eq 0 ]]; then 71 | /bin/launchctl asuser "${console_user_uid}" /bin/launchctl unload "$3${launch_agent_base_path}${launch_agent_plist_name}.plist" 72 | fi 73 | # Load the launch agent 74 | /bin/launchctl asuser "${console_user_uid}" /bin/launchctl load "$3${launch_agent_base_path}${launch_agent_plist_name}.plist" 75 | fi 76 | fi 77 | --------------------------------------------------------------------------------