Intune & Endpoint

Custom compliance settings for macOS in Intune: the script, the rules, and the type mismatch that wastes your afternoon

New in service release 2607. A shell script that emits JSON, a rules file that judges it, and strict data types that fail quietly when you get them wrong.

Intune macOS custom compliance: the script, the rules and the type mismatch. Article banner on grbadhon.com

Intune macOS custom compliance arrived in service release 2607, which finally puts the Mac estate level with Windows and Linux. If you have been approximating Mac compliance with the built-in settings and quietly accepting the gaps, that workaround is over. The Windows equivalent, and the sync mechanics behind both, are in forcing an Intune sync.

What Intune macOS custom compliance actually is

A shell script that emits JSON, plus a JSON rules file that says what the values should be. Intune runs the script, reads the output, compares it against the rules, and surfaces the result alongside the built-in compliance settings in the normal reporting. The pattern is identical to Windows with PowerShell and to Linux, so if you have written one before there is nothing new to learn. Microsoft covers the shared pattern in Use custom compliance settings in Microsoft Intune.

The point is that it evaluates things the built-in settings never covered. Configuration profiles you care about, a specific agent being present and running, a setting your security team asked for that Microsoft has not shipped a toggle for. Compliance state only matters once it reaches Entra, which is where Conditional Access policy design picks it up, and device based Conditional Access needs at least Entra ID P1.

The script

One rule: it must write a single flat JSON object to standard output and nothing else. That constraint is stated in the discovery script reference and it is absolute. Any stray echo, warning or debug line and the whole evaluation fails rather than returning a wrong answer, which is at least an honest failure mode.

detect-mac-posture.sh
#!/bin/bash
# Intune custom compliance for macOS.
# Emits ONE flat JSON object on stdout. Nothing else, ever.

# FileVault
if fdesetup status | grep -q “FileVault is On”; then
    filevault=”true”
else
    filevault=”false”
fi

# Firewall: 0 = off, 1 = on for specific services, 2 = on for essential services
fw=$(/usr/libexec/PlistBuddy -c “Print :globalstate”
     /Library/Preferences/com.apple.alf.plist 2>/dev/null)
[ -z “$fw” ] && fw=0

# System Integrity Protection
if csrutil status | grep -q “enabled”; then
    sip=”true”
else
    sip=”false”
fi

# Minor version of the running OS, useful for gating on patch level
osver=$(sw_vers -productVersion)

# Single line, no trailing output.
echo “{“FileVault”:”$filevault”,”Firewall”:$fw,”SIP”:”$sip”,”OSVersion”:”$osver”}”

The rules file

Each rule names a key from the script output, an operator, a data type and the expected value. The remediation strings are what the user sees in Company Portal, so write them as instructions rather than as a description of the failure. If a device never reports a result at all, work the Intune device not syncing diagnosis order first.

mac-posture-rules.json
{
  “Rules”: [
    {
      “SettingName”: “FileVault”,
      “Operator”: “IsEquals”,
      “DataType”: “String”,
      “Operand”: “true”,
      “MoreInfoUrl”: “https://support.apple.com/guide/mac-help/mh11785”,
      “RemediationStrings”: [
        {
          “Language”: “en_US”,
          “Title”: “Turn on FileVault disk encryption”,
          “Description”: “Open System Settings, then Privacy and Security, then FileVault, and select Turn On.”
        }
      ]
    },
    {
      “SettingName”: “Firewall”,
      “Operator”: “IsGreaterThan”,
      “DataType”: “Int64”,
      “Operand”: 0,
      “MoreInfoUrl”: “https://support.apple.com/guide/mac-help/mh34041”,
      “RemediationStrings”: [
        {
          “Language”: “en_US”,
          “Title”: “Turn on the firewall”,
          “Description”: “Open System Settings, then Network, then Firewall, and switch it on.”
        }
      ]
    },
    {
      “SettingName”: “SIP”,
      “Operator”: “IsEquals”,
      “DataType”: “String”,
      “Operand”: “true”,
      “MoreInfoUrl”: “”,
      “RemediationStrings”: [
        {
          “Language”: “en_US”,
          “Title”: “System Integrity Protection is disabled”,
          “Description”: “Contact IT. Re-enabling SIP requires starting up in Recovery.”
        }
      ]
    }
  ]
}

Caution

Data types are strict and mismatches fail silently in the sense that the device simply reports non-compliant. If the script emits 2 as a number, the rule must use Int64. If it emits "2" as a string, it must use String. Half an hour disappears here if you are not deliberate about it.

Rolling it out without locking anyone out

  1. Set the non-compliance action to nothing at first. Compliance policies are reporting only until Conditional Access consumes them, and you want the reporting before the enforcement.
  2. Run against a pilot group and read the results, not the intent. Custom compliance surfaces real estate variety very quickly, which is the point.
  3. Only then wire it to Conditional Access. A custom rule with a bug plus a block policy is how you lock a fleet of Macs out of the tenant at 09:00.

What I would do

Start with three rules, not thirty. FileVault, firewall, and whichever single thing your security team asks about most often. Three rules that everyone trusts beats a comprehensive policy nobody believes.

And keep the script boring. Every conditional you add is another way for it to emit something that is not valid JSON, and the failure mode for that is a device that reports non-compliant for reasons nobody can see.

Common questions

Yes, since service release 2607. It uses a shell script that outputs JSON plus a JSON rules file, the same pattern already available for Windows with PowerShell and for Linux. Results appear alongside built-in compliance settings in the normal Intune reporting.

A single flat JSON object written to standard output, and nothing else. Any additional echo, warning or debug line causes the evaluation to fail rather than return an incorrect result.

Most often a data type mismatch. If the script emits a bare number the rule must use Int64; if it emits a quoted value it must use String. The device simply reports non-compliant, with no error explaining the type mismatch.

Only once Conditional Access consumes the compliance state. Until then compliance policies are reporting only. Pilot the rules and read the results before wiring them to a Conditional Access block, because a buggy rule plus a block policy locks out the fleet.

Instructions rather than descriptions. They are shown to the user in Company Portal, so tell them which settings pane to open and what to switch on, or tell them to contact IT where self-service is not appropriate.