Skip to main content

Scripts

MACE mascot as a terminal wizard

Shell scripts that audit compliance status and remediate non-compliant settings.

Deploy via MDM to continuously monitor and enforce your security baseline.

What Are Compliance Scripts?โ€‹

Compliance scripts are shell scripts (.sh files) that run directly on a Mac. They do two things:

  1. Audit: Check if settings match your baseline requirements
  2. Remediate: Change settings to match requirements if they don't

Unlike configuration profiles (which declare settings for macOS to enforce), scripts actively execute commands. This makes them more flexible. They can check anything you can query via command line, and fix anything you can change via command line.

How the Compliance Script Worksโ€‹

MACE can generate either a single combined compliance script or individual scripts per rule (configured in Build Options). The combined script contains functions for every enabled rule. Here's what happens when it runs:

๐Ÿ“œScript RunsTriggered by MDM or manually
โ†’
๐Ÿ”Check SettingRuns audit command
โ†’
โš–๏ธCompare ResultAgainst expected value
โ†’
โœ…Pass/FailLog result

Script Modesโ€‹

The generated script has three run modes, plus a set of single-rule, reporting, and exemption flags. It must run as root, and with no arguments in a terminal it opens an interactive menu.

๐Ÿ”--check (Audit Only)

Runs all audit functions and reports pass/fail for each rule. Does NOT change any settings. Use this to assess current compliance status without making changes.

๐Ÿ”ง--fix (Remediate Only)

Remediates the rules the last scan recorded as failing; already-compliant rules are skipped. It needs an existing scan, so run --check first or use --cfc. Exempt rules are never fixed.

๐Ÿ”„--cfc (Check-Fix-Check)

Runs audit, then remediation, then audit again. This verifies that fixes were actually applied successfully. The final audit confirms the device is now compliant.

๐Ÿ“ŠResults: --stats (one-line summary for MDMs), --compliant, --non_compliant, --json
๐ŸŽฏSingle rules: --list, --check-rule <id>, --fix-rule <id>, and --dry-run to show what a fix would run
โž–Exemptions: --exempt <id> <reason> (a reason is required), --unexempt <id>, --exemptions
๐ŸงนHousekeeping: --reset and --reset-all clear stored results without touching exemptions, --reset-ssh, --quiet=1|2, --version, --help
โš™๏ธSettings block at the top of the script: FORCED_MODE, RESET_RESULTS, DRY_RUN, QUIET_LEVEL, RESET_SSH_BEFORE_FIX. A compliance_args array in the org.<baseline>.audit domain can supply arguments from MDM and overrides FORCED_MODE

Mode precedence is FORCED_MODE, then Jamf's $4, then the first argument. Individual per-rule scripts only accept --check and --fix.

Example: What a Rule Looks Like in the Scriptโ€‹

For a rule like "Enable Firewall Logging", the script contains:

# Rule: os_firewall_log_enable
# STIG ID: APPL-15-005001
# Severity: medium

audit_os_firewall_log_enable() {
# Check command and expected result from the rule YAML
check_command='/usr/libexec/ApplicationFirewall/socketfilterfw --getloggingmode 2>/dev/null | grep -c "Log mode is on"'
audit_rule "os_firewall_log_enable" "$check_command" "1"
}

fix_os_firewall_log_enable() {
# Fix command from the rule YAML
rule_fix "os_firewall_log_enable" '/usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on'
}

What's happening here:

  1. The audit_ function hands the rule's check command and expected value to the shared audit_rule helper
  2. The helper runs the command, compares the result, and records the rule as passed, failed, or not applicable
  3. A line like [INFO] [timestamp] os_firewall_log_enable failed (Result: 0, Expected: "1") goes to the log, and an exempt failure is recorded as an allowed exemption with its reason
  4. In fix mode, the fix_ function runs the remediation only if the last scan recorded a failure and the rule isn't exempt

Extension Attributesโ€‹

Extension attributes (EAs) are individual scripts that report a single value back to your MDM. They're designed for MDM inventory collection.

How Extension Attributes Workโ€‹

๐Ÿ“…
MDM triggers inventory collection

On a schedule (e.g., daily) or when you request a device update, your MDM tells the Mac to collect inventory.

