17 KiB
ApplePY
A macOS security review and attack-surface scanner. It can be run on a Mac to produce a Markdown report** and a multi-worksheet XLSX spreadsheet covering CIS benchmark controls, privilege escalation paths, MDM posture, NIST mSCP compliance, living-off-the-land catalogues, and much more. Optionally exports a BloodHound CE–compatible graph JSON file for visual attack-path analysis (courtesy of JAMF Hound).
Disclaimer
For authorised security assessments only. Results are indicative; confirm material findings in your environment before acting on them. Third-party sources and licences are listed in CREDITS.md. All rights to original authors.
Quick start
# Unprivileged scan (no root required), outputs ./applepy-out/report.md and findings.xlsx
./dist/applepy/applepy
# Full scan (unprivileged + privileged checks)
sudo ./dist/applepy/applepy
# Choose a custom output folder
sudo ./dist/applepy/applepy --output-dir ~/Desktop/assessment
See Getting the binary below if you do not yet have dist/applepy/applepy.
What it produces
| File | Description |
|---|---|
report.md |
Narrative summary grouped into themes (e.g. Hardening, MDM, Compliance). Each finding shows severity, risk, impact, and remediation steps. |
findings.xlsx |
Multi-worksheet spreadsheet. The Contents tab links to every category sheet. Each sheet is colour-coded by severity (critical → informational) and includes a MITRE ATT&CK mapping worksheet. |
graph_findings.json |
(Optional — --export-graph-json) JamfHound-compatible BloodHound CE OpenGraph JSON for visual attack-path analysis. |
Privilege model
ApplePY splits its work into two phases so that you do not need root for a partial scan:
| How you run | What runs |
|---|---|
applepy (no sudo) |
Unprivileged phase only. Checks that do not require root. A warning is printed reminding you that the privileged phase was skipped. |
sudo applepy |
Both phases. Unprivileged checks first, then privileged checks (sudoers, SUID/SGID binaries, TCC permissions, world-writable LaunchDaemon binaries, mSCP compliance generation, and so on). |
Optional overrides:
| Flag | Effect |
|---|---|
--unprivileged-only |
Run the unprivileged phase only, even when invoked with sudo. |
--privileged-only |
Run the privileged phase only (combine with sudo; without root, no checks run). |
Getting the binary
Option A — Build the PyInstaller bundle (recommended)
The primary deployment method is a self-contained one-folder bundle built with PyInstaller. It can then transferred to the target host as a single self extracting archive. The bundle embeds the Python runtime, dependencies and optionally the NIST mSCP compliance corpus and Lynis.
Requirements (build machine only): a macOS workstation, Python ≥ 3.11 and git.
cd /path/to/security-review
chmod +x scripts/build_bundle.sh scripts/vendor_compliance_assets.sh
./scripts/build_bundle.sh
This will:
- Shallow-clone NIST mSCP and Lynis into
applepy/data/. - Install ApplePY with the
bundleextra (PyInstaller, PyYAML, xlwt). - Run PyInstaller using
applepy.spec.
Output:
dist/applepy/applepy— the binary you run on the target.dist/applepy/_internal/— bundled libraries, data, and Python runtime (keep alongside the binary).
Do not run
build/applepy/applepy. Thebuild/tree is PyInstaller's working directory — it is missing the_internal/folder and will fail. Always usedist/applepy/.
Rebuilding without re-cloning (e.g. offline):
SKIP_VENDOR_COMPLIANCE=1 ./scripts/build_bundle.sh
If the build fails with a PermissionError removing dist/applepy: a previous sudo applepy run left root-owned files in _internal/.../macos_security/build/. Clear them, then rebuild:
sudo rm -rf dist/applepy
./scripts/build_bundle.sh
Option B — Install from source (development / library)
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
applepy --help
To include compliance reporting in this workflow, run applepy --bootstrap-compliance (see Compliance).
Running on the target Mac
Clear the quarantine flag
When you transfer dist/applepy/ by browser download, email, or certain archive tools, macOS applies a quarantine attribute. Clear it before running:
xattr -dr com.apple.quarantine /path/to/dist/applepy
Verify nothing remains:
xattr -lr /path/to/dist/applepy
Run the scan
# Show all available options
/path/to/dist/applepy/applepy --help
# Unprivileged scan (safe to run without sudo)
/path/to/dist/applepy/applepy --output-dir ./out-user
# Full scan (both phases)
sudo /path/to/dist/applepy/applepy --output-dir ./out-full
# Full scan + graph JSON for BloodHound CE
sudo /path/to/dist/applepy/applepy --output-dir ./out-full --export-graph-json
Unsigned binary on a managed Mac
If macOS shows a security warning on first launch, Control-click → Open in Finder, or go to System Settings → Privacy & Security and click Open Anyway. This is expected for programs that have not been signed with an Apple Developer ID.
What it checks
ApplePY runs registered checks in the unprivileged and privileged phases. Below is a capability overview grouped by area.
Core platform
Host summary, SIP status (csrutil), Gatekeeper, Application Firewall, system extensions listing, LaunchAgent/LaunchDaemon inventories, common interpreters on PATH.
CIS benchmark controls
Over 80 CIS macOS Benchmark Level 1 and Level 2 checks covering: FileVault, screen saver lock, password policy, guest account, remote login, Bluetooth, mDNS, IPv6, Safari settings, Handoff, iCloud services, Siri, AirDrop, Time Machine, printer sharing, wake settings, and many more. Each check emits PASS, FAIL, or WARN with evidence.
Privilege escalation
Read-only detection of common privilege escalation paths:
- Sudoers NOPASSWD —
/etc/sudoersand/etc/sudoers.d/entries that allow password-freesudo. - Unexpected SUID/SGID binaries — files with setuid or setgid bits outside the expected macOS baseline.
- Writable LaunchDaemon binaries — LaunchDaemon plists that reference binaries writable by non-root users.
- Writable PATH directories — directories on
PATH(from/etc/paths) that are writable by non-root users. - High-risk TCC permissions — applications granted full-disk access, camera, microphone, and similar sensitive TCC services.
Dylib hijacking
RPATH ordering and writable-dylib-directory checks for installed applications.
Filesystem posture
World-writable files under LaunchAgent and LaunchDaemon directories (caps configurable via environment variables).
Living-off-the-land catalogues
LOOBins (live with cache; fallback data), GTFOBins (live API with cache), lolapps, lottunnels, cloud CLI presence on PATH, credential path hints (presence only — no secret reads).
Compliance
CIS worksheet pointer, FileVault, pwpolicy, boot/uptime, Time Machine destinations, admin group, default umask, NIST mSCP status and (privileged) baseline generation + compliance script execution with per-rule worksheet output, Lynis quick audit when available.
Common paths
Mounts, guest login, remote login / ARD plists, MDM enrolment hints, browser extension directories, local users (dscl), SSH public key filenames, cron/periodic/at surface, Automator workflow directories, user Keychains filenames.
Extended surface
Routing table, /etc/hosts, DNS settings, per-service HTTP proxies, AirDrop/sharing defaults, shell startup file metadata, kernel extension list, USB/Bluetooth system_profiler snapshots, last logins, Wi-Fi preferences, NFS exports, pmset, CUPS/printing surface, configuration profiles, Spotlight, launchctl print-disabled, sysctl kern snapshot.
Network listeners
TCP LISTEN and UDP sockets via lsof, excluding loopback-only and OS ephemeral port ranges; correlated with process names.
Application surface
Entitlements sample (codesign), SSH directory, TCC / login items / background task store paths, Electron app ASAR and writable parent-directory checks.
MDM posture (read-only)
- Jamf — local plists, receipts, LaunchAgents, helpers, extension attribute cache, listener hints, configuration profile stores, filesystem caps.
- Kandji — Application Support tree, LaunchAgents, receipts, preferences, helpers, listeners, configuration profile store.
AI tools and IDEs
AI CLI tools on PATH (e.g. Claude Code, Cursor, GitHub Copilot CLI), AI desktop apps, local model storage, and IDE surface.
Interpreters and package managers
Python, Node.js, Ruby, Go, Rust, and other runtimes found on PATH or in standard locations, plus Homebrew, pip, npm, and conda distributions.
CLI reference
applepy [options] # from pip install / editable install
python -m applepy [options] # from source tree (without install)
./dist/applepy/applepy [options] # PyInstaller bundle
| Short | Long | Description |
|---|---|---|
--version |
Print version and exit. | |
-h |
--help |
Show help and exit. |
-o PATH |
--output-dir PATH |
Directory for report.md, findings.xlsx, and optional JSON (default: ./applepy-out). |
-v |
--verbose |
Increase log verbosity. Use -vv for debug output. |
-q |
--quiet |
Suppress all output except errors on stderr (disables progress reporting). |
--unprivileged-only |
Run the unprivileged phase only, even with sudo. |
|
--privileged-only |
Run the privileged phase only (use with sudo). |
|
--dry-run |
Run all checks but do not write any output files. | |
--export-graph-json |
Write graph_findings.json in JamfHound-compatible BloodHound CE OpenGraph format. Requires the SpecterOps Jamf extension in BloodHound CE. |
|
--sequential |
Disable parallel check execution within each phase (useful for debugging). | |
--bootstrap-compliance |
Clone NIST mSCP and Lynis into ./vendor, print hints, then exit (requires git and internet access). |
|
HEX |
--heading-color HEX |
XLSX heading background colour as a 6-digit hex code (default: 871727). |
Progress output
By default, ApplePY prints a running progress display to stderr:
- Each check shows a progress bar, percentage, check name, and either
runningor the elapsed time and finding count once complete. - At the end of each phase, a severity breakdown is shown (critical / high / medium / low / informational counts).
-q/--quietsuppresses all of this.NO_COLOR=1disables ANSI colour codes.FORCE_COLOR=1forces colour even when stderr is not a terminal.
Environment variables
| Variable | Description |
|---|---|
APPLEPY_MACOS_SECURITY_ROOT |
Override the path to the macos_security clone (takes precedence over the bundled copy). |
APPLEPY_MSCP_BASELINE |
Baseline YAML stem or filename under baselines/ (e.g. cis_level1). |
APPLEPY_MSCP_SKIP_GENERATE=1 |
Skip generate_guidance.py; only run an existing *_compliance.sh --check. |
APPLEPY_MSCP_COMPLIANCE_LOG_MAX |
Maximum characters retained per stream in compliance evidence (default: 524288). |
APPLEPY_MSCP_FORCE_SUBPROCESS=1 |
(Frozen bundle only) Use a system python3 instead of re-invoking the bundle for generate_guidance.py. |
APPLEPY_PYTHON |
Python 3 interpreter to use for generate_guidance.py (when not frozen, or with APPLEPY_MSCP_FORCE_SUBPROCESS=1). |
APPLEPY_DECK_EXPORT_TXT |
Path to an optional SpecterOps-style deck reference text file. |
APPLEPY_KANDJI_MAX_FILES |
Cap for Kandji Application Support file listing. |
APPLEPY_FS_MAX_SCAN / APPLEPY_FS_MAX_HITS |
Caps for world-writable filesystem walks. |
APPLEPY_CATALOG_INCLUDE_NOISE=1 |
Emit per-binary LOOBins rows for common UI binaries (Dock, Finder, etc.) that are normally summarised only. |
APPLEPY_GTFO_DETAILED=1 |
One finding per on-host GTFOBins match (default: a single summary row). |
APPLEPY_GTFO_LIST_ABSENT=1 |
Add a finding listing every GTFOBins name not found on the host (large; off by default). |
APPLEPY_GTFO_ROLLUP_MAX_LINES |
Maximum lines in the default GTFOBins summary row (default: 120). |
SKIP_VENDOR_COMPLIANCE=1 |
Skip compliance cloning in build_bundle.sh (use when applepy/data/ is already populated). |
NO_COLOR |
Disable ANSI colours in progress output (see no-color.org). |
FORCE_COLOR |
Force ANSI colours even when stderr is not a TTY. |
Compliance — NIST mSCP and Lynis
NIST mSCP (macOS Security Compliance Project)
The NIST mSCP corpus lives at usnistgov/macos_security. ApplePY does not commit it; instead it resolves the corpus in this order:
APPLEPY_MACOS_SECURITY_ROOT(environment variable).- Bundled
applepy/data/macos_security(populated byscripts/vendor_compliance_assets.shat build time). ./vendor/macos_security,./macos_security, then common locations under the user's home directory.
When found, ApplePY runs generate_guidance.py against the selected baseline YAML (auto-selected, or set via APPLEPY_MSCP_BASELINE), then executes the generated *_compliance.sh --check script and parses the resulting audit plist into findings. Temporary artefacts (build/ and the audit plist) are cleaned up after each scan.
Lynis
Lynis is used from PATH if available, then from the bundled applepy/data/lynis/lynis, then from ./vendor/lynis/lynis. Lynis is GPL-licensed — see CREDITS.md.
Bootstrap workflow (source install without bundled data)
# Clone mSCP and Lynis into ./vendor, print export hints, then exit
applepy --bootstrap-compliance
# Set paths for subsequent scans
export APPLEPY_MACOS_SECURITY_ROOT="$PWD/vendor/macos_security"
export PATH="$PWD/vendor/lynis:$PATH"
# Install mSCP Python dependencies
pip install pyyaml xlwt
# Run the full scan
sudo applepy --output-dir ./out
Graph JSON (BloodHound CE)
With --export-graph-json, ApplePY writes graph_findings.json to the output directory. This file is in the JamfHound-compatible BloodHound CE OpenGraph format (v1.1.2), suitable for import into BloodHound CE with the SpecterOps Jamf extension installed.
To ingest: open BloodHound CE → File Ingest → upload graph_findings.json.
See JamfHound on GitHub for extension installation instructions.
Output files in detail
report.md
A narrative summary organised into overarching themes (Hardening, Compliance, MDM, Catalogues, etc.). Each theme section lists findings from critical down to informational. Every finding includes:
- Title and severity (CRITICAL / HIGH / MEDIUM / LOW / INFORMATIONAL / WARN)
- Description of what was found
- Risk — what an attacker could do with this
- Impact — what remediation affects
- Remediation steps
- MITRE ATT&CK technique IDs where applicable
- Evidence — raw command output or parsed data supporting the finding
findings.xlsx
A multi-worksheet spreadsheet:
- Contents — index tab with hyperlinks to each category sheet and a finding count.
- Findings list — all non-informational findings sorted by severity, then ID, with a link to the relevant category sheet.
- Category sheets — one sheet per check category, all findings including informational.
- MITRE — all findings with MITRE technique IDs, for import into threat-modelling tools.
- CIS index — CIS benchmark section cross-reference.
Cells are colour-coded by severity (red for critical through grey for informational). Empty optional fields show an em dash (—) rather than a blank cell.
Developer reference
Running the test suite
pip install -e ".[dev]"
./scripts/verify.sh # Ruff, pytest, ty (if installed), Semgrep (if installed)
Or individually:
ruff check applepy/
pytest
Portable PYTHONPATH layout (no venv, no PyInstaller)
mkdir -p vendor && pip install -t vendor 'openpyxl>=3.1'
PYTHONPATH=vendor:. python -m applepy --output-dir ./out
Note: PyObjC is required on macOS for the full check set and is not available on Linux or Windows.
PyObjC dependency
ApplePY depends on pyobjc-core and pyobjc-framework-Cocoa on macOS for native platform introspection. These packages are macOS-only; pip install on Linux or Windows simply skips them. Run the full scan on a Mac.
Licence
See CREDITS.md for third-party references and licences.