Featured image of post macOS Privilege Management: Deploying SAP's Privileges, DataDog Alerts, and Kolide Monitoring

macOS Privilege Management: Deploying SAP's Privileges, DataDog Alerts, and Kolide Monitoring

Deploying SAP's Privileges to macOS, logging escalation reasons and demotions to DataDog, and validating the deployment with Kolide.

Why we need Privileges or why Apple needs to get better at admin-less actions

Back in 2019-2020, I blogged about using Privileges as it was somewhat freshly out in the Apple Community. It worked well for certain things but had some issues, namely:

  • End-users couldn’t be prompted for the reason for the escalation.
  • Admin demotion on launch didn’t always work effectively.
  • Logging and alerting of escalation requests didn’t work very well.
  • Various little items

Now that SAP has released Privileges 2.X of their software, a lof of these previous issues I was having with our deployment back in 2019-2020, have been resolved. I have also started working at $currentJob, which have the same reoccurring struggle: Needing Admin-less access on macOS and Audits. In every audit I have ever been in, they ask, “Do you restrict admin access on macOS?” So, getting us to easily pass this question with a confirmation of approval while still allowing end-users to promote themselves is the best of both worlds.

I am not a fan of pure admin-less, as macOS is not built or intended to be used in a way where the end user doesn’t have admin access to perform some functions. Privileges creates an easy middle-ground of the best of both worlds.

Why does Apple do the way they do?

Apple requires an end-user to have admin access to several things, mainly end-user-beneficial services. Apple has generally not taken the stance of relaxing any of these permission requirements but locking them down even further, which makes life as both an admin and an end-user quite tricky.

Additionally, adjusting what can be done with or without admin access is impossible. Yes, you could add a user to the lpadmin group, but for most situations, it is impossible to restrict some of these with admin access or provide access for admin-less users. For example, Software Updates (EG: macOS 15 Sequoia) shouldn’t require Administrator access—similar to Feature and Quality updates on Windows.

Arguably, anything network-related (e.g., Changing DNS, Changing Network SSIDs, setting a static IP Address) should not require administrator access to execute. This type of elevation is not needed for any user-space configurations. It would be nice if this could be separated out of the system-level configurations.

For a more comprehensive list of required administrative actions, see below.

Actions on macOS That Require Administrator Access

1. User & Account Management
  • User Accounts
    • Creating, deleting, or modifying user accounts
    • Changing user roles (Standard ↔ Admin)
    • Resetting passwords for other users
    • Modifying parental controls
  • Login & Authentication
    • Managing login items for all users
    • Enabling/disabling automatic login
    • Modifying FileVault encryption settings
    • Approving biometric (Touch ID) system-wide changes
2. System & Security Settings
  • Security & Privacy
    • Configuring Gatekeeper settings (allowing apps from “Anywhere”)
    • Modifying Privacy & Security settings (Full Disk Access, Accessibility, etc.)
    • Enabling/disabling System Integrity Protection (SIP)
    • Modifying Firewall rules and network permissions
  • Software & Updates
    • Installing system updates via System Settings → Software Update
    • Installing/removing system-wide apps in /Applications
    • Approving system extensions (KEXTs)
    • Modifying system-wide LaunchDaemons or LaunchAgents
  • System Configuration
    • Modifying NVRAM/PRAM and SMC settings (via recovery mode)
    • Enabling/disabling Time Machine backups
    • Changing system-wide environment variables
    • Using the console.app or the log command at the system level
3. Network & Connectivity
  • Networking
    • Changing system-wide Wi-Fi, Ethernet, or VPN configurations
    • Modifying Proxy settings
    • Setting custom DNS servers
  • Remote Access
    • Enabling/disabling Remote Login (SSH)
    • Configuring Screen Sharing, Remote Management (ARD)
  • Internet Sharing & Hotspots
    • Enabling/disabling Internet Sharing
    • Modifying Personal Hotspot settings
4. File System & Storage Management
  • Filesystem Permissions
    • Modifying system files in /System, /Library, or /bin
    • Changing ownership or permissions of system files (chmod, chown)
  • Disk & Volume Management
    • Mounting/unmounting encrypted drives
    • Formatting, partitioning, or erasing drives (diskutil)
    • Managing APFS snapshots or encrypted volumes
  • Keychain & Certificates
    • Installing or modifying root certificates
    • Modifying system-wide keychains
5. Device & Hardware Control
  • Peripheral & Hardware Access
    • Installing/removing drivers (e.g., s, USB devices)
    • Allowing USB accessories in macOS Ventura+ (USB Restricted Mode)
  • Apple Silicon (M1/M2) & T2 Security Settings
    • Modifying Startup Security Utility settings
    • Disabling Secure Boot or enabling external booting
  • Virtualization & Development
    • Enabling Hypervisor framework for virtual machines
    • Running macOS inside a VM (e.g., VMware, VirtualBox)
6. Enterprise & IT Administration
  • MDM & Configuration Profiles
    • Installing/removing Mobile Device Management (MDM) profiles
    • Applying system-wide configuration profiles (profiles CLI)
    • Installing/removing enterprise VPN profiles
  • Software Deployment & Automation
    • Installing system-wide software via command-line package managers (e.g., brew affecting /usr/local/)
    • Running automation scripts that modify system settings (sudo)
  • Remote Management & Security Policies
    • Enforcing corporate policies via Jamf, Intune, or other MDM solutions
    • Applying remote lock/wipe via Find My Mac

