diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f73c41f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing + +## Package manager + +This repo is a pnpm workspace ([`pnpm-workspace.yaml`](./pnpm-workspace.yaml)). +pnpm is the only supported package manager — the exact version is pinned in +`packageManager` in [`package.json`](./package.json), and CI installs pnpm from +that field rather than a hard-coded version. Node comes from +[`.nvmrc`](./.nvmrc). + +## Run from source + +For contributors hacking on the web dashboard / server: + +```bash +pnpm install +cp .env.example .env # set DATABASE_URL and generate MANTIS_API_KEY_PEPPER +pnpm run db:migrate +pnpm dev +``` + +Generate the pepper with `openssl rand -base64 32` and add it as +`MANTIS_API_KEY_PEPPER` in `.env`. + +Optional: `pnpm run setup:hooks` installs the repo's pre-commit hook +([`scripts/install-hooks.sh`](./scripts/install-hooks.sh)). + +## What CI runs + +Everything below is taken from +[`.github/workflows/ci.yml`](./.github/workflows/ci.yml), which runs on pushes +to `main`, on pull requests, and on manual dispatch. Each job starts with +`pnpm install --frozen-lockfile`. + +| Job | Command | +| --- | --- | +| typecheck (server + cli + edge) | `pnpm run check` | +| unit + integration tests | `pnpm run db:migrate`, `pnpm test`, `pnpm run test:integration`, `pnpm run test:tier2` | +| next build | `pnpm run build` | +| CLI build artifact | `pnpm --filter @mantis/cli run build`, then `node cli/dist/index.js --version` | +| dependency audit | `pnpm audit --prod --audit-level=high` | + +Notes on the test tiers: + +- `pnpm test` — Vitest unit suite ([`vitest.config.ts`](./vitest.config.ts)). +- `pnpm run test:integration` — full-stack tests that exercise the real route + handlers against a live Postgres + ([`vitest.integration.config.ts`](./vitest.integration.config.ts)). The + suite's `globalSetup` re-applies migrations idempotently. Locally, + `pnpm run test:integration:db` starts a database for you via + [`scripts/test-integration.sh`](./scripts/test-integration.sh). +- `pnpm run test:tier2` — end-to-end against a production `next build` and the + standalone server entrypoint over real HTTP + ([`vitest.tier2.config.ts`](./vitest.tier2.config.ts), + [`scripts/test-tier2.sh`](./scripts/test-tier2.sh)). It covers the two things + handler-level tests cannot see: the proxy host-split as applied by the runtime + matcher, and the wire-level `Set-Cookie` `Secure` attribute. Set + `MANTIS_TIER2_USE_EXISTING_DB=1` to reuse a database you already have running. + +The audit job fails the build on a high or critical advisory in production +dependencies. `pnpm-workspace.yaml` carries overrides that patch known +transitive CVEs; moderate advisories are tracked by Dependabot rather than +blocking. + +## Benchmarks + +```bash +pnpm run bench # CLI only +pnpm run bench:all # CLI + edge + server +``` + +See [`bench/README.md`](./bench/README.md) for what each harness measures and +the recorded baselines. + +## Versioning + +The server, the CLI, the edge worker and the LAN helper are versioned +independently and each has its own `package.json` version. CLI releases are cut +by [`.github/workflows/cli-release.yml`](./.github/workflows/cli-release.yml), +which also regenerates the Homebrew formula in `privacykey/homebrew-tap`. diff --git a/README.md b/README.md index 7311823..5484814 100644 --- a/README.md +++ b/README.md @@ -2,41 +2,46 @@ mantis

+

mantis

+ +

Self-hostable canary key service. API-first.

+

- CLI release - Homebrew tap - CI - Cloudflare Workers edge + Project status + Release + Licence + CI

