How to Run a Read-Only CIS Security Check on macOS
Run a verified, read-only CIS Level 1 security audit on macOS 14, 15, or 26 with NIST mSCP, temporary dependencies, and an auditable local report in /tmp.
TL;DR — I wanted to check a Mac against established security best practices and found NIST’s open-source macOS Security Compliance Project (mSCP). This article shows how to run a verified, read-only CIS Level 1 audit on macOS 14, 15, or 26:
zsh scan_cis.zsh --baseline cis_lvl1The scan does not remediate settings; it writes the report and its temporary dependencies under
/tmp.
I wanted a simple answer to a simple question: how close is this Mac to a CIS security baseline? I did not want a script that quietly changes settings while trying to answer it.
The macOS Security Compliance Project (mSCP)
is NIST’s open-source toolkit for producing macOS security guidance and
compliance checks. It can generate a zsh compliance script for a chosen
baseline; its documented --check mode performs checks only, while --fix
applies remediation. That distinction matters.
For the first pass, I built macos-cis-scan:
a small wrapper that generates an mSCP audit and invokes it only as
--check. It never invokes --fix or --cfc. A failed rule is a prompt to
review a setting, not evidence that the scanner changed it.
Why use a wrapper?
mSCP is the authoritative component here, and its quick guide explains how to select a baseline and generate a compliance script. That is a good route when you are building a managed compliance workflow.
For a person who needs one reproducible local audit, there is still some setup to make explicit: obtain the source, generate the baseline, provide Python and Ruby dependencies, run the generated script with the right privilege, and retain the resulting report. The wrapper keeps that setup in one temporary directory and records the versions and hashes used for the run.
It is deliberately not an installer. It does not create a launch agent, add a profile, alter a CIS setting, or leave a Python environment behind.
What this release supports
At the time of writing, release
v0.1.0
supports the mSCP 2.0 rule library for macOS 14 Sonoma, 15 Sequoia, and 26
Tahoe. The script was tested on Apple Silicon with macOS 26; both Apple
Silicon and Intel portable-Python downloads are supported. It exits before
downloading dependencies on an unsupported macOS version.
macOS 13 Ventura and earlier are intentionally out of scope. They need a separately reviewed workflow built around mSCP 1.0 rather than a forced run of this script.
What you need before you start
For a full audit, the Mac needs:
- macOS 14, 15, or 26;
- an administrator account —
sudois used only immediately before the audit reads privileged configuration; - the bundled
zsh,curl,tar, andshasumcommands; git, plus the systemrubyandgemcommands;- outbound HTTPS access to GitHub, PyPI, and RubyGems; and
- about 1 GB of free space on the volume backing
/tmp.
You do not need to pre-install Python, pip, a virtual environment, Bundler,
or any Ruby gems. The script downloads a fixed portable CPython build, verifies
its SHA-256 before extracting it, and puts Python, gem state, caches, the mSCP
checkout, and the generated audit under a unique directory in /tmp.
The script is explicitly executed with zsh, so it does not matter whether a
person’s interactive shell is zsh, bash, or something else.
Download, verify, and run the audit
Download a named release rather than a copied snippet or a moving branch. You
can copy the following block as a whole: it creates a unique working directory
in /tmp, downloads and verifies the v0.1.0 release, runs the audit, and
then deletes only the downloaded release files. Nothing is piped into a shell.
Choose one baseline:
cis_lvl1— the default and practical starting point for a personal or standard work Mac.cis_lvl2— substantially stricter; pilot it before broad use.
(
set -e
work_dir="$(mktemp -d /tmp/macos-cis-scan-v0.1.0.XXXXXX)"
trap 'rm -rf "$work_dir"' EXIT
cd "$work_dir"
curl -fLO https://github.com/r4kh1m/macos-cis-scan/releases/download/v0.1.0/scan_cis.zsh
curl -fLO https://github.com/r4kh1m/macos-cis-scan/releases/download/v0.1.0/SHA256SUMS
curl -fLO https://github.com/r4kh1m/macos-cis-scan/releases/download/v0.1.0/SHA256SUMS.sig
curl -fLO https://github.com/r4kh1m/macos-cis-scan/releases/download/v0.1.0/r4kh1m-release-signing-key.pub
shasum -a 256 -c SHA256SUMS
{
printf 'r4kh1m-release namespaces="file" '
cat r4kh1m-release-signing-key.pub
} > allowed_signers
ssh-keygen -Y verify -f allowed_signers -I r4kh1m-release -n file -s SHA256SUMS.sig < SHA256SUMS
# Change cis_lvl1 to cis_lvl2 only after a pilot.
zsh scan_cis.zsh --baseline cis_lvl1
)
The parentheses run this workflow in a separate shell, so set -e and the
cleanup trap do not change the user’s interactive shell. A failed download or
verification stops the workflow before the audit. The checksum check answers,
“are these the exact files listed by the release?” The signature check answers,
“was that manifest signed by the release key?” Before trusting a new key,
compare its fingerprint with the release notes. You can also inspect the
script directly — it is a plain zsh file — before running it.
The audit uses only --check mode. It asks for an administrator password
only at the point where mSCP needs to read privileged macOS configuration. It
then prints the location of a directory similar to this:
/tmp/mscp-cis_lvl1.A1b2C3
Inside its report/ directory are two useful files:
report/
├── cis_lvl1_check.txt # the main report: passed and failed rules
└── provenance.txt # technical record: versions, hashes, and mSCP commit
Read cis_lvl1_check.txt first. Keep provenance.txt when you need to
show how the audit was produced. Copy anything you want to retain, then delete
that exact directory:
rm -rf /tmp/mscp-cis_lvl1.A1b2C3
Use the path printed by your own run — do not copy the example blindly.
There are two disposable directories: the workflow automatically removes its
release-files directory, and the printed /tmp/mscp_… directory holds the
report and runtime dependencies. Delete the latter after reading or copying the
report. No application or configuration depends on either directory.
The wrapper preserves and restores the mSCP audit plist and log that mSCP
creates under /Library during the check. It cannot, and should not, erase
macOS security records such as sudo authentication or process events. A
useful audit can be non-remediating without pretending to leave no trace at
all.
How to read a result
For the official baseline and rule guidance, start with NIST’s mSCP Quick Guide and its guide to generated compliance scripts.
A non-compliant result is not a command to change the Mac. CIS recommendations can conflict with a device-management policy, a development workflow, or a real operational need. Start with the rule identifier and its rationale in the report, decide whether the setting is intentional, and make any remediation as a separate, reviewed change.
That separation is the point of this first step: establish evidence before changing the system.
Feedback makes the compatibility story better
The most helpful feedback is a sanitized compatibility report: macOS version, Apple Silicon or Intel, chosen baseline, scanner version, and any failing rule identifiers. Please use GitHub Discussions for questions and experience reports, and GitHub Issues for reproducible bugs.
Do not attach the full audit report to a public thread. It can contain a host name, user names, installed-software information, and security configuration. For a vulnerability in the wrapper itself, follow the project’s private security-reporting process.