Notable Exceptions (That Do Not Require Admin)

Some actions appear to require admin but do not:

  • Installing apps in ~/Applications (user-specific)
  • Using AirDrop, Bluetooth, or Wi-Fi (unless restricted by MDM)
  • Adjusting display settings, Dock preferences, or menu bar settings
  • Running most command-line tools without modifying system files

Setting up checks and balances

So, to get the best of both worlds, we need to determine several things.

  • We need a system or service that monitors activity on macOS. This could be a Log Shipper, Behavior Analytics, MDM, or some other software (e.g., DTEX, JAMF, etc.).
  • We need easy alerting and logging based on the actions and promotion/demotion requests.
    • This can be accomplished with Privileges and a shell script (or python, go, etc).
  • We need an assurance tool to enforce admin-less activities.
    • This will ensure that individuals are not trying to work around the system.
  • We need some form of verification that assurance, files and other processes are working together. This way, we can determine if someone is faking information through the device assurance tool or less intrusive device assurance.

So how will we accomplish that?

Deploying DataDog API Keys in a secure fashion

So, first, we must set up a way to securely deploy our DataDog API key. There are a few methods you could use to accomplish this:

  • Use JAMF to distribute it through a script
  • Deploy the script as part of a no-package file that executes the script
    • Adding the API Key to the Keychain
  • Using a secrets vault to call the API key from 1Password, HashiCorp, GCP, or AWS.

We would prefer reliability here, which means that the API key needs to be on the device for it to work reliably. The risk of the API Key is also quite low, as all it can do is post content into a DataDog instance attached to a specific tag/service.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/zsh

# Ensure an API key is provided
if [[ -z "$4" ]]; then
    echo "Error: No API key provided."
    exit 1
fi

# Store the API key in Keychain
security add-generic-password -s "DatadogAPI" -a "DatadogUploader" -w "$4" -U

# Verify the API key is stored
if security find-generic-password -s "DatadogAPI" -a "DatadogUploader" -w >/dev/null 2>&1; then
    echo "Datadog API key successfully added to Keychain."
else
    echo "Error: Failed to store Datadog API key in Keychain."
    exit 1
fi

exit 0

What this does is:

  • Push the API key into Keychain and call it “DataDog API”
  • This will allow us to call the Datadog API key in scripts that we need to use to upload the log file without putting in “clearer” text.

But then, how will we handle network connectivity issues? Check out the section below for more information.

Deploying the Privileges PKG

Previously, in versions before 1.5.4, you would likely have to bundle the .app to include the launch agent or add any additional functionality you wanted with the tool. Now, with 2.X>, this is being done automatically, so there is little overhead other than pushing out the tool.

We will not discuss in depth how to deploy a PKG file via an MDM, as it depends on each MDM service.

Demoting Administrators to Users immediately on installation

Now, with deploying privileges, one of the few issues remains that users won’t automatically get demoted from Administrator to Standard Users, at least not immediately if you were to wait for those users to reboot or click on the app/dock icon. So, we want to deploy a script that will automatically demote the user to Standard User.

Here are some key deployment notes. Using JAMF is easy enough. We just drop a script in the same policy as the package installation and mark it for “after” run. If you are using other MDMs or package repositories (EG: Munki), here are a few options:

  • If using Munki, you could add this to a post-install script, or you could couple a requires key with the packages, use manifest, etc. There are several different ways to tackle this.
  • Kandji and Mosyle also support the idea of a “post-install” script, which would run immediately after the package is installed. You could also use a Blueprint if you need to install multiple packages in a single run.
  • If using Intune, you would need to wrap the package inside another package to deploy this effectively. This is not a recommended method, as it adds several layers of confusion. Alternatively, you could deploy both applications separately and then use configuration scripts to manage the setup. Again, this is a bit more confusing.

Ultimately, it excludes the administrator and root user and automatically downgrades the logged-in user to admin utilizing PrivilegesCLI -r.

When writing this, I was having some issues getting the Privileges output to read correctly on the demotions. However, I will work on getting this updated and improved so that the script properly exits and validates.

 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
26
27
28
29
30
31
32
33
34
#!/bin/zsh

# Get the currently logged-in user
currentUser=$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')

# Exclude specific users from demotion
if [[ "$currentUser" == "administrator" || "$currentUser" == "root" ]]; then
    echo "Skipping demotion for $currentUser."
    exit 0
fi

# Path to PrivilegesCLI
PRIVILEGES_CLI="/Applications/Privileges.app/Contents/MacOS/PrivilegesCLI"

# Ensure PrivilegesCLI exists
if [[ ! -x "$PRIVILEGES_CLI" ]]; then
    echo "Error: PrivilegesCLI not found. Ensure Privileges.app is installed."
    exit 1
fi

# Run PrivilegesCLI as the current user to demote them
/bin/launchctl asuser $(/usr/bin/id -u "$currentUser") sudo -u "$currentUser" "$PRIVILEGES_CLI" --remove 2>/dev/null

# Verify demotion
sleep 2
demotionStatus=$(/bin/launchctl asuser $(/usr/bin/id -u "$currentUser") sudo -u "$currentUser" "$PRIVILEGES_CLI" --status)

if echo "$demotionStatus" | /usr/bin/grep -q "standard user"; then
    echo "User $currentUser is already a standard user, skipping demotion."
    exit 0
