guideonce.
← All guides

macOS · Intune Migration

Auditing Jamf Config Before You Migrate: the Export-and-Map Technique

One export command turned twenty-four profiles into a build list of three, and found a screen lock that had never locked anything.

Part of a series

This is one guide in Migrating Macs from Jamf to Intune, a set of standalone runbooks you can read in any order. This is the stage I would do first, because it shrinks everything that follows.

Faced with two dozen configuration profiles on the platform you are leaving, the instinct is to reproduce every one of them on the platform you are arriving at. Do not. Most of that list is either the old vendor's own plumbing, which vanishes the moment you leave, or profiles that enforce almost nothing despite alarming names. The trick is one export command and an hour of reading. On my migration it turned twenty-four profiles into a genuine build list of three, and it found a security control that had been silently broken for years.

Why You Should Never Rebuild Profiles Blind

A profile list gives you names, and names lie in both directions. A profile called Restrictions may restrict nothing. A profile called Security & Privacy Policy may quietly carry your firewall, your Gatekeeper posture, your login banner and your screen lock, five real controls hiding behind one innocuous entry. Meanwhile a third of the list exists solely to support the management tool itself, and reproducing those on the new platform is not caution, it is a category error: you would be granting permissions to software that is no longer installed.

So the rule is simple, and everything else in this guide follows from it: decide from the values, never from the names. The values are the one thing the ordinary profile listing does not show you.

Exporting What Is Actually Enforced

On a Mac still managed by the old platform, export the installed profiles including payload values. This is the whole technique in one command:

# The export that matters: full payload XML, values and all
sudo profiles -P -o stdout-xml > jamf-full.xml

# Sanity check it actually wrote something substantial
wc -l jamf-full.xml

Contrast it with the form most people reach for, which lists profiles and payload types and omits every setting inside them:

# The summary form: useful for an inventory, useless for an audit
sudo profiles show
sudo profiles list

If your export comes back thin, you used the summary. Mine ran to about three thousand lines for twenty-four profiles, which sounds daunting and is not, because you are searching it, not reading it.

There is a second route worth knowing: download each profile from the management console as a .mobileconfig file. That also carries the values, and it has a bonus, the downloaded file can be re-imported into Intune as a custom profile, which matters enormously for privacy profiles (see below). For a bulk audit, take the single XML. For the handful you intend to reproduce verbatim, download those individually as well.

Reading the Export

Do not read it top to bottom. Search it. Start with the inventory:

# Every profile display name: your inventory
grep -A1 "ProfileDisplayName" jamf-full.xml

# Every payload type in play, with counts: tells you what is actually being configured
grep "PayloadType" jamf-full.xml | sort | uniq -c | sort -rn

That second command is more useful than it looks. It tells you at a glance whether you are dealing with real configuration or vendor scaffolding, because the vendor's own payload types will bubble straight to the top of the count.

Then, for each profile that is not obviously plumbing, go and look at what it actually sets:

# Find where a profile starts, then read its payloads
grep -n "ProfileDisplayName" jamf-full.xml

# Read a specific range once you know where it lives
sed -n '640,720p' jamf-full.xml

# Or pull the settings for a known payload domain directly
grep -n -A20 "com.apple.security.firewall" jamf-full.xml
grep -n -A20 "com.apple.screensaver" jamf-full.xml

The goal of this pass is not comprehension of every byte. It is to put each profile into one of four buckets.

The Mapping Table, and Four Statuses

Build a table: profile name, payload types inside it, the Intune equivalent, and a status. Four statuses cover everything you will meet.

My twenty-four came out as twelve Done, six Skip, three Build and three Defer. Read that ratio again, because it is the point of the exercise: three profiles of real work, and I would have cheerfully spent a week reproducing twenty-four.

What the twelve “Done” actually were, and why they are not in this guide

Twelve is more than half the list, and it deserves naming rather than a silent tick. Almost every one was our endpoint security agent: onboarding, background service configuration, the system extensions, the network filter, notifications, update policy, and the one privacy grant that turned out to be real, full disk access for the agent itself. All twelve were already built in Intune before this audit ever ran, because standing up endpoint security is its own project and it happened first, deliberately, ahead of the migration work in this series.