+ > [!WARNING] -> **Beta.** APIs, DB schema, and CLI flags may still change before v1.0. Components are versioned independently — CLI `v0.1.6`, full server `v0.1.1`, edge `v0.1.3` at time of writing. Pin a release tag for stability. +> **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. + + +> [!CAUTION] +> **Serve it over HTTPS, never plain HTTP.** Mantis authenticates with an API key sent as a bearer token and a session cookie — over plain HTTP on a routable address both travel in cleartext and can be sniffed. Put it behind a tunnel (the `tailscale` / `cloudflared` compose profiles terminate TLS for you) or a TLS reverse proxy. -Self-hostable mantis key service. API-first. +--- You mint a unique URL; when something fetches it, you get notified through the destinations you configure. Useful for honeypot files on shared drives, fake credentials in `.env`s, SSH-login alarms on jump boxes, detecting site clones, or any "did someone touch the thing they shouldn't" tripwire. -Highlights: +There are two ways to run it. The full **server** (`docker compose`, Postgres-backed) gives you the web dashboard, the notification queue, file and host-event keys, and hit history. The **edge** variant ([`mantis-edge/`](./mantis-edge/README.md)) is a Cloudflare Worker that decrypts URLs at the edge with no database to host. Use it when you only need fire-and-forget hit alerts and want zero infrastructure. + +Components are versioned independently: the server, the CLI, and the edge worker each carry their own version and release on their own cadence. + +## What it does -- **File keys** in 10 formats (`.docx` / `.xlsx` / `.pptx` / `.pdf` / `.svg` / `.html` / `.md` / `.eml` / `.ics` / `.vcf`) plus a 9-file honey-directory `.zip` and Apple Wallet `.pkpass` passes (install / uninstall / fetch callbacks each fire the key) +- **File keys** in 10 formats (`.docx` / `.xlsx` / `.pptx` / `.pdf` / `.svg` / `.html` / `.md` / `.eml` / `.ics` / `.vcf`) plus a honey-directory `.zip` and Apple Wallet `.pkpass` passes (install / uninstall / fetch callbacks each fire the key) - **Host-event installers** — shell, sudo, login, boot, wake, network — for macOS, Linux, and Windows, with parsed `X-Mantis-*` context including SSH client IP -- **Web canaries** — CSS-background + JS clone-detector — plus NFC NDEF tag URLs and printable QR/NFC sticker labels +- **Web canaries** — CSS-background and JS clone-detector — plus NFC NDEF tag URLs and printable QR/NFC sticker labels - **Smart-home triggers** via Home Assistant, Scrypted, and an optional LAN watcher -- **Smart-home actions** — `home_assistant` destination posts to a HA webhook automation, so a hit can flip a switch (e.g. cut a VLAN via the OPNsense integration), fire a scene, or push a phone notification (`mantis install --type homeassistant-receiver` prints a ready-to-paste automation skeleton) +- **Smart-home actions** — the `home_assistant` destination posts to a HA webhook automation, so a hit can flip a switch, fire a scene, or push a phone notification; `mantis install --type homeassistant-receiver` prints the automation skeleton - **Direct notification destinations** — webhook, email, Slack, Discord, Teams — with a Postgres-backed retry queue and per-key dedup -- **Optional stateless variant** ([`mantis-edge/`](./mantis-edge/README.md)) — a Cloudflare Worker that decrypts URLs at the edge, no DB to host -- **Uptime Kuma integration** — per-key status URL flips on hit, watched by Kuma for 80+ notification channel fan-out +- **Uptime Kuma integration** — a per-key status URL flips on hit, so Kuma can fan out to its own notification channels +- **Fleet provisioning** — idempotent key creation keyed on an `external_id`, plus create-only enrollment API keys safe to embed in MDM scripts. See [`docs/FLEET-PROVISIONING.md`](./docs/FLEET-PROVISIONING.md) -## Contents - -- [Getting started](#getting-started) — [stateful server](#quickstart-stateful-server) or [stateless edge](#quickstart-no-server--edge)? -- [Run from source](#run-from-source) — for contributors -- [Components](#components) — in-repo docs for each part -- [Docs](#docs) — full prose documentation - -## Getting started - -**Stateful server or stateless edge?** Run the full **server** (`docker compose`, Postgres-backed) when you want the web dashboard, notification queue, file/host-event keys, and history. Run the **edge** ([`mantis-edge/`](./mantis-edge/README.md)) — a Cloudflare Worker that decrypts URLs at the edge with no DB to host — when you only need fire-and-forget hit alerts and want zero infrastructure. +## Get it The fastest path either way is the guided CLI: `mantis init` asks server-or-edge and walks you through login and your first key. @@ -45,119 +50,44 @@ brew install privacykey/tap/mantis mantis init ``` -### Quickstart (stateful server) +To stand the server up yourself: ```bash git clone https://github.com/privacykey/mantis && cd mantis ./scripts/setup.sh # creates .env with a random DB password + API-key pepper docker compose up -d -# Wait for the boot banner, then read the one-time bootstrap admin key -docker compose logs -f mantis | grep -m1 -A1 "bootstrap API key" -``` - -`setup.sh` is idempotent — re-running it leaves existing secrets untouched. Postgres is never published to the host (it sits on an internal-only docker network), and the DB password is the single value you set in `.env`; the app's `DATABASE_URL` is derived from it. - -The `mantis_live_...` value printed above is the **bootstrap admin key** — it is both your CLI token and your dashboard login. To know it up front instead, pre-set `BOOTSTRAP_API_KEY=mantis_live_...` in `.env` before the first boot. - -Open and paste that same key to sign in to the web dashboard. Then log in the CLI and mint a key: - -```bash -mantis --key mantis_live_... login --url http://localhost:3000 -mantis new "first mantis" -w http://localhost:3000/inbox/demo -``` - -This is fine for evaluation but **don't rely on a laptop deploy for canaries that need to fire when you're away from your machine.** For a real public-reachable deploy — Tailscale Funnel, Cloudflare Tunnel, Railway, Fly.io, or Render — see **[deployment options](https://github.com/privacykey/mantis-docs/blob/main/deployment/README.md)**. - -For Fly.io specifically, one command provisions the app, a Managed Postgres -cluster, the secrets and the first admin key: - -```bash -bash deploy/fly-launch.sh --app my-mantis --region iad ``` -Add `--dry-run` to see every command it would run first. See -[`deploy/fly.toml.example`](./deploy/fly.toml.example) for the config it -generates, and [`.github/workflows/fly-deploy.yml`](./.github/workflows/fly-deploy.yml) -to make later pushes deploy themselves. +`setup.sh` is idempotent — re-running it leaves existing secrets untouched. Postgres is never published to the host; it sits on an internal-only docker network. -> **Serve it over HTTPS, never plain HTTP.** Mantis authenticates with an API key sent as a bearer token and a session cookie — over plain HTTP on a routable address both travel in cleartext and can be sniffed. Put it behind a tunnel (the `tailscale` / `cloudflared` compose profiles terminate TLS for you) or a TLS reverse proxy. The compose setup keeps Postgres on an internal-only network with no published port, so the database is never reachable from the host or LAN. +Full walkthrough, including reading the bootstrap admin key, the one-command Fly.io launch and the stateless edge path: [`docs/GETTING-STARTED.md`](./docs/GETTING-STARTED.md). -### Quickstart (no server / edge) +This is fine for evaluation, but **don't rely on a laptop deploy for canaries that need to fire when you're away from your machine.** -No DB to host: deploy the Cloudflare Worker, then mint URLs that decrypt at the edge. +Every component — CLI, edge worker, LAN helper, benchmarks, deploy assets — has its own reference, indexed at the end of [`docs/GETTING-STARTED.md`](./docs/GETTING-STARTED.md). -```bash -mantis edge keygen # generate the AES key -mantis edge set-key # link the deployed Worker -mantis edge mint # interactive wizard for a stateless URL -``` - -To arm a whole machine at once — one URL per host alarm (login, sudo, wake, -boot, network) plus an install bundle directory: - -```bash -mantis edge device --os macos --name web01 --webhook --bundle ./web01 -``` +## Docs -Two server-backed properties don't cross over, and the command says so on every -run: without a database there is no idempotent re-mint (re-running issues a -fresh set of URLs), and the worker can't remember the last hit, so vectors that -dedupe server-side — network attach — are chattier at the edge. The stateful -equivalent is `mantis device new`, which keeps both. +The prose documentation lives in [`privacykey/docs-mantis`](https://github.com/privacykey/docs-mantis). It is a Mintlify source and the rendered site is not live yet, so these link to the Markdown on GitHub. -See **[`mantis-edge/README.md`](./mantis-edge/README.md)** for Worker deploy and full `mantis edge` usage. +- [Getting started](https://github.com/privacykey/docs-mantis/blob/main/getting-started.md) — five steps from `brew install` to first key +- [Use cases](https://github.com/privacykey/docs-mantis/blob/main/use-cases.md) — defensive, detective, operational and adversarial patterns +- [HTTP API](https://github.com/privacykey/docs-mantis/blob/main/api.md) — endpoints, response kinds, webhook payload shape +- [File keys](https://github.com/privacykey/docs-mantis/blob/main/file-keys.md), [host-event keys](https://github.com/privacykey/docs-mantis/blob/main/host-events.md), [deployment](https://github.com/privacykey/docs-mantis/blob/main/deployment/README.md), [updating](https://github.com/privacykey/docs-mantis/blob/main/updating.md) -## Run from source +## Contributing -For contributors hacking on the web dashboard / server: +The repo is a pnpm workspace and pnpm is the only supported package manager — the version is pinned in `packageManager` and CI reads it from there. Node comes from [`.nvmrc`](./.nvmrc). ```bash -pnpm install -cp .env.example .env # set DATABASE_URL and generate MANTIS_API_KEY_PEPPER -pnpm run db:migrate -pnpm dev +pnpm install --frozen-lockfile +pnpm run check # typecheck: server + CLI + edge + iot-helper +pnpm test # unit tests +pnpm run build # next build ``` -Generate the pepper with `openssl rand -base64 32` and add it as `MANTIS_API_KEY_PEPPER` in `.env`. - -## Components - -Each part of the repo has its own reference: - -- **CLI** — [`cli/README.md`](./cli/README.md) (full reference) and [`cli/COMMAND_MAP.md`](./cli/COMMAND_MAP.md) (command map) -- **Edge worker** — [`mantis-edge/README.md`](./mantis-edge/README.md) -- **IoT / LAN helper** — [`iot-helper/README.md`](./iot-helper/README.md) -- **Benchmarks** — [`bench/README.md`](./bench/README.md) -- **Deploy assets** — [`deploy/`](./deploy/) (one-command Fly.io launch, Render example) -- **MDM fleet canaries** — [`deploy/kandji/`](./deploy/kandji/) (one key per managed Mac, terminal-open alerts) - -## Fleet / MDM provisioning - -`POST /api/keys` is idempotent when you pass an `external_id` (e.g. a machine -serial): the first call mints the key, every re-run returns the same one -(`200` + `"reused": true`), so MDM scripts can enroll on every check-in -without minting duplicates. Pair it with an **enrollment-scoped API key** -(`POST /api/api-keys` with `"scope": "enroll"`) — a create-only credential -that's safe to embed in fleet scripts: if extracted from a device it cannot -list, read, disable, or delete keys, read hits, or log in to the dashboard. - -[`deploy/kandji/`](./deploy/kandji/README.md) has a ready-made Kandji Custom -Script that gives every Mac its own canary and pings it whenever an -interactive terminal opens, plus a central pre-provisioning script driven by -the Kandji API. - -## Docs - -Full documentation: **[github.com/privacykey/mantis-docs](https://github.com/privacykey/mantis-docs)**. - -Common starting points: - -- [Getting started](https://github.com/privacykey/mantis-docs/blob/main/getting-started.md) — five steps from `brew install` to first key -- [Use cases](https://github.com/privacykey/mantis-docs/blob/main/use-cases.md) — defensive / detective / operational / adversarial patterns -- [HTTP API](https://github.com/privacykey/mantis-docs/blob/main/api.md) — endpoints, response kinds, webhook payload shape -- [File keys](https://github.com/privacykey/mantis-docs/blob/main/file-keys.md) and [host-event keys](https://github.com/privacykey/mantis-docs/blob/main/host-events.md) -- [Updating](https://github.com/privacykey/mantis-docs/blob/main/updating.md) — update commands per component +CI also runs the integration and tier-2 suites against a real Postgres, builds the CLI, and audits production dependencies. [`CONTRIBUTING.md`](./CONTRIBUTING.md) has local setup and how to reproduce each job. -## License +## Licence -MIT License. See [`LICENSE`](./LICENSE). +MIT. See [`LICENSE`](./LICENSE). diff --git a/docs/FLEET-PROVISIONING.md b/docs/FLEET-PROVISIONING.md new file mode 100644 index 0000000..d3fe4c8 --- /dev/null +++ b/docs/FLEET-PROVISIONING.md @@ -0,0 +1,17 @@ +# Fleet / MDM provisioning + +Moved out of the top-level README. The content is unchanged. + +`POST /api/keys` is idempotent when you pass an `external_id` (e.g. a machine +serial): the first call mints the key, every re-run returns the same one +(`200` + `"reused": true`), so MDM scripts can enroll on every check-in +without minting duplicates. + +Pair it with an **enrollment-scoped API key** (`POST /api/api-keys` with +`"scope": "enroll"`) — a create-only credential that's safe to embed in fleet +scripts: if extracted from a device it cannot list, read, disable, or delete +keys, read hits, or log in to the dashboard. + +[`deploy/kandji/`](../deploy/kandji/README.md) has a ready-made Kandji Custom +Script that gives every Mac its own canary and pings it whenever an interactive +terminal opens, plus a central pre-provisioning script driven by the Kandji API. diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md new file mode 100644 index 0000000..a6f00fe --- /dev/null +++ b/docs/GETTING-STARTED.md @@ -0,0 +1,117 @@ +# Getting started + +Moved out of the top-level README so it stays short. Everything here was in the +README before; nothing has been added. + +**Stateful server or stateless edge?** Run the full **server** (`docker compose`, +Postgres-backed) when you want the web dashboard, notification queue, +file/host-event keys, and history. Run the **edge** +([`mantis-edge/`](../mantis-edge/README.md)) — a Cloudflare Worker that decrypts +URLs at the edge with no DB to host — when you only need fire-and-forget hit +alerts and want zero infrastructure. + +The fastest path either way is the guided CLI: `mantis init` asks server-or-edge +and walks you through login and your first key. + +```bash +brew install privacykey/tap/mantis +mantis init +``` + +## Quickstart (stateful server) + +```bash +git clone https://github.com/privacykey/mantis && cd mantis +./scripts/setup.sh # creates .env with a random DB password + API-key pepper +docker compose up -d +# Wait for the boot banner, then read the one-time bootstrap admin key +docker compose logs -f mantis | grep -m1 -A1 "bootstrap API key" +``` + +`setup.sh` is idempotent — re-running it leaves existing secrets untouched. +Postgres is never published to the host (it sits on an internal-only docker +network), and the DB password is the single value you set in `.env`; the app's +`DATABASE_URL` is derived from it. + +The `mantis_live_...` value printed above is the **bootstrap admin key** — it is +both your CLI token and your dashboard login. To know it up front instead, +pre-set `BOOTSTRAP_API_KEY=mantis_live_...` in `.env` before the first boot. + +Open and paste that same key to sign in to the web +dashboard. Then log in the CLI and mint a key: + +```bash +mantis --key mantis_live_... login --url http://localhost:3000 +mantis new "first mantis" -w http://localhost:3000/inbox/demo +``` + +This is fine for evaluation but **don't rely on a laptop deploy for canaries +that need to fire when you're away from your machine.** For a real +public-reachable deploy — Tailscale Funnel, Cloudflare Tunnel, Railway, Fly.io, +or Render — see +[deployment options](https://github.com/privacykey/docs-mantis/blob/main/deployment/README.md). + +> [!CAUTION] +> **Serve it over HTTPS, never plain HTTP.** Mantis authenticates with an API +> key sent as a bearer token and a session cookie — over plain HTTP on a +> routable address both travel in cleartext and can be sniffed. Put it behind a +> tunnel (the `tailscale` / `cloudflared` compose profiles terminate TLS for +> you) or a TLS reverse proxy. The compose setup keeps Postgres on an +> internal-only network with no published port, so the database is never +> reachable from the host or LAN. + +### Fly.io in one command + +One command provisions the app, a Managed Postgres cluster, the secrets and the +first admin key: + +```bash +bash deploy/fly-launch.sh --app my-mantis --region iad +``` + +Add `--dry-run` to see every command it would run first. See +[`deploy/fly.toml.example`](../deploy/fly.toml.example) for the config it +generates, and +[`.github/workflows/fly-deploy.yml`](../.github/workflows/fly-deploy.yml) to +make later pushes deploy themselves. + +## Quickstart (no server / edge) + +No DB to host: deploy the Cloudflare Worker, then mint URLs that decrypt at the +edge. + +```bash +mantis edge keygen # generate the AES key +mantis edge set-key # link the deployed Worker +mantis edge mint # interactive wizard for a stateless URL +``` + +To arm a whole machine at once — one URL per host alarm (login, sudo, wake, +boot, network) plus an install bundle directory: + +```bash +mantis edge device --os macos --name web01 --webhook --bundle ./web01 +``` + +Two server-backed properties don't cross over, and the command says so on every +run: without a database there is no idempotent re-mint (re-running issues a +fresh set of URLs), and the worker can't remember the last hit, so vectors that +dedupe server-side — network attach — are chattier at the edge. The stateful +equivalent is `mantis device new`, which keeps both. + +See [`mantis-edge/README.md`](../mantis-edge/README.md) for Worker deploy and +full `mantis edge` usage. + +## Components + +Each part of the repo has its own reference: + +- **CLI** — [`cli/README.md`](../cli/README.md) (full reference) and + [`cli/COMMAND_MAP.md`](../cli/COMMAND_MAP.md) (command map) +- **Edge worker** — [`mantis-edge/README.md`](../mantis-edge/README.md) +- **IoT / LAN helper** — [`iot-helper/README.md`](../iot-helper/README.md) +- **Benchmarks** — [`bench/README.md`](../bench/README.md) +- **Deploy assets** — [`deploy/`](../deploy/) (one-command Fly.io launch, Render + example) +- **MDM fleet canaries** — [`deploy/kandji/`](../deploy/kandji/README.md) (one + key per managed Mac, terminal-open alerts)