else
    echo "Failed to demote $currentUser, but user may already be a standard user."
    exit 0
fi

Deploying an on-action alert for Privileges

So, it’s helpful for us to understand not only why an end-user needs to be promoted to admin but also the time, date, username, machine, and serial number of the device. It is also helpful to have the expected demotion time to cross-check if the demotion occurred at all (whether on time or before the expected time frame).

So, let us go over the script below.

  1. Configure the DataDog API Endpoint. Note that this one specifically uses the EU endpoint and would need to be updated for the US or other locations based on your individual needs.
  2. We will pass the reason from the Privileges.app into the on-action script through $3, and if this fails or isn’t passed, use the log command as a backup to process and find the reason. We will also wait 5 seconds to ensure that the log command can see the contents of the SAP output.
  3. We will check if the user is currently an admin while this script is running to ensure that the log command can work under the admin run.
  4. Check the install.log commands on demotion and output any new installations (as this file can be read without admin logs).
  5. We will sanitize the reason and the log files to make them difficult to exploit through command redirections or hacks (be aware that this isn’t bulletproof).
  6. We add tags to the HTTP post command to differentiate between a privilege escalation and a privilege demotion.
  7. Then, finally, send that data to DataDog.
  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
 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
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
#!/bin/zsh

# Determine the Datadog API key
DATADOG_API_KEY="${4:-$(/usr/bin/security find-generic-password -s "DatadogAPI" -a "DatadogUploader" -w 2>/dev/null)}"

if [[ -z "$DATADOG_API_KEY" ]]; then
    /bin/echo "Error: Datadog API key not found."
    exit 1
fi

DATADOG_ENDPOINT="https://http-intake.logs.datadoghq.eu/api/v2/logs"

# Get system information
currentUser=$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ { print $3 }')
hostname=$(/bin/hostname)
timestamp=$(/bin/date +"%Y-%m-%d %H:%M:%S")
futureTime=$(/bin/date -v+14M -v+56S +"%Y-%m-%d %H:%M:%S")
serialNumber=$(/usr/sbin/ioreg -l | /usr/bin/awk -F'"' '/IOPlatformSerialNumber/ {print $4}')
logfile="/private/tmp/user-initiated-privileges-change.tmp"
passedPromotionReason="$3"

# Check if the user is currently an admin
adminUsers=$(/usr/bin/dscl . -read /Groups/admin GroupMembership 2>/dev/null | /usr/bin/awk '{$1=""; print $0}' | /usr/bin/tr -s ' ')
if echo "$adminUsers" | /usr/bin/grep -qw "$currentUser"; then
    previousStatus="Administrator"
    userWasAdmin=true
else
    previousStatus="Standard User"
    userWasAdmin=false
fi

# Wait for user promotion (up to 5 seconds)
wait_time=0
while ! $userWasAdmin && (( wait_time < 5 )); do
    /bin/echo "Waiting for admin privileges... ($wait_time sec)"
    /bin/sleep 1
    ((wait_time++))
    
    # Recheck admin status
    if echo "$adminUsers" | /usr/bin/grep -qw "$currentUser"; then
        userWasAdmin=true
    fi
done

# Determine new status
privilegeStatus=$([[ $userWasAdmin == true ]] && /bin/echo "Administrator" || /bin/echo "Standard User")
/bin/echo "$privilegeStatus"

# Capture the reason for promotion
if [[ "$privilegeStatus" == "Administrator" ]]; then
    promotionReason="$passedPromotionReason"
    if [[ -z "$promotionReason" ]]; then
        /bin/sleep 5
        logOutput=$(/usr/bin/log show --style syslog --predicate 'process == "PrivilegesDaemon" && eventMessage CONTAINS "SAPCorp"' --info --last 5m 2>/dev/null)
        promotionReason=$(echo "$logOutput" | /usr/bin/grep -oE 'User .* now has administrator privileges for the following reason: ".*"' | /usr/bin/tail -n1 | /usr/bin/sed -E 's/.*for the following reason: "(.*)"/\1/' | /usr/bin/tr -d '\n')
        if [[ -z "$promotionReason" ]]; then
            promotionReason="Failed to obtain reason."
        fi
    fi
else
    promotionReason="User was demoted to standard user automatically."
fi

# Capture installation logs for demotion
installLogEntries="N/A"
if [[ "$privilegeStatus" == "Standard User" ]]; then
    installLogEntries=$(/usr/bin/awk -v d="$($(/bin/date -v-20M +"%b %d %H:%M:%S"))" '$0 > d && ($0 ~ /Installer\[/ || $0 ~ /installd\[/) && ($0 ~ /Installation Log/ || $0 ~ /Install: \"/ || $0 ~ /PackageKit: Installed/)' /var/log/install.log 2>/dev/null)
fi

# Sanitize LogMessage
sanitizedReason="${promotionReason//[^[:print:]]/}"
sanitizedInstallLog="${installLogEntries//[^[:print:]]/}"

# Construct log message
logMessage="User $currentUser changed privilege status at $timestamp on $hostname. Expected removal at $futureTime. Status: $privilegeStatus. Reason: $sanitizedReason"
[[ "$privilegeStatus" == "Standard User" ]] && logMessage+="\n\nInstall Log:\n\n$sanitizedInstallLog"

/bin/echo "$logMessage" | /usr/bin/tee "$logfile"

