Skip to content
Open
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
43 changes: 39 additions & 4 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,24 @@ are called out explicitly below.

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/api/keys` | Create a key. Bearer auth. Body: `{ memo, response_kind?, response_payload?, destinations?, expires_at?, dedupe_window_seconds?, monitor_mode?, monitor_window_seconds? }` |
| `POST` | `/api/keys` | Create a key. Bearer auth. Body: `{ memo, external_id?, response_kind?, response_payload?, destinations?, expires_at?, dedupe_window_seconds?, monitor_mode?, monitor_window_seconds? }`. Supply `external_id` to make creation idempotent — see [below](#idempotent-creation). The one route enrollment-scoped keys may call. |
| `GET` | `/api/keys?limit=&cursor=` | List accessible keys. Bearer auth. Admin keys see all; non-admin keys see rows they created. |
| `GET` | `/api/keys/:id` | Get one accessible key. Bearer auth. |
| `PATCH` | `/api/keys/:id` | Update memo, response, expiry, dedupe, monitor mode/window, `disabled`, or replace destinations. Bearer auth. |
| `DELETE` | `/api/keys/:id` | Hard-delete a key and cascading hits/notifications. Bearer auth. |
| `GET` | `/api/keys/:id/hits?limit=&cursor=` | Paginated hit log for one key. Bearer auth. |
| `GET` | `/api/hits/recent?since=<iso>&cursor=<iso>&key_id=<id>&limit=<n>` | Recent hit feed across accessible keys, used by CLI watch mode. Bearer auth. |
| `GET` | `/api/keys/:id/download?format=<format>` | Download a generated artifact. Bearer or session auth. Formats: `docx`, `xlsx`, `pptx`, `pdf`, `folder`, `nfc-label`, `apple-wallet`, `svg`, `html`, `md`, `eml`, `ics`, `vcf`. |
| `GET` | `/api/keys/:id/download?format=<format>` | Download a generated artifact. Bearer or session auth. Formats: `docx`, `xlsx`, `pptx`, `pdf`, `folder`, `nfc-label`, `apple-wallet`, `svg`, `html`, `md`, `eml`, `ics`, `vcf`, `rtf`, `cookies`, `bookmarks`, `env`, `aws-credentials`, `netrc`, `kubeconfig`, `ovpn`, `rdp`. See [file keys](/file-keys). |
| `POST` | `/api/keys/bulk-download` | Zip one generated artifact per key. Bearer or session auth. Body: `{ ids, format }`, max 50 ids; returns a `.zip` with one artifact (or per-key folder) per key. Ids not visible to the caller are silently skipped, not rejected. |
| `POST` | `/api/keys/device-bundle` | Package an already-minted device suite into an installable zip. Bearer or session auth. Body: `{ device, os, vectors }`, max 20 vectors; returns the install zip, or a JSON file map with `?format=json`. Backs the dashboard's device page and `mantis device new --bundle`. |
| `GET` | `/api/keys/:id/install?type=<type>[&hostname=example.com][&format=json]` | Generated installer snippet for host, web, NFC, and IoT events. Bearer or session auth. |
| `POST` | `/api/keys/:id/reset` | Reset a key's latched monitor state. Bearer or session auth. |
| `POST` | `/api/keys/:id/destinations/:destinationId/signing-secret` | Reveal a webhook destination's plaintext HMAC signing secret. Bearer or session auth. Audited. |
| `POST` | `/api/keys/:id/destinations/:destinationId/rotate-secret` | Rotate a webhook destination's HMAC signing secret and return the new secret once. Bearer or session auth. Audited. |
| `GET` | `/api/api-keys` | List API keys. Bearer auth. Hashes are never returned; non-admin keys see only themselves. |
| `POST` | `/api/api-keys` | Mint a new API key. Bearer auth. Body: `{ name, is_admin? }`; plaintext key returned once. Only admins can mint admin keys. |
| `GET` | `/api/api-keys` | List API keys. Bearer auth. Hashes are never returned; non-admin keys see only themselves. Each row includes its `scope`. |
| `POST` | `/api/api-keys` | Mint a new API key. Bearer auth. Body: `{ name, is_admin?, scope? }`; plaintext key returned once. Only admins can mint admin keys. `scope` is `full` (default) or `enroll` — see [key scope](#api-key-scope-full-vs-enroll). |
| `DELETE` | `/api/api-keys/:id` | Revoke an API key. Bearer auth. Self-revoke is allowed; revoking others requires admin. |
| `GET` | `/api/device-profiles` | The device-profile / vector catalog used by `mantis device`. Bearer or session auth. |
| `GET` | `/api/audit?limit=&cursor=&since=&event_type=&actor=` | Admin-only audit log. Bearer or session auth. |
| `GET` `HEAD` | `/api/health` | **Public unless gated by your proxy.** Liveness + `SELECT 1` readiness. 200 = app and DB ok, 503 = DB failure. |
| `GET` `POST` | `/api/cron/notifications?max=<n>` | Notification retry and retention worker endpoint for serverless deployments. Requires `Authorization: Bearer $CRON_SECRET`; returns 401 if `CRON_SECRET` is unset. |
Expand Down Expand Up @@ -77,6 +80,38 @@ Webhook destinations get an HMAC secret. Outbound raw-webhook deliveries include
`<timestamp>.<json body>`. The plaintext secret is only shown on create, replace,
explicit reveal, or rotate responses; normal listing returns a fingerprint.

## API key scope: full vs enroll

Every API key carries a `scope`, orthogonal to `is_admin`:

- **`full`** (default) — behaves as described throughout this page. Subject to the admin / non-admin visibility rules.
- **`enroll`** — create-only. An enroll key may call **only** `POST /api/keys`. Every other management route (list/read/update/delete keys — including the ones it created — plus `/api/hits/recent`, `/api/api-keys`, the audit log, and any session-reachable route) returns `403 forbidden`, and an enroll key cannot log in to the dashboard. `is_admin: true` together with `scope: "enroll"` is rejected at validation.

Enroll keys are the intended credential for MDM / fleet provisioning: you embed one on every managed machine and accept that a curious user will extract it. An extracted enroll key cannot read hit history, alert routing or signing secrets, and cannot enumerate or list keys — but it is not inert, so size the blast radius before you embed one:

- **It can confirm and retrieve any key whose `external_id` it guesses.** A `POST /api/keys` that collides with an existing `external_id` returns that key's trigger URL, memo, `public_id` and expiry (`"reused": true`, HTTP `200`) — see [Idempotent creation](#idempotent-creation) — regardless of which API key created it. `mantis device` derives `external_id`s deterministically as `mantis:device:<os>:<normalized-name>:<slug>`, so an attacker who knows your naming convention can guess a machine's ids and read back that machine's canary URLs, which is exactly what lets an intruder route around the tripwires. Each such claim is recorded in the audit log as `key.claimed`.
- **It can supply `destinations` on creation**, and Mantis fires the activation ping synchronously — so the key can make your instance POST to an attacker-chosen HTTP(S) endpoint (private, loopback and metadata addresses are rejected unless `ALLOW_PRIVATE_WEBHOOKS=1`) or, if `SMTP_URL` is set, send it mail.

See the Kandji recipe in the product repo's `deploy/kandji/`.

## Idempotent creation

`POST /api/keys` also accepts an optional `external_id` (1–128 chars, matching
`^[A-Za-z0-9][A-Za-z0-9._:-]*$`) stored on a unique column. When supplied, a
repeat POST with the same `external_id` returns the **existing** key —
`"reused": true` with HTTP `200` instead of `201` — rather than minting a
duplicate. The other body fields (`memo`, `destinations`, …) apply only when the
row is actually created; a later claim never mutates what the key was first
configured with. Keys created without an `external_id` are unaffected (unique
constraint treats NULLs as distinct).

This is the mechanism the fleet-enrollment flow relies on — one key per machine
serial, so re-running enrollment on a reimaged machine reuses its key instead of
littering the list. Enroll-scoped callers (and callers claiming another creator's
`external_id`) get a reduced response shape — trigger URL and identity only, no
alert routing or signing secrets. A claim that races a concurrent delete returns
`409 conflict`; retry.

## Response kinds for the trigger endpoint

| `response_kind` | Payload | Result |
Expand Down
4 changes: 2 additions & 2 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ src/ # Next.js server: dashboard, API, public triggers
new/ # create form
[id]/ # detail, hits, downloads, installers, destinations
settings/wallet/ # admin Apple Wallet / PassKit config
settings/notifications/ # admin instance-wide (global) notify destinations
api/
keys/... # authenticated key CRUD, hits, downloads, installers
api-keys/... # authenticated API-key listing, minting, revocation
Expand Down Expand Up @@ -48,8 +49,7 @@ src/ # Next.js server: dashboard, API, public triggers
safe-body.ts # bounded request body readers for JSON/text routes
public-only-hosts.ts # split public/dashboard host routing
retention.ts # optional row-level cleanup
proxy.ts # Host/path guard for public-only deployments
middleware.ts # wires proxy.ts host-split gate into the request path
proxy.ts # Next request-path entrypoint (formerly middleware.ts, renamed in Next 16); host/path guard for public-only deployments
instrumentation.ts # boot hook: migrations, bootstrap key, notify worker

cli/ # @mantis/cli terminal client
Expand Down
121 changes: 115 additions & 6 deletions cli.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
---
title: "CLI reference"
description: "Every mantis CLI command and flag, verified against the v0.1.6 source."
description: "Every mantis CLI command and flag, verified against the v0.2.0 source."
sidebarTitle: "CLI"
---

The `mantis` CLI is the primary way to work with a Mantis server (and with the
stateless [edge worker](/edge-deployment)). This page documents **every** command
and flag in the CLI, transcribed from the v0.1.6 source. For the conceptual
and flag in the CLI, transcribed from the v0.2.0 source. For the conceptual
map — diagrams, mental model, common workflows — see `cli/COMMAND_MAP.md` in the
app repo.

New to the CLI? Run `mantis init` for guided, interactive setup, or `mantis`
with no arguments for a context-aware welcome screen.

<Note>
This reference covers CLI **v0.1.6**. Check yours with `mantis --version`, and
This reference covers CLI **v0.2.0**. Check yours with `mantis --version`, and
run `mantis doctor` after upgrading the server to confirm compatibility.
</Note>

Expand Down Expand Up @@ -167,7 +167,7 @@ shot. `[memo]` is a human-readable label.

| Flag | What it does |
|---|---|
| `-N, --notify <spec>` | Destination as `<channel>:<target>`. Channels: `webhook`, `email`, `slack`, `discord`, `teams`. Repeatable. |
| `-N, --notify <spec>` | Destination as `<channel>:<target>`. Channels: `webhook`, `email`, `slack`, `discord`, `teams`, `home_assistant`. Repeatable. |
| `-w, --notify-webhook <url>` | Shortcut for `--notify webhook:<url>`. Repeatable. |
| `-e, --notify-email <email>` | Shortcut for `--notify email:<email>`. Repeatable. |
| `-r, --response-kind <kind>` | Trigger response shape: `gif`, `empty`, `json`, `redirect`, `html` |
Expand Down Expand Up @@ -229,6 +229,27 @@ The input CSV is capped at **64 MiB**. Raise it with
[`MANTIS_BULK_CREATE_MAX_BYTES`](#environment-only-settings) if you trust the
file.

**Per-row CSV columns.** The flags above set one value for *every* row. To vary a
row from the rest, add these columns to the input CSV. `response_kind`,
`response_payload`, and `expires_at` override the matching flag for that row only;
the `notify` / `notify_<channel>` columns add their destinations *on top of* the
`--notify` flags rather than replacing them:

| Column | Effect |
|---|---|
| `memo` | The memo. If absent, falls back to an `area` then a `name` column (or use `--memo-column` / `--memo-template`). |
| `notify` | One or more `<channel>:<target>` destinations for that row (`;`-separated), added on top of the `--notify` defaults. |
| `notify_<channel>` | A per-channel column — `notify_webhook`, `notify_email`, `notify_slack`, `notify_discord`, `notify_teams`, `notify_home_assistant`; the cell is the target. |
| `response_kind` | Per-row response shape, as the flag. |
| `response_payload` | Per-row payload JSON, as the flag. |
| `expires_at` | Per-row expiry, as the flag. |

**Output columns.** The output CSV is your input with six columns appended
(reused in place if a same-named column already exists, matched
case-insensitively): `mantis_memo`, `mantis_id`, `mantis_public_id`,
`mantis_url`, `mantis_created_at`, and `mantis_error` (populated per row on
failure, so a partial run tells you exactly which rows didn't create).

### `mantis list` (alias `ls`)

List keys.
Expand Down Expand Up @@ -309,6 +330,20 @@ Download generated bait files for an existing key. Each flag writes one file.
| `--eml <file>` | `.eml` email message |
| `--ics <file>` | Calendar event |
| `--vcf <file>` | Contact card |
| `--rtf <file>` | `.rtf` document — beacons on open like `.docx`, but plain text |
| `--cookies <file>` | Netscape `cookies.txt` session jar |
| `--bookmarks <file>` | Browser `bookmarks.html` export |
| `--env <file>` | `.env` credentials file |
| `--aws-credentials <file>` | `~/.aws/credentials` file |
| `--netrc <file>` | `.netrc` (auto-read by curl/wget/git) |
| `--kubeconfig <file>` | kubeconfig with a bait API server |
| `--ovpn <file>` | OpenVPN profile |
| `--rdp <file>` | Remote Desktop profile |

The last eight are the credential/config-store bait formats: unlike the document
formats above (`--rtf` included, which beacons on open like `.docx`), they fire
when the URL inside is **used**, not when the file is opened. Save each under the name the real thing has (`cookies.txt`, `.netrc`,
`~/.aws/credentials`, …) — see [file keys](/file-keys#credential-and-config-stores).

### `mantis install <id>`

Expand Down Expand Up @@ -342,13 +377,48 @@ plugin-provided type). See [Host events](/host-events).
| `js-clone-detector` | Web | Page runs on an unexpected hostname |
| `nfc-ndef` | Tag | NFC tag URL is opened |
| `homeassistant` | Smart home | HA automation calls the generated `rest_command` |
| `homeassistant-receiver` | Smart home | Ready-to-paste HA automation that listens on a Mantis webhook destination and reacts (drops the activation ping, example actions) |
| `scrypted` | Smart home | Scrypted Script sees the selected device event |

Plugins can register additional types; `mantis install` validates against the
union of built-ins plus installed plugins.

---

## Device suites

`mantis device <subcommand>` — mint and install the full set of host alarms for
one machine in a single step. Where `mantis new --install` templates one alarm,
a device suite mints **one key per vector** (shell login, sudo, wake, boot,
network) for a named machine, so a hit tells you *which* alarm fired rather than
just "something happened on web01". Each key is keyed idempotently by
device-name + vector, so re-running for a rebuilt machine reuses its keys instead
of minting a second set.

### `mantis device profiles`

List the vectors each OS profile would mint (and which need extra setup). No flags.

### `mantis device new`

Mint one key per host alarm for a machine.

| Flag | What it does |
|---|---|
| `-o, --os <os>` | Target OS: `macos`, `linux`, `windows`, or `auto`. `auto` uses this machine's OS — only safe when you're on the target machine. |
| `-n, --name <name>` | Machine these alarms are for; appears in every memo. Defaults to this host's name only with `--install`. |
| `--vectors <list>` | Comma-separated alarm slugs (see `mantis device profiles`); defaults to the profile's recommended set. |
| `--all` | Every alarm in the profile, including ones needing extra setup (e.g. macOS wake needs `sleepwatcher`). |
| `--bundle <path>` | Write the install bundle (`.zip` of installers + a bootstrap script) to this path. |
| `--install` | Apply the alarms to **this** machine now — touches LaunchAgents / systemd units / scheduled tasks, so it confirms first. |
| `-y, --yes` | Skip the `--install` confirmation. |
| `--dry-run` | Show what would be minted, and mint nothing. |

`--bundle` is the reversible option — you read the script before running it;
`--install` is the apply-now shortcut for the machine you're on.

---

## Hits & monitoring

### `mantis hits <id>`
Expand Down Expand Up @@ -406,12 +476,12 @@ Reset a key's tripped monitor state (latch mode). No flags.

`mantis destinations <subcommand>` (alias `dest`) — incrementally manage
notification destinations on a key. Channels: `webhook`, `email`, `slack`,
`discord`, `teams`.
`discord`, `teams`, `home_assistant`.

| Command | Flags |
|---|---|
| `destinations list <key-id>` (alias `ls`) | none |
| `destinations add <key-id> [channel] [target]` | `--channel <channel>` (`webhook`/`email`/`slack`/`discord`/`teams`), `--target <target>` (URL or email) |
| `destinations add <key-id> [channel] [target]` | `--channel <channel>` (`webhook`/`email`/`slack`/`discord`/`teams`/`home_assistant`), `--target <target>` (URL or email) |
| `destinations rm <key-id> <destination-id>` (alias `remove`) | none |
| `destinations test <key-id>` | `-y, --yes` (skip the confirmation prompt) |
| `destinations rotate-secret <key-id> <destination-id>` | `-y, --yes` (skip the confirmation prompt) |
Expand All @@ -420,6 +490,12 @@ notification destinations on a key. Channels: `webhook`, `email`, `slack`,
- `test` fires a synthetic hit on the key URL and reports which destinations succeeded.
- `rotate-secret` rotates the HMAC signing secret on a webhook destination; the new secret is shown **once**.

These subcommands manage destinations **on one key**. To route *every* key's
hits to a shared destination without re-entering it each time, set an
instance-wide destination in the dashboard at `/settings/notifications` (admin
only) — see [global destinations](/configuration#global-notification-destinations).
There is no CLI subcommand for the global set.

---

## Audit log
Expand All @@ -444,6 +520,17 @@ Worker) key flow. See [Edge deployment](/edge-deployment).

Generate a 32-byte AES key for an edge worker (prints to stdout). No flags.

### `mantis edge deploy`

Deploy the `mantis-edge` Worker (wraps `wrangler deploy`) and capture its URL.
Runs the worker's own `wrangler` via `npx`, so no global install is needed.

| Flag | What it does |
|---|---|
| `--dir <path>` | Worker directory to deploy from (defaults to `./` or `./mantis-edge`) |
| `--set-key` | After a successful deploy, store the AES key locally for the deployed URL (prompts) |
| `[wranglerArgs...]` | Extra args forwarded verbatim to `wrangler deploy` — put them after `--` |

### `mantis edge set-key [worker] [key]`

Store an edge AES key in the OS keychain for a given worker URL. `[worker]` is
Expand Down Expand Up @@ -490,6 +577,28 @@ on a TTY to launch the interactive wizard.

`--edge-key` is named separately from the global `--key` to avoid a collision.

### `mantis edge device`

The stateless counterpart to [`mantis device new`](#mantis-device-new): mint one
edge URL per host alarm for a machine and write an install bundle **directory**,
with no server and no database. Same vector model as `mantis device`, plus the
edge minting flags.

| Flag | What it does |
|---|---|
| `-o, --os <os>` | Target OS: `macos`, `linux`, `windows`, or `auto` |
| `-n, --name <name>` | Machine these alarms are for; appears in every memo |
| `--vectors <list>` | Comma-separated alarm slugs (see `mantis device profiles`); defaults to the recommended set |
| `--all` | Every alarm in the profile, including ones needing extra setup |
| `--bundle <dir>` | Write the install bundle to this **directory** (must not already have contents) |
| `--install` | Apply the alarms to this machine now (asks first) |
| `-y, --yes` | Skip the `--install` confirmation |
| `--dry-run` | Show what would be minted, and mint nothing |
| `--worker <url>` | Worker base URL (`https://…`); falls back to the current profile's edge worker |
| `--webhook <url>` | Webhook URL the worker POSTs on hit — every minted URL embeds it |
| `--channel <channel>` | Destination channel formatter: `webhook`, `slack`, `discord`, `teams` |
| `--edge-key <base64url>` | Override the stored AES key for this mint |

### `mantis edge install <url>`

Generate an installer snippet for a stateless edge URL — the same snippets
Expand Down
Loading
Loading