26 | """,
27 | helpNames: [.long]
28 | )
29 | // Required - notification type
30 | @Option(help: """
31 | alert or banner - REQUIRED.
32 |
33 | """)
34 | var type: String = ""
35 | // The notifications message
36 | @Option(help: """
37 | The notifications message.
38 |
39 | """)
40 | var message: String = ""
41 | // Optional action
42 | @Option(help: """
43 | The action to be performed under the users account when the message is clicked.
44 |
45 | • Passing 'logout' will prompt the user to logout.
46 | • If passed a single item, this will be launched via: /usr/bin/open
47 | • More complex commands can be passed, but the 1st argument needs to be a binaries path.
48 |
49 | For example: \"/usr/bin/open\" will work, \"open\" will not.
50 |
51 | """)
52 | var messageaction: String = ""
53 | // Optional message button text
54 | @Option(help: """
55 | Adds a button to the message, with the label being what is passed.
56 |
57 | """)
58 | var messagebutton: String = ""
59 | // Optional action when the alert button is clicked
60 | @Option(help: """
61 | The action to be performed under the users account when the optional message button is clicked.
62 |
63 | • Passing 'logout' will prompt the user to logout.
64 | • If passed a single item, this will be launched via: /usr/bin/open
65 | • More complex commands can be passed, but the 1st argument needs to be a binaries path.
66 |
67 | For example: \"/usr/bin/open\" will work, \"open\" will not.
68 |
69 | """)
70 | var messagebuttonaction: String = ""
71 | // Triggers rebrand function
72 | @Option(help: """
73 | Requires root privileges and that the calling process needs either Full Disk Access (10.15+) or at \
74 | a minimum App Management (macOS 13+) permissions, as well as the notifying applications being given \
75 | permission to post to Notification Center. Any of these permissions can be granted manually, but \
76 | ideally via PPPCP's delivered via an MDM.
77 |
78 | If successful and someone is logged in, Notification Center is restarted.
79 |
80 | """)
81 | var rebrand: String = ""
82 | // Option to remove a specific notification or all notifications delivered
83 | @Option(help: """
84 | \"prior\" or \"all\". If passing \"prior\", the full message will be required too. \
85 | Including all passed flags.
86 |
87 | """)
88 | var remove: String = ""
89 | // Optional sound to play when notification is delivered
90 | @Option(help: """
91 | The sound to play when notification is delivered. Pass \"default\" for the default \
92 | macOS sound, else the name of a sound in /Library/Sounds or /System/Library/Sounds.
93 |
94 | If the sound cannot be found, macOS will use the \"default\" sound.
95 |
96 | """)
97 | var sound: String = ""
98 | // Optional subtitle for the notification
99 | @Option(help: """
100 | The notifications subtitle.
101 |
102 | """)
103 | var subtitle: String = ""
104 | // Optional Title for the notification
105 | @Option(help: """
106 | The notifications title.
107 |
108 | """)
109 | var title: String = ""
110 | // Enables verbose logging
111 | @Flag(help: """
112 | Enables logging of actions. Check console for 'Notifier' messages.
113 |
114 | """)
115 | var verbose = false
116 | }
117 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Notifier - 16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "Notifier - 32x32.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Notifier - 32x32 1.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "Notifier - 64x64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Notifier - 128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "Notifier - 256x256.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Notifier - 256x256 1.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "Notifier - 512x512.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Notifier - 512x512 1.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "Notifier - 1024x1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png
--------------------------------------------------------------------------------
/Notifier/Notifier/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Functions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Functions.swift
3 | // Notifier
4 | //
5 | // Copyright © 2024 dataJAR Ltd. All rights reserved.
6 | //
7 |
8 | // Imports
9 | import Cocoa
10 | import SystemConfiguration
11 |
12 | // Changes the .app's icons restarting Notification Center if rebranding was successful
13 | func changeIcons(brandingImage: String, loggedInUser: String, parsedResult: ArgParser) {
14 | // Confirm we're root before proceeding
15 | rootCheck(parsedResult: parsedResult, passedArg: "--rebrand")
16 | // If verbose mode is enabled
17 | if parsedResult.verbose {
18 | // Progress log
19 | NSLog("\(#function.components(separatedBy: "(")[0]) - brandingImage: \(brandingImage)")
20 | }
21 | // Get the details of the file at brandingImage
22 | let imageData = getImageDetails(brandingImage: brandingImage, parsedResult: parsedResult)
23 | // For each application in brandingArray
24 | for notifierApp in [GlobalVariables.mainAppPath, GlobalVariables.alertAppPath, GlobalVariables.bannerAppPath] {
25 | // Rebrand each app starting ewith the Notifier.app, this way if this fails the rest are skipped
26 | updateIcon(brandingImage: brandingImage, imageData: imageData!, objectPath: notifierApp,
27 | parsedResult: parsedResult)
28 | }
29 | // If verbose mode is enabled
30 | if parsedResult.verbose {
31 | // Progress log
32 | NSLog("\(#function.components(separatedBy: "(")[0]) - Successfully rebranded Notifier")
33 | }
34 | // If someone is logged in
35 | if loggedInUser != "" {
36 | // If we're logged in and/or Notification Center is runnning register the applications with Notification Center
37 | registerApplications(parsedResult: parsedResult)
38 | // If no-one is logged in
39 | } else {
40 | // If verbose mode is enabled
41 | if parsedResult.verbose {
42 | // Progress log
43 | NSLog("\(#function.components(separatedBy: "(")[0]) - Skipping registration as not logged in")
44 | }
45 | }
46 | // Exit
47 | exit(0)
48 | }
49 |
50 | // Create JSON from passed string
51 | func createJSON(messageContent: MessageContent, parsedResult: ArgParser, rootElements: RootElements) -> String {
52 | // Var declaration
53 | var contentJSON = Data()
54 | var fullJSON = Data()
55 | var rootContent = rootElements
56 | // If verbose mode is enabled
57 | if parsedResult.verbose {
58 | // Progress log
59 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageContent: \(messageContent)")
60 | }
61 | // Try to convert messageContent to JSON
62 | do {
63 | // Create a JSONEncoder object
64 | let jsonEncoder = JSONEncoder()
65 | // Set formatting to sortedKeys - to make sure that the base64 is static
66 | jsonEncoder.outputFormatting = .sortedKeys
67 | // Turn messageContent into JSON
68 | contentJSON = try jsonEncoder.encode(messageContent)
69 | // If encoding into JSON fails
70 | } catch {
71 | // Post error
72 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: error.localizedDescription, functionName:
73 | #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
74 | }
75 | // If verbose mode is enabled
76 | if parsedResult.verbose {
77 | // Progress log
78 | NSLog("""
79 | \(#function.components(separatedBy: "(")[0]) - contentJSON: \(String(data: contentJSON, encoding: .utf8)!)
80 | """)
81 | }
82 | // Add to contentJSON, but base64 encoded
83 | rootContent.messageContent = contentJSON.base64EncodedString()
84 | // Try to convert jsonContent to JSON
85 | do {
86 | // Create a JSONEncoder object
87 | let jsonEncoder = JSONEncoder()
88 | // Set formatting to sortedKeys - to make sure that the base64 is static
89 | jsonEncoder.outputFormatting = .sortedKeys
90 | // Turn jsonContent into JSON
91 | fullJSON = try jsonEncoder.encode(rootContent)
92 | // If encoding into JSON fails
93 | } catch {
94 | // Post error
95 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: error.localizedDescription, functionName:
96 | #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
97 | }
98 | // If verbose mode is enabled
99 | if parsedResult.verbose {
100 | // Progress log
101 | NSLog("\(#function.components(separatedBy: "(")[0]) - rootJSON: \(String(data: fullJSON, encoding: .utf8)!)")
102 | }
103 | // Return fullJSON, base64 encoded
104 | return fullJSON.base64EncodedString()
105 | }
106 |
107 | // Gets the modification date and time of the file and return as epoch
108 | func getImageDetails(brandingImage: String, parsedResult: ArgParser) -> (NSImage?) {
109 | // Var declaration
110 | var imageData: NSImage?
111 | // If the file exists
112 | if FileManager.default.fileExists(atPath: brandingImage) {
113 | // Create imageData from the file passed to brandingImage
114 | imageData = NSImage(contentsOfFile: brandingImage)
115 | // If imageData isValid is nil, then brandingImage is not a valid icon
116 | if (imageData?.isValid) == nil {
117 | // Post error
118 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "\(brandingImage) is not a valid image...",
119 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
120 | // Exit
121 | exit(1)
122 | }
123 | // Return the image data
124 | return imageData
125 | // If the file doesn't exist
126 | } else {
127 | // Post error
128 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Cannot locate: \(brandingImage)....", functionName:
129 | #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
130 | // Exit
131 | exit(1)
132 | }
133 | }
134 |
135 | // Checks that notification center is running, and exit if it's not
136 | func isNotificationCenterRunning(parsedResult: ArgParser) {
137 | // Exit if Notification Center isn't running for the user
138 | guard !NSRunningApplication.runningApplications(withBundleIdentifier:
139 | "com.apple.notificationcenterui").isEmpty else {
140 | // Post warning
141 | postToNSLogAndStdOut(logLevel: "WARNING", logMessage: "Notification Center is not running...",
142 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
143 | // Exit
144 | exit(1)
145 | }
146 | // If verbose mode is enabled
147 | if parsedResult.verbose {
148 | // Progress log
149 | NSLog("\(#function.components(separatedBy: "(")[0]) - Notification Center is running")
150 | }
151 | }
152 |
153 | // Returns username if a user is logged in, and "" if at the loginwindow
154 | func loggedInUser() -> String {
155 | // Get the name of the logged in user
156 | let loggedInUser = SCDynamicStoreCopyConsoleUser(nil, nil, nil)! as String
157 | // If no-one or loginwindow is returned
158 | if loggedInUser == "loginwindow" || loggedInUser == "" {
159 | return ""
160 | // Else if we have someone logged in
161 | } else {
162 | // Return their username
163 | return loggedInUser
164 | }
165 | }
166 |
167 | // Parses the action text, splitting it into argv compliant format
168 | func parseAction(actionString: String, parsedResult: ArgParser) -> [MessageContent.TaskObject] {
169 | // Var declaration
170 | var regexDict = [String: String]()
171 | var taskArguments = [String]()
172 | var taskPath = String()
173 | var tempActionString = actionString
174 | // If verbose mode is enabled
175 | if parsedResult.verbose {
176 | // Progress log
177 | NSLog("\(#function.components(separatedBy: "(")[0]) - actionString: \(actionString)")
178 | }
179 | // The regex pattern we're to use
180 | let regexPattern = try? NSRegularExpression(pattern: "\'(.*?)\'|\"(.*?)\"")
181 | // Apply pattern to actionText, returning matches
182 | let regexMatches = regexPattern?.matches(in: actionString, options: [],
183 | range: NSRange(location: 0, length: actionString.utf16.count))
184 | // Iterate over each match
185 | for regexMatch in regexMatches! {
186 | // Set the range
187 | let regexRange = regexMatch.range(at: 1)
188 | // Create a Range object
189 | if let swiftRange = Range(regexRange, in: actionString) {
190 | // Create key in regex dict with the and %20 escape spaces
191 | regexDict[actionString[swiftRange].description] =
192 | actionString[swiftRange].description.replacingOccurrences(of: " ", with: "%20")
193 | }
194 | }
195 | // If we have items in regexDict
196 | if !regexDict.isEmpty {
197 | // For each key value pair we have in regexDict
198 | for (matchedKey, matchedValue) in regexDict {
199 | // Replace the occurences of matchedKey with matchedValue
200 | tempActionString = tempActionString.replacingOccurrences(of: matchedKey, with: matchedValue)
201 | }
202 | // Set taskArguments to the amended string
203 | taskArguments = Array(tempActionString.components(separatedBy: " "))
204 | // Iterate over the array elements which contain %20
205 | for arrayElement in taskArguments where arrayElement.contains("%20") {
206 | // Get the index of the element
207 | let arrayIndex = taskArguments.firstIndex(of: arrayElement)
208 | // Update the element in the array, removing %20, single and double quotes
209 | taskArguments[arrayIndex!] = arrayElement.replacingOccurrences(of: "%20", with: " ")
210 | .replacingOccurrences(of: "\'", with: "").replacingOccurrences(of: "\"", with: "")
211 | }
212 | // If regexDict is empty
213 | } else {
214 | // Set taskArguments to the passed action
215 | taskArguments = Array(tempActionString.components(separatedBy: " "))
216 | }
217 | // If we only have a single task
218 | if taskArguments.count == 1 {
219 | // If we've been passed logout
220 | if taskArguments[0].lowercased() == "logout" {
221 | // Set the tasks path to open, this is to mimic pre-3.0 behaviour
222 | taskPath = "logout"
223 | // Clear taskArguments
224 | taskArguments = []
225 | // If one item and not passed logout
226 | } else {
227 | // Set the tasks path to open, this is to mimic pre-3.0 behaviour
228 | taskPath = "/usr/bin/open"
229 | }
230 | // If we have more than one task, the 1st argument starts with /
231 | } else if taskArguments[0].hasPrefix("/") {
232 | // Set the tasks path to the 1st item within the taskArguments
233 | taskPath = taskArguments[0]
234 | // Remove the above from the taskArguments
235 | taskArguments.remove(at: 0)
236 | // If we have more than one task, and the 1st argument does not start with /
237 | } else {
238 | // Post warning
239 | postToNSLogAndStdOut(logLevel: "WARNING", logMessage: """
240 | \(taskArguments[0]) is not a path to a binary, not adding to notification...
241 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
242 | // Return an empty TaskObject
243 | return [MessageContent.TaskObject(taskPath: "", taskArguments: [])]
244 | }
245 | // If verbose mode is enabled
246 | if parsedResult.verbose {
247 | // Progress log
248 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)")
249 | }
250 | // Return a TaskObject with taskPath and taskArguments set
251 | return [MessageContent.TaskObject(taskPath: taskPath, taskArguments: taskArguments)]
252 | }
253 |
254 | // Passes messageContentJSON to the relevant app, exiting afterwards
255 | func passToApp(commandJSON: String, loggedInUser: String, notifierPath: String, parsedResult: ArgParser) {
256 | // Var declaration
257 | var taskArguments = [String]()
258 | var taskPath = String()
259 | // If the user running the app isn't the logged in user (root for example)
260 | if NSUserName() != loggedInUser {
261 | // Set taskPath to su as we need to use that to run as the user
262 | taskPath = "/usr/bin/su"
263 | // Create taskArguments
264 | taskArguments = [
265 | "-l", "\(loggedInUser)", "-c", "\'\(notifierPath)\' \(commandJSON)"
266 | ]
267 | // If the person running the app is the logged in user
268 | } else {
269 | // Set taskPath to the notifying apps path
270 | taskPath = notifierPath
271 | // Set taskArguments to the base64 of messageContentJSON
272 | taskArguments = [commandJSON]
273 | }
274 | // If verbose mode is enabled
275 | if parsedResult.verbose {
276 | // Progress log
277 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)")
278 | }
279 | // Launch the wanted notification app as the user
280 | runTask(parsedResult: parsedResult, taskArguments: taskArguments, taskPath: taskPath)
281 | // If we're not rebranding
282 | if parsedResult.rebrand == "" {
283 | // Exit
284 | exit(0)
285 | }
286 | }
287 |
288 | // Post to both NSLog and stdout
289 | func postToNSLogAndStdOut(logLevel: String, logMessage: String, functionName: String, parsedResult: ArgParser) {
290 | // If verbose mode is enabled
291 | if parsedResult.verbose {
292 | // Progress log
293 | NSLog("\(logLevel): \(functionName) - \(logMessage)")
294 | // verbose mode isn't enabled
295 | } else {
296 | // Print to stdout
297 | print("\(logLevel): \(logMessage)")
298 | }
299 | }
300 |
301 | // Registers the notifying applications in Notificaton Center
302 | func registerApplications(parsedResult: ArgParser) {
303 | // Var declaration
304 | var taskArguments = [String]()
305 | var taskPath = String()
306 | // If verbose mode is enabled
307 | if parsedResult.verbose {
308 | // Progress log
309 | NSLog("\(#function.components(separatedBy: "(")[0])")
310 | }
311 | // Get the username of the logged in user
312 | let loggedInUser = loggedInUser()
313 | // Initialize a rootElements object
314 | let rootElements = RootElements(removeOption: "prior")
315 | // Initialize a messageContent object with a message body of a UUID
316 | let messageContent = MessageContent(messageBody: UUID().uuidString)
317 | // Create the JSON to pass to the notifying app
318 | let commandJSON = createJSON(messageContent: messageContent, parsedResult: parsedResult,
319 | rootElements: rootElements)
320 | for notifierPath in [GlobalVariables.alertAppPath + "/Contents/MacOS/Notifier - Alerts",
321 | GlobalVariables.bannerAppPath + "/Contents/MacOS/Notifier - Notifications"] {
322 | // Pass commandJSON to the relevant app, exiting afterwards
323 | passToApp(commandJSON: commandJSON, loggedInUser: loggedInUser, notifierPath: notifierPath,
324 | parsedResult: parsedResult)
325 | // If verbose mode is enabled
326 | if parsedResult.verbose {
327 | // Progress log
328 | NSLog("""
329 | \(#function.components(separatedBy: "(")[0]) - commandJSON: \(commandJSON), notifierPath: \
330 | \(notifierPath)
331 | """)
332 | }
333 | }
334 | // If verbose mode is enabled
335 | if parsedResult.verbose {
336 | // Progress log
337 | NSLog("\(#function.components(separatedBy: "(")[0]) - restarting Notification Center")
338 | }
339 | // If we're logged in
340 | if loggedInUser != "" {
341 | // Path for the task
342 | taskPath = "/usr/bin/su"
343 | // Arguments for the task
344 | taskArguments = ["-l", loggedInUser, "-c", "/usr/bin/killall -u \(loggedInUser) NotificationCenter"]
345 | // If verbose mode is enabled
346 | if parsedResult.verbose {
347 | // Progress log
348 | NSLog("""
349 | \(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)
350 | """)
351 | }
352 | // Run the task, ignoring returned exit status
353 | runTask(parsedResult: parsedResult, taskArguments: taskArguments, taskPath: taskPath)
354 | // If we're not logged in
355 | } else {
356 | // If verbose mode is enabled
357 | if parsedResult.verbose {
358 | // Progress log
359 | NSLog("\(#function.components(separatedBy: "(")[0]) - not logged in, skipping Notification Center restart")
360 | }
361 | }
362 | }
363 |
364 | // Make sure we're running as root, exit if not
365 | func rootCheck(parsedResult: ArgParser, passedArg: String) {
366 | // If we're not root
367 | if NSUserName() != "root" {
368 | // Post error
369 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """
370 | The argument: \(passedArg), requires root privileges, exiting...
371 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
372 | // Exit
373 | exit(1)
374 | }
375 | }
376 |
377 | // Runs the passed task
378 | func runTask(parsedResult: ArgParser, taskArguments: [String], taskPath: String) {
379 | // If verbose mode is enabled
380 | if parsedResult.verbose {
381 | // Progress log
382 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)")
383 | }
384 | // Create Task
385 | let task = Process()
386 | // Set task to call the binary
387 | task.executableURL = URL(fileURLWithPath: taskPath)
388 | // Set task arguments
389 | task.arguments = taskArguments
390 | // Run the task
391 | try? task.run()
392 | // Wait until task exits
393 | task.waitUntilExit()
394 | }
395 |
396 | // Attempts to update the app passed to objectPath's icon
397 | func updateIcon(brandingImage: String, imageData: NSImage, objectPath: String, parsedResult: ArgParser) {
398 | // Revert the icon, always returns false and this helps the OS realise that ther has been an icon change
399 | NSWorkspace.shared.setIcon(nil, forFile: objectPath, options: NSWorkspace.IconCreationOptions([]))
400 | // Set the icon, returns bool
401 | let rebrandStatus = NSWorkspace.shared.setIcon(imageData, forFile: objectPath, options:
402 | NSWorkspace.IconCreationOptions([]))
403 | // If we have succesfully branded the item at objectPath
404 | if rebrandStatus {
405 | // If verbose mode is enabled
406 | if parsedResult.verbose {
407 | // Progress log
408 | NSLog("""
409 | \(#function.components(separatedBy: "(")[0]) - Successfully updated icon for \(objectPath), \
410 | with icon: \(brandingImage)
411 | """)
412 | }
413 | // If we encountered and issue when rebranding...
414 | } else {
415 | // Post error
416 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """
417 | Failed to update icon for \(objectPath), with icon: \(brandingImage). Please make sure that the calling \
418 | process has the needed App Management or Full Disk Access PPPCP deployed, and try again.
419 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult)
420 | // Exit
421 | exit(1)
422 | }
423 | }
424 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSApplicationCategoryType
22 | public.app-category.utilities
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | LSUIElement
26 |
27 | NSHumanReadableCopyright
28 | Copyright © 2024 dataJAR Ltd. All rights reserved.
29 | NSMainNibFile
30 | MainMenu
31 | NSPrincipalClass
32 | NSApplication
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Notifier.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Notifier/Notifier/Structures.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Structures.swift
3 | // Notifier
4 | //
5 | // Copyright © 2024 dataJAR Ltd. All rights reserved.
6 | //
7 |
8 | // Imports
9 | import Foundation
10 |
11 | // Variables to be used globally
12 | struct GlobalVariables {
13 | // Path to the main apps bundle
14 | static let mainAppPath = Bundle.main.bundlePath
15 | // Path to the alert apps bundle
16 | static let alertAppPath = mainAppPath + "/Contents/Helpers/Notifier - Alerts.app"
17 | // Path to the banner apps bundle
18 | static let bannerAppPath = mainAppPath + "/Contents/Helpers/Notifier - Notifications.app"
19 | }
20 |
21 | // Structure of MessageContent
22 | struct MessageContent: Codable {
23 | // Optional - action to perform when the message is clicked
24 | var messageAction: [TaskObject]?
25 | // The notifications message (required)
26 | var messageBody: String?
27 | // Optional - message button label
28 | var messageButton: String?
29 | // Optional - action to perform when the message button is clicked
30 | var messageButtonAction: [TaskObject]?
31 | // Optional - the sound played when the notification has been delivered
32 | var messageSound: String?
33 | // Optional - the notifications subtitle
34 | var messageSubtitle: String?
35 | // Optional - the notifications title
36 | var messageTitle: String?
37 | // Arguments for the task object
38 | struct TaskObject: Codable {
39 | // The tasks executable
40 | var taskPath: String?
41 | // Arguments to pass to the task executable
42 | var taskArguments: [String]?
43 | }
44 | // Initialize MessageContent
45 | init(messageAction: [TaskObject]? = nil, messageBody: String? = nil, messageButton: String? = nil,
46 | messageButtonAction: [TaskObject]? = nil, messageSound: String? = nil, messageSubtitle: String? = nil,
47 | messageTitle: String? = nil) {
48 | self.messageAction = messageAction
49 | self.messageBody = messageBody
50 | self.messageButton = messageButton
51 | self.messageButtonAction = messageButtonAction
52 | self.messageSound = messageSound
53 | self.messageSubtitle = messageSubtitle
54 | self.messageTitle = messageTitle
55 | }
56 | }
57 |
58 | // Structure of RootElements
59 | struct RootElements: Codable {
60 | // Optional - the messages content
61 | var messageContent: String?
62 | // Optional - removes a specific notification or all notifications delivered
63 | var removeOption: String?
64 | // Optional - enables verbose logging
65 | var verboseMode: String?
66 | // Initialize MessageContent
67 | init(messageContent: String? = nil, removeOption: String? = nil, verboseMode: String? = nil) {
68 | self.messageContent = messageContent
69 | self.removeOption = removeOption
70 | self.verboseMode = verboseMode
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Notifier/Notifier/UserNotifications.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserNotifications.swift
3 | // Notifiers - Alerts
4 | //
5 | // Copyright © 2024 dataJAR Ltd. All rights reserved.
6 | //
7 |
8 | // Imports
9 | import UserNotifications
10 |
11 | // Sets the notifications body to what was passed to --message
12 | func setNotificationBody(parsedResult: ArgParser) -> String {
13 | // If verbose mode is enabled
14 | if parsedResult.verbose {
15 | // Progress log
16 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.message)")
17 | }
18 | // Returns the value passed to --message
19 | return parsedResult.message
20 | }
21 |
22 | // Sets the notifications message button to what was passed to --messagebutton
23 | func setNotificationMessageButton(parsedResult: ArgParser) -> String {
24 | // If verbose mode is enabled
25 | if parsedResult.verbose {
26 | // Progress log
27 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.messagebutton)")
28 | }
29 | // Returns the value passed to --messagebutton
30 | return parsedResult.messagebutton
31 | }
32 |
33 | // Sets the notifications sound to what was passed to --sound
34 | func setNotificationSound(parsedResult: ArgParser) -> String {
35 | // If verbose mode is enabled
36 | if parsedResult.verbose {
37 | // Progress log
38 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.sound)")
39 | }
40 | // Returns the value passed to --sound
41 | return parsedResult.sound
42 | }
43 |
44 | // Sets the notifications subtitle to what was passed to --subtitle
45 | func setNotificationSubtitle(parsedResult: ArgParser) -> String {
46 | // If verbose mode is enabled
47 | if parsedResult.verbose {
48 | // Progress log
49 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.subtitle)")
50 | }
51 | // Returns the value passed to --subtitle
52 | return parsedResult.subtitle
53 | }
54 |
55 | // Sets the notifications title to what was passed to --title
56 | func setNotificationTitle(parsedResult: ArgParser) -> String {
57 | // If verbose mode is enabled
58 | if parsedResult.verbose {
59 | // Progress log
60 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.title)")
61 | }
62 | // Returns the value passed to --title
63 | return parsedResult.title
64 | }
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Licensed under the Apache License, Version 2.0
3 | Copyright 2024 Jamf LTD
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6 |
7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8 |
9 |
10 | # Notifier
11 |
12 |
13 | Notifier is a Swift app which can post alert or banner notifications on macOS 10.15+ clients.
14 |
15 | Notifications are delivered via the [UserNotifications Framework](https://developer.apple.com/documentation/usernotifications)
16 |
17 | # Usage
18 | ## Basic Usage
19 | ```
20 | OVERVIEW: Notifier 3.1: Posts alert or banner notifications.
21 |
22 | USAGE: --type --message
23 | --type --remove prior
24 | --type --remove all
25 | --rebrand
26 |
27 | OPTIONS:
28 | --type alert or banner - REQUIRED.
29 |
30 | --message The notifications message.
31 |
32 | --messageaction
33 | The action to be performed under the users account
34 | when the message is clicked.
35 |
36 | • Passing 'logout' will prompt the user to logout.
37 | • If passed a single item, this will be launched via:
38 | /usr/bin/open
39 | • More complex commands can be passed, but the 1st
40 | argument needs to be a binaries path.
41 |
42 | For example: "/usr/bin/open" will work, "open" will
43 | not.
44 |
45 | --messagebutton
46 | Adds a button to the message, with the label being
47 | what is passed.
48 |
49 | --messagebuttonaction
50 | The action to be performed under the users account
51 | when the optional message button is clicked.
52 |
53 | • Passing 'logout' will prompt the user to logout.
54 | • If passed a single item, this will be launched via:
55 | /usr/bin/open
56 | • More complex commands can be passed, but the 1st
57 | argument needs to be a binaries path.
58 |
59 | For example: "/usr/bin/open" will work, "open" will
60 | not.
61 |
62 | --rebrand Requires root privileges and that the calling process
63 | needs either Full Disk Access (10.15+) or at a
64 | minimum App Management (macOS 13+) permissions, as
65 | well as the notifying applications being given
66 | permission to post to Notification Center. Any of
67 | these permissions can be granted manually, but
68 | ideally via PPPCP's delivered via an MDM.
69 |
70 | If successful and someone is logged in, Notification
71 | Center is restarted.
72 |
73 | --remove "prior" or "all". If passing "prior", the full
74 | message will be required too. Including all passed
75 | flags.
76 |
77 | --sound The sound to play when notification is delivered.
78 | Pass "default" for the default macOS sound, else the
79 | name of a sound in /Library/Sounds or
80 | /System/Library/Sounds.
81 |
82 | If the sound cannot be found, macOS will use the
83 | "default" sound.
84 |
85 | --subtitle The notifications subtitle.
86 |
87 | --title The notifications title.
88 |
89 | --verbose Enables logging of actions. Check console for
90 | 'Notifier' messages.
91 |
92 | --help Show help information.
93 |
94 | ```
95 |
96 | ## Examples
97 | The below aim to give you an idea of what so the various notification options look like, across macOS versions as well as Light and Dark mode.
98 |
99 | As you can see, some of the behaviour and appearence is OS dependent.
100 |
101 | **Example 1** This example shows a basic banner notification.
102 |
103 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode
104 | :-------------------------:|:-------------------------:
105 |
|
106 |
107 | ```
108 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message"
109 | ```
110 | ##
111 | **Example 2** This example shows a basic alert notification.
112 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode
113 | :-------------------------:|:-------------------------:
114 |
|
115 |
116 | ```
117 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "message" --messagebutton "Logout" --messagebuttonaction "Logout"
118 | ```
119 | ##
120 | **Example 3** This example shows both alert & banner notifications, (sleep used for the example gif, not needed in use but below for completeness sake).
121 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode
122 | :-------------------------:|:-------------------------:
123 |
|
124 |
125 | ```
126 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "Please Logout" --messagebutton "Logout" --messagebuttonaction "logout" --title "Logout";
127 | /bin/sleep 2;
128 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "👋" --title "Notification"
129 | ```
130 | ##
131 | **Example 4** This example shows selective remove of a delivered notificaition via `--remove prior`. Where applicable this would also remove notifications from within Notification Center itself.
132 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode
133 | :-------------------------:|:-------------------------:
134 |
|
135 |
136 | ```
137 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message 'Look at me!!';
138 | /bin/sleep 2;
139 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "message" --messagebutton "Logout";
140 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message 'Look at me!!' --remove prior
141 | ```
142 | ##
143 | **Example 5** This example shows removal of all alert notifications and also the differences of delivery of alert notifications across macOS versions, additionally this removes any prior delivered notificaions from Notification Center. Sleep used for the example gif, not needed in use but below for completeness sake.
144 |
145 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode
146 | :-------------------------:|:-------------------------:
147 |
|
148 |
149 | ```
150 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message;
151 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message1;
152 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message2;
153 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message3;
154 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message4;
155 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message5;
156 | /bin/sleep 5;
157 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --remove all
158 | ```
159 | ##
160 | **Example 6** This example shows Notifier 3.0+'s `--rebrand` argument in use, this allows for rebranding of Notifier without having to venture into Xcode etc.
161 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode
162 | :-------------------------:|:-------------------------:
163 |
|
164 |
165 | ```
166 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "message";
167 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message";
168 | /bin/sleep 2;
169 | /usr/bin/sudo /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --rebrand /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ErasingIcon.icns;
170 | /bin/sleep 2;
171 | /usr/bin/sudo /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message";
172 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --rebrand /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericFileServerIcon.icns;
173 | /bin/sleep 2;
174 | /usr/bin/sudo /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message";
175 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --rebrand /Applications/Utilities/Notifier.app/Contents/Resources/AppIcon.icns;
176 | /bin/sleep 2;
177 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message"
178 | ```
179 |
180 | # Deployment
181 |
182 | ## PPPC
183 | It's recommended that the below profile is recommended to be deployed before Notfier itself, to UAMDM devices.
184 | (https://github.com/dataJAR/Notifier/blob/master/profile/Allow%20Notifier%20Notifications.mobileconfig)
185 |
186 | This will allow Notifier to post Notifications without prompting the user to allow.
187 |
188 | Additionally, if you're looking to make use of the `--rebrand` flag the calling process needs either Full Disk Access/SystemPolicyAllFiles (10.15+) or at a
189 | minimum App Management/SystemPolicyAppBundles (macOS 13+) permissions, as well as the notifying applications being given permission to post to Notification Center.
190 |
191 | Any of these permissions can be granted manually, but ideally via PPPCP's delivered via an MDM.
192 |
193 | ## PKG
194 | PKG's will be supplied for every release, & can be found in the [releases](https://github.com/dataJAR/Notifier/releases) section.
195 |
196 | ## Flow
197 |
198 | The below is the advised deployment flow for Notifier.
199 |
200 | 1. If rebranding - deploy a PPPC for your management tool of choice, granting either Full Disk Access/SystemPolicyAllFiles (10.15+) or App Management/SystemPolicyAppBundles (macOS 13+).
201 | 2. Deploy the [Notications PpPC](https://github.com/dataJAR/Notifier/blob/master/profile/Allow%20Notifier%20Notifications.mobileconfig) to UAMDM devices
202 | 3. Deploy the latest [Notifier PKG](https://github.com/dataJAR/Notifier/releases)
203 | 4. If rebranding - deploy the image to use when rebranding.
204 | 5. if rebranding - deploy a script or payload free package etc which rebrands Notifier via your management tool. This needs to be down with admin/root privileges.
205 |
206 | # How it works
207 | The main Notifier.app parses arguments passed to it (via [Argument Parser](https://apple.github.io/swift-argument-parser/documentation/argumentparser/), and then posts the parsed argument to the two notifying applications included with the the /Contents/Helpers folder of Notifier.app:
208 |
209 | ```
210 | /Applications/Utilities/Notifier.app/Contents/Helpers/
211 | Notifier - Alerts.app
212 | Notifier - Notifications.app
213 | ```
214 | Additionally the Notifier.app also checks tha Notification Center is running, & runs the notifying apps in the user context.
215 |
216 | The notifying apps (/Applications/Utilities/Notifier.app/Contents/Helpers/Notifier - Alerts.app and /Applications/Utilities/Notifier.app/Contents/Helpers/Notifier - Notifications.app) both post notifications via the [UserNotifications Framework](https://developer.apple.com/documentation/usernotifications).
217 |
218 | # Miscellaneous
219 |
220 | ## Resetting Notifications Center
221 | The below _should_ reset Notifications Center, but please test & submit a PR with a better method as applicable.
222 |
223 | 1. `/bin/rm -rf $(getconf DARWIN_USER_DIR)/com.apple.notificationcenter/*`
224 | 2. Logout & then log back in, or:
225 | * For macOS 10.15+: `/usr/bin/killall "NotificationCenter"`
226 | 3. Test the Notifier once more
227 |
228 | # FAQs
229 |
230 | **Q1:** Why start at version 2?
231 |
232 | **A1:** At dataJAR, we already had a v1 "Notifier" app deployed.
233 | ##
234 | **Q2:** How can you post both banner & alert notifications?
235 |
236 | **A2:** See [How it works](#how-it-works)
237 | ##
238 | **Q3:** The users are being prompted to allow.. help!
239 |
240 | **A3:** This can be due to a few things:
241 |
242 | 1. Was the notifications profile installed? If not, install.
243 | 1. Is the device under UAMDM? If not, check with your MDM vendor on making the device UAMDM. Then try again & maybe reinstall the profile once under UAMDM.
244 | 1. Did you change the Bundle ID of either the Alert or Banner applications? If so you'll need to amend the notifications profile accordingly.
245 | ##
246 | **Q4:** Is that logo magen...
247 |
248 | **A4:** No, it's a shade of pink...
249 | ##
250 | **Q5:** Banner or Notifications?
251 |
252 | **A5:** The temporary notifications are referred to as "banner" notifications in Apple's documentation, but it seems that most folks call them "Notifications" with the persistent notifications being referred to as alerts by folks as well as within Apple's documentation.
253 | ##
254 | **Q6:** I'm seeing alerts when expecting banner notifications, & vice versa
255 |
256 | **A6:** Check the Notification settings within System Preferences/System Settings, it's possible that the incorrect option has been selected or set via a profile.
257 | ##
258 | **Q7:** --remove prior, didn't clear my last message.
259 |
260 | **A7:** Make sure you pass EXACTLY the same message to be cleared.
261 | ##
262 | **Q8:** I've rebranded, but the old icon is being shown..
263 |
264 | **A8:** Please reset Notification Center as per the [Resetting Notifications Center](#resetting-notifications-center), then try again.
265 | ##
266 | **Q9:** Does rebranding work when no one is logged in?
267 |
268 | **A9:** Yep, and no restart of Notification Center is needed.
269 | ##
270 | **Q10** Why are notifcations all of a sudden showing a prohibted sign across the icon?
271 |
272 | **A10** This is a [macOS issue](https://macmule.com/2021/10/28/notifications-showing-a-prohibitory-symbol-after-upgrading-macos-monterey/) that has goes back some years, to resolve either restart Notification Center or the Mac.
273 |
274 | # Alternatives
275 | The below projects can be used instead of Notifier & were very much instrumental in Notifiers creation
276 |
277 | ## Alert Notifications
278 | [Yo](https://github.com/sheagcraig/yo)
279 |
280 | ## Banner Notifications
281 | [Terminal Notifier](https://github.com/julienXX/terminal-notifier)
282 |
283 |
--------------------------------------------------------------------------------
/catalog-info.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: backstage.io/v1alpha1
2 | kind: Component
3 | metadata:
4 | name: datajar
5 | annotations:
6 | sonarqube.org/project-key: com.jamf.team-red-dawg:Notifier
7 | spec:
8 | type: service
9 | owner: datajar
10 | lifecycle: production
11 | system: datajar
12 |
--------------------------------------------------------------------------------
/package/make-notifier-pkg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ##########################################################################################
4 | #
5 | # In accessing and using this Product, you confirm that you accept the terms of our
6 | # Product Licence in full and agree to comply with the terms of such Licence.
7 | #
8 | # https://datajar.co.uk/product-licence/
9 | #
10 | ##########################################################################################
11 | #
12 | # CHANGE LOG
13 | # 1.0 - Created
14 | #
15 | ##########################################################################################
16 | #
17 | # This script creates Notifier-.pkg within this repos package dir.
18 | #
19 | # Needs to be ran as root/sudo, and requires the following arguments to be passed to it:
20 | #
21 | # $1 - The team id of a "Developer ID Installer" identity within the running users keychain:
22 | # https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates/
23 | # $2 - Name of an App-specific password within the running users keychain, to be used for notarization
24 | # https://support.apple.com/en-us/102654
25 | # $3 - pkg receipt identifier
26 | # uk.dataJAR.Notifier
27 | #
28 | ##########################################################################################
29 | ################################### Global Variables #####################################
30 | ##########################################################################################
31 |
32 | # Exit if any commands error
33 | set -e
34 |
35 | # Extension of the script (.py, .sh etc)..
36 | scriptExtension=${0##*.}
37 |
38 | # Overall name of the family of software we are installing, with extension removed
39 | swTitle="$(/usr/bin/basename "$0" ."$scriptExtension")"
40 |
41 | # Script Version
42 | ver="1.0.1"
43 |
44 | # Full path to the dir this script is in
45 | scriptDir="$(/usr/bin/dirname "$0")"
46 |
47 | # Path to Notifier.app
48 | notifierPath="${scriptDir}/ROOT/Applications/Utilities/Notifier.app"
49 |
50 | # Path to scripts dir the packages dir
51 | pkgScriptsDir="${scriptDir}/scripts/"
52 |
53 | # Developer ID Installer identity
54 | teamID="${1}"
55 |
56 | # App specific password for notarization
57 | appPassword="${2}"
58 |
59 | # pkg identifier
60 | pkgIdentifier="${3}"
61 |
62 | ##########################################################################################
63 | #################################### Start functions #####################################
64 | ##########################################################################################
65 |
66 | setup()
67 | {
68 | # Make sure we're root
69 | if [ "$(id -u)" != "0" ]
70 | then
71 | /bin/echo "ERROR: This script must be run as root"
72 | return 1
73 | fi
74 |
75 | # Make sure that there is a Notifier.app in the package folder
76 | if [ ! -e "${notifierPath}" ]
77 | then
78 | /bin/echo "ERROR: Notifier.app not found at: ${notifierPath}, exiting..."
79 | return 1
80 | fi
81 |
82 | # Get the value of $1, error if doesn't exist
83 | if [ -z "${teamID}" ]
84 | then
85 | /bin/echo "ERROR: teamID argument passed to script..."
86 | /bin/echo
87 | return 1
88 | # If we have been passed an value to $1
89 | else
90 | # Make sure that we have a "Developer ID Installer: ${teamdID}" identity in the users keychain
91 | developerIDInstaller=$(/usr/bin/security find-identity -v 'login.keychain' -s -p basic | /usr/bin/awk -F "\"" '/Developer ID Installer:.*?'"${teamID}"'/ {print $2}')
92 | # If we haven't found the required Developer ID Installer
93 | if [ -z "${developerIDInstaller}" ]
94 | then
95 | # Error if we cannot find Developer ID Installer: ${teamdID} in the keychain
96 | /bin/echo "ERROR: no \"${teamID}\" Developer ID Installer certificate found in login.keychain, exiting..."
97 | return 1
98 | fi
99 | fi
100 |
101 | # Get the value of $2, error if doesn't exist
102 | if [ -z "${appPassword}" ]
103 | then
104 | /bin/echo "ERROR: missing argument $2..."
105 | /bin/echo
106 | return 1
107 | # If we have been passed an value to $2
108 | else
109 | # Make sure that $appPassword exists in the user keychain
110 | if [ -z "$(/usr/bin/security find-generic-password login.keychain -D "application password" -G "${appPassword}")" ]
111 | then
112 | # Error if we cannot find appPassword in the keychain
113 | /bin/echo "ERROR: password: \"${appPassword}\" not found in login.keychain, exiting..."
114 | return 1
115 | fi
116 | fi
117 |
118 | # Get the value of $3, error if doesn't exist
119 | if [ -z "${pkgIdentifier}" ]
120 | then
121 | /bin/echo "ERROR: missing argument $3..."
122 | /bin/echo
123 | return 1
124 | fi
125 | }
126 |
127 |
128 | start()
129 | {
130 | # Logging start
131 | /bin/echo "Running ${swTitle} ${ver}..."
132 | }
133 |
134 |
135 | finish()
136 | {
137 | # Logging finish
138 | /bin/echo "Finished: $(/bin/date)"
139 | }
140 |
141 | usage()
142 | {
143 | /bin/echo "
144 | ABOUT: This script creates Notifier-.pkg within this repos package dir.
145 |
146 | USAGE: Ths script needs to be ran as root/sudo, and requires the following arguments to be passed to it:
147 |
148 | \$1 - The team id of a "Developer ID Installer" identity within the running users keychain:
149 | https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates/
150 | \$2 - Name of an App-specific password within the running users keychain, to be used for notarization:
151 | https://support.apple.com/en-us/102654
152 | \$3 - pkg receipt identifier (for example: uk.dataJAR.Notifier)
153 | "
154 | }
155 |
156 | getNotifierVersion()
157 | {
158 | # Queries Notifier.app for version
159 | /bin/echo "Getting notifier version from ${notifierPath}/Contents/Info.plist..."
160 | notifierVersion=$(/usr/bin/defaults read "${notifierPath}"/Contents/Info.plist CFBundleShortVersionString)
161 | /bin/echo "notifier version: ${notifierVersion}..."
162 | }
163 |
164 |
165 | getMinOSVersion()
166 | {
167 | # Queries Notifier.app for min os version
168 | /bin/echo "Getting min os version from ${notifierPath}/Contents/Info.plist..."
169 | minOS=$(/usr/bin/defaults read "${notifierPath}"/Contents/Info.plist LSMinimumSystemVersion)
170 | /bin/echo "minOS: ${minOS}..."
171 | }
172 |
173 | setPermissions()
174 | {
175 | # Sets user:group ownership
176 | /bin/echo "Setting root:wheel for: ${scriptDir}/ROOT/..."
177 | /usr/sbin/chown -R root:wheel "${scriptDir}/ROOT/"
178 |
179 | # Set perms
180 | /bin/echo "Setting 755 permissions for: ${scriptDir}/ROOT/..."
181 | /bin/chmod -R 755 "${scriptDir}/ROOT/"
182 |
183 | # Mark Notifier.app as executable
184 | /bin/echo "Marking: ${notifierPath} as executable..."
185 | /usr/bin/sudo /bin/chmod -R +x "${notifierPath}"
186 | }
187 |
188 | createPKG()
189 | {
190 | # Creates a component plist
191 | /bin/echo "Creating ${scriptDir}/notifier-app.plist"...
192 | /usr/bin/pkgbuild --analyze --root "${scriptDir}"/ROOT/ "${scriptDir}"/notifier-app.plist
193 |
194 | # Set BundleIsRelocatable to NO in component plist
195 | /bin/echo "Setting \"BundleIsRelocatable\" to NO in ${scriptDir}/notifier-app.plist..."
196 | /usr/bin/plutil -replace BundleIsRelocatable -bool NO "${scriptDir}"/notifier-app.plist
197 |
198 | # Creates notifier.pkg in ${scriptDir}/
199 | /bin/echo "Creating ${scriptDir}/notifier-${notifierVersion}.pkg"
200 | /usr/bin/pkgbuild --identifier "${pkgIdentifier}" --root "${scriptDir}"/ROOT/ --sign "${developerIDInstaller}" --install-location '/' --version "${notifierVersion}" --min-os-version "${minOS}" --component-plist "${scriptDir}"/notifier-app.plist "${scriptDir}"/Notifier-"${notifierVersion}".pkg
201 | }
202 |
203 | notarizePKG()
204 | {
205 | # Notarizes the PKG created within the createPKG function
206 | /bin/echo "Notarizing ${scriptDir}/Notifier-${notifierVersion}.pkg..."
207 | /usr/bin/xcrun notarytool submit "${scriptDir}"/Notifier-"${notifierVersion}".pkg --keychain-profile "${appPassword}" --wait
208 | }
209 |
210 | clearPermissions()
211 | {
212 | # Sets perms on ROOT dir recursively after building a pkg
213 | /bin/echo "Clearing permissions on: ${scriptDir}/ROOT/..."
214 | /bin/chmod -R 777 "${scriptDir}/ROOT/"
215 | }
216 |
217 | ##########################################################################################
218 | #################################### End functions #######################################
219 | ##########################################################################################
220 |
221 | if ! setup "$@"
222 | then
223 | usage
224 | else
225 | start
226 | getNotifierVersion
227 | getMinOSVersion
228 | setPermissions
229 | createPKG
230 | notarizePKG
231 | clearPermissions
232 | finish
233 | fi
234 |
--------------------------------------------------------------------------------
/profile/Allow Notifier Notifications.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadContent
6 |
7 |
8 | NotificationSettings
9 |
10 |
11 | AlertType
12 | 1
13 | BadgesEnabled
14 |
15 | BundleIdentifier
16 | uk.co.dataJAR.NotifierNotifications
17 | CriticalAlertEnabled
18 |
19 | NotificationsEnabled
20 |
21 | ShowInLockScreen
22 |
23 | ShowInNotificationCenter
24 |
25 | SoundsEnabled
26 |
27 |
28 |
29 | AlertType
30 | 2
31 | BadgesEnabled
32 |
33 | BundleIdentifier
34 | uk.co.dataJAR.NotifierAlerts
35 | CriticalAlertEnabled
36 |
37 | NotificationsEnabled
38 |
39 | ShowInLockScreen
40 |
41 | ShowInNotificationCenter
42 |
43 | SoundsEnabled
44 |
45 |
46 |
47 | PayloadDescription
48 |
49 | PayloadDisplayName
50 | Allow Notifier Notifications
51 | PayloadEnabled
52 |
53 | PayloadIdentifier
54 | com.github.erikberglund.ProfileCreator.640EBF16-58E0-42CE-83D3-9BC3A2A6010E.com.apple.notificationsettings.86ADF17C-0236-44C6-B2C2-00037C0C1CC3
55 | PayloadOrganization
56 | dataJAR Ltd
57 | PayloadType
58 | com.apple.notificationsettings
59 | PayloadUUID
60 | 86ADF17C-0236-44C6-B2C2-00037C0C1CC3
61 | PayloadVersion
62 | 1
63 |
64 |
65 | PayloadDescription
66 |
67 | PayloadDisplayName
68 | Allow Notifier Notifications
69 | PayloadEnabled
70 |
71 | PayloadIdentifier
72 | C4B6FAAA-D592-4FA2-BF68-F8994C05FF0D
73 | PayloadOrganization
74 | dataJAR Ltd
75 | PayloadRemovalDisallowed
76 |
77 | PayloadScope
78 | System
79 | PayloadType
80 | Configuration
81 | PayloadUUID
82 | C4B6FAAA-D592-4FA2-BF68-F8994C05FF0D
83 | PayloadVersion
84 | 1
85 |
86 |
87 |
--------------------------------------------------------------------------------