# Determine dynamic Datadog tag
addtags=$([[ "$privilegeStatus" == "Administrator" ]] && /bin/echo "privilege-escalation-request" || /bin/echo "privilege-escalation-revoke")

# Prepare JSON log data for Datadog
LOG_DATA=$(/bin/cat <<EOF
{
  "ddsource": "macos",
  "service": "Privileges",
  "ddtags": "$addtags",
  "hostname": "$hostname",
  "username": "$currentUser",
  "serialnumber": "$serialNumber", 
  "timestamp": "$timestamp",
  "message": "$(/bin/echo "$logMessage" | /usr/bin/sed 's/"/\\"/g')"
}
EOF
)

# Send log to Datadog
/usr/bin/curl -s -o /dev/null -w "%{http_code}" -X POST "$DATADOG_ENDPOINT" \
     -H "Content-Type: application/json" \
     -H "DD-API-KEY: $DATADOG_API_KEY" \
     -d "$LOG_DATA" | /usr/bin/grep -q 202 || { /bin/echo "Failed to send log to Datadog."; exit 1; }

exit 0

Deploying Privileges Mobile Configuration / JSON Schema

For JAMF, JSON Schema configurations seem to be the way to manage the configurations going forward. We have an internal one, but thankfully, Tony Young (@tonyyo11) has been creating one himself, which is shown below (for Privileges v2.2).

