Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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`.
164 changes: 47 additions & 117 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,46 @@
<img src=".github/assets/banner.png" alt="mantis" width="640">
</p>

<h1 align="center">mantis</h1>

<p align="center">Self-hostable canary key service. API-first.</p>

<p align="center">
<a href="https://github.com/privacykey/mantis/releases?q=cli-v"><img alt="CLI release" src="https://img.shields.io/github/v/release/privacykey/mantis?filter=cli-v*&label=CLI&color=2f7df0"></a>
<a href="https://github.com/privacykey/homebrew-tap"><img alt="Homebrew tap" src="https://img.shields.io/badge/brew-privacykey%2Ftap%2Fmantis-FBB040?logo=homebrew&logoColor=white"></a>
<a href="https://github.com/privacykey/mantis/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/privacykey/mantis/actions/workflows/ci.yml/badge.svg"></a>
<a href="./mantis-edge/"><img alt="Cloudflare Workers edge" src="https://img.shields.io/badge/edge-Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white"></a>
<a href="https://github.com/privacykey/.github/blob/main/STATUS.md#mantis"><img alt="Project status" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fprivacykey%2F.github%2Fmain%2Fbadges%2Fmantis.json"></a>
<a href="https://github.com/privacykey/mantis/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/privacykey/mantis?label=release"></a>
<a href="LICENSE"><img alt="Licence" src="https://img.shields.io/github/license/privacykey/mantis?label=licence"></a>
<a href="https://github.com/privacykey/mantis/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/privacykey/mantis/ci.yml?branch=main&label=ci"></a>
</p>

<!-- disclosure:start -->
> [!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.
<!-- disclosure:end -->

> [!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 <key> --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 <key> --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.

Expand All @@ -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 <http://localhost:3000> 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 <worker-url> # 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 <url> --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).
17 changes: 17 additions & 0 deletions docs/FLEET-PROVISIONING.md
Original file line number Diff line number Diff line change
@@ -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.
Loading