So Done here means “verified as already covered,” not “built as part of this guide.” If you are following this series and do not already have equivalent tooling deployed, that is a real gap this guide will not close for you, and no amount of auditing Jamf will hand it to you, because it was never a Jamf profile to reproduce, it was a product to stand up. Deploying endpoint security on Intune-managed Macs is a substantial job in its own right and is out of scope here. Treat it as a hard prerequisite to this whole series, not a stage within it.

Keep the table. It is your worklist while you build, and afterwards it is the cleanest audit of what your estate actually enforces that you will produce all year. When someone asks what you enforce on the Macs and why, this is the answer, and it took an hour.

Three Categories, Two of Which Are Free

Once you read values instead of names, everything falls into three groups.

Vendor tooling that dies with the old platform. The management framework, its notification profiles, its remote-assist components, its app-installer machinery, and the privacy grants that let all of that function. Six of my twenty-four were this. Every one is a Skip, and not reproducing them is not laziness, it is correctness: the applications they refer to will not exist on the machine. The tell is in the payload identifiers, which sit in the vendor's own namespace rather than Apple's. The display name may be generic; the identifier never lies.

Near-empty profiles. Profiles that enforce essentially nothing. My Restrictions profile was the purest example: its entire payload set familyControlsEnabled=false and nothing else. It restricted nothing. It had a serious name and it did no work. Reproducing "restrict nothing" is not a task.

The genuine build list. What survives. For me: three small Settings Catalog profiles, a firewall, a Gatekeeper policy, and a combined login-window and screen-lock policy. Everything else was already covered, was tooling, or was empty.

Read the values, not the names

The entire technique rests on this one habit. The most important-sounding profile on the machine may be empty, and an innocuously named one may carry your whole security posture. Only the values tell you which, and the values are precisely what the summary export leaves out. That is why -o stdout-xml matters and profiles show does not.

Worked Example: the Bundle That Was Mostly Empty

One profile was named for security and privacy, and its payload list was intimidating: screensaver, security preferences, diagnostics submission, login window, FileVault, restrictions, firewall, Gatekeeper, and managed preferences. Reproducing that blind would have been a day's work and half a dozen new Intune profiles.

Reading the values collapsed it. Here is what it actually enforced, payload by payload:

# Firewall: on, and literally nothing else
EnableFirewall       = true
EnableStealthMode    = false
BlockAllIncoming     = false

# Gatekeeper: the standard posture
EnableAssessment          = true
AllowIdentifiedDevelopers = true

# Screensaver / login window
idleTime             = 900        # 15 minutes
loginWindowIdleTime  = 900
LoginwindowText      = "<the banner text>"
SHOWFULLNAME         = false      # name+password fields, not a user list
DisableAutoLogin     = true

# Restrictions: the scary-sounding one
familyControlsEnabled = false     # ...and that is the entire payload

So the big security bundle was really three small things and one no-op. It became:

  1. Firewall (Settings Catalog): enable firewall on. And an improvement, stealth mode on, which the old platform had explicitly off. Stealth mode is low-risk hardening; there was no reason for it to be off other than nobody having revisited it. I deliberately left block all incoming off, because blocking everything inbound breaks remote support, screen sharing and SSH, and that is a bigger operational cost than the marginal security gain.
  2. Gatekeeper (Settings Catalog): assessment on, allow identified developers. A straight one-for-one.
  3. Login Window & Screen Lock (Settings Catalog): the banner, 900-second idle, name-and-password fields rather than a user list, auto-login disabled. Plus a fix, covered in the next section.
  4. Restrictions: skipped. Reproducing nothing is nothing.

FileVault was in that bundle too, and was already handled properly as its own Settings Catalog policy, see the FileVault guide, which is a good illustration of why the mapping table has a Done column rather than assuming everything in an old bundle needs rebuilding.

A small thing I did on purpose, and would do again

The login banner text on the new platform is identical to the old one except for a single letter: the new build says authorised where the old said authorized. It is a silent tell. Anyone who knows can glance at a login screen and know instantly which MDM owns that machine, with no console, no commands, no interrogating the device. During a migration where both platforms coexist for months, that is worth more than it sounds, and it costs nothing.

The Audit Finds Things That Were Already Broken

This is the part I did not expect, and it is the best argument for doing the audit at all.