This makes it substantially easier to deploy the Mobile Configuration profile.

  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
 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
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
{
    "description": "Comprehensive schema for Privileges configuration.",
    "title": "Privileges Configuration (corp.sap.privileges)",
    "type": "object",
    "properties": {
        "ExpirationInterval": {
            "description": "Set a fixed time interval after which administrator privileges expire and revert to standard user rights. A value of 0 disables the timeout.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "integer",
                    "default": 15
                }
            ]
        },
        "ExpirationIntervalMax": {
            "description": "Set a maximum time interval for a user to request administrative privileges. Allows users to choose any timeout value up to the specified one.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "integer",
                    "default": 20
                }
            ]
        },
        "EnforcePrivileges": {
            "description": "Enforce specific privileges. Values can be 'admin', 'user', or 'none'. Enforces privileges immediately and restricts further changes.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "string",
                    "enum": [
                        "admin",
                        "user",
                        "none"
                    ],
                    "default": "admin"
                }
            ]
        },
        "ShowInMenuBar": {
            "description": "If set to true, a Privileges status item is displayed in the Menu Bar.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "HideSettingsButton": {
            "description": "If set to true, the Settings button is no longer displayed in the app.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "HideSettingsFromDockMenu": {
            "description": "If set to true, the Settings menu item is no longer displayed in the Dock tile's menu.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "HideSettingsFromStatusItem": {
            "description": "If set to true, the Settings menu item is no longer displayed in the status item's menu.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "HideHelpButton": {
            "description": "If set to true, the 'Help (?)' button is no longer displayed in the app.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "HelpButtonCustomURL": {
            "description": "If specified, this url is called instead of the Privileges Github URL if the user clicks the help button. Malformed URLs and non-http(s) URLs are ignored.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "string",
                    "default": "https://your_support_url_goes_here"
                }
            ]
        },
        "LimitToGroup": {
            "description": "Restrict use of the application to a specified group or list of groups. Specify as a string or array of strings.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": [
                        "string",
                        "array"
                    ],
                    "items": {
                        "type": "string"
                    },
                    "default": "group_name_goes_here"
                }
            ]
        },
        "LimitToUser": {
            "description": "Restrict use of the application to a specified user or list of users. Variables such as $USERNAME can be used if supported by the management system.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": [
                        "string",
                        "array"
                    ],
                    "items": {
                        "type": "string"
                    },
                    "default": "username_goes_here"
                }
            ]
        },
        "ReasonRequired": {
            "description": "Specifies whether users must provide a reason for requesting administrator privileges. When true, privileges cannot be changed from the Privileges Dock tile menu.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "ReasonCheckingEnabled": {
            "description": "If set to true, the text the user enters for a reason is roughly parsed for valid words. If the text does not contain any valid words, the Request Privileges button remains grayed out.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "PassReasonToExecutable": {
            "description": "Specifies whether the reason for requesting administrator privileges should be passed to the executable configured with PostChangeExecutablePath. Passed as $3 if enabled.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "ReasonMinLength": {
            "description": "If 'ReasonRequired' is true, specifies the minimum number of characters for the reason. Defaults to 10.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "integer",
                    "default": 20
                }
            ]
        },
        "ReasonMaxLength": {
            "description": "If 'ReasonRequired' is true, specifies the maximum number of characters for the reason. Defaults to 250.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "integer",
                    "default": 50
                }
            ]
        },
        "ReasonPresetList": {
            "description": "If 'ReasonRequired' is true, allows pre-defining a list of possible reasons for becoming an admin. This creates an additional pop-up menu in the dialog box (only for the GUI version of Privileges). If no exact match is found, the default localization is used. If there is no default localization, the en localization is used. If there is no en localization, the dictionary is skipped.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "default": {
                                "type": "string"
                            },
                            "en": {
                                "type": "string"
                            },
                            "de": {
                                "type": "string"
                            },
                            "es": {
                                "type": "string"
                            },
                            "it": {
                                "type": "string"
                            }
                        }
                    }
                }
            ]
        },
        "RemoteLogging": {
            "description": "Configuration for logging server settings.",
            "type": "object",
            "properties": {
                "ServerType": {
                    "description": "The type of logging server. Supported values are 'syslog' and 'webhook'.",
                    "type": "string",
                    "enum": [
                        "syslog",
                        "webhook"
                    ],
                    "default": "syslog"
                },
                "ServerAddress": {
                    "description": "The address of the logging server. The server address can be an IP Address or host name if a syslog server is configured. For Webhooks, please provide an http/https URL",
                    "type": "string",
                    "default": "ip_address_or_host_name_goes_here"
                },
                "WebhookCustomData": {
                    "description": "You may use this dictionary to pass custom data like machine name, serial number, Jamf Pro ID, etc. to the webhook. This data is added to the webhook's json as 'custom_data'.",
                    "type": "object",
                    "properties": {
                        "name": {
                            "description": "An string to pass custom data to the webhook. If your MDM supports variables, you may pass $COMPUTERNAME",
                            "type": "string",
                            "default": "$COMPUTERNAME"
                        },
                        "serial": {
                            "description": "An string to pass custom data to the webhook. If your MDM supports variables, you may pass $COMPUTERNAME",
                            "type": "string",
                            "default": "$SERIALNUMBER"
                        },
                        "jamfid": {
                            "description": "An string to pass custom data to the webhook. If your MDM supports variables, you may pass $COMPUTERNAME",
                            "type": "string",
                            "default": "$JSSID"
                        }
                    }
                },
                "SyslogOptions": {
                    "description": "Syslog-specific options.",
                    "type": "object",
                    "properties": {
                        "ServerPort": {
                            "description": "An integer specifying the port of the logging server. If not specefied, the port defaults to 514 or to 6514 if TLS is enabled.",
                            "type": "integer",
                            "default": 514
                        },
                        "UseTLS": {
                            "description": "If set to true, TLS is enabled for the connection. Please make sure your clients have a certificate installed that mattches Apple's documentation. Please see https://support.apple.com/en-us/103769 for further information.",
                            "type": "boolean",
                            "default": false
                        },
                        "LogFacility": {
                            "description": "An integer specifying the syslog facility. If not specified, facility defaults to 4 (security). Please see https://tools.ietf.org/html/rfc5424#section-6.2.1 for further information.",
                            "type": "integer",
                            "default": 4
                        },
                        "LogSeverity": {
                            "description": "An integer specifying the syslog facility. If not specified, facility defaults to 6 (informational). Please see https://tools.ietf.org/html/rfc5424#section-6.2.1 for further information.",
                            "type": "integer",
                            "default": 6
                        },
                        "MaximumMessageSize": {
                            "description": "An integer specifying the maximum size of the syslog message (header + event message). If not specified, the vaule defaults to 480 which is the minimum maximum message size a syslog server must support. If the syslog message is larger than the specified maximum, the message will be truncated at the end. Please see https://tools.ietf.org/html/rfc5424#section-6.2.1 for further information.",
                            "type": "integer",
                            "default": 480
                        }
                    }
                }
            }
        },
        "RequireAuthentication": {
            "description": "Specifies whether authentication is required to obtain administrator privileges. When true, users are prompted for their account password or Touch ID if available.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "RevokePrivilegesAtLogin": {
            "description": "If set to true, the user's administrator privileges are revoked at login.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "HideOtherWindows": {
            "description": "By default, Privileges hides open windows to show the desktop and ensure that only the Privileges window is visible on the screen. Set HideOtherWindows to false to disable this function.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "AllowCLIBiometricAuthentication": {
            "description": "Specifies whether biometric authentication is allowed in the Privileges CLI to obtain administrator privileges.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "EnableSmartCardSupport": {
            "description": "Specifies whether to enable smart card support for authentication. Since the modern Local Authentication framework does not yet support smart cards/PIV tokens, enabling this option will cause the application to fall back to the older Authorization Services. (Must also set RequireAuthentication. Available in Privileges 2.2)",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "AllowPrivilegeRenewal": {
            "description": "If set to true, renewing privileges requires the same kind of authentication as initially requesting administrator privileges.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "RenewalFollowsAuthSetting": {
            "description": "If set to true, renewing privileges requires the same kind of authentication as initially requesting administrator privileges.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "PostChangeActionOnGrantOnly": {
            "description": "If set to true, the application or script, specified in PostChangeExecutablePath, will only be executed if administrator privileges are granted to a user, but not the privileges are revoked.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "boolean",
                    "default": true
                }
            ]
        },
        "PostChangeExecutablePath": {
            "description": "If set, the PrivilegesAgent executes the given application or script and provides the current user's user name ($1) and its privileges (admin or user, $2) as launch arguments.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "string",
                    "default": "/Library/Application Support/Privileges/privileges_changed.sh"
                }
            ]
        },
        "RevokeAtLoginExcludedUsers": {
            "description": "If RevokePrivilegesAtLogin is set to true, the specified users are excluded from privilege revocation at login. Variables such as $USERNAME can be used if supported by the management system.",
            "anyOf": [
                {
                    "title": "Not Configured",
                    "type": "null"
                },
                {
                    "title": "Configured",
                    "type": "array",
                    "items": {
                       "type": "string"
                    }
                }
            ]
        }
    }
}

