内容
<p align="center">
<img src="media/vulnerator-ai-wallpaper-desktop.jpeg" alt="VULNERATOR AI — cyberpunk key art: a hacker astronaut floating in orbit around a 'DATA HAVEN' station amid drifting servers and circuit boards, with the death-metal 'Vulnerator AI' logo and the taglines 'Unholy Pentest', 'offerings to the Kali' and 'summoned from the depths of the OODA abyss · mmxxvi'." width="880">
</p>
<p align="center">
<a href="https://github.com/irraz/vulnerator-ai/releases/latest">
<img src="https://img.shields.io/github/v/release/irraz/vulnerator-ai?style=for-the-badge&label=version&color=b03a3a&labelColor=0a0a0a" alt="Latest release">
</a>
</p>
<p align="center">
<sub><b>Built with</b></sub>
</p>
<p align="center">
<a href="https://www.docker.com/">
<img src="https://img.shields.io/badge/Docker-containers-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker containers">
</a>
<a href="https://hermes-agent.nousresearch.com/">
<img src="https://img.shields.io/badge/Hermes%20Agent-Nous%20Research-7A5CFF?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0wIDE4Yy00LjQxIDAtOC0zLjU5LTgtOHMzLjU5LTggOC04IDggMy41OSA4IDgtMy41OSA4LTggOHoiLz48L3N2Zz4=&logoColor=white" alt="Hermes Agent — Nous Research">
</a>
<a href="https://www.kali.org/">
<img src="https://img.shields.io/badge/Kali%20Linux-pentest%20runtime-557C94?style=for-the-badge&logo=kalilinux&logoColor=white" alt="Kali Linux">
</a>
</p>
<p align="center">
<sub>
Containerized with <a href="https://www.docker.com/">Docker</a> ·
driven by <a href="https://hermes-agent.nousresearch.com/">Hermes Agent</a> from
<a href="https://nousresearch.com/">Nous Research</a> ·
tooling on <a href="https://www.kali.org/">Kali Linux</a>
</sub>
</p>
OODA pentest driven by **Hermes** talking **directly** to a **Kali** container via MCP.
## What it is
`vulnerator-ai` runs an OODA (Observe-Orient-Decide-Act) pentest loop where
the driving brain is [Hermes](https://github.com/NousResearch/hermes) and the
tools run on a Kali governed by an MCP server. Hermes decides what
to run and launches it **directly** against the target through the Kali MCP.
It is a deliberately minimal design: **no core, no REST API, no UI, no gates**.
Four containers (the agent, two tool MCP servers and the MCP front door) and the agent driving
end to end.
## Architecture (4 containers)

- **kali** — deterministic runtime for pentest and collection tools (nmap, Prowler,
ScoutSuite, Cloudsplaining, ...). Exposes a `kali-mcp` server on `:8900` (and the artifacts
channel on `:8901`).
- **playwright-mcp** — real browser (headless Chromium, official Microsoft image) exposed
over MCP on `:8931`. Gives Hermes the web surface (navigation/DOM/interaction) without re-enabling
any native toolset. The `web-pentest` skill uses it.
- **ooda-agent** — Hermes (`nousresearch/hermes-agent`) as the OODA loop driver. It is a client
of both MCPs (`kali:8900`, `playwright-mcp:8931`) and runs the pentest tools directly.
It keeps its gateway alive with `hermes gateway run` (OpenAI-compatible API server on `:8642`,
**published to the host** by default so you can pilot it from an external assistant — see
"Pilot Hermes from an external assistant").
- **vulnerator-mcp** — a thin MCP **front door** over Hermes' `:8642` runs API so external MCP
clients (Claude Code / Kiro, BugTraceAI, ...) can trigger/observe/steer/stop a pentest as MCP
tools. It is **up by default** (4th container) because the intended UX is driving the engine
from an MCP client. In the default stack it runs **streamable-HTTP on the internal network**
(`http://vulnerator-mcp:8700`, **no host port**) and is **fail-closed** on
`VULNERATOR_MCP_INBOUND_TOKEN`. For **local** Claude Code / Kiro the recommended path is instead
the **stdio** mode (`docker run -i …`, zero network surface, no token). It wraps, does **not**
replace, the HTTP API: it exposes **only** the runs lifecycle (no raw `kali_run`, no
`/v1/chat/completions`) so the client stays a trigger/observer, never the brain. See "Consume
Hermes from an MCP client (`vulnerator-mcp`)" below.
There are no `api` or `ui` services: the agent talks directly to the MCPs (Kali and browser),
and the MCP front door only forwards the runs lifecycle.
### Tools included in the Kali image
The Kali image bakes in a **curated** set of pentest CLIs the skills assume exist and invoke via
`kali_run` (the base `kalilinux/kali-rolling` image is minimal and ships none of them). This is a
deliberate, small selection — **not** the full `kali-linux-headless` metapackage — to keep the
image reasonably sized:
| Baked-in tool | Used for |
| --- | --- |
| `nmap` | network/port/service enumeration |
| `whatweb` | web tech fingerprinting |
| `nuclei` | templated vulnerability scanning |
| `ffuf` | web fuzzing |
| `sqlmap` | SQL injection testing |
| `gobuster` | directory/DNS/vhost brute forcing |
| `impacket-scripts` | Impacket CLI helpers (AD/network) — invoked with the `impacket-` prefix, e.g. `impacket-secretsdump`, `impacket-GetUserSPNs` |
| `bloodhound.py` | BloodHound Python ingestor (AD) — command `bloodhound-python` |
Plus the cloud-collection tooling in the Python venv (Prowler, ScoutSuite, Cloudsplaining, Pacu).
Anything else a specific engagement needs can still be `apt install`-ed on the fly inside the
`kali` container; note it lives in the container's ephemeral layer and disappears on the next
`down`/`up` (bake it into the `Dockerfile` to make it permanent — see "Start clean between
exercises").
## How to bring it up
**All keys and credentials go in a `.env` file** at the repo root. It is not
versioned (it is in `.gitignore`) and is the only place where you put secrets. Start by copying
the template, which documents every variable:
```sh
cp .env.example .env
```
Open the `.env` and fill in the values. At a minimum you need the three required ones:
| Variable | Required | Where to put it / how to get it |
| --- | --- | --- |
| `OPENROUTER_API_KEY` | **Yes** | Your OpenRouter API key (the provider of the LLM that reasons the OODA). Create it at <https://openrouter.ai/keys> and paste it into the `.env`. |
| `HERMES_API_SERVER_KEY` | **Yes** | Key for Hermes' API server (min. 8 chars). Generate one with `openssl rand -hex 32` and paste it into the `.env`. It is also the Bearer to pilot Hermes over HTTP. |
| `VULNERATOR_MCP_INBOUND_TOKEN` | **Yes** | Inbound Bearer for the `vulnerator-mcp` front door, which is **up by default** in streamable-HTTP mode. Generate one with `openssl rand -hex 32` and paste it into the `.env`. The default stack is **fail-closed** on this: `docker compose up` aborts without it. It is a **separate** secret from `HERMES_API_SERVER_KEY` and gates a different edge. (The local **stdio** mode for Claude Code / Kiro does not need it.) |
The **AWS credentials are OPTIONAL**: they are only needed if you are going to use the cloud
collection tools (Prowler / ScoutSuite / Cloudsplaining). If you are not auditing AWS, leave these
variables empty in the `.env`:
| Variable | Required | Where to put it / how to get it |
| --- | --- | --- |
| `AWS_ACCESS_KEY_ID` | No | **Read-only** credential for the account you are AUTHORIZED to audit. In the `.env`. |
| `AWS_SECRET_ACCESS_KEY` | No | Secret for that same credential. In the `.env`. |
| `AWS_SESSION_TOKEN` | No | Only for temporary credentials (STS). In the `.env`. |
| `AWS_REGION` | No | Default region (e.g. `us-east-1`). In the `.env`. |
| `OPENROUTER_MODEL` | No | Overrides the OpenRouter model Hermes reasons with. Empty = the profile default (`mistralai/mistral-large-4-0`). **Caveat:** the project uses Mistral Large on purpose because models like Claude (`anthropic/*`) refuse the authorized-offensive pentest framing (OpenRouter/Anthropic returns "violative cyber content"); overriding with such a model may make the agent refuse to work. |
| `VULNERATOR_MCP_HOST_PORT` | No | HOST port to publish the `vulnerator-mcp` streamable-HTTP server. By default it publishes **no** host port (internal docker network only). Set this **and** uncomment the service's `ports:` block in `docker-compose.yml` to publish on a controlled machine. |
The rest of the `.env.example` variables (host ports, artifacts token, Prowler
scoping) are optional and come with defaults; leave them empty unless you need to
change them. Once your `.env` is ready:
```sh
docker compose up -d --build
```
### Local inference with Ollama (optional)
By default Hermes reasons with Mistral Large on OpenRouter. If you prefer to run the model
yourself, you can point Hermes at an [Ollama](https://ollama.com) endpoint (OpenAI-compatible)
instead. This is **opt-in** and fully controlled from the `.env`:
| Variable | Required | Where to put it / how to get it |
| --- | --- | --- |
| `OLLAMA_BASE_URL` | No | Empty = use OpenRouter (default). Set it to an Ollama OpenAI-compatible endpoint to use Ollama instead: `http://host.docker.internal:11434/v1` for a local host Ollama, or `http://<host>:11434/v1` for a remote one. When set, `OPENROUTER_API_KEY` is **not** required. |
| `OLLAMA_MODEL` | No | The Ollama model name used in Ollama mode, e.g. `qwen2.5-coder:32b`. |
At startup the stack branches on `OLLAMA_BASE_URL`: when it is empty the OpenRouter path is used
exactly as before (and a missing `OPENROUTER_API_KEY` still aborts startup); when it is set, the
startup script runs `hermes config set model.provider custom`, `model.base_url <OLLAMA_BASE_URL>`
and `model.default <OLLAMA_MODEL>`, so Hermes talks to Ollama.
Caveats before you rely on it:
- **Context window (hard blocker):** Hermes refuses to start against an endpoint serving less than
~64K tokens of context, and Ollama defaults to as low as 4K. Raise it **server-side** where Ollama
runs with `OLLAMA_CONTEXT_LENGTH=64000` (it cannot be raised through the API).
- **Networking:** make Ollama listen on all interfaces (`OLLAMA_HOST=0.0.0.0`) so the container can
reach it. On Linux, reaching a host-run Ollama relies on the
`extra_hosts: ["host.docker.internal:host-gateway"]` mapping already set on `ooda-agent` (Docker
Desktop on macOS/Windows resolves `host.docker.internal` automatically).
- **Model suitability (experimental):** every pentest action routes through MCP, so the model must do
reliable tool/function-calling and must not refuse the authorized-offensive pentest framing. Parity
with Mistral-Large-on-OpenRouter is **not guaranteed** — treat local inference as experimental and
validate your chosen model before relying on it.
## How to drive the pentest (interactive TUI)
```sh
docker compose up -d --build
docker compose exec -it ooda-agent hermes chat
```
`hermes chat` opens the agent's interactive TUI/REPL. From there you ask it for a pentest
in natural language; Hermes drives the OODA and runs the tools on Kali
**directly** with the `kali_run` tool (talking to `kali:8900`).
`hermes gateway run` (the container's command) keeps the container alive and starts
Hermes' API server on `:8642`. That API server is an **alternative programmatic** path
(OpenAI-compatible) and is **published to the host** by default, to pilot Hermes from an
external assistant without entering the TUI (see the next section).
### Quickstart against a lab target (Juice Shop)
To practice the `web-pentest` skill without going out to the internet or touching real systems,
the repo ships an **optional** target override, `docker-compose.target.yml`, that adds an
[OWASP Juice Shop](https://owasp.org/www-project-juice-shop/) service. Juice Shop is a
**deliberately-vulnerable** web application (OWASP project), the de facto standard for practicing
web pentesting (XSS, SQLi, broken auth, etc.).
Bring the whole stack up **with** the target by layering both compose files:
```sh
docker compose -f docker-compose.yml -f docker-compose.target.yml up -d --build
```
The target joins the **same internal docker network** and publishes **no port to the host**, so it
only exists inside the docker environment — scoping the exercise to a controlled lab. Hermes/Kali
reach it by its DNS name at **`http://juice-shop:3000`**. Point an engagement at that URL, e.g. via
the interactive TUI:
```sh
docker compose -f docker-compose.yml -f docker-compose.target.yml exec -it ooda-agent hermes chat
# then ask, in natural language:
# "Authorized web pentest against http://juice-shop:3000 (scope: that host only).
# Enumerate, fingerprint, and test for injection/auth flaws. Report confirmed findings."
```
This is the authorized target for practicing the `web-pentest` skill. Only the web lab is in scope
here (no AD or other labs for now).
## Pilot Hermes from an external assistant (Kiro / Claude)
Sometimes you don't want to enter the `hermes chat` TUI: you want an assistant (Kiro, Claude, a
CI script) to **trigger and observe** a pentest over HTTP. Hermes already exposes an
**OpenAI-compatible** API server (enabled in the profile, `agent-ooda/profile/config.yaml` →
`platforms.api_server`); all you need is to reach it.
**Role of the external assistant: trigger and observer, not brain.** The one that reasons and runs the
pentest is Hermes (driven by the `mistralai/mistral-large-4-0` model, chosen on purpose
because Claude rejects the offensive framing of authorized pentesting). The assistant only sends the
engagement prompt and reads the events; it must **not** try to "think" the pentest or reinterpret
the Kali tools.
Authentication: all calls go with `Authorization: Bearer <HERMES_API_SERVER_KEY>`
(the same key from the `.env`).
### Two access modes
**Mode A — published port (recommended).** The compose publishes `:8642` to the host (configurable
with `HERMES_API_HOST_PORT`, default `8642`). The assistant calls `http://localhost:8642`
directly:
```sh
export KEY=$(grep '^HERMES_API_SERVER_KEY=' .env | cut -d= -f2)
# alive?
curl -s http://localhost:8642/health # -> {"status":"ok"}
```
**Mode B — without opening a port (`exec`).** If you prefer not to publish `:8642` to the host, delete the
`ports:` block of the `ooda-agent` service and talk to the API from inside the container:
```sh
docker compose exec ooda-agent \
curl -s http://localhost:8642/health
# If you launched the stack with '-p <project>', pass it here too:
# docker compose -p pentest-ana exec ooda-agent curl -s http://localhost:8642/health
```
All the `curl` calls below are the same in both modes; only the prefix changes (direct host vs.
`docker compose exec ooda-agent`).
### Main flow (runs API)
The `runs` pattern is designed precisely for this: launch a long job and subscribe to its
progress instead of handling the streaming by hand.
```sh
# 1) Launch the pentest. The body carries the engagement in natural language in the "input" field.
# It responds {"run_id":"run_abc123","status":"started"}.
curl -s http://localhost:8642/v1/runs \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "Authorized pentest against 10.0.0.0/24 (scope: that /24 only). Enumerate, identify exposed services and test access paths. Report confirmed findings with evidence."
}'
# 2) Observe progress: SSE stream of lifecycle events (OODA, tool.start/complete, ...).
curl -N http://localhost:8642/v1/runs/run_abc123/events \
-H "Authorization: Bearer $KEY"
# 2b) Or query the point-in-time status (status: started | running | completed | failed | cancelled).
curl -s http://localhost:8642/v1/runs/run_abc123 \
-H "Authorization: Bearer $KEY"
# 3) (Optional) Inject guidance mid-run. Only while status=running; it is delivered at the
# next tool boundary. If the run no longer accepts steer, it responds 409.
curl -s http://localhost:8642/v1/runs/run_abc123/steer \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"input": "Focus on port 445 first."}'
# 4) (Optional) Interrupt the run.
curl -s http://localhost:8642/v1/runs/run_abc123/stop \
-H "Authorization: Bearer $KEY"
```
Fields of the `POST /v1/runs` body (verified against Hermes' official docs): `input`
(string, required) and, optional, `session_id`, `instructions`, `conversation_history`,
`previous_response_id`. For safe retries you can send the `Idempotency-Key` header.
### Alternative: `POST /v1/chat/completions` (OpenAI-compatible)
If your client already speaks the OpenAI format, you can use the classic endpoint (SSE streaming)
instead of the runs API:
```sh
curl -s http://localhost:8642/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "hermes-agent",
"messages": [{"role": "user", "content": "Authorized pentest against 10.0.0.0/24 (scope: that /24 only). Report confirmed findings with evidence."}]
}'
```
### Instructions for the assistant
Pattern a Kiro/Claude should follow to pilot an engagement:
1. `GET /health` to confirm the stack is alive (`{"status":"ok"}`).
2. `POST /v1/runs` with the engagement in `input` (explicit target and **scope**, in natural
language). Save the `run_id` from the response.
3. Follow the run to a terminal state: read the SSE from `GET /v1/runs/{run_id}/events`, or
poll `GET /v1/runs/{run_id}` until `completed` / `failed` / `cancelled`.
4. Summarize the result to the user (findings, final status). For the formal deliverable, Hermes
itself generates the Markdown report via the `pentest-report` skill and leaves it as an artifact in the
data channel (`:8901`).
The assistant **does not decide** the tools nor reinterpret the Kali output: that is Hermes' job.
Its role is to trigger, observe and summarize.
### Security note
Publishing `:8642` to the host **lowers the network barrier** the stack had on purpose (before, the API
server was only reachable over the internal docker network). From there on, the endpoint's only protection
is the `HERMES_API_SERVER_KEY` Bearer: use it **only on a controlled machine/environment**, and if you don't
need external access use Mode B (`exec`, without opening a port). This is consistent with the
⚠️ Warning further down: there is no scope-guard or authorization gate, responsibility for the
scope rests entirely with the operator.
## Consume Hermes from an MCP client (`vulnerator-mcp`)
If your tooling speaks **MCP** rather than raw HTTP (Claude Code, Kiro, Cursor, BugTraceAI, …),
the stack ships a thin MCP server — **`vulnerator-mcp`** (`docker/mcp/`) — that wraps Hermes'
`:8642` runs API and exposes it as MCP tools. It wraps, it does **not** replace, the HTTP API: it
exposes **only** the runs lifecycle, so the client stays a trigger/observer and never the brain.
It runs in **two modes**: a **streamable-HTTP** service that is **up by default** with the stack
(for in-docker consumers like BugTraceAI, and for host-published use), and a **stdio** mode that
**local** Claude Code / Kiro spawn as a subprocess (recommended locally; zero network surface).
Five tools + a health check, mapped 1:1 onto `:8642`:
| Tool | Does |
| --- | --- |
| `start_pentest(input, scope, …)` | starts a run (`POST /v1/runs`); returns `{run_id, status, replayed}` |
| `get_run_status(run_id)` | the full run object (status + `completed`/`partial`/`interrupted` + output + usage) |
| `stream_run_events(run_id, cursor, …)` | pages the SSE events with a `seq` cursor (`{events, next_cursor, done}`); also emits one MCP progress notification per page (batch-completion telemetry) over streamable-HTTP |
| `steer_run(run_id, input)` | injects a correction (`POST …/steer`); surfaces the 409 `run_not_accepting_steer` verbatim |
| `stop_run(run_id)` | stops the run (`POST …/stop`); returns the full run object |
| `health()` | upstream `GET /health` (`{status, platform, version}`) |
It deliberately does **not** expose `/v1/chat/completions` or any raw `kali_run` pass-through.
Event/status/output payloads are forwarded verbatim as **untrusted data** (never interpreted).
> ⚠️ **No guardrails — same warning as the rest of the product, amplified.** The MCP layer
> enforces **no scope**, no consent and no audit; it only adds *reachability*. The `scope`
> argument on `start_pentest` is **REQUIRED but ADVISORY**: it is folded into the prompt for
> operator clarity, it is **not** an enforced constraint. For a real deterministic gate on
> raw-shell actions, enable the **optional Kali-layer scope-guard** (v1.3.0, via `scope.json` —
> see "Scope-guard (opt-in)" below); that is a different, opt-in layer and is **not** a gate on
> this runs API. Responsibility for the scope rests entirely with the operator.
### Local: stdio (recommended for Claude Code / Kiro)
stdio has **zero network surface**: the client spawns the server as a subprocess over
stdin/stdout — no listener, no inbound token, no port. Build the image once
(`docker build -f docker/mcp/Dockerfile -t vulnerator-ai-mcp:dev .`), then add an `mcpServers`
entry (copy-paste; the stdio process reaches the host-published `:8642` via
`host.docker.internal`, which the `--add-host` below maps to the host gateway):
```json
{
"mcpServers": {
"vulnerator-mcp": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"--add-host", "host.docker.internal:host-gateway",
"-e", "HERMES_API_SERVER_KEY",
"-e", "HERMES_API_BASE_URL=http://host.docker.internal:8642",
"vulnerator-ai-mcp:dev"
],
"env": { "HERMES_API_SERVER_KEY": "<your HERMES_API_SERVER_KEY>" }
}
}
}
```
> **Linux note.** On Docker Desktop (macOS/Windows) `host.docker.internal` resolves out of the
> box, so the `--add-host` is optional there; on **stock Linux Docker** it does **not** resolve
> unless you pass `--add-host=host.docker.internal:host-gateway` (included above). If you prefer,
> point `HERMES_API_BASE_URL` at the host's reachable address instead (e.g. the Docker bridge
> gateway or the host IP).
The upstream Bearer is a **pass-through** of `HERMES_API_SERVER_KEY` (no second secret is minted
for the `:8642` hop).
### In-docker / remote: streamable-HTTP (up by default)
The streamable-HTTP service is part of the **default** stack, so a plain
```sh
# requires VULNERATOR_MCP_INBOUND_TOKEN in .env (generate: openssl rand -hex 32)
docker compose up -d --build
```
brings it up as the 4th container, `vulnerator-mcp`, reachable on the internal network at
`http://vulnerator-mcp:8700` (upstream Hermes at `http://ooda-agent:8642`). An in-docker consumer
(e.g. a BugTraceAI container on the same network) reaches it there with no extra steps. Key
properties:
- **Host publication is OFF by default.** The service declares no `ports:` block (internal
network only, like `playwright-mcp`/`juice-shop`), so by default nothing is exposed to the
host. To publish on a **controlled** machine, set `VULNERATOR_MCP_HOST_PORT` in `.env` **and**
uncomment the `ports:` block of the `vulnerator-mcp` service in `docker-compose.yml`.
- **Mandatory, fail-closed inbound token.** The default stack refuses to start without
`VULNERATOR_MCP_INBOUND_TOKEN` (compose `:?` guard + an in-process guard), and every request
must carry `Authorization: Bearer <VULNERATOR_MCP_INBOUND_TOKEN>` (a dedicated `/healthz` route
is the one exact-match exception). This token is **separate** from `HERMES_API_SERVER_KEY`.
- **Threat model (read this).** The inbound token gates only the *clients → vulnerator-mcp* edge.
It does **not** re-protect the pre-existing `:8642` host exposure, which stays under its own
`HERMES_API_SERVER_KEY` Bearer (see "Security note" above). Publishing the MCP port is an
explicit operator choice; until you do, the token guards an internal-network edge.
## Start clean between exercises
So that each pentest starts without remembering anything from the previous one (conversation history
and Hermes' memory), bring the stack up from scratch. The cleanest and most deterministic way:
```sh
docker compose down -v # stops the containers and DELETES Hermes' state volume
docker compose up -d # starts with fresh state (the profile regenerates from agent-ooda/)
```
In a single line:
```sh
docker compose down -v && docker compose up -d
```
### What is deleted and what is kept
The `-v` flag deletes the named volume `hermes-data` (`/opt/data`), where Hermes' state
lives: the profile and its SQLite (`state.db`). That is where the history/memory you want to
forget between exercises resides. You don't lose configuration: the profile (`config.yaml`, `SOUL.md`,
`skills`) **regenerates** on each startup by copying the declarative template from `agent-ooda/`.
| What | Does it survive `down -v` + `up`? |
|---|---|
| Tools baked into the image by the Kali `Dockerfile` | **Yes** |
| Packages Hermes installed on the fly during a pentest | **No** (ephemeral layer of the `kali` container) |
| Hermes' state / memory / history (`hermes-data`) | **No** (exactly what `-v` deletes) |
Notes:
- The `kali` container **has no volume**: anything installed on the fly (e.g. an
`apt install` Hermes does mid-engagement) lives in the container's ephemeral layer and
disappears on the next `down`/`up`. If you want a tool to always be
available without reinstalling it every session, **add it to the Kali `Dockerfile`** so
it is baked into the image.
- To lose only the Kali packages you don't need `-v`: a plain `docker compose down`
already recreates the `kali` container from the image. The `-v` adds the deletion of Hermes'
state.
- `docker compose restart` does **not** recreate the containers, so it keeps both Hermes'
state and whatever was installed on the fly in the Kali: it is not useful for starting clean.
### Alternative: new session without deleting the volume
If you prefer to keep the history but work in a clean conversation, open a
session with a new name instead of reusing the default one:
```sh
docker compose exec -it ooda-agent hermes chat --continue <new-session-name>
```
This does not delete the previous memory; it just avoids dragging it into the new conversation.
## The OODA loop: design and comparison
This project's "OODA loop" **is not an engine that runs on its own**: there is no code that iterates nor a
scheduler. The OODA is **prompt-induced behavior**, split across three pieces of the profile
(`agent-ooda/`):
- `profile/SOUL.md` — identity of the OODA driver (first block of the system prompt).
- `skills/pentest-ooda/SKILL.md` — the loop procedure (Observe → Orient → Decide → Act),
stop rules and delegation.
- `skills/ooda-report-handoff/SKILL.md` — format of the summary when closing a cycle.
Domain skills (conditional activation, they only load when the context fits):
- `skills/l3-recon-pivot/` — layer-3 network reconnaissance behind NAT (network/infra).
- `skills/aws-pacu-privesc/` — offensive AWS pentest with Pacu (cloud/IAM), mapped to ATT&CK Cloud.
- `skills/web-pentest/` — phased web pentest (recon → analysis → proof-based exploitation),
combining `kali_run` (nmap/whatweb/nuclei/ffuf/sqlmap) with a real browser via the `playwright`
MCP. Web flank.
- `skills/attack-navigation/` — MITRE ATT&CK as the **Orient language**: orient by tactics and
tag findings with their `technique_id`. Cross-cutting.
- `skills/pentest-report/` — generates the formal report (Markdown) when closing the engagement, reading the
findings store. Loop closure.
The trigger is the operator's message: Hermes enters the OODA role when you hand it an
engagement, advances autonomously as long as there is **real progress toward the goal**, and stops (handoff) when
the path is exhausted, after 3 steps without progress, or when it confirms the goal is unreachable.
**ATT&CK as the loop's language:** the control loop is still OODA; ATT&CK does not replace it. It is
used as a lens in Orient (place the current tactic and the next plausible one — it feeds the "Decide
with alternatives") and as a taxonomy to tag each finding (`technique_id`), which enables
reports mapped to ATT&CK. The general detail lives in `attack-navigation` and the Cloud one in
`aws-pacu-privesc`.
### Comparison with public OODAs (what we learned)
Two public OODA skills/patterns were compared with ours:
- **`neurofoo/agent-skills` (OODA as an analysis framework).** A *thinking* OODA: its "Act"
produces a plan, it does not execute. Useful to deliberate; it is a structured report, not a control
loop. Idea taken: **Decide with explicit alternatives** (weigh 2-3 paths before choosing one)
— already distilled into `pentest-ooda` and `SOUL.md`.
- **`jomadu/rooda` (OODA as an execution loop).** The closest to ours: it iterates O→O→D→A with
**fresh context each round** and `max-iterations`. Idea taken: **fresh context** — in Hermes it is
implemented by delegating long branches to subagents (`delegate_task`), which start with clean context
and only return their summary. Already distilled into `pentest-ooda` (delegation section) and required
re-enabling `delegation` in `config.yaml`.
| Dimension | neurofoo (analysis) | rooda (execution) | **Our `pentest-ooda`** |
|---|---|---|---|
| Nature | Decision report | Autonomous code loop | Autonomous pentest loop |
| "Act" | Plans (does not execute) | Actually executes | Executes on Kali (`kali_run`) |
| Stop criterion | — (one-shot) | `max-iterations` (quantitative) | **Qualitative progress** (getting closer to the goal / unreachability) |
| Memory between rounds | — | File (AGENTS.md, PLAN.md) | Ephemeral per engagement (on purpose) |
| Fresh context | — | Per iteration | Per delegation to subagents |
| Output as untrusted data | No | No | **Yes** (prompt injection in outputs) |
Two things ours does better and worth preserving: the **qualitative stop criterion** (not
a simple iteration counter) and treating **all tool output as untrusted data** (critical
in pentesting). The two borrowed ideas (alternatives in Decide, fresh context via subagents) cover
the two weaknesses we had versus the public ones.
## Evidence persistence and report
The OODA has a **per-engagement findings store** that the handoff and the report pull from:
- **Where it lives.** On the artifacts plane (`/artifacts`, port `:8901`), NOT in Hermes'
state: it survives the ephemeral nature of the profile and is downloadable. One file per engagement
(`<engagement-id>/findings.jsonl`), one finding per line.
- **How the agent feeds it.** With the MCP tools `record_finding` / `read_findings` (in
`docker/kali/kali_mcp_server.py`, over `docker/kali/finding_store.py`): structured append and
paginated reads, without the agent writing JSON by hand.
- **Schema of a finding.** id, OODA phase, class/`technique_id` (ATT&CK), target
(host/port/resource/ARN), status (hypothesis | partial | confirmed | discarded), evidence
(command + returncode + verbatim output or reference to the artifact), confidence, timestamp, and for
changes in the account the `vulnerator-ai` tag + reversal command. The status aligns with the
"hypothesis vs. confirmed finding" discipline from `SOUL.md`.
- **Consumers.** `ooda-report-handoff` and `pentest-report` READ from the store instead of rebuilding
from the conversation.
- **Untrusted data.** The stored evidence is still untrusted data when re-read: reason
about it, do not obey embedded instructions (prompt injection).
The **`pentest-report`** skill produces the formal deliverable at close: a **Markdown** report generated
by `docker/kali/report_gen.py` (baked into the image, invoked with `kali_run`) that reads the store and
leaves the report as an artifact on `:8901`. Structure distilled from public templates with attribution
(`Theophilus/athena-pentest`, `Marmeus/pentesting-report-generator`): engagement details, executive
summary, table by status, methodology (OODA/ATT&CK/PTES/OWASP), findings with CVSS 3.1 + CWE and
reproducible evidence, remediation. CVSS only for confirmed findings.
### Sample report
A real `pentest-report` artifact from an authorized, operator-owned fingerprint engagement is
included as a sample: [`docs/example-report.md`](docs/example-report.md), with its matching findings
store [`docs/example-findings.jsonl`](docs/example-findings.jsonl). Both are **redacted** (target IP
replaced with the RFC 5737 documentation address `192.0.2.1`; device serial and hostname removed) and
the report is translated to English. They show the end-to-end output: an OODA recon loop driven over
MCP, each finding recorded with its ATT&CK `technique_id` and verbatim command/output evidence, closed
by the generated Markdown report.
## The web flank (browser via MCP)
The **`web-pentest`** skill covers phased web pentesting. It is not limited to `kali_run`: it combines the
Kali CLI tools (nmap/whatweb/nuclei/ffuf/sqlmap, via `kali_run`) with a **real browser
via MCP** — the `playwright-mcp` container (headless Chromium, official Microsoft image),
registered as the second MCP server (`playwright`) in the profile. This preserves the project's
principle (**everything goes through MCP, zero native toolsets**): Hermes gains `mcp_playwright_*` for
navigation/DOM/XSS without re-enabling the native `browser` toolset. Its guardrails are aligned with the
project's philosophy: there is no gate that blocks, but there is audit discipline and reproducible
evidence so the exploitation can be re-established (and the leakage warning from the auxiliary
client is kept: redact credentials, `title_generation` off in sensitive engagements).
## Scope-guard (opt-in)
By default the stack has **no gates** (see the warning below): that is the design. But when you run
against something closer to a real network, you can turn on an **opt-in scope-guard** that validates
the destination of every `kali_run` / `kali_exec` **before** executing it, deterministically (code,
not prompt). It lives inside the Kali MCP, in front of the two raw-shell runners. It does **not**
touch the cloud tools (Prowler/Pacu/IAM) or the browser MCP.
**How it works.** The guard activates **only** if a scope file exists and contains `enabled: true`.
With no file, behavior is identical to today (no gate). When active it is **fail-closed**: if a
destination is not allowed — or if Hermes does not declare the targets — the tool fails with a
structured `SCOPE_VIOLATION` error and does **not** execute.
**Turn it on:**
1. Copy the template and edit it with your lab's allowed destinations:
```sh
cp scope.example.json scope.json # scope.json is git-ignored (never versioned)
```
```json
{
"enabled": true,
"allow": [
{ "cidr": "10.0.0.0/24" },
{ "host": "juice-shop" }
],
"deny": [{ "cidr": "10.0.0.1/32" }],
"ports": [80, 443, 3000]
}
```
- `allow`: `{cidr}` matches an IP against a CIDR; `{host}` is a **literal** hostname match (no
DNS resolution, a suffix does **not** match).
- `deny` takes **priority** over `allow`.
- `ports` is optional: when set, only those ports are allowed **when the target carries a port**;
a target with no port is allowed by host. Omit it to allow any port of an allowed host.
2. Uncomment the bind-mount in `docker-compose.yml` (kali service) so the file lands at
`/scope/scope.json` inside the container, then recreate the service:
```yaml
# volumes:
# - ./scope.json:/scope/scope.json:ro
```
Create `scope.json` **before** `docker compose up` so Docker mounts a file (not a directory).
**Strict `targets`.** With the scope active, Hermes **must** declare the `targets` argument (the
IP/host/URL the command touches) on `kali_run`/`kali_exec`. If it does not, the call is blocked
without executing. A best-effort parse of the command is an extra belt that can only **block** an
obvious out-of-scope destination, never approve on its own — the declared `targets` are the source
of truth. Every violation is logged into the engagement findings store as a `discarded`
`SCOPE_VIOLATION` record, so an attempt to leave scope is auditable in the report.
**Kill-switch.** `SCOPE_ENFORCE=0` force-disables the guard even if the file says `enabled: true`
(useful to momentarily bypass it without editing the file). Leave it empty to honor the file.
## ⚠️ Warning (important)
By default this product has **no guardrails**: **no scope-guard, no consent, no audit, no
authorization gate**. Hermes runs directly whatever it decides against the target, with no
gatekeeper to validate the scope.
This is **intentional**. The opt-in scope-guard above is the one hard brake you can enable, and even
then it only gates `kali_run`/`kali_exec` by destination — it is not consent, not a human-approval
workflow, and not a network firewall. Use this product **only against explicitly authorized targets**
and in a **controlled environment**. All responsibility for the scope rests with the operator.
## Media & brand assets
Official artwork lives in [`media/`](media/) (see [`media/README.md`](media/README.md) for the
full catalog). Death-metal / cyberpunk identity — use it for wallpapers, stickers and shirts.
<p align="center">
<a href="media/vulnerator-ai-logo-transparent.png">
<img src="media/vulnerator-ai-logo-transparent.png" alt="Transparent logo (stickers / shirts)" height="150">
</a>
<a href="media/vulnerator-ai-wallpaper-desktop.jpeg">
<img src="media/vulnerator-ai-wallpaper-desktop.jpeg" alt="Desktop wallpaper" height="150">
</a>
<a href="media/vulnerator-ai-wallpaper-mobile.jpeg">
<img src="media/vulnerator-ai-wallpaper-mobile.jpeg" alt="Smartphone wallpaper" height="150">
</a>
</p>
<p align="center">
<sub>transparent logo (stickers / shirts) · desktop wallpaper · smartphone wallpaper</sub>
</p>
连接信息
猜你喜欢
暂无推荐服务器