The screensaver payload set idleTime=900. Fifteen minutes of inactivity and the screensaver kicks in. Sensible. Compliant-looking. And I nearly reproduced it verbatim and moved on.

Except the payload had no askForPassword, and no askForPasswordDelay. Search for them yourself, on your own export, before you assume:

# Does the screen actually LOCK, or does it just go dark?
grep -n "askForPassword\|askForPasswordDelay" jamf-full.xml

# For context, what the screensaver payload actually contains
grep -n -A15 "com.apple.screensaver" jamf-full.xml

Nothing. The result is a Mac whose screen goes blank after fifteen minutes and never asks for a password. Move the mouse and you are back in. The screensaver was doing its job and the screen lock was never enforced at all, on an estate that would have told you, in good faith, that it had a fifteen-minute screen lock. It had a fifteen-minute screensaver. Those are extremely different things.

Nobody did anything wrong here. Somebody set an idle time years ago, the profile said screensaver, it looked right in the console, and no one read the payload again. It took a migration and a grep to notice.

So the new Login Window & Screen Lock profile reproduces the old settings and fixes that: require password after the screensaver starts, with a short grace delay. It is the one place in the whole parity exercise where I deliberately did not achieve parity, because parity would have meant faithfully reproducing a broken control.

Parity is the floor, not the goal

Your mapping table's job is to make sure nothing is lost. It is not to enshrine every decision the previous decade made. When you are reading values anyway, you are being handed a free audit of controls nobody has looked at in years. Take it. Ask of each one: does this actually do what its name implies? Sometimes the honest answer is no, and the migration is the only reason you will ever find out.

Building the Three Replacements

The audit told you what to build. This is the building, because a guide that identifies three profiles and then abandons you is not a runbook. All three are Settings Catalog profiles, all three are quick, and all three are assigned to your broad dynamic group (the country-neutral baseline every corporate Mac should have, see the enrolment guide for how that group is built and why its membership rule is the thing most likely to silently defeat you).

For each one: Devices → Configuration → Create → New Policy, platform macOS, profile type Settings catalog.

1. Firewall

Add settings, search Firewall, and set:

Make your compliance policy agree with this

If you have a Compliance policy with an incoming connections = Block check under Device Security, and this firewall profile leaves block-all-incoming off, compliance will mark every healthy Mac non-compliant for a posture you chose on purpose. Set that compliance check to Not configured so the two agree. Two policies disagreeing about the same control is a quiet way to fail your own fleet, and it is maddening to diagnose because both look correct in isolation.

2. Gatekeeper

Add settings, search Gatekeeper (it may appear under system policy control), and set:

That is the whole profile, and it is a one-for-one reproduction. Do not be tempted to tighten it to App Store only in passing; that is a real change with real user impact and it belongs in its own conversation, not smuggled in during a migration. Migration is the wrong moment to relitigate policy, with the single exception of controls that turn out to be broken.

3. Login Window and Screen Lock

This one is the combination of two old profiles plus the fix. Add settings, search Login Window and Screensaver, and set:

Then verify on a real machine rather than trusting the console, because this is the control you just discovered nobody had verified in years:

# Did the screen-lock fix actually land?
sudo profiles -P -o stdout-xml | grep -A5 "askForPassword"

# Login window behaviour
sudo defaults read /Library/Preferences/com.apple.loginwindow 2>/dev/null

And then actually test it: let the screen go idle, come back, and confirm it asks. It takes fifteen minutes of not looking at a laptop, which is not a hardship, and it is the only thing that proves the control works.

Privacy Profiles: Reproduce, Never Retype

Privacy preferences profiles, the ones that pre-grant apps full disk access, accessibility, or the ability to send events to other apps, are the one category you must not hand-rebuild in a Settings Catalog. Every grant is tied to an exact code requirement string identifying the trusted application, and if you retype it and get one character wrong, the grant silently does not apply. No error. Just an app that mysteriously lacks the access it needs, weeks later, in a way nobody connects back to the migration.

So the rule: export the original profile and import it as a custom profile (Devices → Configuration → Create → macOS → Templates → Custom), so the code requirement crosses verbatim rather than through your keyboard.