For a comparison, our configuration in plist format for Privileges with the above code is:

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>ExpirationInterval</key>
    <integer>15</integer>
    <key>ExpirationIntervalMax</key>
    <integer>60</integer>
    <key>AllowPrivilegeRenewal</key>
    <false/>
    <key>RenewalFollowsAuthSetting</key>
    <true/>
    <key>RequireAuthentication</key>
    <true/>
    <key>AllowCLIBiometricAuthentication</key>
    <false/>
    <key>PostChangeExecutablePath</key>
    <string>/path/to/script/on-action.sh</string>
    <key>PassReasonToExecutable</key>
    <true/>
    <key>PostChangeActionOnGrantOnly</key>
    <false/>
    <key>RevokePrivilegesAtLogin</key>
    <true/>
    <key>RevokeAtLoginExcludedUsers</key>
    <array>
      <string>administrator</string>
      <string>root</string>
    </array>
    <key>HideOtherWindows</key>
    <true/>
    <key>ReasonRequired</key>
    <true/>
    <key>ReasonMinLength</key>
    <integer>15</integer>
    <key>ReasonMaxLength</key>
    <integer>250</integer>
    <key>ReasonCheckingEnabled</key>
    <true/>
    <key>HideSettingsButton</key>
    <true/>
    <key>ShowInMenuBar</key>
    <false/>
  </dict>
</plist>

Adding Privileges to Dock

This is fairly straightforward. The main user experience with Privileges is either in the menu bar or in the dock, and we want this to be front and center and easy for users. Given that Apple has included a notch on the main laptop displays, which causes icons to get cut off, the dock is the better option. So we need to add it automatically to the employees’ docks without interrupting or breaking the experience for their existing dock.

Side note: if you experience menu bar and notch issues, I highly recommend Ice.app (Open Source and Free, but you should donate) to help you manage the menu bar.

Below is a script that should help add the Privileges app to the dock. It’s straightforward and based on existing code that works.

 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
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
#!/bin/zsh

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# 
# version 2.2
# Modified by: Andrew Doering
# Based on original work by: Mischa van der Bent
# 
# DESCRIPTION
# This script ensures Privileges.app is in the Dock at the end, replacing it if already present.
# It uses dockutil to manage the Dock and safely exits to prevent Jamf Pro policy failures.
#
# REQUIREMENTS
# dockutil Version 3.0.0 or higher installed to /usr/local/bin/
# Compatible with macOS 11.x and higher
# 
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export PATH=/usr/bin:/bin:/usr/sbin:/sbin

# Variables
appPath="/Applications/Privileges.app"
position="end"
dockutil="/usr/local/bin/dockutil"

# Get the currently logged-in user
currentUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }' )
uid=$(id -u "${currentUser}")
userHome=$(dscl . -read /users/${currentUser} NFSHomeDirectory | cut -d " " -f 2)
plist="${userHome}/Library/Preferences/com.apple.dock.plist"

# Function to run commands as the logged-in user
runAsUser() {
    if [[ "${currentUser}" != "loginwindow" ]]; then
        launchctl asuser "$uid" sudo -u "${currentUser}" "$@"
    else
        echo "No user logged in"
        exit 1
    fi
}

# Check if dockutil is installed
if [[ ! -x "$dockutil" ]]; then
    echo "dockutil not installed in /usr/local/bin, exiting"
    exit 1
fi

# Add or replace Privileges.app in the Dock
echo "Ensuring Privileges.app is in the Dock at the end."
runAsUser "$dockutil" --add "$appPath" --position "$position" --replacing "Privileges" "$plist"

# Ensure script exits cleanly to avoid Jamf Pro failures
exit 0

Configuring JAMF EAs

If you need a JAMF Extension Attribute in place of using something in Datadog (or your equivalent SIEM) or to complement it, you can utilize one of the commands in the DataDog script to provide this into JAMF. This coincidentally was also requested on #privileges on MacAdmins right after I posted the previous script, so it was an easy addition.

Ultimately, all this does is query the log for anything about SAP Privileges, and if needed, you could shorten this time frame to a day, week, or some other time frame. Then, we tail the last 5 lines and provide that as an EA Result. The resulting output should look something like this (these were the reasons I kept escalating when trying to promote myself for the DataDog setup):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ sh scripts/jamf-ea-privileges-5-reasons.sh [18:01:06]
<result>
"I need to continue testing."
"Test this configuration against datadog"
"This is a test of the tool."
"Test again for new code"
"This is a new test of the configuration."
"Try passing the reason through with a new setup"
"This is a test with new version"
"I want to bang my head against the wall."
"I want to sing at the top of my lungs."
"Need to test something"
</result>

The actual JAMF EA script is below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/zsh

# Fetch the last 10 logs containing the admin privilege escalation reasons
log_output=$(/usr/bin/log show --style syslog --predicate 'process == "PrivilegesDaemon" && eventMessage CONTAINS "SAPCorp"' --info | grep -i "privileges for the following reason")

# Extract the reasons using awk
reasons=$(echo "$log_output" | awk -F'for the following reason: ' '{print $2}' | tail -n 5)

# Jamf EA output
echo "<result>"
echo "$reasons"
echo "</result>"

Configuring Kolide to Verify Administrator Status

So now that we have Logging and Alerting in place, what do we do about posture checking and enforcement?

