How to Run a Read-Only CIS Security Check on macOS

stackandink.com ·

7 min read Original article ↗

TL;DRmacos-mscp-scan generates an unchanged NIST mSCP CIS Level 1 or Level 2 audit and invokes only read-only --check mode. Preparation runs without sudo; the user starts the separate privileged audit in a normal Terminal. The scanner never applies remediation.

Auditing a personally administered Mac rather than CIS compliance? Read Personal Mac Security: Run a Practical Local Check. The repository now defaults to a tailored personal profile, while raw CIS baselines remain available explicitly.

I wanted a reproducible answer to a narrow question: how does this Mac compare with an unchanged CIS security baseline? I did not want a script to change settings while trying to answer it.

The macOS Security Compliance Project (mSCP) is an open-source collaboration involving NIST, NASA, DISA and Los Alamos National Laboratory. It provides macOS security guidance and compliance checks, and generates a zsh compliance script for a selected baseline. Its documented --check mode checks configuration, while --fix applies remediation.

macos-mscp-scan is a small wrapper around that process. It pins inputs, isolates dependencies, preserves evidence for the run and invokes the generated script only as --check. It never invokes --fix or --cfc.

CIS audit or personal Mac security audit?

Choose the baseline from the question you need to answer:

  • cis_lvl1 — unchanged CIS Level 1 compliance checks;
  • cis_lvl2 — unchanged and stricter CIS Level 2 checks, with potentially greater usability or compatibility impact; or
  • personal — selected effective protections for a personally administered, non-MDM Mac; this is the repository default and not a CIS assessment.

A personal Mac can have FileVault enabled yet fail a raw CIS rule because an MDM profile does not prevent the user from disabling it later. Both results can be correct: one measures effective state, the other central enforcement. Do not rename or reinterpret the personal report as CIS compliance.

This article continues with raw CIS. For the design decisions, exclusions and human/AI workflows of the tailored profile, use the personal Mac security audit guide.

Why use a wrapper around NIST mSCP?

mSCP is the authoritative component. NIST explains what compliance scripts are and how to generate them. A local reproducible audit still has setup to make explicit: obtain a known source revision, select the baseline, provide Python and Ruby dependencies, run with the required privileges and retain enough provenance to understand the result later.

The wrapper keeps its dependencies and reports under the current user’s macOS temporary directory, records versions and hashes, and verifies the prepared run before the audit starts. It does not create a launch agent, install a configuration profile or remediate a CIS finding.

Supported macOS versions and requirements

The current signed release is v0.4.0. It supports macOS 14 Sonoma, 15 Sequoia and 26 Tahoe on Apple Silicon and Intel. The full privileged validation audit was run on macOS 26 Apple Silicon.

For a complete run, the Mac needs:

  • an administrator account for the separate audit phase;
  • bundled macOS command-line tools plus git, system ruby and gem;
  • HTTPS access to GitHub, PyPI and RubyGems; and
  • about 1 GB free on the volume backing $TMPDIR.

Python, pip packages, Bundler and Ruby gems are isolated under scanner-owned temporary run and cache directories. Nothing is installed into system or user gem locations.

Download and verify the signed release

These macOS commands run as the current user without sudo. They create a unique directory under /private/tmp, download the four v0.4.0 release assets, verify the SHA-256 manifest and detached SSH signature, and extract the source. Nothing is piped into a shell.

Before trusting the release key for the first time, compare its fingerprint with a value obtained through a channel you already trust. The versioned release notes publish the expected fingerprint, but they live in the same GitHub repository and therefore are not independent verification. The checksum detects a corrupted or different archive; the detached signature proves that the manifest was signed by the corresponding key. First-use identity trust still depends on how you verify that key.

(
  set -e
  release_dir="$(mktemp -d /private/tmp/macos-mscp-scan-v0.4.0.XXXXXX)"
  cd "$release_dir"

  curl -fLO https://github.com/r4kh1m/macos-mscp-scan/releases/download/v0.4.0/macos-mscp-scan-v0.4.0.tar.gz
  curl -fLO https://github.com/r4kh1m/macos-mscp-scan/releases/download/v0.4.0/SHA256SUMS
  curl -fLO https://github.com/r4kh1m/macos-mscp-scan/releases/download/v0.4.0/SHA256SUMS.sig
  curl -fLO https://github.com/r4kh1m/macos-mscp-scan/releases/download/v0.4.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

  tar -xzf macos-mscp-scan-v0.4.0.tar.gz
  printf '\nScanner directory: %s/macos-mscp-scan-v0.4.0\n' "$release_dir"
)