But check whether you need it at all first, because this is where the audit pays off spectacularly. Find every privacy payload and see what it targets:

# Find the privacy/TCC payloads
grep -n "com.apple.TCC.configuration-profile-policy" jamf-full.xml

# What apps do they target, and what are they granted?
grep -n "Identifier\|CodeRequirement\|SystemPolicyAllFiles\|Accessibility\|AppleEvents" jamf-full.xml

I had four privacy profiles and fully expected this to be the painful part of the migration. It was not:

Four profiles, zero work. But only because I read the identifiers. Had I assumed, I would have either wasted a day reproducing three profiles for software that no longer exists, or, worse, skipped all four and quietly broken my security agent.

Proving Parity: the Before-and-After Diff

Here is the step that makes this a runbook rather than an essay, and the one I see missing everywhere. You have mapped, you have built, and now you believe you have parity. Prove it.

You already have the old machine's export. Take the same export from a freshly built machine on the new platform and compare them.

# On the OLD platform's Mac (you did this at the start)
sudo profiles -P -o stdout-xml > old-platform.xml

# On the NEW, freshly built Mac, after it has fully checked in
sudo profiles -P -o stdout-xml > new-platform.xml

Do not diff the files directly, they will differ in a thousand irrelevant ways: UUIDs, organisation strings, ordering, the vendor's own scaffolding. Compare the things that matter, which are the payload types and the settings inside them:

# Which payload domains exist on each? This is the real comparison.
grep -o "com\.apple\.[a-zA-Z.]*" old-platform.xml | sort -u > old-domains.txt
grep -o "com\.apple\.[a-zA-Z.]*" new-platform.xml | sort -u > new-domains.txt
diff old-domains.txt new-domains.txt

# Anything in the left column is a control you had and may have dropped.
# Anything in the right column is new, and should be there on purpose.

Then check the individual settings you care about actually landed with the values you intended, rather than trusting the console's word for it:

# Firewall: on? stealth on (your improvement)? incoming still allowed (deliberate)?
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
/usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode
/usr/libexec/ApplicationFirewall/socketfilterfw --getblockall

# Gatekeeper: assessment enabled?
spctl --status

# Screen lock: the thing that was broken. Does it now actually ask?
sudo profiles -P -o stdout-xml | grep -A5 "askForPassword"

# Login window: banner and field behaviour
sudo defaults read /Library/Preferences/com.apple.loginwindow 2>/dev/null

Every line in the left-hand column of that diff is a question you must answer, and there are only two acceptable answers: I reproduced it, or I skipped it on purpose and here is the row in my table saying why. "I did not notice" is not on the list. That is what turns a mapping table from a to-do list into an audit.

Expect the diff to be noisy in the old machine's favour, and expect most of that noise to be vendor plumbing you correctly did not reproduce. That is the technique working. What you are hunting is the one real control hiding among the scaffolding.

Troubleshooting and Gotchas

The export has no values, only names

You used profiles show or profiles list. Both deliberately omit payload values. Use sudo profiles -P -o stdout-xml, or download the .mobileconfig files from the console. If the output is only a few hundred lines for a couple of dozen profiles, that is your answer.

A profile looks important but enforces nothing

Good. That is the outcome the technique exists to surface, and it is more common than anyone expects. Confirm against the payload, put it down as Skip with a note saying why, and move on with a clear conscience. The note matters more than the decision: in six months, "skipped because the payload set one flag and restricted nothing" is worth ten times "skipped".

A reproduced privacy grant silently does not work

The code requirement does not match, which is exactly why you do not retype them. Delete your hand-built version, export the original as a .mobileconfig, and import it as a custom profile so the requirement crosses verbatim. If the app was never going to be installed on the new platform anyway, delete it and do not replace it.

I cannot tell whether a profile is vendor tooling

Look at the payload identifiers rather than the display name. Anything in the old vendor's own namespace, its management framework, remote assist, notifications, app installers, dies with the vendor. Apple's own domains (com.apple.*) are the real configuration. This single distinction resolves most of the list in about a minute.

The urge to reproduce everything just in case

Resist it, and be clear that it is not the cautious option. Reproducing vendor plumbing on a platform where that vendor no longer exists is not belt-and-braces, it is either wasted effort or, in the case of privacy grants for departed software, an actively wrong configuration you will be puzzling over later. The table is the discipline: if it says Skip, it is Skip, and the note next to it is your evidence.

