Skip to main content
Glama
README.md
# OpenVAS-MCP

[![Lint & Test](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/ci.yml)
[![Docker](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/docker.yml/badge.svg?branch=main)](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/docker.yml)
[![Integration tests](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/integration.yml/badge.svg?branch=main)](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/integration.yml)
[![Startup egress audit](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/startup-egress.yml/badge.svg?branch=main)](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/actions/workflows/startup-egress.yml)
![Coverage](https://raw.githubusercontent.com/CyberSecAuto-Labs/OpenVAS-MCP/badges/coverage-badge.svg)

A self-hosted MCP server that gives AI agents structured access to [OpenVAS / Greenbone](https://github.com/greenbone/openvas-scanner) vulnerability scanning — without sending your data anywhere.

OpenVAS has no native interface for AI agents. Most integrations require cloud connectivity or expose GVM credentials to every client. OpenVAS-MCP solves this:

- **Local-first.** Talks only to your GVM instance. No telemetry, no external calls — [verified by CI](.github/workflows/startup-egress.yml).
- **Credential isolation.** AI agents authenticate to the MCP server; the server holds the single GVM service account.
- **Thin bridge.** Returns structured scan data as-is. Analysis and reporting logic belong in the agent or a platform built on top.

See [docs/architecture.md](docs/architecture.md) for a full architecture diagram and design details.

## Quick start

### 0. Vibeinstall (optional, if you trust claude more than yourself)

Run in your terminal:

```bash
claude "install this, make no mistake."
```

If you prefer to stay in control, follow the manual setup below.

### 1. Get a GVM instance

Don't have one? Spin up the bundled Greenbone Community Edition stack:

```bash
docker compose -f docker/openvas/compose.yaml up -d
```

### 2. Connect an MCP client

#### stdio (Claude Desktop, Cursor, Windsurf, Cline, …)

**Requirements:** Python 3.10+

```bash
git clone https://github.com/CyberSecAuto-Labs/OpenVAS-MCP
cd OpenVAS-MCP
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

Add to `mcpServers` in your client config file:

```json
{
  "mcpServers": {
    "openvas": {
      "command": "/path/to/.venv/bin/python",  // ← edit this to your venv path
      "args": ["-m", "openvas_mcp"],
      "env": { "GVM_PASSWORD": "secret" }  // ← edit this to your GVM password
    }
  }
}
```

Config file locations:

| Client | Path |
|---|---|
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
| Cursor | `~/.cursor/mcp.json` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| Cline / Roo Code | via the MCP panel in the VS Code extension |

#### HTTP/SSE (networked agents)

**Requirements:** Docker

Download the compose files from the [latest release](https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/releases/latest) and run:

```bash
# GVM running locally via Unix socket
MCP_API_KEYS="supersecrettoken:my-agent" GVM_PASSWORD=secret docker compose up

# GVM on a remote host via TCP
MCP_API_KEYS="supersecrettoken:my-agent" GVM_HOST=192.168.1.10 GVM_PASSWORD=secret docker compose up
```

> [!NOTE]
> `MCP_API_KEYS` is a comma-separated list of `token:name` pairs sent as a Bearer token by the MCP client. Multiple clients: `"tok1:agent1,tok2:agent2"`. Pass `MCP_ALLOW_UNAUTHENTICATED=1` instead to skip auth on a trusted network.

Point your MCP client at the server:

```json
{
  "mcpServers": {
    "openvas": {
      "url": "http://your-server:8000/sse",  // ← edit this to your server address
      "headers": {
        "Authorization": "Bearer supersecrettoken"  // ← your MCP_API_KEYS token
      }
    }
  }
}
```

> [!WARNING]
> Plain TCP connections (`GVM_HOST` set, `GVM_TLS` unset) send GVM credentials unencrypted. Use `GVM_TLS=1` or a Unix socket for anything beyond local dev.

### All-in-one dev setup

Greenbone Community Edition + MCP server from source in one go:

```bash
# Start the Greenbone stack
docker compose -f docker/openvas/compose.yaml up -d

# Start the MCP server, connected via gvmd socket
GVM_PASSWORD=secret docker compose -f compose.yaml -f compose.override.yaml up --build
```

> [!TIP]
> See [`compose.override.yaml`](compose.override.yaml) for how the socket volume is mounted.

## Configuration

| Variable | Default | Description |
|---|---|---|
| `GVM_PASSWORD` | — | GVM password (required) |
| `GVM_SOCKET_PATH` | `/run/gvmd/gvmd.sock` | Unix socket path (default connection) |
| `GVM_HOST` | — | Connect via TCP instead of socket (IPv4 and IPv6) |
| `MCP_TRANSPORT` | `stdio` | `stdio`, `sse`, or `streamable-http` |
| `MCP_API_KEYS` | — | Bearer API keys for HTTP transport auth (`token:name,...`) |
| `MCP_FILTER_VALIDATION` | `strict` | `strict` rejects a filter term GVM would silently drop; `warn` logs it and passes it through |

See [docs/configuration.md](docs/configuration.md) for the full reference, including TLS options, policy file, scan limits, and logging.

## Available tools

| Tool | Description |
|---|---|
| `list_targets` | Return all scan targets, with host counts |
| `create_target` | Create a target with specified hosts/CIDRs |
| `list_tasks` | Return scan tasks with severity, last-report and target info, optionally narrowed with a GMP filter term |
| `start_scan` | Create and start a scan against a target |
| `start_task` | Re-run an existing scan task by UUID |
| `get_scan_status` | Poll status and progress of a running scan |
| `fetch_scan_results` | Retrieve findings, optionally filtered by minimum severity |

**Example:** `"Scan 192.168.1.0/24 and show me anything above severity 7"` — the agent calls `create_target` → `start_scan` → `get_scan_status` → `fetch_scan_results(min_severity=7.0)`.

**Example:** `"Re-run the weekly scan"` — the agent calls `list_tasks(filter_string="name~weekly")` to resolve the task UUID, then `start_task` → `get_scan_status` → `fetch_scan_results`. Unlike `start_scan`, this adds a report to the existing task's history instead of creating a duplicate task. GVM applies a default page size to `list_tasks`; pass `rows=-1` to return every task.

**Example:** `"Re-run every single-host task whose last report is over a month old and scored above 5"` — a single `list_tasks` call returns every field those three criteria need, with no follow-up call to resolve host counts:

```python
list_tasks(filter_string="severity>5 and last<-1M rows=-1")
# [{"id": "11111111-1111-1111-1111-111111111111", "name": "weekly-dmz-scan",
#   "status": "Done", "progress": "-1",
#   "last_report": "22222222-2222-2222-2222-222222222222",
#   "last_report_date": "2026-08-14T02:15:00+02:00",
#   "severity": 6.5, "report_count": 3, "finished_report_count": 3, "trend": "same",
#   "target_id": "33333333-3333-3333-3333-333333333333",
#   "target_name": "dmz-web-01", "host_count": 1}, …]
```

The agent then keeps the rows with `host_count == 1` and calls `start_task` on each. A `null` `severity` or `host_count` means *unresolved* (no report yet, or the target is gone), never zero.

### GMP filter syntax

Three things about GMP filters are easy to get wrong, so `list_tasks` documents them in its tool description and rejects what GVM would quietly mishandle:

| | |
|---|---|
| **Terms OR by default** | `severity>5 total<4` returns the *union* — more rows, not fewer. Write `severity>5 and total<4` to intersect. |
| **`m` is minutes, `M` is months** | `last<-1m` means "older than one minute" and matches nearly everything. Use `last<-1M`. Units: `s m h d w M y`. |
| **Unknown terms are dropped, not rejected** | GVM silently ignores `zzzbogus<4` and returns every task. The server rejects unsupported keywords and unparseable values with `validation_error` instead — set `MCP_FILTER_VALIDATION=warn` to pass them through. |

## Release integrity

Every release image is:

- **Signed** with [cosign](https://github.com/sigstore/cosign) keyless OIDC signing — no long-lived key to compromise.
- **SBOM attached** — a [CycloneDX](https://cyclonedx.org) JSON bill of materials is generated with [syft](https://github.com/anchore/syft) and attached to each GitHub Release.
- **Vulnerability-scanned** — [grype](https://github.com/anchore/grype) scans the SBOM on every PR (`vuln-scan.yml`) and at release time, failing on fixable `high` severity findings (`--only-fixed`). CPython CVEs whose only fixes land on a release line the image does not ship are filtered out as unactionable — see [CI pipelines](docs/ci.md#interpreter-cves-and-the-release-line-filter).
- **Egress-audited** — the [`startup-egress` workflow](.github/workflows/startup-egress.yml) traces `connect()` syscalls via `strace` on every push and PR, asserting no unexpected outbound connections at startup. Integration tests extend this to live GMP code paths.

Verify the image signature before running:

```bash
cosign verify \
  --certificate-identity-regexp "https://github.com/CyberSecAuto-Labs/OpenVAS-MCP/.github/workflows/release.yml@refs/tags/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/cybersecauto-labs/openvas-mcp:<version>
```

## Docs

- [docs/architecture.md](docs/architecture.md) — architecture diagram, component overview, and transport details
- [docs/configuration.md](docs/configuration.md) — full environment variable reference, TLS, policy file, scan limits, logging
- [docs/design.md](docs/design.md) — design decisions and known limitations
- [docs/ci.md](docs/ci.md) — CI workflows, guarantees, and tradeoffs

## License

[Apache 2.0](LICENSE)