The downloaded source directory is disposable. Remove that exact path in Finder, or with rm -rf after checking it carefully, when you no longer need it. Reports and the dependency cache live separately beneath $TMPDIR.

Prepare CIS Level 1 or Level 2

Change into the exact directory printed by the previous block. Preparation is unprivileged: it downloads pinned dependencies, generates the selected audit, builds its guidance and hashes the result. It does not inspect security settings.

For CIS Level 1:

cd /private/tmp/macos-mscp-scan-v0.4.0.EXAMPLE/macos-mscp-scan-v0.4.0
zsh ./scan_cis.zsh --baseline cis_lvl1 --prepare-only

For CIS Level 2:

zsh ./scan_cis.zsh --baseline cis_lvl2 --prepare-only

The script prints PREPARATION COMPLETE — USER ACTION REQUIRED and one fully quoted --run-prepared command. Run that exact command in your normal interactive Terminal. The wrapper verifies the prepared files, explains the action and asks for confirmation before macOS requests the administrator password.

The two-phase design keeps privileged authorization explicit. It also makes the workflow safe to prepare with an AI agent: the agent must stop at the user action boundary, and the password is entered only into the user’s Terminal.

What the read-only audit changes

The generated mSCP script is invoked only with --check, so it does not apply recommended settings. mSCP temporarily creates a result plist beneath /Library/Preferences and a log beneath /Library/Logs. The wrapper copies the result into the private report and restores any previous files, or removes files created by the run.

Normal macOS events such as sudo authentication and process execution remain in system records. A non-remediating audit should not pretend to leave no trace at all.

Where to find the CIS report

The scanner stores its data beneath the current user’s standard temporary directory rather than ~/Library:

${TMPDIR%/}/macos-mscp-scan/
├── cache/                         # reusable dependencies
├── runs/cis_lvl1.XXXXXX/report/  # one isolated prepared or completed run
├── latest-prepared-cis_lvl1.txt  # run awaiting user action
└── latest-cis_lvl1.txt           # latest completed CIS Level 1 run

The Level 2 pointer uses cis_lvl2 in the same pattern. Each report includes:

report/
├── cis_lvl1_check.txt
├── scan-output.audit.plist
├── baseline.yaml
├── provenance.txt
├── audit-completed.txt
├── scan-output_baseline.log
└── generated HTML and PDF guidance

Start with cis_lvl1_check.txt, then use the HTML or PDF to read each rule’s rationale, remediation, impact and references. Confirm that audit-completed.txt exists and records a zero exit status before relying on the totals.

$TMPDIR is disposable. macOS may purge it after a restart, during maintenance or under storage pressure. Copy any report you need to retain to a permanent, private location. To delete dependencies without removing completed reports, run zsh ./scan_cis.zsh --clear-cache; this changes no security setting.

How to interpret CIS findings

In scan-output.audit.plist, finding = true means the check found a non-compliant condition and finding = false means it did not. Start with the text report because it distinguishes an explicit pass from N/A, an execution error or a manual-review result.

The plist is a flat result set. Rule order and identifier do not indicate severity, exploitability, urgency or remediation cost. For every finding:

  1. locate the exact rule in the generated guidance;
  2. read its rationale, check, remediation and impact;
  3. account for device role, data sensitivity, exposed services, organizational policy and compensating controls; and
  4. make any change separately, then rescan.

A failed rule is not an instruction to modify the Mac blindly. CIS guidance can conflict with an approved device-management policy, a development workflow or a real operational need. Evidence comes first; remediation is a distinct, reviewed action.

The audit is also not a vulnerability scanner, a general open-port scan or a universal macOS security score.

Keep security reports private

Reports can contain host and user names, paths, installed-software information and security configuration. Do not attach a complete unredacted report to a public issue or send it to an external AI service without explicit review.

Use GitHub Discussions for usage questions and sanitized compatibility experience, and GitHub Issues for reproducible bugs. Security reports follow the repository’s private security-reporting process.