Kolide can easily play a role in this when accessing your own internal services, allowing end-users to efficiently self-resolve and remediate the non-admin requirement on their device. The frequency of this, however, can cause some slight experience issues, such as an increased waiting time to log in to a SaaS service.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
WITH
_admingroup AS (
  select gid from groups
  where groupname = 'admin'
),
_admin_users AS (
  select uid, uuid, username, description
  from _admingroup
  join user_groups using(gid)
  join users using(uid)
),
_filtered AS (
  select JSON_GROUP_ARRAY(JSON_OBJECT('uid', uid, 'username', username, 'description', description)) AS users,
  count(*) AS count
  from _admin_users
  WHERE username not in ('root', 'administrator', 'jamfadmin')
)

select
*,
IIF(count = 0, 'PASS', 'FAIL') AS KOLIDE_CHECK_STATUS
from _filtered;

The exact process, to some extent, could also be done through Crowdstrike’s Indications of Compromise or Real-Time Response, which also use OSQuery.

Validating LaunchAgents, LaunchDaemons, and Files are loaded on the device

Using JAMF

Below are two extension attributes that would allow you to build smart groups and compliance checks based on whether the scripts that we have previously deployed exist on the device in a more rough manner. We don’t do SHA checking, for example, but this could easily be implemented into the code structure to allow for it.

 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
26
27
28
29
30
31
32
33
34
#!/bin/zsh

## JAMF Extension Attribute: Check if LaunchAgent and LaunchDaemon are Valid
AGENT_PLIST="/Library/LaunchAgents/corp.sap.privileges.agent.plist"
AGENT_EXPECTED_PROGRAM="/Applications/Privileges.app/Contents/MacOS/PrivilegesAgent.app/Contents/MacOS/PrivilegesAgent"
AGENT_EXPECTED_TEAM="7R5ZEU67FQ"

DAEMON_PLIST="/Library/LaunchDaemons/corp.sap.privileges.daemon.plist"
DAEMON_EXPECTED_PROGRAM="/Applications/Privileges.app/Contents/MacOS/PrivilegesDaemon"
DAEMON_EXPECTED_TEAM="7R5ZEU67FQ"

status="Valid"

# Check LaunchAgent
if [[ ! -f "$AGENT_PLIST" ]]; then
    status="Missing LaunchAgent"
elif ! /usr/bin/defaults read "$AGENT_PLIST" 2>/dev/null | /usr/bin/grep -q "$AGENT_EXPECTED_PROGRAM"; then
    status="Modified LaunchAgent"
elif ! /usr/bin/defaults read "$AGENT_PLIST" 2>/dev/null | /usr/bin/grep -q "$AGENT_EXPECTED_TEAM"; then
    status="Modified LaunchAgent"
fi

# Check LaunchDaemon
if [[ ! -f "$DAEMON_PLIST" ]]; then
    status="Missing LaunchDaemon"
elif ! /usr/bin/defaults read "$DAEMON_PLIST" 2>/dev/null | /usr/bin/grep -q "$DAEMON_EXPECTED_PROGRAM"; then
    status="Modified LaunchDaemon"
elif ! /usr/bin/defaults read "$DAEMON_PLIST" 2>/dev/null | /usr/bin/grep -q "$DAEMON_EXPECTED_TEAM"; then
    status="Modified LaunchDaemon"
fi

# Output the result
echo "<result>$status</result>"
exit 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/zsh

## JAMF Extension Attribute: Check if the Privileges Scripts Exist
TARGET_DIR="/Users/Shared/privileges-scripts"
TARGET_FILE="$TARGET_DIR/on-action.sh"

if [[ -d "$TARGET_DIR" && -f "$TARGET_FILE" ]]; then
    echo "<result>Exists</result>"
else
    echo "<result>Missing</result>"
fi

exit 0

Using Kolide

Personally, I believe user remediation and focusing on user-based self-service solutions are the way forward. I like Kolide better for this so that you, as an admin, can provide the tools for the end user to resolve their issues themselves. This builds both education and self-ownership of the issue in the employee. The only drawback to this method is pressure and time crunch. Then, Tech Support can help resolve this.

So, the best route is to build a check-in Kolide that helps with this and provides self-service remediation, including links to reset files or tools through a self-service portal (e.g., JAMF, Munki, Company Portal, etc.).

