← All notes

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.

  • macos
  • security
  • cis
  • compliance
  • mscp

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_lvl1

The 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 — sudo is used only immediately before the audit reads privileged configuration;
  • the bundled zsh, curl, tar, and shasum commands;
  • git, plus the system ruby and gem commands;
  • 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.