DOCSMuster
Documentation
Muster is a self-hosted web app for security teams: draw incident response playbooks as diagrams, version them, run them step by step during an incident or an exercise, and produce the incident report as a PDF.
Quick start
With Docker, nothing else to install:
git clone <your-fork-url>
cd muster
docker compose up --build
With Node.js 22.13 or later:
npm install
npm run demo
Then open http://localhost:8080 and click one of the demo accounts on the sign-in page.
Demo accounts
| Account | Password | Can |
|---|---|---|
admin | muster-admin | everything, plus users and the audit log |
editor | muster-editor | edit and publish playbooks, edit report templates |
analyst | muster-analyst | declare incidents, run them, write reports |
viewer | muster-viewer | read everything |
Demo mode also loads a closed incident with its report (IR-2026-041) and a tabletop exercise in progress, so every screen has content.
Demo data is disposable: when a new version of Muster changes it, a demo-mode server replaces the old demo database on start. A real install (DEMO_MODE=false) is never touched. To start the demo from scratch at any time, run docker compose down -v or delete the data-demo folder.
A short tour
- Playbooks lists the ten ready-made playbooks. Open Ransomware: it has three published versions and an unpublished change.
- In the editor, select a step and press A to add an action after it, D for a decision, E for an escalation, F for an end. Drag the orange handle under a step onto another step to connect them. Drafts save automatically.
- History compares any two versions on the diagram and lists every change.
- Incidents → Declare an incident: choose Exercise and a clock speed of ×60 to see SLAs tick in minutes per second. Tick tasks, answer decisions, close the incident.
- Create incident report, edit the executive summary and your notes, then Download PDF.
- Reports → Templates: upload your logo, change the colour and the report ID pattern, rename, hide or reorder sections. The preview updates as you type.
Open the app in two browsers with two accounts: incident progress, playbook changes and new reports appear live for everyone.
Running it for real
Set DEMO_MODE=false. On first start Muster creates one admin account: set ADMIN_PASSWORD, or read the generated password in the logs. Then add your team in Admin.
# docker-compose.yml
services:
muster:
build: .
ports: ["8080:8080"]
environment:
DEMO_MODE: "false"
ADMIN_PASSWORD: "a-long-random-passphrase"
COOKIE_SECURE: "true" # served over HTTPS
TRUST_PROXY: "true" # behind a reverse proxy
volumes:
- muster-data:/data
restart: unless-stopped
volumes:
muster-data:
The ten default playbooks are a starting point. Review owners, SLAs and escalation contacts with your team before relying on them.
Configuration
| Variable | Default | Meaning |
|---|---|---|
PORT | 8080 | HTTP port |
DATA_DIR | ./data (/data in Docker) | where the SQLite database lives. Back this folder up. |
DEMO_MODE | false (true in the compose file) | demo accounts and sample data on first start |
ADMIN_PASSWORD | random | password of the first admin when demo mode is off |
COOKIE_SECURE | false | set to true when served over HTTPS |
TRUST_PROXY | false | set to true behind a reverse proxy, to log real client IPs |
Reverse proxy
Put Muster behind a reverse proxy that terminates TLS (nginx, Caddy, Traefik). Live updates use server-sent events on /api/events: disable response buffering there if your proxy buffers.
# nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/events {
proxy_pass http://127.0.0.1:8080;
proxy_buffering off;
proxy_read_timeout 1h;
}
# Caddy: streams SSE without extra settings
muster.example.org {
reverse_proxy 127.0.0.1:8080
}
Backups
Everything lives in one SQLite database in DATA_DIR. Copy the folder with the container stopped, or take a hot backup:
sqlite3 /data/muster.db ".backup /backups/muster-$(date +%F).db"
Roles
Roles are cumulative: each one can do everything the one before it can. They are checked on every API endpoint, not only in the interface.
| Action | Viewer | Analyst | Editor | Admin |
|---|---|---|---|---|
| Read playbooks, incidents, reports, templates | Yes | Yes | Yes | Yes |
| Declare and run incidents and exercises | — | Yes | Yes | Yes |
| Create and edit reports | — | Yes | Yes | Yes |
| Edit, publish and restore playbooks | — | — | Yes | Yes |
| Edit report templates | — | — | Yes | Yes |
| Archive playbooks, delete reports | — | — | — | Yes |
| Manage users, read the audit log | — | — | — | Yes |
The last active admin can't be demoted or disabled. Changing someone's role or disabling them ends their sessions.
Playbooks
A playbook is a graph of steps. Each step has a type, an owner, an SLA in minutes, a NIST phase (Detection, Containment, Eradication, Recovery, Lessons learned), instructions and a checklist.
| Step | Key | Drawn as | Use it for |
|---|---|---|---|
| Action | A | white card | something to do, with its checklist |
| Decision | D | black hexagon | a question; each answer is a labelled connection |
| Escalation | E | red card | handing over: crisis cell, CISO, legal |
| End | F | stadium | closing the incident |
Editor shortcuts: Ctrl Z undo, Ctrl Shift Z or Ctrl Y redo, Ctrl S save, Delete remove the selection, Esc deselect.
Validation
Publishing is blocked while the playbook has errors: no start or more than one, no end, a step that can't be reached from the start, a dead end, a decision with a single answer. Missing owners, SLAs or answer labels are warnings.
Versions
Each playbook has one shared draft and immutable published versions with a message and an author. If two editors save the draft at the same time, the second is told and nothing is overwritten. History compares any two versions on the diagram (added, changed, removed) and lists every change field by field; Restore copies an old version into the draft. Every incident stores a frozen copy of the version it runs on, so later edits never change a past incident.
YAML format
Every playbook exports to YAML (GET /api/playbooks/:id/yaml, or the YAML button in the editor). An excerpt of PB-002:
# PB-002 Phishing email — draft (live: v1)
playbook: "Phishing email"
id: PB-002
category: "Phishing"
schema: muster/v1
steps:
- id: analyse
type: action
title: "Analyse the email"
phase: "Detection"
owner: "L1 Analyst"
sla_minutes: 20
checklist:
- "Extract headers and URLs"
- "Detonate attachments in the sandbox"
- "Find every other recipient"
next:
- to: malicious
- id: malicious
type: decision
title: "Is the email malicious?"
next:
- to: purge
when: "Yes"
- to: benign
when: "No"
Incidents
Declare an incident (real time) or an exercise (tabletop, with a faster clock: ×1, ×10, ×60 or ×300). Pick the playbook, the version and the severity. Muster then shows one step at a time:
- the phase rail across the top and the elapsed time;
- the step's SLA bar, its instructions and checklist;
- decisions as questions, with one button per answer;
- the route on the frozen diagram, the timestamped timeline, and the analyst notes that go into the report.
Everything anyone does is pushed live to everyone else looking at the incident. Stop response ends it early and records that in the timeline.
Reports and templates
When an incident is closed, Create incident report drafts it from the run: executive summary, timeline, SLA performance (met or missed per step), key decisions, analyst notes and sign-off. Edit any text, then download the PDF or export Markdown. The PDF is rendered on the server from the same data and template as the preview.
Templates
Editors manage templates in Reports → Templates: organisation name, logo, accent colour (with a contrast check and a one-click fix), classification label, footer, and the sections: rename, hide, reorder. One template is the default.
Report ID pattern
| Token | Becomes |
|---|---|
{YYYY} | year, e.g. 2026 |
{YY} {MM} {DD} | two-digit year, month, day |
{YYMMDD} | 261024 |
{SEQ} | the template's counter, zero-padded to the width you set |
{PB} | the playbook code, e.g. PB-001 |
IR-{YYYY}-{SEQ} gives IR-2026-042; {PB}-REG-{YYMMDD}-{SEQ} gives PB-005-REG-261024-007.
HTTP API
The interface uses a plain JSON API, which you can call too. Sign in with POST /api/auth/login; the session is a cookie. Every write must carry the header X-Muster: 1 (CSRF protection).
curl -c jar -H 'X-Muster: 1' -H 'Content-Type: application/json' \
-d '{"username":"analyst","password":"muster-analyst"}' \
http://localhost:8080/api/auth/login
curl -b jar http://localhost:8080/api/incidents
| Method | Path | Role | What it does |
|---|---|---|---|
| GET | /api/health | — | liveness check |
| POST | /api/auth/login · /logout | — | open or close a session |
| GET | /api/auth/me | — | the signed-in user, or null |
| POST | /api/auth/password | viewer | change your password |
| GET | /api/events | viewer | server-sent events stream of changes |
| GET | /api/playbooks · /:id · /:id/yaml | viewer | list, read, export |
| POST | /api/playbooks | editor | create, or duplicate one |
| PATCH | /api/playbooks/:id | editor | name, category, severity, description |
| PUT | /api/playbooks/:id/draft | editor | save the draft (with conflict detection) |
| POST | /api/playbooks/:id/publish · /restore | editor | publish a version, restore one into the draft |
| DELETE | /api/playbooks/:id | admin | archive |
| GET | /api/incidents · /:id | viewer | list, read |
| POST | /api/incidents | analyst | declare an incident or start an exercise |
| POST | /api/incidents/:id/advance · /tasks · /notes · /meta · /speed · /stop | analyst | move on, tick tasks, notes, name and severity, clock speed, stop |
| POST | /api/incidents/:id/report | analyst | create the report |
| GET | /api/reports · /:id · /:id/pdf · /:id/markdown | viewer | list, read, export |
| PATCH | /api/reports/:id | analyst | edit the ID, severity, summary, notes or template; refresh from the incident |
| DELETE | /api/reports/:id | admin | delete |
| GET | /api/templates | viewer | list templates |
| POST | /api/templates · /:id/default | editor | create, make default |
| PUT | /api/templates/:id | editor | update |
| DELETE | /api/templates/:id | editor | delete |
| GET | /api/users · /api/audit | admin | accounts, audit log |
| POST | /api/users | admin | create an account |
| PATCH | /api/users/:id | admin | role, name, disable |
Architecture
packages/core shared TypeScript: types, the 10 playbooks, validation, diffs,
incident engine, report data
packages/pdf report → PDF (pdf-lib), shared by the server and the online build
apps/api Node.js HTTP API: SQLite storage, accounts and roles, live updates
apps/web React interface
scripts/ build (esbuild) and demo launcher
tests/ API tests (node:test) and a browser walk-through (Playwright)
- No framework, few dependencies. The API uses Node's built-in
httpandnode:sqlite; the only runtime dependency ispdf-lib, bundled intodist/server.js. The Docker image needs nonode_modules. - Storage is isolated in
apps/api/src/db.tsandrepo.ts. Moving to PostgreSQL means rewriting those two files. - Security: scrypt password hashing, HttpOnly SameSite session cookies, the
X-Musterheader on every write, login rate limiting, a strict Content-Security-Policy, role checks on every endpoint and an audit log.
Development
npm run dev # rebuilds on change; run `npm start` in another terminal
npm test # API tests
npm run test:e2e # browser walk-through, screenshots in tests/screens
npm run typecheck
The browser walk-through needs Chromium once: npx playwright install chromium.
This site
The site lives in site/ and is published by .github/workflows/pages.yml on every push to main. In the repository settings, set Pages → Source to GitHub Actions. The hero film is rendered from code in hero-film/ using real screens of the app.
Online version
npm run build:online produces dist-online/muster.html, a single page that runs the same interface without a server, as a Claude artifact: data lives in the page's shared storage, people sign in with their Claude account, and the share menu sets roles (Editor edits playbooks and templates, Contributor runs incidents and writes reports, Viewer reads). npm run test:online checks it in a browser against a stand-in for that runtime.
Known limits
- Accounts are local. SSO (OIDC, SAML) is not built in yet.
- SQLite suits a single server and a team-sized load. For high availability, move the storage layer to PostgreSQL.
- PDF text uses the standard Helvetica and Courier fonts, which cover Western European languages. Other scripts are dropped from the PDF; the on-screen report shows them.