Scope: MacScope is a read-only macOS security posture and vulnerability scanner. It correlates native macOS state, Apple security-release data, compliance checks, installed-software inventory, and package vulnerability matches. It does not exploit vulnerabilities, remove malware, change firewall settings, apply remediation, or prove that a machine is compromised. It writes only project-local tool state and the output directory selected by the user.
- Overview
- Executive Summary
- Architecture
- What MacScope Collects
- Direct Observation vs. Inference
- Requirements
- MacScope.app
- Quick Start
- Install the Pinned Toolchain
- Command Reference
- Filesystem Scope and Exclusions
- Privilege Boundary
- Native macOS Coverage
- SOFA Apple Security Coverage
- osquery Inventory Coverage
- mSCP Compliance Coverage
- Syft SBOM Coverage
- Grype Vulnerability Coverage
- Evidence and Output Layout
- Scan Document Contract
- Offline HTML Report
- Finding and Coverage Taxonomy
- Findings Library
- Network Use, Retries, and Timeouts
- Live Scan Progress
- Machine-Readable Event Stream
- Interactive CLI Banner
- Errors and Failure Behavior
- Privacy and Evidence Handling
- Development and Testing
- Repository Layout
- Current Limitations
- Upstream Projects and Data Sources
- Responsible Use
- Project Status and License
MacScope brings several established macOS and software-security data sources into one typed Go command-line application:
- native Apple command-line security probes;
- the SOFA v2 macOS security feed;
- osquery system inventory;
- a reviewed subset of the macOS Security Compliance Project;
- Syft software bill of materials generation; and
- Grype vulnerability matching.
The result is not a simple pass/fail checklist. Every scan records what was attempted, what completed, what was excluded, which executable and data versions were used, which evidence supports each finding, and where coverage remains partial.
MacScope produces two primary outputs:
scan.json— the strict, machine-readable scan document.report.html— an optional self-contained report generated offline from a validatedscan.json.
The native macOS application adds guided scan setup, enterprise preflight checks, live instrument activity, finding explanations, coverage review, and local scan history without requiring Terminal or development tools.
MacScope currently provides:
- native posture checks for SIP, FileVault, Gatekeeper, Application Firewall, firewall stealth mode, Block All, automatic update checking, PF status, Remote Login, and XProtect versions;
- version-and-build-aware macOS CVE assessment using SOFA;
- XProtect baseline comparison using SOFA data;
- application, startup-item, launchd, and listening-socket inventory using osquery;
- four reviewed CIS Level 1 checks translated from mSCP Tahoe Revision 3;
- system and materialized user-home package discovery using Syft;
- vulnerability matching using a validated, project-local Grype database;
- repeatable user-selected file and directory exclusions;
- automatic exclusion of OneDrive content and dataless iCloud placeholders;
- optional, narrowly scoped
sudocollection without running the scanner as root; - SHA-256-linked evidence artifacts and pinned upstream provenance;
- deterministic, JavaScript-free offline HTML reporting;
- structured JSON warnings and errors;
- an interactive milestone progress bar with current collector, spinner, and elapsed time; and
- a randomized, animated ANSI/FIGlet-style terminal banner with version and build identity.
MacScope distinguishes posture observations from compromise evidence:
| Observation | What MacScope concludes | What MacScope does not conclude |
|---|---|---|
| A security control is disabled | The current configuration weakens the named control. | The Mac is compromised. |
| A package version matches a vulnerability record | The recorded package identity and version require applicability review. | Vulnerable code ran or was exploited. |
| Remote Login is enabled | SSH service access is configured. | The service is reachable from the LAN or internet. |
| PF is disabled | PF is not currently reported active by pfctl. |
Application Firewall is disabled. |
| Application Firewall Block All is disabled | Block All is not enabled. | The firewall is disabled or the host is externally reachable. |
| A scan area is partial | Named content or checks were not fully assessed. | Unscanned areas are safe. |
MacScope.app is the native SwiftUI interface for the same typed Go scanning engine. It is designed for nontechnical users while keeping the evidence and privilege boundaries visible:
- guided Standard scan setup with named, credential-free profiles;
- user-selected file and directory exclusions, automatic OneDrive exclusion, and locally materialized iCloud coverage without intentional cloud hydration;
- preflight verification of the bundled engine and third-party executable hashes, writable local storage, free space, permissions, and vulnerability-database readiness;
- live, filterable command activity with measured versus indeterminate progress clearly distinguished;
- dashboard, Attention, Findings, Instruments, Coverage, History, and Findings Library views;
- local-only storage under
~/Library/Application Support/MacScope; and - no invented security score, telemetry, silent upload, or automatic remediation.
Enhanced Read-Only scanning remains unavailable in the GUI until its signed Service Management helper and fixed-operation XPC protocol pass the privilege-boundary release gates. The application never asks for or stores a macOS password.
Developers can generate and build the Xcode project with:
cd app
xcodegen generate --spec project.yml
xcodebuild -project MacScope.xcodeproj -scheme MacScope -configuration Debug buildflowchart TB
User["User runs MacScope as a standard account"]
CLI["Typed Go CLI and orchestrator"]
subgraph Local["Local read-only collection"]
Native["Apple-native probes"]
OQ["Pinned osquery inventory"]
MSCP["Reviewed mSCP rule subset"]
Syft["Pinned Syft SBOM"]
end
subgraph Optional["Optional narrow privilege boundary"]
Sudo["sudo child collector"]
PF["PF status"]
SSH["Remote Login state"]
end
subgraph Network["Versioned upstream data"]
SOFA["SOFA v2 macOS feed"]
GrypeDB["Validated Grype database"]
end
Grype["Pinned Grype matcher"]
Validate["Schema and artifact validation"]
JSON["scan.json"]
Evidence["evidence/ artifacts"]
HTML["Offline report.html"]
User --> CLI
CLI --> Native
CLI --> OQ
OQ --> MSCP
CLI --> Syft
CLI -. "only with --privileged" .-> Sudo
Sudo --> PF
Sudo --> SSH
CLI --> SOFA
Syft --> Grype
GrypeDB --> Grype
Native --> Validate
OQ --> Validate
MSCP --> Validate
SOFA --> Validate
Grype --> Validate
PF --> Validate
SSH --> Validate
Validate --> JSON
Validate --> Evidence
JSON --> HTML
The privileged process returns structured evidence to the standard-user parent. The parent validates and writes the complete result, so third-party tools and report generation do not run as root.
| Collector | Coverage | Privilege | Network | Preserved evidence |
|---|---|---|---|---|
| Native macOS commands | Host identity, controls, XProtect | Standard user | No | Exact command, exit status, stdout, stderr, timestamps |
| Narrow privileged collector | PF status and Remote Login | Optional sudo child |
No | Same command evidence returned to parent |
| SOFA v2 | macOS releases, CVEs, KEV state, XProtect baselines | Standard user | HTTPS | Exact feed response and SHA-256 |
| osquery | Apps, startup items, launchd definitions, listening sockets | Standard user | No | Query, output, errors, timing, row counts |
| mSCP adapter | Four reviewed CIS Level 1 checks | Standard user | No | Pinned translation manifest and rule provenance |
| Syft | Startup-volume and materialized home-directory package inventory | Standard user | No remote enrichment | SBOM, runtime scope, command metadata |
| Grype | Package vulnerability matches | Standard user | Database update only | Database identity, raw matches, command metadata |
MacScope intentionally avoids collecting the hardware serial number and platform identifiers. The hardware probe uses the minimal system_profiler detail level and only retains the model identifier and chip description required by the scan schema.
MacScope keeps observed state, data-source matching, and interpretation separate.
- output from fixed native macOS commands;
- rows returned by fixed osquery SQL;
- versions and hashes reported by pinned executables;
- package identities and file locations reported by Syft;
- records returned by the exact SOFA feed;
- Grype database metadata and normalized matches; and
- filesystem metadata used to identify dataless iCloud items.
- a disabled security control is translated into a configuration finding;
- a newer exact macOS release/build in SOFA is translated into a version-based CVE exposure inference;
- a lower local XProtect version is translated into an update-posture finding;
- a reviewed mSCP query result is compared with its expected value; and
- a Grype package/version match is translated into a software-vulnerability finding.
- exploitability on the specific hardware and runtime configuration;
- proof of exploitation or malware execution;
- LAN, NAT, VPN, or internet reachability;
- absence of vulnerabilities outside collected coverage;
- full CIS compliance;
- safety of excluded, unreadable, or cloud-only files; or
- correctness of every upstream advisory or package identity.
The current dependency lock targets:
- macOS on Apple Silicon (
darwin/arm64); - Go
1.26.5for development and builds; - osquery
5.21.0; - Syft
1.50.0; - Grype
0.116.1; and - mSCP Tahoe Revision 3 for macOS 26 compliance translation.
Additional requirements:
- Terminal access;
- HTTPS access to download the pinned tools;
- HTTPS access to the SOFA feed and Grype database service during a scan;
- enough free space for the project-local Grype database, SBOM, raw vulnerability report, and evidence; and
- administrator authorization only when
--privilegedis explicitly requested.
The mSCP checks are supported only on macOS 26. Other macOS versions receive an explicit unsupported/partial compliance result rather than a false compliance claim.
From the repository root, after installing the pinned tools:
./.tools/go/bin/go test ./...
./.tools/go/bin/go build -trimpath -o ./bin/macscope ./cmd/macscope
./bin/macscope scan --output ./scan-results/first-scan
./bin/macscope report \
--input ./scan-results/first-scan/scan.json \
--output ./scan-results/first-scan/report.htmlOpen report.html locally in a browser. Treat both the JSON and HTML as potentially sensitive system inventory.
MacScope does not use Homebrew executables or arbitrary programs from PATH. It resolves project-local tools under .tools/ and verifies them against tools.lock.json.
The commands below are intended for a fresh checkout. Review every calculated digest against tools.lock.json before extracting or executing a download.
mkdir -p .tools/downloads
curl --fail --location \
https://go.dev/dl/go1.26.5.darwin-arm64.tar.gz \
--output .tools/downloads/go1.26.5.darwin-arm64.tar.gz
shasum -a 256 .tools/downloads/go1.26.5.darwin-arm64.tar.gz
tar -xzf .tools/downloads/go1.26.5.darwin-arm64.tar.gz -C .tools
./.tools/go/bin/go versionmkdir -p .tools/osquery/5.21.0 .tools/downloads
curl --fail --location \
https://github.com/osquery/osquery/releases/download/5.21.0/osquery-5.21.0_1.macos_arm64.tar.gz \
--output .tools/downloads/osquery-5.21.0_1.macos_arm64.tar.gz
shasum -a 256 .tools/downloads/osquery-5.21.0_1.macos_arm64.tar.gz
COPYFILE_DISABLE=1 tar -xzf .tools/downloads/osquery-5.21.0_1.macos_arm64.tar.gz \
-C .tools/osquery/5.21.0 \
--strip-components=3 \
opt/osquery/lib/osquery.app
ln -s osquery.app/Contents/MacOS/osqueryd \
.tools/osquery/5.21.0/osqueryi
shasum -a 256 .tools/osquery/5.21.0/osqueryi
codesign --verify --deep --strict .tools/osquery/5.21.0/osquery.appmkdir -p \
.tools/syft/1.50.0 \
.tools/grype/0.116.1 \
.tools/grype/db \
.tools/downloads
curl --fail --location \
https://github.com/anchore/syft/releases/download/v1.50.0/syft_1.50.0_darwin_arm64.tar.gz \
--output .tools/downloads/syft_1.50.0_darwin_arm64.tar.gz
curl --fail --location \
https://github.com/anchore/grype/releases/download/v0.116.1/grype_0.116.1_darwin_arm64.tar.gz \
--output .tools/downloads/grype_0.116.1_darwin_arm64.tar.gz
shasum -a 256 \
.tools/downloads/syft_1.50.0_darwin_arm64.tar.gz \
.tools/downloads/grype_0.116.1_darwin_arm64.tar.gz
tar -xzf .tools/downloads/syft_1.50.0_darwin_arm64.tar.gz \
-C .tools/syft/1.50.0 syft
tar -xzf .tools/downloads/grype_0.116.1_darwin_arm64.tar.gz \
-C .tools/grype/0.116.1 grype
shasum -a 256 \
.tools/syft/1.50.0/syft \
.tools/grype/0.116.1/grypeMacScope verifies each third-party executable before collection and rehashes osquery, Syft, and Grype after their work to detect substitution during a scan. Version, commit, platform, archive, executable, mSCP baseline, and source digests are maintained in tools.lock.json and the typed collectors.
Usage:
macscope scan --output <directory> [--privileged] [--events-json] [--exclude <absolute-path>]...
macscope report --input <scan.json> --output <report.html>
macscope version
macscope help
./bin/macscope scan --output ./scan-results/scan-001The output directory may be new or existing, but MacScope refuses to overwrite scan.json or any evidence artifact already present there. Use a unique directory for each run.
./bin/macscope scan \
--output ./scan-results/scan-privileged \
--privilegedThe password prompt belongs to /usr/bin/sudo. MacScope never accepts, reads, logs, transmits, or stores the password.
./bin/macscope scan \
--output ./scan-results/custom-scope \
--exclude "/Users/example/Documents/Private Project" \
--exclude "/Users/example/Downloads/large-archive.tar"Add --exclude once for every selected path. Paths must be absolute and exist when the scan starts. MacScope cleans, sorts, and deduplicates the list. It refuses relative paths, missing paths, and the filesystem root /.
./bin/macscope report \
--input ./scan-results/custom-scope/scan.json \
--output ./scan-results/custom-scope/report.html./bin/macscope version
./bin/macscope helpSyft scans dir:/ on the startup volume as the signed-in user. The goal is broad package discovery without downloading cloud placeholders or spending test time in known large, volatile, duplicate, or private locations.
- readable system software locations;
- installed applications visible on the startup volume;
- user home directories; and
- iCloud files that are already materialized locally.
/dev;/Volumesand duplicate macOS data volumes;/System/Volumes;- temporary, cache, migration, model-asset, and volatile data trees;
/private/var;- all recognized OneDrive roots, provider containers, and legacy home links;
- MacScope's
.tools/,bin/, andscan-results/directories; - the protected
authservertree and itsweakpass_editsymlink; and - iCloud items carrying the macOS
SF_DATALESSfilesystem flag.
MacScope walks iCloud filesystem metadata without opening file contents. A dataless directory is excluded with its subtree, while a dataless file is excluded exactly. This allows already-local iCloud data to remain in scope without requesting downloads of cloud-only files.
- Directory choice: excludes the directory entry and complete subtree.
- File choice: excludes that exact file.
- Glob metacharacters in literal paths are escaped.
- Choices are combined with fixed exclusions.
- The resulting runtime scope is preserved as
evidence/syft/scan-scope.yaml.
These exclusions apply only to Syft filesystem-content and package scanning. Native probes and osquery inventory still run because they do not recursively read the selected file contents. Syft coverage is always marked partial, with a reason that records the number and type of omissions.
Excluded content is never deleted, moved, opened for content scanning, or modified by the exclusion preparation process.
The complete scanner must run as the signed-in user:
./bin/macscope scan --output ./scan-results/example --privilegedDo not run:
sudo ./bin/macscope scan --output ./scan-results/exampleMacScope rejects a root orchestrator. When --privileged is selected, the parent invokes the same executable through /usr/bin/sudo with a private internal collector command. That child:
- requires effective UID
0; - executes only the fixed PF and Remote Login probes;
- returns strictly decoded structured JSON to the parent; and
- does not run SOFA, osquery, Syft, Grype, schema writing, or HTML generation.
If authorization is canceled or fails, the requested privileged scan stops with an explicit error. MacScope does not silently continue with reduced privileged coverage.
Without --privileged, PF and Remote Login receive not_scanned coverage records and informational coverage-gap findings. That makes the overall run status partial.
MacScope executes fixed absolute-path commands. It does not execute user-supplied commands or arbitrary upstream remediation scripts.
| Probe | Command source | Finding behavior |
|---|---|---|
| macOS version/build | /usr/bin/sw_vers |
Host identity only |
| Hardware model/chip | /usr/sbin/system_profiler minimal JSON |
Host identity only; no serial retained |
| System Integrity Protection | /usr/bin/csrutil status |
High if disabled |
| FileVault | /usr/bin/fdesetup status |
High if disabled |
| Gatekeeper | /usr/sbin/spctl --status |
High if disabled |
| Application Firewall | socketfilterfw --getglobalstate |
Medium if disabled |
| Firewall stealth mode | socketfilterfw --getstealthmode |
Observation; mSCP evaluates compliance separately |
| Firewall Block All | socketfilterfw --getblockall |
Observation only |
| Automatic update checking | /usr/sbin/softwareupdate --schedule |
Medium if disabled |
| XProtect configuration | Apple bundle plist | Compared with SOFA baseline |
| XProtect framework | Apple app bundle plist | Compared with SOFA baseline |
| XProtect plugin service | Apple XPC bundle plist | Compared with SOFA baseline |
| PF runtime status | /sbin/pfctl -s info |
Privileged observation only |
| Remote Login | /usr/sbin/systemsetup -getremotelogin |
Low if enabled |
Application Firewall and PF are independent controls. An Application Firewall result does not establish PF state, and a PF result does not establish Application Firewall state. Block All being off is not equivalent to the firewall being off.
Each probe records the exact argument vector, timestamps, exit code, execution error, and exact stdout/stderr bytes encoded in the artifact. A command or parse failure creates failed coverage and a tool_error finding rather than silently dropping the probe.
MacScope requests:
https://sofafeed.macadmins.io/v2/macos_data_feed.json
The adapter requires the SOFA v2 structure and validates required release, CVE, actively exploited, KEV, XProtect, timestamp, URL, array, and digest fields. Unrelated additive fields are ignored so compatible feed extensions do not break the scanner.
- Match the installed macOS major release.
- Require the exact installed product version.
- Require the installed build to appear in the release's published build set.
- Compare with newer security releases for the same major version.
- Preserve CVE identifiers, upstream severity, actively exploited state, KEV state, references, and release metadata.
If the exact version/build cannot be established, MacScope reports a coverage limitation instead of inferring exposure from a nearby release.
MacScope compares local versions for:
- XProtect configuration data;
- XProtect framework; and
- XProtect plugin service.
A lower installed version produces an update-posture finding. It is not evidence that malware is present.
The exact response body is stored in evidence/sofa/ with its SHA-256 and source metadata.
MacScope pins osquery 5.21.0 by archive, executable digest, commit, runtime version, and platform. It runs the project-local osqueryi with persistent storage, events, and extensions disabled.
Fixed queries collect:
- application bundle name, path, identifier, short version, and bundle version;
- Background Task Management startup-item metadata;
- launchd definition path, label, program, load/keepalive state, user, and group; and
- listening socket address, port, protocol, family, PID, process name, and process path.
Results are decoded into query-specific typed rows. Required identities, absolute paths, unique application paths, and numeric socket fields are validated. A wildcard or loopback listener is reported as local socket evidence; it is not automatically described as remotely reachable.
Each query has a 30-second timeout and preserves its SQL command, raw JSON, stderr, timestamps, exit status, and parse result under evidence/osquery/.
The compliance adapter pins mSCP Tahoe Revision 3 by tag, commit, release timestamp, source archive, baseline digest, and individual rule-file digests.
The upstream CIS Level 1 baseline contains 100 rules. MacScope currently implements four reviewed rules as fixed osquery SQL:
| mSCP rule | MacScope severity |
|---|---|
| Ensure System Integrity Protection is Enabled | High |
| Enable Gatekeeper | High |
| Enable macOS Application Firewall | Medium |
| Enable Firewall Stealth Mode | Medium |
The translation manifest is preserved as:
evidence/mscp/tahoe_rev3_cis_lvl1_osquery_subset.json
MacScope always marks mSCP coverage partial because four rules are not the complete baseline. It does not execute upstream remediation, generated sudo compliance scripts, mobile configuration deployment, or arbitrary shell embedded in rule YAML.
MacScope pins Syft 1.50.0 and verifies:
- executable SHA-256;
- reported application name;
- reported version;
- Git commit; and
darwin/arm64platform identity.
Syft scans the startup volume using a runtime configuration built from the fixed config/syft.yaml plus fixed, dataless-iCloud, and user-selected exclusions.
The fixed configuration disables:
- Syft application-update checks;
- remote package enrichment;
- remote license searches;
- Maven local repository use; and
- unnecessary license-content collection.
The complete Syft JSON SBOM is preserved even when it is large. Scan command metadata includes the source name, macOS version, runtime scope path, timestamps, exit status, stderr, output byte count, and SHA-256.
MacScope pins Grype 0.116.1 using the same executable identity checks as Syft.
The matching sequence is:
flowchart LR
Update["Update project-local database"]
Status["Read and validate database status"]
Hash["Hash database metadata and tool"]
SBOM["Read preserved Syft SBOM"]
Match["Run Grype with automatic updates disabled"]
Normalize["Normalize and deduplicate matches"]
Finding["Produce evidence-linked findings"]
Update --> Status --> Hash
SBOM --> Match
Hash --> Match --> Normalize --> Finding
The config/grype.yaml file disables Grype application-update checks, external sources, and automatic database updates during matching. MacScope performs the separate database update explicitly, validates status and age, then matches against that known project-local state.
MacScope deterministically merges duplicate normalized vulnerability/package pairs returned from multiple database namespaces only when package identity is the same. It retains references, fixed versions, CVSS, EPSS, known-exploited state, and match methods.
- Exact ecosystem matches receive high confidence.
- CPE-derived matches receive medium confidence and require manual identity/applicability review.
A Grype finding is a package-version match, not proof that the vulnerable code path exists, ran, or was exploited.
A complete run resembles:
scan-results/example/
├── scan.json
├── report.html # only after the report command
└── evidence/
├── grype/
│ ├── db-status.json
│ ├── db-update-attempt-*.json
│ ├── grype-version.json
│ ├── vulnerability-match.execution.json
│ └── vulnerability-report.json
├── mscp/
│ └── tahoe_rev3_cis_lvl1_osquery_subset.json
├── native/
│ ├── application-firewall.json
│ ├── automatic-update-check.json
│ ├── filevault.json
│ ├── firewall-block-all.json
│ ├── firewall-stealth.json
│ ├── gatekeeper.json
│ ├── hardware-model.json
│ ├── macos-version.json
│ ├── sip.json
│ ├── xprotect-config-version.json
│ ├── xprotect-framework-version.json
│ └── xprotect-plugin-version.json
├── osquery/
│ ├── apps.json
│ ├── launchd.json
│ ├── listening-ports.json
│ ├── mscp-*.json
│ ├── startup-items.json
│ └── version.json
├── sofa/
│ └── macos_data_feed_v2.json
└── syft/
├── filesystem-sbom.execution.json
├── filesystem.sbom.syft.json
├── scan-scope.yaml
└── syft-version.json
Privileged runs additionally preserve evidence/native/pf-status.json and evidence/native/remote-login.json.
Every referenced artifact uses a path relative to the scan output directory, a media type, and a lowercase SHA-256. Before writing, MacScope verifies that:
- every artifact is referenced by exactly one evidence record;
- every referenced artifact has supplied content;
- there are no duplicate artifact paths;
- calculated and recorded digests match; and
- paths cannot escape the selected output directory.
Files are created without overwrite permission. Evidence is synced and closed before the scan document is finalized.
scan.json uses schema version 1 and contains:
- scanner identity and version;
- UUID-style run ID;
- hostname, OS, architecture, macOS version/build, model, and chip;
- UTC start and completion timestamps;
- overall run status;
- privilege request and effective-UID evidence;
- executable, feed, matcher, and database provenance;
- per-area coverage records;
- evidence records and artifact references; and
- findings, vulnerabilities, affected components, remediation, and references.
Validation rejects:
- unknown JSON fields or trailing JSON values;
- null top-level arrays;
- unknown tool or evidence references;
- duplicate IDs or artifact paths;
- invalid enum values;
- unsafe absolute or traversal artifact paths;
- malformed SHA-256, CVE, GHSA, or OSV identities;
- non-HTTPS upstream references;
- out-of-range CVSS or EPSS values;
- inconsistent known-exploited state;
- inconsistent privilege evidence; and
- collector or finding timestamps outside the scan interval.
Top-level tools, coverage, evidence, and findings fields are always JSON arrays, including when empty.
An overall run is completed only when every coverage record is complete. Any partial, not_scanned, or failed coverage record makes the overall status partial. Individual tool failures remain visible in coverage and findings.
The report command:
- reads at most 512 MiB from a regular
scan.jsonfile; - strictly decodes and revalidates the document;
- calculates and displays the source JSON SHA-256;
- refuses to overwrite an existing output file;
- embeds all styling locally;
- includes no JavaScript, analytics, remote fonts, or remote assets; and
- HTML-escapes host fields, package paths, descriptions, and remediation text.
Coverage failures and limitations appear before finding details. Findings are sorted deterministically by:
- severity;
- known-exploited status;
- confidence;
- maximum CVSS;
- maximum EPSS;
- title; and
- stable finding ID.
| Category | Intended meaning |
|---|---|
os_vulnerability |
macOS release/build vulnerability inference |
software_vulnerability |
Installed package/version vulnerability match |
configuration |
Security-control or compliance state |
network_exposure |
Local service or network-related state |
persistence |
Persistence-related observation or assessment |
threat_indicator |
Threat-oriented observation when implemented |
coverage_gap |
Explicitly unassessed or partial scope |
tool_error |
Collector, parser, verification, or data failure |
Severities are info, low, medium, high, and critical. Confidence levels are low, medium, high, and confirmed.
host_identityapple_updatessecurity_controlspersistencenetwork_exposureinstalled_softwaredependency_vulnerabilitiesthreat_indicators
Coverage states are complete, partial, not_scanned, and failed. Every non-complete record must include an explicit reason.
MacScope keeps one validated, structured rule-family catalog at internal/catalog/findings-catalog.json. The scanner rejects emitted finding sources that cannot be resolved to that catalog, and the native application reads the bundled copy for its in-app Findings Library.
The same catalog generates two master handbooks:
Regenerate both deterministically after catalog changes:
.tools/go/bin/go run ./cmd/catalogdoc \
--markdown docs/findings-library.md \
--html docs/findings-library.htmlThe handbooks describe supported rule families; they are not scan reports and do not claim that any listed condition was detected on a particular Mac.
| Operation | Limit | Retry behavior |
|---|---|---|
| SOFA HTTPS request | 20 seconds per request; 8 MiB response limit | Up to 3 attempts with 250 ms and 500 ms delays |
| osquery query | 30 seconds per query | No hidden fallback |
| Syft startup-volume scan | 30 minutes | No hidden fallback |
| Grype database update | 10 minutes per attempt | Up to 3 attempts with 1 s and 2 s delays |
| Grype database status | 10 minutes | No hidden fallback |
| Grype SBOM matching | 10 minutes | No hidden fallback |
| HTML report input | 512 MiB maximum | Offline; no network |
Retry warnings are emitted as structured JSON. After the final attempt, MacScope reports the last error rather than substituting stale or unverified data.
When stderr is an interactive terminal, every scan displays a live red progress line:
[█████████████░░░░░░░░░░░] 55% / Scanning readable system and home-directory packages with Syft; large scopes can take time elapsed 03:42
The display identifies each major phase:
- scan and exclusion validation;
- native macOS identity, security controls, and XProtect collection;
- optional PF and Remote Login collection through
sudo; - SOFA CVE, KEV, release, and XProtect retrieval;
- osquery application, persistence, listener, and mSCP inventory;
- Syft executable verification, scope preparation, and package scanning;
- Grype executable verification, database update/validation, and matching; and
- schema assembly, artifact hashing, and output writing.
The percentage represents completed orchestration milestones, not a claim that MacScope knows the exact number of files Syft will inspect or packages Grype will match. During a long phase, the spinner and elapsed timer continue updating while the percentage remains at the current honest milestone.
The progress line is written to stderr, pauses cleanly for the macOS sudo prompt, and temporarily clears itself before structured retry warnings. Piped or redirected stderr suppresses progress automatically so automation retains stable output.
Set NO_COLOR=1 or TERM=dumb for plain milestone lines. Disable progress completely with:
MACSCOPE_NO_PROGRESS=1 ./bin/macscope scan --output ./scan-results/example--events-json reserves stdout for a versioned newline-delimited JSON event stream intended for the native MacScope GUI and other integrations:
./bin/macscope scan \
--output ./scan-results/gui-run \
--events-jsonThe animated banner and terminal progress display are suppressed in this mode. Each stdout line is an independent JSON envelope with schema_version, monotonically increasing sequence, UTC timestamp, type, and one typed payload. Protocol events cover scan lifecycle, collector-aware progress, command lifecycle, exact output chunks, successful completion, failure, and cancellation:
{"schema_version":"1","sequence":2,"timestamp":"2026-08-24T20:14:00Z","type":"progress","progress":{"collector_id":"syft","percent":55,"message":"Scanning readable system and home-directory packages with Syft; large scopes can take time"}}Commands emit command_started, zero or more command_output records, and command_completed. Start records contain the executable, argument array, relevant environment overrides, and only the byte count and SHA-256 of standard input—not the input content. Output data is chunked at no more than 24 KiB and JSON-encoded in data_base64, so clients can reconstruct exact stdout and stderr bytes without assuming UTF-8. may_contain_private_data warns clients that paths, usernames, process arguments, or other host details may be present.
The event stream is also sensitive local evidence. The native app preserves it as events.ndjson beside the scan output and retains only a bounded recent window in memory for the live display. Human-readable errors and retry warnings remain on stderr. A successful stream ends with scan_completed containing the validated scan.json path. A failed or canceled stream ends with scan_failed.
When stdout is an interactive terminal, MacScope:
- randomly selects one of three built-in ASCII/FIGlet-style designs;
- animates through bright red, yellow, green, cyan, blue, magenta, and red;
- shows MacScope version, VCS revision, modified-build state, platform, and Go version; and
- transitions into the selected CLI command.
No figlet, lolcat, Homebrew package, or network request is required.
Piped and redirected output suppresses the banner automatically. Available controls:
NO_COLOR=1 ./bin/macscope help
TERM=dumb ./bin/macscope help
MACSCOPE_NO_BANNER=1 ./bin/macscope helpNO_COLOR and TERM=dumb retain a plain banner in an interactive terminal. MACSCOPE_NO_BANNER=1 disables it completely.
CLI errors are written to stderr as one-line JSON:
{"level":"error","type":"scan","message":"validate user-selected filesystem exclusions: ..."}Usage errors also print the command synopsis and return exit code 2. Runtime, scan, report, privilege, and banner failures return exit code 1.
MacScope does not silently:
- downgrade a requested privileged scan;
- use an unpinned executable from
PATH; - accept a mismatched executable or database digest;
- overwrite existing evidence;
- ignore malformed upstream data;
- turn invalid rows into valid inventory;
- omit a failed collector from coverage; or
- claim complete coverage when anything is partial, skipped, or failed.
Scan output can contain sensitive information, including:
- hostname and Mac model;
- installed application and package names, versions, and paths;
- home-directory paths included in package evidence;
- launchd and startup-item definitions;
- local listener addresses, ports, PIDs, and process paths;
- security-control state;
- user-selected exclusion paths; and
- raw upstream vulnerability matches.
Operational guidance:
- keep
scan-results/private; - review
scan.json,report.html, andevidence/before sharing; - do not publish home paths or application inventories unintentionally;
- use a new output directory for every scan;
- preserve the complete evidence directory when retaining a report;
- hash archives before transferring them; and
- do not treat unfamiliar packages or listeners as malicious without corroboration.
.gitignore excludes .tools/, bin/, and scan-results/. The repository is designed to publish source, fixed configuration, tests, CI, and lock metadata without publishing local databases, downloaded executables, builds, or private host reports.
Use the pinned project-local Go toolchain:
./.tools/go/bin/gofmt -w ./cmd ./internal
./.tools/go/bin/go vet ./...
./.tools/go/bin/go test ./...
./.tools/go/bin/go test -race ./...
./.tools/go/bin/go test -shuffle=on -count=3 ./...
./.tools/go/bin/go build -trimpath -o ./bin/macscope ./cmd/macscopeThe current test suite covers CLI parsing, banner behavior, strict model validation, artifact safety, native parsers, privilege decoding, SOFA retries and schema validation, osquery parsing, mSCP provenance, Syft scope generation, Grype match normalization, scan writing, and offline report generation.
ci.yml runs on a macOS 26 hosted runner for pushes and pull requests. It uses:
- read-only repository permissions;
- immutable GitHub Action commit revisions;
- disabled checkout credential persistence;
- formatting and
go vetchecks; - race-enabled and shuffled tests;
- a reproducible CLI build; and
- non-interactive help/version smoke tests.
CI does not install osquery, Syft, or Grype; update vulnerability databases; request sudo; scan the runner; upload artifacts; or publish reports.
MacScope/
├── .github/workflows/ci.yml # macOS build and test workflow
├── .gitignore # excludes tools, builds, and private reports
├── README.md
├── assets/
│ ├── macscope-dashboard-icon.png # simplified 512 px dashboard/GitHub icon
│ └── macscope-logo.png # primary square MacScope logo
├── cmd/macscope/ # executable entry point
├── config/
│ ├── grype.yaml # offline matching policy
│ └── syft.yaml # no-enrichment SBOM policy
├── go.mod # Go language and toolchain version
├── internal/
│ ├── artifact/ # in-memory artifact records
│ ├── banner/ # terminal startup presentation
│ ├── cli/ # strict command parsing
│ ├── model/ # schema types, codec, and validation
│ ├── mscp/ # reviewed compliance translation
│ ├── native/ # fixed Apple-native probes
│ ├── osquery/ # pinned inventory collector
│ ├── privilege/ # narrow sudo boundary
│ ├── progress/ # live terminal phase and progress display
│ ├── report/ # deterministic offline HTML
│ ├── scan/ # orchestration and safe output writing
│ ├── sofa/ # SOFA client and assessment adapter
│ ├── supplychain/ # Syft, Grype, scope, and findings
│ └── version/ # version/build identity
└── tools.lock.json # pinned versions, commits, URLs, and digests
Generated local directories:
.tools/ # pinned Go and scanner dependencies, runtime scope, Grype DB
bin/ # MacScope executable
scan-results/ # private scan documents, evidence, and HTML reports
- Current pinned third-party binaries target Apple Silicon only.
- mSCP translation covers 4 of 100 CIS Level 1 rules and macOS 26 only.
- Syft coverage is intentionally partial because of fixed and user-selected exclusions.
- Dataless iCloud content is not downloaded or scanned.
- OneDrive content is not scanned.
- Protected or unreadable paths are not bypassed by running Syft as root.
- External volumes and duplicate system/data volumes are excluded.
- Network listeners are local observations; MacScope does not test reachability from another device.
- Package vulnerability matches can contain upstream identity or applicability errors.
- SOFA macOS findings are version/build inferences, not runtime exploit tests.
- There is no malware removal, remediation execution, continuous monitoring, GUI, daemon, MDM deployment, or remote fleet controller.
- The scanner currently updates the Grype database for each complete scan rather than providing a separate offline database-management command.
- A large system/home SBOM and raw vulnerability report may consume substantial time and disk space.
MacScope integrates but does not replace or claim ownership of these projects:
| Project | Role in MacScope | Pinned release |
|---|---|---|
| SOFA | macOS security releases, CVEs, KEV, XProtect baselines | Feed schema v2 |
| osquery | Typed local system inventory | 5.21.0 |
| mSCP | Compliance rule provenance | Tahoe Revision 3 |
| Syft | Software bill of materials | 1.50.0 |
| Grype | Vulnerability database and matching | 0.116.1 |
| Go | Build toolchain | 1.26.5 |
Exact commits, archive URLs, archive SHA-256 values, executable SHA-256 values, platforms, and mSCP baseline identity are authoritative in tools.lock.json.
Apple, macOS, FileVault, Gatekeeper, XProtect, and related marks are trademarks of Apple Inc. MacScope is an independent research and defensive-security project and is not affiliated with or endorsed by Apple, the upstream projects, or their maintainers.
Run MacScope only on Macs you own or are explicitly authorized to assess. Review organizational policy before collecting application, persistence, listener, or home-directory package inventory.
Findings should be verified before remediation or escalation. Security controls can be intentionally disabled for documented research, recovery, development, or compatibility workflows. Package matches and unusual processes require supporting evidence before they are described as vulnerable, malicious, or compromised.
MacScope is currently 0.1.0-dev and under active development. The command schema is versioned, but CLI flags, collector coverage, evidence formats, and dependency pins may change before a stable release.
MacScope is licensed under the Apache License 2.0. The license permits commercial use, distribution, modification, patent use, and private use, subject to its license-notice and change-notice conditions. It includes limitations of liability and warranty and does not grant trademark rights.
Copyright 2026 hideouts-io.
Third-party tools and data sources retain their own licenses and terms. MacScope's license does not relicense osquery, mSCP, Syft, Grype, SOFA, Go, Apple software, or downloaded vulnerability data.
No source commit, remote repository creation, or GitHub upload is performed automatically by MacScope.
