diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5cba33a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,130 @@ +# Contributing to privacycommand + +Issues and pull requests are welcome. This file holds the setup detail that used +to sit in the top-level [`README.md`](README.md). + +## Two build paths + +The repository builds the same sources two ways. `Package.swift` and the Xcode +project both live under `privacycommand/`, not at the repository root. + +**Swift Package Manager** builds the headless pieces — the `privacycommandCore` +analyser, the `auditctl` CLI and its `auditctlKit` half, the guest agent, and the +shared guest protocol. This is the fast iteration loop and it is what CI runs: + +```sh +cd privacycommand +swift build +swift test +``` + +From the repository root, [`just`](https://github.com/casey/just) wraps the same +commands: `just build`, `just test`, `just clean`. + +**Xcode** is the only path that builds and ships the GUI app, because the SwiftUI +app target and the privileged-helper target exist only in the project file: + +```sh +cd privacycommand +open privacycommand.xcodeproj +``` + +On a fresh checkout you need to do two things once: + +1. **File → Add Package Dependencies…** → `https://github.com/sparkle-project/Sparkle`, + Up to Next Major from `2.9.1` (the version the project file pins). Tick the + `Sparkle` product on the `privacycommand` target. +2. Select the **privacycommand** and **privacycommandHelper** targets in turn → + Signing & Capabilities → set Team. Both must match, or the helper will refuse + the app's XPC connection at runtime. + +Then ⌘B. Everything else — the Copy Files phases that place the helper and its +LaunchDaemon plist inside the bundle, the entitlements, the target dependency — +is already wired in the project. + +Deeper references live next to the sources: + +- [`privacycommand/README.md`](privacycommand/README.md) — source-tree map, why + each target exists, signing and entitlements quick reference, troubleshooting. +- [`privacycommand/HELPER.md`](privacycommand/HELPER.md) — privileged helper + bundling, signing and the verification recipe. +- [`privacycommand/docs/GUEST_AGENT.md`](privacycommand/docs/GUEST_AGENT.md) — + building and deploying the in-VM guest agent. +- [`.github/ARCHITECTURE.md`](.github/ARCHITECTURE.md) — how the pieces fit + together and how data moves between them. + +## The auditctl CLI + +`auditctl` is a command-line front end over the same analyser, useful for +scripting and CI. Build it once, then call the binary directly: + +```sh +cd privacycommand +swift build -c release +BIN=.build/release/auditctl +``` + +A one-shot static audit takes a path or an installed-app name: + +```sh +$BIN /System/Applications/Calculator.app +$BIN slack --short # one-line verdict +$BIN slack --tree # frameworks / XPC / helpers / login-item tree +$BIN slack --json # machine-readable +$BIN slack --warnings # findings section only +$BIN slack --exact # exact name match instead of substring +$BIN slack --warn-exit # exit 1 when there are warn/error findings +``` + +Exit codes: `0` analysed cleanly · `1` analysis failed, or `--warn-exit` with +findings · `2` bad arguments or target not found · `4` ambiguous name. + +The `preview` command inspects app updates before you take them. With no +arguments it checks your outdated Homebrew casks: + +```sh +$BIN preview +$BIN preview --all-apps --only-noteworthy --min-tier warn +$BIN preview --json +$BIN preview --fetch firefox # download the incoming build and diff it +``` + +`preview` is inform-only: it never runs `brew`, never blocks an update, and +always exits 0. It understands `.dmg` and `.zip` cask artifacts; `.pkg` is +skipped. With `--fetch`, an incoming build is analysed *before* Gatekeeper has +cleared it, so a one-off notarization difference can simply be a fresh-download +artifact — the output flags this when it happens. + +Running `auditctl` with no arguments opens an interactive browser when stdin and +stdout are a terminal, and prints usage otherwise so CI callers do not hang. + +## What CI runs + +Two workflows run on every pull request against `main`: + +- [`ci.yml`](.github/workflows/ci.yml) — builds every SPM target, runs the full + test suite, then runs the built `auditctl` against `Calculator.app` as an + end-to-end smoke test of the analyser's exit-code contract. +- [`app-ci.yml`](.github/workflows/app-ci.yml) — an unsigned Xcode build and test + of the app target, through a reusable workflow in `privacykey/gh-workflows`. + This one also runs on pushes to `main`. + +Run `swift test` from `privacycommand/` before opening a pull request and confirm +it passes. + +## Before you open a pull request + +- For UI changes, attach a before/after screenshot. +- For a new analysis signal, add a Knowledge Base entry alongside the detector. + privacycommand explains what every finding means in plain English, and that + contract is worth keeping. +- Releases are cut by tagging: `just release `, where the tag must match + `MARKETING_VERSION` in the Xcode project. The pipeline itself lives in + `privacykey/gh-workflows`; [`.github/workflows/release.yml`](.github/workflows/release.yml) + is a thin caller and documents the secrets it needs. + +## Security issues + +Do not open a public issue for a vulnerability. [`.github/SECURITY.md`](.github/SECURITY.md) +has the reporting address, the response times I aim for, and what is in and out +of scope. diff --git a/README.md b/README.md index d1ff009..394309f 100644 --- a/README.md +++ b/README.md @@ -6,154 +6,85 @@ **drop an app. see everything it touches.** -Forensic permission audits for any macOS app — built for security teams, IT, and privacy-conscious users. +Forensic permission audits for any macOS app. -[![Latest release](https://img.shields.io/github/v/release/privacykey/privacycommand?style=flat-square&color=4338CA&label=release)](https://github.com/privacykey/privacycommand/releases/latest) -[![Downloads](https://img.shields.io/github/downloads/privacykey/privacycommand/total?style=flat-square&color=A5B4FC&label=downloads)](https://github.com/privacykey/privacycommand/releases) -[![Homebrew](https://img.shields.io/badge/homebrew-privacykey%2Ftap-FBBF24?style=flat-square&logo=homebrew&logoColor=white)](https://github.com/privacykey/homebrew-tap) -[![macOS 13+](https://img.shields.io/badge/macOS-13%2B-1E1B4B?style=flat-square&logo=apple&logoColor=white)](https://www.apple.com/macos) -[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](LICENSE) - -[Download](https://github.com/privacykey/privacycommand/releases/latest) · [Release notes](https://github.com/privacykey/privacycommand/releases) · [Report an issue](https://github.com/privacykey/privacycommand/issues) +[![Project status](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fprivacykey%2F.github%2Fmain%2Fbadges%2Fprivacycommand.json)](https://github.com/privacykey/.github/blob/main/STATUS.md#privacycommand) +[![Release](https://img.shields.io/github/v/release/privacykey/privacycommand?label=release)](https://github.com/privacykey/privacycommand/releases/latest) +[![Licence](https://img.shields.io/github/license/privacykey/privacycommand?label=licence)](LICENSE) +[![CI](https://img.shields.io/github/actions/workflow/status/privacykey/privacycommand/app-ci.yml?branch=main&label=ci)](https://github.com/privacykey/privacycommand/actions/workflows/app-ci.yml) ---- - + > [!WARNING] -> **This is a beta — pre v1.0.** privacycommand is under active development. -> Expect occasional crashes, behaviour changes between releases, and -> a report format that may still shift. If you see any bugs, please -> attach the matching `.ips` from `~/Library/Logs/DiagnosticReports/` -> to a [new issue](https://github.com/privacykey/privacycommand/issues/new) -> or report a [security vulnerability](https://github.com/privacykey/privacycommand/security/advisories/new) through github or the security email. - -## What it does - -Drop a `.app` bundle (or a `.dmg`) onto privacycommand and it produces a full forensic report of what the app actually touches: - -- **Static analysis** — entitlements, code-signing (the 10-character signing Team ID expanded to the developer's name), notarization deep-dive (stapler / spctl / SHA-256), URL schemes, document types, hard-coded domains and URLs, embedded launch agents and helpers, third-party SDK fingerprints (LaunchDarkly, Firebase, Mixpanel, AdMob, …), feature flags and trial-state strings, secrets and license-key names, anti-analysis signals, dylib hijacking surface, and Apple's Privacy Manifest cross-checked against what the binary actually uses. -- **Forensic binary summary** — a plain-English read of what the main binary links and calls, including an **outbound network call-sites** map: which functions can open a connection, the networking symbols they reach for (BSD sockets, `getaddrinfo`, CFNetwork, Network.framework `nw_*`), and any host or URL literals sitting next to them. Any call site can be **decompiled on demand** (if you have Ghidra installed), and for relaunchable, non-hardened-runtime apps you can capture a **live call stack** that ties an actual outbound connection back to the function that opened it. -- **App Store privacy labels** — when the bundle was installed from the Mac App Store, privacycommand fetches the developer's declared Privacy Nutrition Labels from `apps.apple.com` and shows them next to its static-analysis findings, so you can see whether the developer's claims line up with what the binary contains. -- **Telemetry callout** — a Dashboard card flags how many analytics, advertising, and attribution SDKs the bundle ships, with a heat-graded count and per-category breakdown. -- **Background Task Management** — every login item, launch agent, daemon, and helper the app has registered, fetched via the privileged helper so there's no admin prompt. -- **Feature flags & trials** — the names of `isPro`, `isTrial`, `subscription_status`, `experiment_id`, and platform-specific switches (LaunchDarkly, Optimizely, Firebase Remote Config, PostHog, Statsig, Unleash) that the binary checks at runtime. -- **Monitored runs** — launch the inspected app under privacycommand and watch its file events (via an optional privileged helper running `fs_usage`), network destinations (reverse-DNS-labelled, with click-through IP lookups, row highlighting, and a highlighted-only filter), child processes, pasteboard / camera / microphone / screen-recording activity, USB device interactions, and resource usage in real time. -- **Network kill switch** — block the inspected app's outbound traffic system-wide via a `pf` anchor installed by the helper. Watch how the app handles being cut off. -- **VM mode** — a guest agent that runs inside a macOS VM (VirtualBuddy / UTM / Parallels) and ships observations back to the host across the VM boundary, for analysing apps you'd rather not run on your bare-metal machine. -- **Compare runs** — diff any two saved reports side by side from the History tab. Added and removed entitlements, domains, SDKs, login items, and findings are colour-cued, with a "show only changes" toggle — so you can see exactly what an app update introduced. -- **Batch scan** — point privacycommand at a folder (or all of `/Applications`) and triage many apps at once in a sortable, filterable table: risk tier, warning/error counts, and headline signals per app. The same analyzer runs on each; one click opens any app in the main window for the full deep-dive. -- **Reports** — every finding exports as JSON, HTML, or PDF. - -## Install +> **Pre-1.0 — no stable release yet.** Anything can change in any release, including a patch: APIs, CLI flags, config keys, file formats, and data already on disk. Keep your own backups. +> **Project status.** The badge above is generated from [the privacykey status list](https://github.com/privacykey/.github/blob/main/STATUS.md), which says what I promise for this project and every other one. + -### Homebrew (recommended) - -```sh -brew install privacykey/tap/privacycommand -``` +--- -That's it. The tap lives at [`privacykey/homebrew-tap`](https://github.com/privacykey/homebrew-tap); `brew upgrade --cask privacycommand` keeps it current. When privacycommand detects it's running from a Homebrew Caskroom, it disables in-app updates so `brew` stays in charge of the on-disk version. +privacycommand takes a `.app` bundle (or a `.dmg`) and reports what the app actually touches: the entitlements it claims, the permissions it will ask for, the domains and URLs compiled into its binary, the third-party SDKs it ships, the login items and helpers it registers — and, if you let it, what it does while it runs. -### Direct download +It is built for people who want evidence rather than a vendor's word: security teams, IT, and anyone deciding whether a download deserves a place in `/Applications`. Every finding carries a plain-English explanation from an in-app knowledge base, so a report is readable without a reverse-engineering background. -Grab the signed + notarized `.dmg` from the [latest release](https://github.com/privacykey/privacycommand/releases/latest) and drag the app to `/Applications`. In-app updates are handled by [Sparkle 2](https://sparkle-project.org); they're **off by default** — you opt in via Settings → Updates. +All analysis happens on your machine, and the app ships no analytics of its own. The exact list of network calls it makes is in [`docs/PRIVACY.md`](docs/PRIVACY.md). -### Build from source +privacycommand Dashboard -```sh -git clone https://github.com/privacykey/privacycommand.git -cd privacycommand/privacycommand -open privacycommand.xcodeproj -``` +## What it does -Then in Xcode: -1. **File → Add Package Dependencies…** → `https://github.com/sparkle-project/Sparkle` (Up to Next Major from `2.9.0`). Tick the `Sparkle` product on the `privacycommand` target. -2. Select the **privacycommandHelper** target → Signing & Capabilities → set Team to match the app. -3. ⌘B. +- **Static analysis** — entitlements, code signing (the 10-character Team ID expanded to the developer's name), a notarization deep-dive (stapler / spctl / SHA-256), URL schemes, document types, hard-coded domains, embedded launch agents and helpers, feature-flag and trial-state strings, secrets and licence-key names, anti-analysis signals, dylib hijacking surface, and Apple's Privacy Manifest checked against what the binary actually uses. +- **SDK fingerprints** — which analytics, advertising and attribution SDKs the bundle ships, with a heat-graded count and per-category breakdown. +- **Outbound call sites** — which functions can open a connection, the networking symbols they reach for (BSD sockets, `getaddrinfo`, CFNetwork, `nw_*`), and any host or URL literals sitting next to them. Any call site can be decompiled on demand if you have Ghidra installed. +- **App Store privacy labels** — for Mac App Store bundles, the developer's declared Privacy Nutrition Labels sit next to the static findings, so you can see whether the claims match the binary. +- **Background Task Management** — every login item, launch agent, daemon and helper the app has registered, read through the privileged helper. +- **Monitored runs** — launch the inspected app under privacycommand and watch, in real time, its file events (via the optional helper running `fs_usage`), network destinations with reverse-DNS labels, child processes, pasteboard / camera / microphone / screen-recording activity, USB device interactions and resource usage. +- **Network kill switch** — cut the app off from the destinations it is contacting, via a `pf` anchor installed by the helper, and watch how it copes. +- **VM mode** — a guest agent that runs inside a macOS VM (VirtualBuddy / UTM / Parallels) and ships observations back to the host, for apps you would rather not run on bare metal. +- **Compare runs** — diff any two saved reports from the History tab. Added and removed entitlements, domains, SDKs, login items and findings are colour-cued, with a "show only changes" toggle. +- **Batch scan** — point it at a folder, or at all of `/Applications`, and triage many apps at once in a sortable table of risk tiers, warning counts and headline signals. +- **Reports** — every finding exports as JSON, HTML or PDF. -The Swift Package Manager target builds with `swift build` from `privacycommand/`; tests run with `swift test`. +## Get it -## Command line +Requires macOS 13 or later. -The repo also builds `auditctl`, a small CLI over the same analyzer — handy for scripting and CI. Build it once with `swift build -c release` from `privacycommand/`, then: +**Homebrew** — the cask lives in [`privacykey/homebrew-tap`](https://github.com/privacykey/homebrew-tap): ```sh -# Full static report for a single app (pretty-printed; non-zero exit on parse failure) -swift run -c release auditctl /Applications/SomeApp.app - -# Privacy-preview apps *before* you update them. With no arguments, preview -# checks your outdated Homebrew casks; --all-apps scans everything installed. -swift run -c release auditctl preview -swift run -c release auditctl preview --all-apps --only-noteworthy --min-tier warn -swift run -c release auditctl preview --json - -# --fetch downloads each incoming cask build and diffs it against the version -# you have installed, so you can see what an update *adds* before you take it. -swift run -c release auditctl preview --fetch firefox +brew install --cask privacykey/tap/privacycommand ``` -`preview` is **inform-only**: it never runs `brew`, never blocks an update, and always exits 0. It reuses the same static analyzer and risk scoring as the app, and understands `.dmg` and `.zip` cask artifacts (`.pkg` is skipped). Heads-up: with `--fetch`, an incoming build is analyzed *before* Gatekeeper has cleared it, so a one-off "notarization" difference can just be a fresh-download artifact (the output flags this). +`brew upgrade --cask privacycommand` keeps it current. When privacycommand detects it is running from a Homebrew Caskroom it disables in-app updates, so `brew` stays in charge of the on-disk version. -## Screenshots +**Direct download** — take the signed and notarized `.dmg` from the [latest release](https://github.com/privacykey/privacycommand/releases/latest) and drag the app to `/Applications`. In-app updates use [Sparkle 2](https://sparkle-project.org) against an EdDSA-signed [appcast feed](https://privacykey.github.io/privacycommand/appcast.xml); automatic checks are **off by default** and you opt in under Settings → Updates. -> _Screenshots go here once the brand site is up. The Dashboard renders telemetry, App Store privacy labels, live probes, and a forensic findings list; the Static tab walks every signal we extract from the binary._ +**Command line** — the repo also builds `auditctl`, a CLI over the same analyser. Its `preview` command inspects Homebrew casks *before* you update them; it never runs `brew`, never blocks an update, and always exits 0. Building and using it is covered in [CONTRIBUTING.md](CONTRIBUTING.md#the-auditctl-cli). -CleanShot 2026-04-29 at 19 09 22 +## Docs +There is no docs site yet. What exists lives in the repo: -## How it works - -privacycommand is a SwiftUI app backed by a pure-Swift analyzer library and an optional privileged helper. The architecture is split deliberately: - -| Layer | Path | Notes | -|---|---|---| -| Analyzer logic | [`privacycommand/Sources/privacycommandCore/`](privacycommand/Sources/privacycommandCore/) | Headless, AppKit-free; runs from CLI, tests, GUI | -| App UI | [`privacycommand/Sources/privacycommand/`](privacycommand/Sources/privacycommand/) | SwiftUI views + view-models | -| Privileged helper | [`privacycommand/Sources/privacycommandHelper/`](privacycommand/Sources/privacycommandHelper/) | XPC service installed via `SMAppService.daemon` | -| Guest agent (VM) | [`privacycommand/Sources/privacycommandGuestAgent/`](privacycommand/Sources/privacycommandGuestAgent/) | Runs in-VM, ships observations to the host | - -Read [`architecture.md`](ARCHITECTURE.md) for the longer version. - -## Updates - -Updates ship through two channels that share the same DMG: - -1. **Direct downloads** receive in-app updates via Sparkle 2. Auto-checks are off by default — opt in via Settings → Updates. -2. **Homebrew** users update via `brew upgrade --cask privacycommand`. privacycommand detects Cask installs and disables Sparkle's installer to stay out of `brew`'s way. - -The appcast feed is hosted on `gh-pages` at `https://privacykey.github.io/privacycommand/appcast.xml` and signed with EdDSA — see [`docs/RELEASES.md`](docs/RELEASES.md) for the release flow. - -## Privacy & telemetry posture - -privacycommand is a privacy tool and behaves like one: - -- **No analytics.** privacycommand does not ship any analytics SDKs. There is no telemetry endpoint, no install counter, no crash-report bucket. -- **Network calls are explicit and bounded.** - - DNS reverse lookups for destinations the inspected app contacts (so the Network tab can label `8.8.8.8` as `dns.google`). - - Mac App Store privacy-label lookups against `itunes.apple.com` and `apps.apple.com` — keyed by the inspected app's bundle ID, never your data. - - Sparkle appcast fetch from `privacykey.github.io` when you check for updates. -- **All analysis runs locally.** The inspected app's contents never leave your machine. -- **The helper is opt-in.** Without the helper, privacycommand still works — file-event monitoring is unavailable and the Background Task Management audit asks before triggering an admin prompt. +- [`.github/ARCHITECTURE.md`](.github/ARCHITECTURE.md) — the targets, why they are separate, how data moves between them. +- [`privacycommand/README.md`](privacycommand/README.md) — source-tree map, signing and entitlements reference, troubleshooting. +- [`privacycommand/HELPER.md`](privacycommand/HELPER.md) — privileged helper bundling, signing and verification. +- [`privacycommand/docs/GUEST_AGENT.md`](privacycommand/docs/GUEST_AGENT.md) — VM guest-agent walkthroughs. +- [`docs/PRIVACY.md`](docs/PRIVACY.md) — every network call the app makes, and why. +- [`NOTICES.md`](NOTICES.md) — third-party notices. ## Contributing -Issues and PRs welcome. Before opening a PR: - -- Run `swift test` from `privacycommand/` and confirm it passes. -- For UI changes, attach a before/after screenshot. -- For new analysis signals, add a Knowledge Base entry alongside the detector — privacycommand explains what every finding means in plain English, and we want to keep that contract. - -## Security disclosures +Issues and pull requests are welcome. CI runs two workflows on every pull request: the SPM build and test suite plus an `auditctl` smoke test, and an unsigned Xcode build of the app target. Reproduce the first locally from `privacycommand/`: -If you find a security issue, please **don't** open a public issue. Email `security@privacykey.org` with the details. We aim to respond within 72 hours. - -## Related products +```sh +swift build +swift test +``` -privacycommand is a **privacykey** project +Or `just build` and `just test` from the repo root. Setup, the Xcode path, and what to include in a pull request are in [CONTRIBUTING.md](CONTRIBUTING.md). -- **[privacycommand](https://github.com/privacykey/privacycommand)** — macOS forensic permission auditor _(this repo)_ +Found a security issue? Please do not open a public issue — [`.github/SECURITY.md`](.github/SECURITY.md) has the reporting address and what is in scope. -## License +## Licence -Released under the [MIT License](LICENSE). +Released under the [MIT licence](LICENSE). diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md new file mode 100644 index 0000000..a238bd6 --- /dev/null +++ b/docs/PRIVACY.md @@ -0,0 +1,51 @@ +# Privacy and telemetry posture + +privacycommand is a privacy tool and behaves like one. This page states exactly +what it sends and what it does not, so you can check the claim rather than take +it. This content used to live in the top-level [`README.md`](../README.md). + +## No analytics + +privacycommand ships no analytics SDKs. There is no telemetry endpoint, no +install counter, and no crash-report bucket. The Swift package manifest declares +no third-party dependencies at all, and the Xcode project adds exactly one — +[Sparkle 2](https://sparkle-project.org), for in-app updates. + +## All analysis runs locally + +The contents of the app you inspect never leave your machine. Static analysis, +disassembly, decompilation, monitoring and report generation all happen on the +host (or, in VM mode, inside your own virtual machine). + +## The network calls it does make + +They are explicit and bounded: + +- **Reverse DNS lookups** for the destinations the inspected app contacts, so the + Network tab can label `8.8.8.8` as `dns.google`. +- **Mac App Store privacy-label lookups** against `itunes.apple.com` and + `apps.apple.com`, keyed by the inspected app's bundle identifier — never by + anything about you. +- **The Sparkle appcast fetch** from `privacykey.github.io` when you check for + updates. Automatic checks are off by default; you opt in under + Settings → Updates. Homebrew Cask installs suppress the in-app updater + entirely. +- **Incoming-build downloads**, only when you explicitly ask for them: the + `auditctl preview --fetch` path retrieves a cask artifact from its own + publisher so it can be diffed against the version you have installed. + +`com.apple.security.network.client` is the only entitlement covering outbound +network access. The app also holds `com.apple.security.automation.apple-events`, +which is not network access: it lets the guest-agent panel ask installed VM +front-ends (VirtualBuddy, UTM, Parallels, VMware) over AppleScript which VMs +exist. The hardened-runtime escapes are deliberately absent — no `allow-jit`, no +`allow-dyld-environment-variables`, no `disable-library-validation`. + +## The privileged helper is opt-in + +Without the helper, privacycommand still works — you lose file-event monitoring, +and the Background Task Management audit asks before triggering an admin prompt. +The helper is a minimal XPC service installed through `SMAppService.daemon`; it +validates callers by Team ID on connect, and its whole job is to run +`fs_usage`, `sfltool` and `pfctl` on the app's behalf. See +[`privacycommand/HELPER.md`](../privacycommand/HELPER.md).