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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ See [docs/features.md](docs/features.md) for a full breakdown of each tool's cap
>
> 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 the API key's permissions, see [Required permissions](docs/permissions.md) (stateful) or [Serverless permissions](docs/permissions-serverless.md) (Elastic Cloud Serverless Security projects). The stateful Quickstart 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
175 changes: 175 additions & 0 deletions docs/permissions-serverless.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Permissions — Elastic Cloud Serverless (Security project)

This guide covers the MCP app on **Elastic Cloud Serverless Security projects**. Serverless ships a curated set of built-in role identities; you can also create custom roles if the built-ins don't fit.

> **Stateful deployments:** See [permissions.md](./permissions.md) for self-managed and Elastic Cloud Hosted.

---

## Built-in role identities

Serverless Security projects pre-provision role-specific users in the file realm. You don't create or configure them — they're ready to use. Below is the observed behavior of three representative tiers against all MCP app operations.

### Role tier summary

| Operation | `t1_analyst` | `t2_analyst` | `soc_manager` |
|---|:---:|:---:|:---:|
| **Alerts** | | | |
| Fetch alerts | ✓ | ✓ | ✓ |
| Acknowledge alert | ✓ | ✓ | ✓ |
| Get alert context | ✓ | ✓ | ✓ |
| Endpoint events readable | ✓ | ✓ | ✓ |
| **Cases** | | | |
| List cases | ✓ | ✓ | ✓ |
| Get case | ✓ | ✓ | ✓ |
| Create case | ✗ | ✓ | ✓ |
| Update case | ✗ | ✓ | ✓ |
| Add comment | ✗ | ✓ | ✓ |
| Attach alert to case | ✗ | ✓ | ✓ |
| **Rules** | | | |
| Find rules | ✓ | ✓ | ✓ |
| Noisy rules | ✓ | ✓ | ✓ |
| Create rule | ✗ | ✗ | ✓ |
| Patch rule | ✗ | ✗ | ✓ |
| Bulk rule action | ✗ | ✗ | ✓ |
| List exceptions | ✓ | ✓ | ✓ |
| Add exception | ✗ | ✗ | ✓ |
| **Attack Discovery** | | | |
| Fetch discoveries | ✓ | ✓ | ✓ |
| List AI connectors | ✓ | ✓ | ✓ |
| Assess confidence | ✓ | ✓ | ✓ |
| Get discovery detail | ✓ | ✓ | ✓ |
| Acknowledge discoveries | ✓ | ✓ | ✓ |
| **Threat Hunt** | | | |
| Execute ES\|QL | ✓ | ✓ | ✓ |
| List indices (`_cat/indices`) | ✗ | ✗ | ✗ |
| Get field mapping | ✗ | ✗ | ✓ |
| **Sample Data** | | | |
| Check existing data | ✓ | ✓ | ✓ |
| Generate sample data | ✗ | ✗ | ✓ |
| Cleanup sample data logs | ✗ | ✗ | ✓ |
| Cleanup sample data alerts | ✓ | ✓ | ✓ |

### Role capability profiles

**`t1_analyst`** — read-only across all Security surfaces. Can read and acknowledge alerts, view cases and rules, run ES|QL queries, and access Attack Discovery. Cannot write cases, create or modify rules, list raw index names, or generate sample data. Closest stateful equivalent: `viewer` + alert-write index privileges.

**`t2_analyst`** — adds full case management on top of `t1_analyst`. Can create, update, and comment on cases and attach alerts to cases. Still cannot manage rules or list indices. Closest stateful equivalent: `editor` on Cases only, `viewer` on everything else.

**`soc_manager`** — full operational access. Can manage rules (create, patch, bulk actions), add exceptions, generate and clean up sample data, and get field mappings. The one gap shared with all tiers is `listIndices` — `_cat/indices` is restricted by the cluster privilege `read_project_routing` that Serverless built-ins hold (not `monitor`).

### `listIndices` limitation