๐Ÿ“œ
Mac runs each EA script

The MDM agent executes every extension attribute script on the device.

๐Ÿ“ค
Script outputs a result

Each script prints a value (like "Compliant" or "Non-Compliant") in a format the MDM expects.

๐Ÿ“Š
MDM stores the value

The result appears in the device's inventory record. You can search, report, and create smart groups based on these values.

Example Extension Attributeโ€‹

MACE writes one reporting script per MDM per baseline into build/extension_attributes/, not one per rule. Each one reads the results the compliance script left in /Library/Preferences/<baseline>_audit.plist and reports the failed rule IDs:

#!/bin/zsh
# jamf_cis_lvl2_extension_attribute.sh
# Reports the rules that failed the last compliance scan

failed=$(read_failed_rules_from_audit_plist)

if [[ -z "$scan_data" ]]; then
echo "<result>No Scan Data</result>"
elif [[ -z "$failed" ]]; then
echo "<result>Compliant</result>"
else
echo "<result>$failed</result>"
fi

What each MDM gets:

  • Jamf Pro: jamf_<baseline>_extension_attribute.sh with <result>โ€ฆ</result> output: Compliant, No Scan Data, or the failed rule IDs
  • Intune, Workspace ONE, Addigy: one plain-text line such as Non-Compliant (3) - Failed Rules: a,b,c. Workspace ONE gets both a sensor (ws1_โ€ฆ) and a custom attribute (ws1ca_โ€ฆ); Intune also gets a compliance discovery script and rules JSON for custom compliance
  • Iru: iru_โ€ฆ custom attribute
  • Fleet: fleet_<baseline>_policies.sql, osquery policies to paste into Fleet

A README in the folder walks through the setup for each. The scripts only report; the compliance script has to be running on the Mac for them to have data.

Deploying Scripts via MDMโ€‹

Jamf Proโ€‹

๐Ÿ” Deploying the Audit Script

1
Go to Settings โ†’ Scripts

In Jamf Pro, navigate to Settings โ†’ Computer Management โ†’ Scripts.

2
Create a new script

Click "New" and paste the contents of your compliance script. Jamf policy scripts run as root, which the script requires.

3
Set script parameters

Add a parameter for the mode: $4 can be --check, --fix, or --cfc.

4
Create a Policy

Go to Computers โ†’ Policies โ†’ New. Add your script and set the parameter to --check for audit-only runs.

5
Set trigger and scope

Choose when to run: Recurring Check-in (daily/weekly), Startup, or manual trigger. Scope to your target computers.

๐Ÿ“Š Deploying Extension Attributes

1
Go to Settings โ†’ Extension Attributes

In Jamf Pro, navigate to Settings โ†’ Computer Management โ†’ Extension Attributes.

2
Create a new Extension Attribute

Click "New" and set Data Type to "String". Set Input Type to "Script".

3
Paste the EA script

Copy the contents of jamf_<baseline>_extension_attribute.sh from build/extension_attributes/ into the script field.

4
Save and wait for inventory

The EA runs during the next inventory collection. You can force this by running sudo jamf recon on a test Mac.

5
Create Smart Groups (optional)

Create a Smart Computer Group where the EA is not "Compliant" and is not "No Scan Data" to identify devices needing remediation. MACE's Jamf upload can create these groups for you.

Microsoft Intuneโ€‹

๐Ÿ“œ Deploying Scripts in Intune

1
Go to Devices โ†’ macOS โ†’ Shell scripts

In the Intune admin center, navigate to Devices โ†’ macOS โ†’ Shell scripts.

2
Add a new script

Click "Add" and upload your compliance script file.

3
Configure script settings

Set "Run script as signed-in user" to No (runs as root). Set "Hide script notifications" based on your preference.

4
Set the mode in the script

Intune macOS shell scripts take no arguments, so set Forced Mode in the Build Hub (or edit FORCED_MODE at the top of the script) to --check, --fix, or --cfc before uploading.

5
Assign to groups

Assign the script to device groups. Set the run frequency (once, every day, etc.).

Other MDMsโ€‹

Scripts work with any MDM that supports running shell scripts on macOS:

๐ŸŸฃIruUse Custom Scripts library item. Set to run on a schedule or once.
โšซWorkspace ONEUse Scripts feature. Configure trigger and assignment.
๐ŸŸคAddigyUse a Custom Software item or script policy to run on a schedule or once.
๐Ÿ”ดFleetUse Scripts to run shell scripts on enrolled Macs, on demand or via policy automation.

What Happens on the Macโ€‹

When your MDM runs the compliance script, here's the sequence:

๐Ÿ“ฅ
MDM agent receives the command

The MDM agent on the Mac (like Jamf binary or Intune agent) receives instruction to run the script.

๐Ÿ”
Script runs with root privileges

Most compliance checks need root access to read system settings. The MDM agent runs the script as root.

๐Ÿ”
Audit functions execute

Each rule's check command runs. Commands like defaults read, systemsetup, or csrutil status query current settings.

๐Ÿ“
Results are logged

Results go to /Library/Logs/<hostname>_<baseline>_baseline.log and a matching .csv, failures to /Library/Logs/<baseline>_failures.log, per-rule status to /Library/Preferences/<baseline>_audit.plist, and a summary to the unified log and stdout. Your MDM captures the stdout.

๐Ÿ”ง
Fix functions run (if --fix or --cfc)

Remediation commands execute to change settings. This might run defaults write, enable services, or modify system files.

๐Ÿ“ค
Exit code returned to MDM

The script exits 0 when it ran, 2 for a usage error or unknown flag, and 1 if it isn't root, isn't zsh, or --fix was run before any scan. Failed rules don't change the exit code; read the summary line, the audit plist, or the reporting scripts for that. Run from an MDM with no mode and no terminal, it prints usage and exits 0 without scanning, so always set a mode.

Audit Preferences and Exemptionsโ€‹

The compliance script reads an audit preferences domain, org.<baseline>.audit, that holds one entry per rule. An exempt rule is still checked, but a failure is logged as an allowed exemption, kept out of the failures log, and never remediated.

How Exemptions Workโ€‹

<!-- org.<baseline>.audit: one dictionary per rule -->
<key>os_firewall_log_enable</key>
<dict>
<key>exempt</key><true/>
<key>exempt_reason</key><string>Logging handled by the SIEM agent</string>
</dict>

The script looks for exemptions in three places, in order: user-scoped managed preferences, device managed preferences, then the local /Library/Preferences/org.<baseline>.audit.plist. Results are stored separately in /Library/Preferences/<baseline>_audit.plist.

Where exemptions come from:

  • The project. Rules you mark exempt in the Compliance Editor are pre-set with their reasons in the audit preferences MACE builds. See Exempting Rules.
  • The Mac. An admin can add one locally with sudo ./script.sh --exempt <rule_id> "reason", remove it with --unexempt, and list them with --exemptions. The interactive menu has the same options.
  • Jamf's editor. With Write JSON Schema instead on, Jamf Pro admins manage exemptions from the Custom Settings UI.

Deploying the Audit Preferencesโ€‹

The audit preferences file can be deployed as:

  • A .mobileconfig profile pushed via MDM (recommended, since managed preferences are read first and can't be edited on the Mac)
  • A .plist file copied to /Library/Preferences/
  • An Intune XML preference file or a Jamf JSON schema, depending on the Format you build with

Exempting a rule in the project only takes effect on a Mac once the updated audit preferences land there. Profiles already deployed keep enforcing the setting until they're re-uploaded.

Best Practicesโ€‹

๐Ÿงช
Test on pilot devices first

Run the script manually on test Macs before deploying via MDM. Verify the checks work correctly and fixes don't break anything.

๐Ÿ”
Start with audit only

Deploy with --check first to understand your current compliance posture. Review results before running --fix.

๐Ÿ“…
Schedule regular audits

Run the audit script on a recurring schedule (daily or weekly) to continuously monitor compliance status.

๐ŸŽฏ
Use Smart Groups for remediation

Create MDM groups based on EA results. Scope remediation policies only to non-compliant devices.

๐Ÿ“Š
Monitor for drift

Settings can change over time. Regular audits catch when devices drift out of compliance.