If you've spent any time configuring user authentication on... Full Story
By Manny Fernandez
September 28, 2026
macOS XProtect: How Apple’s Built-In Malware Defense Actually Works
Three engines, two bundle locations, two update pipelines, and the commands to verify all of it
| Objective | Understand every moving part of XProtect on modern macOS, verify it is current, read what it detects, and know where it stops protecting you. |
| Target audience | Mac admins, security engineers, SEs, and anyone who has been told “Macs have antivirus built in” and wants to see the receipts. |
| Platforms | macOS Sequoia 15, Tahoe 26, and Golden Gate 27 (notes for Sonoma and earlier where they differ) |
| Versions at time of writing | XProtect 5362 (26 Sep 2026), XProtect Remediator 163 (18 Sep 2026) |
Executive Summary
Every Mac ships with a malware defense stack that most users never see and most admins never verify. It has no menu bar icon, no scan button, and no dashboard. Apple does not publish release notes for it. Yet it updates roughly weekly, blocks known malware at launch, sweeps the disk daily, and quietly reports suspicious behavior back to Apple.
The catch is that “XProtect” is no longer one thing. On current macOS it is three distinct engines, and the signature data behind the first one now lives in two locations fed by two separate update pipelines. If you only check one of them, you can believe a Mac is current when the copy actually doing the scanning is not. This guide breaks the whole thing apart, then gives you the CLI to verify and troubleshoot each piece.
Prerequisites and Architecture
Assumed knowledge
- Comfortable in Terminal with
sudo,defaults,grep, and the unifiedlogcommand. - Basic awareness of Gatekeeper, notarization, and quarantine extended attributes.
- Familiarity with YARA rule syntax helps for the rule-reading section but is not required.
Lab requirements
- A Mac running macOS Sequoia 15 or later (the
xprotectcommand does not exist on Sonoma 14 or earlier). - An admin account for the
sudosteps. - Outbound access to Apple software update servers and iCloud (CloudKit). No Apple Account sign-in is required.
The three XProtects
| Engine | What it does | When it runs | Where it lives |
|---|---|---|---|
| XProtect (classic) | Signature scan of code against YARA rules before it executes. Part of Gatekeeper. | On demand: first launch, changed executables, and other Gatekeeper assessments | XProtect.bundle (two copies on Sequoia+, see below) |
| XProtect Remediator (XPR) | Background scanners that hunt for known malware families already on disk and remove or remediate them. | About once a day, scheduled by the DAS-CTS background scheduler | /Library/Apple/System/Library/CoreServices/XProtect.app |
| XProtect Behavioural (Bastion) | Rule-based monitoring for sensitive locations and actions. Reports violations to Apple security intelligence. | Continuously, as events occur | Bastion rules shipped inside XProtect.app |
Field note: XPR and Bastion ship together as one package (XProtectPayloads) via Software Update. Classic XProtect ships separately as XProtectPlistConfigData. Those are the two names you will see in install history and in tools like SilentKnight.
Two bundles, two pipelines
Before Sequoia, classic XProtect was a single bundle updated by Software Update. Starting with macOS 15, Apple added a second copy under /var/protected/xprotect/, and that copy is the one Gatekeeper actually scans against. It is fed by its own background service, XProtectUpdateService, which pulls updates from iCloud (CloudKit) using an anonymous account. The original copy still exists and still updates through softwareupdated.