All built-in Serverless Security roles use `cluster: read_project_routing` rather than the `cluster: monitor` privilege that stateful deployments use. `_cat/indices/<pattern>` requires `monitor`, so the index-picker in the Threat Hunt tool cannot enumerate available indices for any built-in role. Workaround: use a [custom role](#custom-roles) that includes `cluster: monitor`.

---

## Connecting with a built-in role identity

The MCP app requires an API key. Create one in Kibana under **Stack Management → API Keys** while logged in as the user whose role you want to use. The key inherits that user's role permissions.

Use the `encoded` value from the created key as `elasticsearchApiKey` in your cluster config.

---

## Custom roles

Custom roles are supported on Serverless Security projects (GA since October 2024). Create them with `PUT /_security/role/<name>` in Kibana Dev Tools. The Kibana feature privilege names on serverless differ slightly from stateful 9.4+:

| Feature | Serverless privilege (all/read) |
|---|---|
| SIEM | `feature_siemV5.all` / `feature_siemV5.read` |
| Cases | `feature_securitySolutionCasesV3.all` / `feature_securitySolutionCasesV3.read` |
| Rules | `feature_securitySolutionRulesV2.all` / `feature_securitySolutionRulesV2.read` |
| Alerts | `feature_securitySolutionAlertsV1.all` / `feature_securitySolutionAlertsV1.read` |
| AI Assistant | `feature_securitySolutionAssistant.all` / `feature_securitySolutionAssistant.read` |
| Attack Discovery | `feature_securitySolutionAttackDiscovery.all` / `feature_securitySolutionAttackDiscovery.read` |
| Timeline | `feature_securitySolutionTimeline.all` / `feature_securitySolutionTimeline.read` |
| Notes | `feature_securitySolutionNotes.all` / `feature_securitySolutionNotes.read` |
| Actions/Connectors | `feature_actions.all` / `feature_actions.read` |

Note: `feature_securitySolutionRulesV2` on serverless vs `feature_securitySolutionRulesV4` on stateful 9.4+.

### Full-access custom role (serverless)

```
PUT /_security/role/mcp_app_full_serverless
{
"cluster": ["monitor"],
"indices": [
{
"names": [
".alerts-security.alerts-default",
".alerts-security.attack.discovery.alerts-default",
".adhoc.alerts-security.attack.discovery.alerts-default",
".internal.alerts-security.alerts-default-*",
".internal.alerts-security.attack.discovery.alerts-default-*",
".internal.adhoc.alerts-security.attack.discovery.alerts-default-*",
"logs-*",
"risk-score.risk-score-latest-*"
],
"privileges": ["read", "write", "monitor", "view_index_metadata"]
}
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": [
"feature_siemV5.all",
"feature_securitySolutionCasesV3.all",
"feature_securitySolutionTimeline.all",
"feature_securitySolutionNotes.all",
"feature_securitySolutionRulesV2.all",
"feature_securitySolutionAlertsV1.all",
"feature_securitySolutionAssistant.all",
"feature_securitySolutionAttackDiscovery.all",
"feature_actions.all"
],
"resources": ["space:default"]
}
]
}
```

Adding `cluster: monitor` fixes the `listIndices` gap that all built-in roles share.

### Read-only custom role (serverless)

```
PUT /_security/role/mcp_app_readonly_serverless
{
"cluster": [],
"indices": [
{
"names": [
".alerts-security.alerts-default",
".alerts-security.attack.discovery.alerts-default",
".adhoc.alerts-security.attack.discovery.alerts-default",
".internal.alerts-security.alerts-default-*",
".internal.alerts-security.attack.discovery.alerts-default-*",
".internal.adhoc.alerts-security.attack.discovery.alerts-default-*",
"logs-*",
"risk-score.risk-score-latest-*"
],
"privileges": ["read", "view_index_metadata"]
}
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": [
"feature_siemV5.read",
"feature_securitySolutionCasesV3.read",
"feature_securitySolutionTimeline.read",
"feature_securitySolutionNotes.read",
"feature_securitySolutionRulesV2.read",
"feature_securitySolutionAlertsV1.read",
"feature_securitySolutionAssistant.read",
"feature_securitySolutionAttackDiscovery.read",
"feature_actions.read"
],
"resources": ["space:default"]
}
]
}
```
2 changes: 1 addition & 1 deletion docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This guide defines the least-privilege roles for the Elastic Security MCP app on

> **Space ID:** Kibana index patterns include a `<space-id>` segment (e.g., `.alerts-security.alerts-<space-id>`). For most deployments this is `default`. Replace `<space-id>` with your actual space ID throughout this guide. The app currently targets the `default` space.

> **Serverless:** This guide targets stateful deployments. Serverless projects ship a different set of built-in roles (`t1_analyst`, `soc_manager`, etc.) and aren't covered here yet.
> **Serverless:** This guide targets stateful deployments. For Elastic Cloud Serverless Security projects, see [permissions-serverless.md](./permissions-serverless.md).

---

Expand Down
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"dev": "concurrently --raw \"node scripts/build-views.js --watch\" \"tsx watch main.ts\"",
"typecheck": "tsc --noEmit",
"test:permissions": "tsx scripts/test-permissions/runner.ts",
"test:permissions:serverless": "tsx scripts/test-permissions/runner.ts --mode serverless --role serverless",
"test:permissions:serverless:all": "tsx scripts/test-permissions/runner.ts --mode serverless --role serverless_all",
"skills:zip": "bash scripts/build-skill-zips.sh",
"mcpb:pack": "bash scripts/build-mcpb.sh",
"lint": "eslint .",
Expand Down
77 changes: 75 additions & 2 deletions scripts/test-permissions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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.

Supports both **stateful** (default) and **serverless** deployment modes — see [Serverless mode](#serverless-mode) below.

## Quick Start

```bash
Expand All @@ -20,14 +22,85 @@ Exit code is `0` if every check passes (or is skipped); `1` otherwise.

| Flag | Description |
|---|---|
| `--role full\|readonly\|both` | Which role(s) to test (default: `both`). |
| `--mode stateful\|serverless` | Deployment mode (default: `stateful`). Changes the default admin username and enables serverless role aliases. |
| `--role <name\|alias>` | Which role(s) to test (default: `both`). See below for valid names and aliases. |
| `--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. |

**`--role` values:**

| Value | Expands to |
|---|---|
| `full` | Custom full-access role |
| `readonly` | Custom read-only role |
| `both` (default) | `full` + `readonly` |
| `all` | `full` + `readonly` + `quickstart_full` + `quickstart_readonly` |
| `quickstart_full` | Quickstart built-in `editor` + companion role |
| `quickstart_readonly` | Quickstart built-in `viewer` + companion role |
| `quickstart` | `quickstart_full` + `quickstart_readonly` |
| `serverless_t1_analyst` | Serverless built-in `t1_analyst` user (observe-only) |
| `serverless_t2_analyst` | Serverless built-in `t2_analyst` user (observe-only) |
| `serverless_soc_manager` | Serverless built-in `soc_manager` user (observe-only) |
| `serverless` | All 3 serverless built-in roles |
| `serverless_all` | All stateful asserted + all serverless observe-only |
| `none` | No roles (cleanup-stale only) |

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

## Serverless mode

For Elastic Cloud Serverless (Security project type), start a local serverless cluster and then run:

```bash
# Start serverless ES (port 9200)
yarn es serverless --projectType=security

# Start serverless Kibana (port 5601)
yarn start --serverless=security

# In example-mcp-app-security, configure .env:
# ELASTICSEARCH_URL=http://localhost:9200
# KIBANA_URL=http://localhost:5601/kbn
# ELASTIC_PASSWORD=changeme
# (ELASTIC_USERNAME defaults to "elastic_serverless" in serverless mode)

npm run test:permissions:serverless
```

The serverless runner uses `--mode serverless`, which:
- Defaults admin username to `elastic_serverless` (instead of `elastic`)
- Authenticates the three built-in role users (`t1_analyst`, `t2_analyst`, `soc_manager`) via `grant_api_key` — they exist as file-realm users with password `changeme`
- Runs each built-in role in **observe-only mode**: all operations are exercised and results are printed, but no assertions are made. The exit code is only affected by the asserted custom roles (`full`, `readonly`) if those are also included

Custom roles (`full`, `readonly`) work on serverless too — `PUT /_security/role` is supported since the GA of custom roles in Serverless Security (Oct 2024).

To run both built-in observed roles and custom asserted roles against serverless in one pass:

```bash
npm run test:permissions:serverless:all
```

### Output for serverless built-in roles

Observed reports look like:

```
── SERVERLESS_T1_ANALYST (observe-only) ──
Layer A: skipped (built-in role — privileges not enumerable from a role descriptor)
Layer B (operations, observed — no pass/fail assertions):
[alerts]
✓ fetchAlerts — pass: array(1)
✓ acknowledgeAlert — pass: ok
...
[rules]
✗ createRule — 403: denied (403/401)
...
```

Symbols show what actually happened: `✓` = call succeeded, `✗` = 403/401, `→` = skipped or other. These results inform [`docs/permissions-serverless.md`](../../docs/permissions-serverless.md).

## 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.
Expand Down Expand Up @@ -69,7 +142,7 @@ A privilege documented in the full role is missing from `roles.ts`. Diff against
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`.
`.env` isn't loading or is missing one of `ELASTICSEARCH_URL`, `KIBANA_URL`, `ELASTIC_PASSWORD`. `ELASTIC_USERNAME` is optional (defaults to `elastic` in stateful mode, `elastic_serverless` in serverless mode). 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.
Expand Down
32 changes: 30 additions & 2 deletions scripts/test-permissions/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,35 @@ export type AssertedRoleName =
| "readonly"
| "quickstart_full"
| "quickstart_readonly";
export type RoleName = AssertedRoleName;

export type ServerlessRoleName =
| "serverless_t1_analyst"
| "serverless_t2_analyst"
| "serverless_soc_manager";

/**
* Maps each serverless built-in role identity to the Elastic Cloud
* Serverless Security project role name and the file-realm password
* used by the local dev stack.
*
* All local serverless file-realm users share password `changeme`
* (see kibana-main serverless_resources/users). The runner mints
* per-run API keys via `grant_type: "password"` — no role creation
* or native user creation is needed.
*
* Note: `viewer` is a platform-level role with no pre-provisioned file-realm
* user in the Security project. Security-specific tiers start at `t1_analyst`.
*/
export const SERVERLESS_BUILTINS: Record<
ServerlessRoleName,
{ roleName: string; password: string }
> = {
serverless_t1_analyst: { roleName: "t1_analyst", password: "changeme" },
serverless_t2_analyst: { roleName: "t2_analyst", password: "changeme" },
serverless_soc_manager: { roleName: "soc_manager", password: "changeme" },
};

export type RoleName = AssertedRoleName | ServerlessRoleName;

/**
* Custom-role descriptors for the asserted "Advanced" path. These
Expand Down Expand Up @@ -190,7 +218,7 @@ export const QUICKSTART_COMPANION_DESCRIPTORS: Record<
};

/** Any role identity the runner may exercise. */
export type AnyRoleName = AssertedRoleName;
export type AnyRoleName = RoleName;

export type OperationGroup =
| "alerts"
Expand Down
Loading
Loading