├── .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 | 
47 |
48 | ### Manual
49 | 
50 |
51 | ### UAMDM
52 | 
53 |
54 | ### Simplified Diagram
55 | 
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 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
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 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
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 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
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 |
--------------------------------------------------------------------------------