| Component | Path | Updated by | Source |
|---|---|---|---|
| Classic XProtect (active, Sequoia+) | /var/protected/xprotect/XProtect.bundle |
XProtectUpdateService |
iCloud / CloudKit |
| Classic XProtect (legacy copy) | /Library/Apple/System/Library/CoreServices/XProtect.bundle |
softwareupdated |
Apple software update servers |
| XProtect Remediator + Bastion | /Library/Apple/System/Library/CoreServices/XProtect.app |
softwareupdated |
Apple software update servers |
| Command tool (Sequoia+) | /usr/bin/xprotect |
macOS updates | n/a |
Inside the bundle, Contents/Resources/ holds the detection content. The two files you care about most are XProtect.yara (the main YARA rule set for binaries) and XPScripts.yr (rules aimed at script-based threats such as malicious osascript payloads), alongside supporting property lists.
Gotcha: The Install Security Responses and system files toggle in Software Update settings only controls the softwareupdated pipeline. Turn it off and your legacy bundle and XProtect Remediator stop updating, but XProtectUpdateService keeps refreshing the active bundle regardless. There is no user control over that service.
Step-by-Step Implementation Workflow
Step 1: Inventory every XProtect version on the Mac
Goal: Know exactly which version each component is running, because they can and do drift apart.
Action: Query the active bundle with the xprotect tool, then read the legacy bundle and XPR versions straight from their Info.plist files.
# Active classic XProtect (the copy Gatekeeper scans against)
xprotect version
# Legacy classic XProtect copy and XProtect Remediator
CS=/Library/Apple/System/Library/CoreServices
defaults read "$CS/XProtect.bundle/Contents/Info.plist" CFBundleShortVersionString
defaults read "$CS/XProtect.app/Contents/Info.plist" CFBundleShortVersionString
# Install history for both packages, newest entries included
system_profiler SPInstallHistoryDataType \
| grep -A 4 -E "XProtect(PlistConfigData|Payloads)"
# Receipts for anything XProtect related
pkgutil --pkgs | grep -i xprotect
GUI verification: Apple menu, About This Mac, More Info, System Report, then Software and Installations. Sort by Install Date and look for XProtectPlistConfigData and XProtectPayloads.
Step 2: Check for and force updates on both pipelines
Goal: Bring both classic bundles and XPR current instead of waiting for background schedulers.
Action: Use softwareupdate for the legacy bundle and XPR, then xprotect for the active bundle. Config-data updates are hidden from a normal --list, so you must pass --include-config-data.
# Pipeline 1: Apple software update servers (legacy bundle + XPR)
softwareupdate --list --include-config-data
sudo softwareupdate --install --include-config-data "<LABEL-FROM-LIST>"
# Pipeline 2: iCloud (active bundle, Sequoia and later)
sudo xprotect check # version available from iCloud
sudo xprotect update # download and activate it
xprotect version # confirm
Practitioner tip: Apple does not always publish to both pipelines at the same moment, and some releases go to Sequoia and later only. On 26 Sep 2026, for example, XProtect 5362 initially landed through iCloud for Sequoia+ before anything else. Always run the xprotect pair even if softwareupdate shows nothing.
GUI verification: Re-run the System Report Installations view. A fresh XProtectPlistConfigData entry confirms the legacy copy; xprotect version is the only reliable check for the active copy.
Step 3: Read what XProtect actually detects
Goal: Stop treating XProtect as a black box. Its rules are plain-text YARA on your disk.
Action: Inspect the active rules, count them, search for a family, and diff the two copies.
ACTIVE=/var/protected/xprotect/XProtect.bundle/Contents/Resources
LEGACY=/Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/Resources
sudo ls -l "$ACTIVE"
# How many rules are loaded (public and private)
sudo grep -cE '^(private )?rule ' "$ACTIVE/XProtect.yara"
# List rule names
sudo grep -E '^rule ' "$ACTIVE/XProtect.yara" | awk '{print $2}' | sort | less
# Pull the full definition for a family you care about
sudo grep -i -A 30 '<FAMILY-KEYWORD>' "$ACTIVE/XProtect.yara" | less
# Script-focused rules (osascript, shell droppers)
sudo grep -E '^rule ' "$ACTIVE/XPScripts.yr" | awk '{print $2}'
# Do the two copies match?
sudo diff -q "$LEGACY" "$ACTIVE" && echo "Bundles identical"
Rule names map to Apple’s internal family labels (the MACOS.FAMILY.VARIANT style you will see in community write-ups). Reading them tells you which threats Apple considers active this week: recent releases have focused heavily on infostealers and AppleScript-driven droppers.
Step 4: Inspect XProtect Remediator and its schedule
Goal: Confirm the XPR scanner modules exist and understand how scans are scheduled.
Action: List the scanner executables and the launch property lists that drive daily scans.
XPR=/Library/Apple/System/Library/CoreServices/XProtect.app
# One executable per malware family scanner (plus the XProtect core)
ls -1 "$XPR/Contents/MacOS/"
# Scan schedule definitions: agent runs as the user, daemon as root
ls -1 "$XPR/Contents/Resources/" | grep -E 'scan\.plist$'
plutil -p "$XPR/Contents/Resources/com.apple.XProtect.daemon.scan.plist"
Each module targets a named family (long-running adware such as Adload and Pirrit, test modules such as Eicar, and newer infostealer families). Scans are dispatched by the Duet Activity Scheduler and Centralised Task Scheduling system (DAS-CTS), so a Mac that sleeps constantly or is always on battery-saving constraints may scan less often than you expect.
Gotcha: As of XPR 163 (18 Sep 2026), double-clicking XProtect.app or launching it from a helper such as XProCheck no longer runs a manual scan. It opens and quits immediately with no error. Rely on the scheduled scans and their log output instead.
Step 5: Monitor detections in the unified log
Goal: See what XProtect is doing: rule loads, Gatekeeper scan verdicts, XPR scan results, and updates.
Action: Query the relevant subsystems. XPR writes structured events that are the closest thing to a detection report you will get.
# XProtect Remediator results (structured events), last 24 hours
P='subsystem == "com.apple.XProtectFramework.PluginAPI"'
P="$P && category == \"XPEvent.structured\""
log show --last 1d --info --style compact --predicate "$P"
# Gatekeeper-time XProtect scans (look for the rules location line)
log show --last 2h --style compact --predicate 'subsystem == "com.apple.xprotect"'
# iCloud update service activity
log show --last 1d --style compact \
--predicate 'subsystem BEGINSWITH "com.apple.security.XProtectFramework"'
# Watch live while you launch a newly downloaded app
log stream --style compact --predicate 'subsystem == "com.apple.xprotect"'
On Sequoia and later, xprotect logs is a convenience wrapper around a log show query if you prefer not to remember predicates.
Step 6: Report XProtect health across a fleet
Goal: Surface all three versions per device in your MDM so drift is visible.
Action: Deploy an extension attribute (Jamf Pro shown; adapt the output for Kandji, Intune, or Mosyle custom attributes).
#!/bin/zsh
# Jamf Pro Extension Attribute: XProtect version health
CS=/Library/Apple/System/Library/CoreServices
ver() {
/usr/bin/defaults read "$1/Contents/Info.plist" \
CFBundleShortVersionString 2>/dev/null || echo "n/a"
}
legacy=$(ver "$CS/XProtect.bundle")
xpr=$(ver "$CS/XProtect.app")
active="n/a"
if [[ -x /usr/bin/xprotect ]]; then
active=$(/usr/bin/xprotect version 2>/dev/null | awk '/Version/ {print $2}')
fi
echo "<result>active=${active} legacy=${legacy} xpr=${xpr}</result>"
Build a smart group on active lagging the current release by more than one version. If you defer or block Software Update via MDM, remember that this also freezes XPR, even though the active classic bundle keeps updating from iCloud.
Verification and Validation
Run these checks after any update or on a suspect endpoint. Output formats shift slightly between macOS releases, so match on the values rather than exact spacing.
$ xprotect version
Version: 5362 Installed: 2026-09-26
$ sudo xprotect check
Latest version available: 5362
$ defaults read /Library/Apple/System/Library/CoreServices/XProtect.app/\
Contents/Info.plist CFBundleShortVersionString
163
| Check | Command | Healthy result |
|---|---|---|
| Active bundle current | xprotect version |
Matches the latest published version |
| Nothing pending from iCloud | sudo xprotect check |
Available version equals installed version |
| Legacy bundle current | softwareupdate --list --include-config-data |
No XProtect config-data item listed |
| XPR current | defaults read .../XProtect.app/Contents/Info.plist CFBundleShortVersionString |
163 or later |
| XPR actually scanning | log show with the XPEvent.structured predicate |
Scan events within the last 24 to 48 hours |
| Bundles in sync | sudo diff -q "$LEGACY" "$ACTIVE" |
No differences reported |
Troubleshooting and Gotchas
1. The two classic bundles report different versions
Symptom: System Report or SilentKnight shows one version, xprotect version shows another.
Cause: The pipelines publish independently. iCloud can lag Software Update, or lead it, by hours or days. Some releases are Sequoia+ only, so older macOS tops out at an earlier version.
Resolution: Trust xprotect version for what Gatekeeper is using. Run sudo xprotect update, then recheck. If it still lags, wait for XProtectUpdateService rather than fighting it.
2. sudo xprotect check returns an error
Symptom: Got error checking for update: Error ... instead of a version number.
Cause: The iCloud path has been unreliable at release boundaries. It failed consistently in Golden Gate betas and briefly on the 27.0, 26.7, and 15.8 releases in mid-September 2026 before Apple fixed it server-side around 17 to 18 September.
Resolution: Try sudo xprotect update anyway, since it can succeed when check fails. If both fail, confirm CloudKit is reachable through your proxy or firewall, then fall back to waiting on the background service.
# Is the update service reaching iCloud at all?
Q='subsystem BEGINSWITH "com.apple.security.XProtectFramework"'
Q="$Q OR subsystem == \"com.apple.cloudkit\""
log show --last 1h --style compact --predicate "$Q" | grep -iE 'update|error|fail'
3. XPR never seems to scan, or will not run manually
Symptom: No XPEvent.structured entries for days, or XProtect.app quits instantly when launched.
Cause: Manual runs are blocked as of XPR 163. Missing scheduled scans usually mean DAS-CTS keeps deferring the activity (sleep, thermal, or energy budget), or XPR is stale because Software Update config-data installs are disabled or deferred by MDM.
Resolution: Leave the Mac awake on power for a period, confirm XPR is current, and check that Install Security Responses and system files is enabled or that your MDM is not blocking config data.
4. Treating XProtect as your EDR
XProtect is signature and rule based against known families. It has no user-facing alerting beyond a Gatekeeper block dialog, no quarantine console, no telemetry you control, and no response actions. The Bastion data goes to Apple, not to your SOC. It is an excellent floor, not a ceiling: layer a real EDR (FortiEDR, FortiClient EPP, or whatever your stack runs) on managed Macs and use the checks above to make sure the floor is solid.
Quick Reference
| Task | Command |
|---|---|
| Active bundle version | xprotect version |
| Check iCloud for update | sudo xprotect check |
| Install iCloud update | sudo xprotect update |
| List hidden config-data updates | softwareupdate --list --include-config-data |
| Install a config-data update | sudo softwareupdate --install --include-config-data "<LABEL>" |
| XPR version | defaults read $CS/XProtect.app/Contents/Info.plist CFBundleShortVersionString |
| Install history | system_profiler SPInstallHistoryDataType | grep -A 4 XProtect |
| Count YARA rules | sudo grep -cE '^(private )?rule ' $ACTIVE/XProtect.yara |
| XPR scan results | log show --last 1d --info --predicate "$P" |
| Live Gatekeeper scans | log stream --predicate 'subsystem == "com.apple.xprotect"' |
Bottom line: XProtect is three engines and two data copies. Verify the active bundle with xprotect, keep Software Update config data flowing for XPR, read the YARA to know what you are covered for, and never mistake it for a SOC-grade tool.
Recent posts
-
-
DNS is one of those technologies that quietly underpins... Full Story
-
BGP issues on FortiGate firewalls usually trace back to... Full Story
-
Every time your laptop talks to your router, a... Full Story
-
If you've spent any time configuring NAT on a... Full Story
-
If you have spent any time configuring firewall policies... Full Story
-
High availability on FortiGate is one of those features... Full Story
-
If you've configured SD-WAN on a FortiGate, you've almost... Full Story
-
FortiLink is the management protocol that turns a FortiSwitch... Full Story
-
FortiSwitches are pretty rock solid from Mean Time Between... Full Story
-
This is a quicky tip. Have you ever gone... Full Story
-
DNS is one of those quiet pieces of internet... Full Story
-
This article is an updated version of the previous... Full Story
-
You will add ns2 as a secondary (slave) BIND9... Full Story
-
In the process of deploying my lab, I needed... Full Story
-
RFC 8805, used to be known as Self-Correcting IP... Full Story
-
Years back, I wrote an article about certificate pinning. ... Full Story
-
FortiGates have the ability to send alerts to Microsoft... Full Story
-
In this post, I am going to walk through... Full Story
-
Troubleshooting VoIP on a FortiGate can feel like trying... Full Story
-
Prior to FortiOS 7.0, there were three commands to... Full Story
-
In this post, I am going to go over... Full Story
-
What we are going to do: We are going... Full Story
-
Choosing between FGCP (FortiGate Clustering Protocol) and FGSP (FortiGate... Full Story
-
Creating a VLAN on macOS (The "Pro" Move) A... Full Story
-
This blog post explores the logic behind how macOS... Full Story
-
Pretty Fly for a Wi-Fi Tell My Wi-Fi Love... Full Story
-
Part of my daily gig is creating BoMs (Bill-of-Materials)... Full Story
-
ICMP introduces several security risks, but careful filtering, rate... Full Story
-
The command diag debug application dhcps -1 enables full... Full Story
-
In the world of FortiOS, execute tac report is... Full Story
-
LLDP; What is it The Link Layer Discovery Protocol... Full Story
-
What it actually does When you run diagnose fdsm... Full Story
-
Monkey Bites are bite-sized, high-impact security insights designed for... Full Story
-
I have run macOS in macOS with Parallels but... Full Story
-
Don't be confused with my other FortiNAC posts where... Full Story
-
This is the third session in a multi-part article... Full Story
-
Today I was configuring key-based authentication on a FortiGate... Full Story
-
Netcat, often called the "Swiss Army knife" of networking,... Full Story
-
At its core, IEEE 802.1X is a network layer... Full Story
-
In case you did not see the previous FortiNAC... Full Story
-
This is our 5th session where we are going... Full Story
-
Now that we have Wireshark installed and somewhat configured,... Full Story
-
The Philosophy of Packet Analysis Troubleshooting isn't about looking... Full Story
-
I have been playing with all forms of grep... Full Story
-
Three engines, two bundle locations, two update pipelines, and... Full Story
-
2026 refresh of the original IPTable Firewall GUI post... Full Story