This check will validate that the installed Privileges LaunchDaemons and LaunchAgents are correctly placed in the specific spots and meet the expected file contents. If you need to update the contents of the check for a new file or additional items, you can use shasum -a 256 {filepath} to obtain the shasum and copy bits of the code below to verify any extra files.

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
WITH sha_checks AS (
  SELECT 
    'corp.sap.privileges' AS service_name,
    CASE 
      WHEN (SELECT sha256 FROM hash WHERE path = '/Library/LaunchAgents/corp.sap.privileges.agent.plist') = '6594b238231b47555b5a0fb5b0372d069c2762b28fc654a8c81f1bb70509530b' 
           THEN 'PASS' ELSE 'FAIL' 
    END AS agent_hash_status,
    CASE 
      WHEN (SELECT sha256 FROM hash WHERE path = '/Library/LaunchDaemons/corp.sap.privileges.daemon.plist') = '7118621fe9b6e6c32949dd8c8b0dda04a6aaf1389f8c4894a0e3b03ae77505ff' 
           THEN 'PASS' ELSE 'FAIL' 
    END AS daemon_hash_status
),
file_checks AS (
  SELECT 
    'corp.sap.privileges' AS service_name,
    CASE
      WHEN EXISTS (SELECT 1 FROM launchd WHERE label = 'corp.sap.privileges.agent') THEN 'YES'
      ELSE 'NO'
    END AS found_agent_in_launchd,
    CASE
      WHEN EXISTS (SELECT 1 FROM launchd WHERE label = 'corp.sap.privileges.daemon') THEN 'YES'
      ELSE 'NO'
    END AS found_daemon_in_launchd,
    CASE
      WHEN EXISTS (SELECT 1 FROM file WHERE path = '/Library/LaunchAgents/corp.sap.privileges.agent.plist') THEN 'YES'
      ELSE 'NO'
    END AS found_agent_plist,
    CASE
      WHEN EXISTS (SELECT 1 FROM file WHERE path = '/Library/LaunchDaemons/corp.sap.privileges.daemon.plist') THEN 'YES'
      ELSE 'NO'
    END AS found_daemon_plist
)
SELECT 
  sha_checks.service_name,
  agent_hash_status,
  daemon_hash_status,
  found_agent_in_launchd,
  found_daemon_in_launchd,
  found_agent_plist,
  found_daemon_plist,
  CASE
    WHEN agent_hash_status = 'PASS' AND daemon_hash_status = 'PASS'
      AND found_agent_plist = 'YES' AND found_daemon_plist = 'YES'
      AND found_agent_in_launchd = 'YES' AND found_daemon_in_launchd = 'YES'
    THEN 'PASS'
    ELSE 'FAIL'
  END AS KOLIDE_CHECK_STATUS
FROM sha_checks
JOIN file_checks ON sha_checks.service_name = file_checks.service_name;

You can then set up documentation in Kolide that informs why this is being done and how to resolve the situation.

No Network Connectivity? No Problem?

While I have not tested this quite yet, this is the next step in the script’s evolution. We still need to understand the escalation reasons even if a network connection is not available (for example, being on an Airplane, purposely avoiding the network to “hide behavior,” etc.). This can potentially work with the Jamf EA agent, but it does not resolve the solution of sending this data to the SEIM.

To accomplish this, we would need to add a few items to the script and modify it to save content when unable to connect or ping an IP Address. A sample of this can be found below:

 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
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
#!/bin/zsh

CACHE_FILE="/private/var/tmp/datadog_offline_logs.json"

# Function to check network connectivity
check_network() {
    /sbin/ping -c 1 8.8.8.8 >/dev/null 2>&1
    return $?
}

# Function to send logs to Datadog
send_log_to_datadog() {
    local log_data="$1"
    local response_code

    response_code=$(/usr/bin/curl -s -o /dev/null -w "%{http_code}" -X POST "$DATADOG_ENDPOINT" \
        -H "Content-Type: application/json" \
        -H "DD-API-KEY: $DATADOG_API_KEY" \
        -d "$log_data")

    if [[ "$response_code" -ne 202 ]]; then
        /bin/echo "Failed to send log to Datadog. HTTP response: $response_code"
        return 1
    fi

    return 0
}

# Function to process cached logs
process_cached_logs() {
    if [[ -f "$CACHE_FILE" ]]; then
        /bin/echo "Processing cached logs..."
        
        while IFS= read -r line; do
            send_log_to_datadog "$line" && /usr/bin/sed -i '' '1d' "$CACHE_FILE"
        done < "$CACHE_FILE"

        # If file is empty, remove it
        [[ ! -s "$CACHE_FILE" ]] && /bin/rm -f "$CACHE_FILE"
    fi
}

# Prepare JSON log data
LOG_DATA=$(cat <<EOF
{
  "ddsource": "macos",
  "service": "Privileges",
  "ddtags": "$addtags",
  "hostname": "$hostname",
  "username": "$currentUser",
  "serialnumber": "$serialNumber", 
  "timestamp": "$timestamp",
  "message": "$(echo "$logMessage" | sed 's/"/\\"/g')"
}
EOF
)

# Check network status
if check_network; then
    # Process any cached logs first
    process_cached_logs

    # Send current log to Datadog
    if ! send_log_to_datadog "$LOG_DATA"; then
        /bin/echo "$LOG_DATA" >> "$CACHE_FILE"
    fi
else
    /bin/echo "No network. Caching log..."
    /bin/echo "$LOG_DATA" >> "$CACHE_FILE"
fi

exit 0

This should save some of the contents to a file. Obviously, the /tmp folder will be cleared on reboot, so this is not the most adequate place to put the file contents. You should modify this to be in /Users/Shared/ or some other suitable location.

You would also need to add a launch agent that runs every X minutes or on every reboot to send that data to DataDog when connectivity is re-established.

An example LaunchAgent.plist file can be found below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.example.datadogloguploader</string>

    <key>ProgramArguments</key>
    <array>
      <string>/bin/zsh</string>
      <string>/path/to/your/script.sh</string>
    </array>

    <key>StartInterval</key>
    <integer>300</integer>  <!-- Retry every 5 minutes -->

    <key>KeepAlive</key>
    <dict>
      <key>NetworkState</key>
      <true/>
    </dict>
  </dict>
</plist>

Summary & Resources

All the available code samples above are available at the following privileges-resources Github Link.

I would recommend stopping by #privileges on MacAdmins, and if you don’t have an account, you can sign up here.

Feel free to comment below or reach out to the channel above to discuss this.

Thanks for stopping by!
Built with Hugo