The diff is enormous and I cannot see anything

You are diffing the raw XML. Do not. Extract the payload domains and diff those, as above. UUIDs and organisation strings differ on every machine and tell you nothing. If it is still noisy, filter to com.apple.* only, since Apple's domains are the actual configuration and everything else is scaffolding from one vendor or the other.

The Command Reference

Run on the Mac. The export commands need sudo.

Exporting

# THE export: full payload XML with values
sudo profiles -P -o stdout-xml > jamf-full.xml

# Summary forms: inventory only, no values
sudo profiles show
sudo profiles list

# Did it actually write something real?
wc -l jamf-full.xml

Reading the export

# Inventory of profile names
grep -A1 "ProfileDisplayName" jamf-full.xml

# Payload types, ranked: shows real config vs vendor scaffolding at a glance
grep "PayloadType" jamf-full.xml | sort | uniq -c | sort -rn

# Locate a profile, then read its range
grep -n "ProfileDisplayName" jamf-full.xml
sed -n 'START,ENDp' jamf-full.xml

# Read a specific payload domain wherever it appears
grep -n -A20 "com.apple.security.firewall" jamf-full.xml
grep -n -A15 "com.apple.screensaver" jamf-full.xml
grep -n -A20 "com.apple.loginwindow" jamf-full.xml

# Privacy/TCC payloads and what they target
grep -n "com.apple.TCC.configuration-profile-policy" jamf-full.xml
grep -n "Identifier\|CodeRequirement" jamf-full.xml

# The screen-lock question worth asking of every estate
grep -n "askForPassword\|askForPasswordDelay" jamf-full.xml

Proving parity

# Export from both, compare payload domains (not raw XML)
grep -o "com\.apple\.[a-zA-Z.]*" old-platform.xml | sort -u > old-domains.txt
grep -o "com\.apple\.[a-zA-Z.]*" new-platform.xml | sort -u > new-domains.txt
diff old-domains.txt new-domains.txt

Verifying the rebuilt controls on the new platform

# Firewall state, stealth mode, block-all
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
/usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode
/usr/libexec/ApplicationFirewall/socketfilterfw --getblockall

# Gatekeeper
spctl --status

# Login window settings
sudo defaults read /Library/Preferences/com.apple.loginwindow 2>/dev/null

# Which profiles landed on the new machine at all
sudo profiles list

Quick-Action Checklist

  1. On a Mac still managed by the old platform, export with values: sudo profiles -P -o stdout-xml.
  2. Pull the inventory of display names, and rank the payload types.
  3. For every profile, read the values. Never decide from the name.
  4. Mark vendor tooling Skip: it dies with the old platform, and reproducing it is wrong, not cautious.
  5. Mark near-empty payloads Skip, with a note saying what they actually contained.
  6. Build only the genuine gaps. Expect a handful, not two dozen. Assign them to the broad dynamic group.
  7. Make your compliance policy agree with what you built, especially the firewall incoming-connections check.
  8. While you are in there, ask whether each control does what its name implies. Check askForPassword specifically.
  9. Reproduce privacy profiles by importing the original .mobileconfig as a custom profile. Never retype a code requirement.
  10. Confirm each privacy profile's target app is still deployed before reproducing it.
  11. Prove parity: export from a new build, diff the payload domains, and answer every line in the left column.
  12. Verify the rebuilt controls on the device itself, not in the console.
  13. Keep the table. It is your audit record.

Closing Thoughts

A migration is one of the very few moments when someone is forced to look at every control an estate enforces and ask, honestly, whether it still does anything. Export what is actually applied, read the values rather than the names, and a frightening list becomes a short one: mine went from twenty-four profiles to three, and the three took an afternoon.

But the reason to do this is not the time it saves, welcome as that is. It is the screen lock. Fifteen minutes of idle, a screensaver, and no password prompt, sitting there for years, in an estate that sincerely believed it had a screen lock. One grep found it. Nothing else was ever going to. That is what an audit is, and you are being handed one free, right at the moment you are best placed to act on it. Take it, keep the table, and prove your parity with a diff rather than a feeling.


← All guides