Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ad08aa3
Add minimum required permissions documentation
maxcold Apr 23, 2026
17c393f
Add test:permissions tooling and fix index monitor privilege gap
maxcold Apr 27, 2026
a2601d3
Fix acknowledgeAlert wildcard bug and document backing-index privileges
maxcold Apr 27, 2026
5c9c7e9
Add Quickstart asserted suite to test:permissions runner
maxcold Apr 28, 2026
0b44d9b
Lead permissions doc with built-in role Quickstart
maxcold Apr 28, 2026
5fd0cf7
Add acknowledgeDiscoveries privilege check (H1)
maxcold Apr 29, 2026
1bbc70b
Add assessConfidence + getDiscoveryDetail privilege checks (M6)
maxcold Apr 29, 2026
398653e
Add cleanupSampleData privilege checks for logs + alerts (H2)
maxcold Apr 29, 2026
2662700
Fix README env vars to match runner.ts
maxcold Apr 29, 2026
c8ec693
Fix appendix: Generate sample data writes alerts too
maxcold Apr 29, 2026
d60b7df
Use admin grant API to mint user API keys
maxcold Apr 29, 2026
6d9c241
Add getAlertContext privilege check (H3)
maxcold Apr 30, 2026
c87e7a2
Add getMapping privilege check (M3)
maxcold Apr 30, 2026
872c1b4
Recognize embedded "status_code":403 in bulk-endpoint errors
maxcold Apr 30, 2026
46eb5c8
Add bulkAction privilege check (H4)
maxcold Apr 30, 2026
148b345
Add listExceptions + addException privilege checks (H5)
maxcold Apr 30, 2026
6f76925
Doc updates from PR review feedback
maxcold May 8, 2026
b245303
Merge remote-tracking branch 'origin/main' into docs/minimum-required…
maxcold May 8, 2026
9abbf7d
docs: add Dev Tools TL;DR section to permissions guide
maxcold May 13, 2026
6b456cb
Merge remote-tracking branch 'origin/main' into docs/minimum-required…
maxcold May 13, 2026
af3e1cf
docs: align permissions.md with CLUSTERS_JSON config
maxcold May 13, 2026
8bcfec4
scripts: port test-permissions to new service/client architecture
maxcold May 13, 2026
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ See [docs/features.md](docs/features.md) for a full breakdown of each tool's cap
> **Just want to try it?** Download [`example-mcp-app-security.mcpb`](https://github.com/elastic/example-mcp-app-security/releases/latest) and double-click it. No Node.js, no cloning, no config files.
>
> Claude Desktop handles the rest — during install, fill in your Elasticsearch URL, Kibana URL, and API key. See [Creating an API key](docs/setup-local.md#creating-an-api-key) if you need to generate one first.
>
> For the API key's permissions, see [Required permissions](docs/permissions.md). The recommended Quickstart there uses Kibana's built-in **editor** (full-featured) or **viewer** (read-only) role plus a small companion role for index access — fastest unless you need a fully scripted custom role.

For other hosts (Cursor, VS Code, Claude Code) or building from source, see [Installation](#installation) below.

Expand Down
523 changes: 523 additions & 0 deletions docs/permissions.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/setup-claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ claude mcp add elastic-security \
> **Pinning a version:** Replace `elastic-security-mcp-app.tgz` with `elastic-security-mcp-app-<version>.tgz` (e.g., `elastic-security-mcp-app-0.2.0.tgz`).
>
> **Keeping secrets out of shell history:** swap `CLUSTERS_JSON` for `CLUSTERS_FILE=/absolute/path/to/clusters.json` pointing at a JSON file with the same array. See [Creating an API key](./setup-local.md#creating-an-api-key) and [Cluster configuration](./setup-local.md#cluster-configuration).
>
> **Permissions:** For production use, create a scoped role instead of using `superuser`. See [Minimum required permissions](permissions.md) for ready-to-paste role definitions.

## Option 2: Local server (stdio)

Expand Down
2 changes: 2 additions & 0 deletions docs/setup-claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Claude Desktop installs the extension and then opens a configuration dialog. Fil
- **Elasticsearch API Key** — see [Creating an API key](./setup-local.md#creating-an-api-key)
- **Kibana URL**

> **Permissions:** For production use, create a scoped role instead of using `superuser`. See [Minimum required permissions](permissions.md) for ready-to-paste role definitions.

After install:

- Claude Desktop may show the connector as disabled at first. Toggle it on to enable the server.
Expand Down
2 changes: 2 additions & 0 deletions docs/setup-cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Click to install:
<!-- cursor-mcp-config:END -->

> After clicking, Cursor opens its MCP settings with `CLUSTERS_JSON` pre-filled. Replace the placeholder URLs and API key with your real values. See [Creating an API key](./setup-local.md#creating-an-api-key) for how to generate your credentials.
>
> **Permissions:** For production use, create a scoped role instead of using `superuser`. See [Minimum required permissions](permissions.md) for ready-to-paste role definitions.

Or add manually to `.cursor/mcp.json`:

Expand Down
2 changes: 1 addition & 1 deletion docs/setup-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ You need an Elasticsearch API key with sufficient privileges for the operations
- **Kibana UI:** Go to **Stack Management > API Keys > Create API key**
- **Elastic docs:** [Elasticsearch API keys](https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys)

For a quick start, a key with the `superuser` role works for all tools. For production, scope the key to the minimum required privileges.
For a quick start, a key with the `superuser` role works for all tools. For production, scope the key to the minimum required privileges — see [Minimum required permissions](permissions.md) for ready-to-paste role definitions.

Kibana API keys and Elasticsearch API keys are the same underlying credential type. This project uses the same `elasticsearchApiKey` value for both Elasticsearch and Kibana requests, so you only need to configure one API key.

Expand Down
2 changes: 2 additions & 0 deletions docs/setup-vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Add to `.vscode/mcp.json`:
> **Pinning a version:** Replace `elastic-security-mcp-app.tgz` with `elastic-security-mcp-app-<version>.tgz` (e.g., `elastic-security-mcp-app-0.2.0.tgz`).
>
> **Keeping secrets out of `mcp.json`:** replace `CLUSTERS_JSON` with `CLUSTERS_FILE` pointing at the absolute path of a JSON file containing the same array. See [Cluster configuration](./setup-local.md#cluster-configuration).
>
> **Permissions:** For production use, create a scoped role instead of using `superuser`. See [Minimum required permissions](permissions.md) for ready-to-paste role definitions.

## Option 2: Local server (stdio)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"start:stdio": "node dist/main.js --stdio",
"dev": "concurrently --raw \"node scripts/build-views.js --watch\" \"tsx watch main.ts\"",
"typecheck": "tsc --noEmit",
"test:permissions": "tsx scripts/test-permissions/runner.ts",
"skills:zip": "bash scripts/build-skill-zips.sh",
"mcpb:pack": "bash scripts/build-mcpb.sh",
"lint": "eslint .",
Expand Down
99 changes: 99 additions & 0 deletions scripts/test-permissions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Permissions Test Tooling

Verifies that the role definitions documented in [`docs/permissions.md`](../../docs/permissions.md) actually work end-to-end against a real Elasticsearch + Kibana cluster. Provisions both documented roles, creates scoped API keys, and exercises every documented operation through the existing `src/elastic/*` business-logic modules.

## Quick Start

```bash
# Make sure .env has ELASTICSEARCH_URL, KIBANA_URL, ELASTIC_PASSWORD
# (and ELASTIC_USERNAME if not the default "elastic").

@KDKHD KDKHD May 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MCP app uses an API key for auth - could we keep it consistent and use the same env variables? Then users dont need to maintain 2 sets of credentials

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script is used for development and needs admin-level credentials that are distinct from the runtime MCP key:

  • It calls PUT /_security/role/... (needs manage_security), POST /_security/api_key/grant with grant_type: password (acts on behalf of a user), and seeds sample data into logs-* and .alerts-security.alerts-*.
  • MCP ELASTICSEARCH_API_KEY is by design least-privilege over those alert/case indices and does not carry manage_security. So even if we accepted the same env var name, users would have to maintain a second, a more privileged key under it to run the test runner.
  • Password-based api_key/grant is also the cleanest local-dev path because it works out of the box with elastic:changeme and produces user-scoped keys, which is what we want for testing.

# The runner authenticates as that user via Basic auth to bootstrap an
# admin API key, so the user must have manage_security plus enough
# privileges to seed sample data — `superuser` works for local dev.

npm run test:permissions
```

Exit code is `0` if every check passes (or is skipped); `1` otherwise.

## Flags

| Flag | Description |
|---|---|
| `--role full\|readonly\|both` | Which role(s) to test (default: `both`). |
| `--cleanup-stale` | Delete leftover `mcp-app-test-*` roles and API keys before running. Useful after a crashed run. |
| `--no-cleanup` | Skip cleanup at the end and print the provisioned API keys so you can re-use them for manual debugging. |
| `--verbose`, `-v` | Print fixtures, stale-cleanup actions, and other debug info. |
| `-h`, `--help` | Show help. |

Pass flags via `--`, e.g. `npm run test:permissions -- --role readonly --verbose`.

## What it does

1. **Pre-flight.** Loads admin credentials from `.env`. Calls `checkExistingData()`; if the cluster has zero security alerts, calls `generateSampleData({ count: 50 })` to seed.
2. **Capture fixtures.** Picks one alert, one case (creates one if none exist), and the first detection rule.
3. **For each role (`full`, then `readonly`):**
- Creates a role `mcp-app-test-<role>-<suffix>` and an API key scoped to it.
- **Layer A** — calls `POST /_security/user/_has_privileges` as the scoped key, asserting that every privilege listed in the role descriptor is granted.
- **Layer B** — exercises one or more operations per group (alerts, cases, rules, attack-discovery, threat-hunt, sample-data) using the actual `src/elastic/*` functions. Reads must succeed for both roles. Writes must succeed for `full` and 403 for `readonly`.
4. **Report** — prints per-role results grouped by operation group. Symbols: `✓` pass, `✗` fail, `→` skipped.
5. **Cleanup** — always (in a `finally` block, and on `SIGINT`): invalidates API keys and deletes roles created by the run.

Created cases / rules / sample documents are tagged `mcp-app-test` and are **not** automatically deleted. If they accumulate, clean them up via Kibana or with a tag-scoped delete.

## Output Interpretation

Each line in the report looks like:

```
✓ acknowledgeAlert (expect 403) — denied (403/401)
✗ createCase (expect 403) — expected 403 but call succeeded
→ patchRule (expect 403) — no fixture available for this check
```

- `pass` (`✓`) — outcome matched the expectation (call succeeded when expected `ok`, or call returned 403/401 when expected `403`).
- `fail` (`✗`) — outcome diverged from the expectation. The detail explains what happened.
- `skipped` (`→`) — the cluster did not have a fixture for this op (e.g. no rule to patch). Skipped checks do **not** affect the exit code.

The summary line lists totals: `Summary: N passed, M failed, K skipped`.

## Troubleshooting

**Read-only writes return `ok` instead of `403` (e.g. `expected 403 but call succeeded`).**
The read-only role is over-privileged. Compare `roles.ts` to the read-only role in [`docs/permissions.md`](../../docs/permissions.md) — usually a stale `"write"` snuck into `indices.privileges`, or an `*.all` Kibana feature privilege replaced an `*.read` one.

**Full-featured writes return `403`.**
A privilege documented in the full role is missing from `roles.ts`. Diff against the spec.

**Layer A reports `missing privileges: ...`.**
The role descriptor sent in the `PUT /_security/role` body doesn't include the listed privileges, or Elasticsearch rejected one of them (typo / removed feature). Check that the Kibana feature names match your stack version. The defaults target 9.4+ — see the version-specific tables in `docs/permissions.md`.

**`Fatal error: ELASTICSEARCH_URL, KIBANA_URL, and ELASTIC_PASSWORD must be set...`**
`.env` isn't loading or is missing one of `ELASTICSEARCH_URL`, `KIBANA_URL`, `ELASTIC_PASSWORD`. `ELASTIC_USERNAME` is optional (defaults to `elastic`). The script reads them via `dotenv/config`.

**`Seeding completed but no security alerts were created.`**
`generateSampleData` ran but didn't end up writing alerts. Usually means the admin key lacks `write` on `.alerts-security.alerts-default`. Use a key with at least the privileges in the full role.

**Leftover `mcp-app-test-*` roles or API keys.**
A previous run was killed before cleanup. Re-run with `--cleanup-stale` to wipe them, or delete manually via Kibana > Stack Management.

## Architecture

```
scripts/test-permissions/
├── roles.ts # Role descriptors + operation check matrix
├── elastic-admin.ts # PUT/DELETE role, POST/DELETE api_key, _has_privileges probe
├── runner.ts # Orchestrator: provision → seed → test → report → cleanup
└── README.md # This file
```

The runner reuses business logic from `src/elastic/*` and swaps the API key on the singleton config (`setConfig`) between admin and each scoped role. This forces sequential per-role execution; that's fine at this scale.

## Updating the Test Matrix

When you add or remove an operation from `src/elastic/*`, also update `operationChecks` in [`roles.ts`](./roles.ts):

- Add an entry naming the function and its group.
- Set `expect.full` to `"ok"` and `expect.readonly` to either `"ok"` (read) or `"403"` (write).
- If the call needs a fixture that may not exist, add `skipUnless` and gate it on the fixture.
- If the function silences errors internally (e.g. wraps each ES call in `try { ... } catch {}`), it's not testable here — either refactor the function to surface 403s or pick a different operation that hits the same privilege.
Loading
Loading