Skip to content
Open
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
13 changes: 13 additions & 0 deletions exploitation/llmsectest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Python
__pycache__/
*.py[cod]
*$py.class
.venv/
.env

# Logs
*.log
reports/*.sarif
reports/*.html
reports/*.json
reports/*.md
52 changes: 52 additions & 0 deletions exploitation/llmsectest/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
SANDBOX_NAME := $(shell uv run python -c 'import tomllib, pathlib; print(tomllib.loads(pathlib.Path("config/config.toml").read_text())["target"]["sandbox"])')
SANDBOX_DIR := ../../sandboxes/$(SANDBOX_NAME)

.PHONY: help setup attack stop all sync lock format

# Default target
help:
@echo "LLMSecTest Exploitation - Available Commands:"
@echo ""
@echo " make setup - Build and start the local LLM sandbox"
@echo " make attack - Run an LLMSecTest scan against the sandbox"
@echo " make stop - Stop and remove the sandbox container"
@echo " make all - Run setup, attack, and stop in sequence"
@echo " make sync - Sync dependencies with uv"
@echo " make lock - Lock dependencies with uv"
@echo " make format - Format code with black, isort, and mypy"
@echo ""
@echo "Environment:"
@echo " - Sandbox Directory: $(SANDBOX_DIR)"
@echo ""

sync:
uv sync

lock:
uv lock

format:
uv run black .
uv run isort .
uv run mypy .

setup:
@echo "🚀 Setting up Red Team environment..."
$(MAKE) -C $(SANDBOX_DIR) run-gradio-headless
@echo "⏳ Waiting for service to be ready..."
@sleep 5
@echo "✅ Environment ready!"

attack: sync
@echo "🔍 Running LLMSecTest scan..."
@mkdir -p reports
uv run python attack.py

stop:
@echo "🧹 Tearing down Red Team environment..."
$(MAKE) -C $(SANDBOX_DIR) stop-gradio
$(MAKE) -C $(SANDBOX_DIR) down
@echo "✅ Environment cleaned up!"

all: stop setup attack stop
@echo "LLMSecTest Exploitation - Completed!"
164 changes: 164 additions & 0 deletions exploitation/llmsectest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Red Team Example: LLMSecTest on the LLM Sandbox

This directory runs **[LLMSecTest](https://github.com/wehnsdaefflae/llmsectest)** against the
`llm_local` sandbox. LLMSecTest is an MIT-licensed scanner for the OWASP Top 10 for LLM
Applications that ships as a **pytest plugin**: a scan is a test run, a failing security test
is a CVSS v4.0 scored OWASP finding, and the output is **SARIF** that a CI code-scanning tab
reads without conversion.

Two things make it complementary to the scanners already in `exploitation/`:

* it emits **SARIF** with the OWASP taxonomy, so findings land in GitHub code scanning
alongside the rest of a pipeline's results;
* `--repo` adds a **white-box** LLM03 supply-chain scan of the sandbox's own dependency
manifests, so this example covers a category a purely black-box scanner cannot reach.

---

## Table of Contents

1. [Attack Strategy](#attack-strategy)
2. [Prerequisites](#prerequisites)
3. [Running the Sandbox](#running-the-sandbox)
4. [Configuration](#configuration)
5. [Attack Workflow](#attack-workflow)
6. [Cleaning Up](#cleaning-up)
7. [Files Overview](#files-overview)
8. [OWASP Top 10 Coverage](#owasp-top-10-coverage)
9. [Two notes about this sandbox](#two-notes-about-this-sandbox)

---

## Attack Strategy

```mermaid
graph LR
subgraph "Attacker Environment (Local)"
AttackScript[attack.py]
Config[config/config.toml]
Reports[Reports<br/>SARIF + HTML]
end

subgraph "LLMSecTest"
Suite[pytest probe suite<br/>LLM01-LLM10]
Repo[--repo<br/>LLM03 manifest scan]
end

subgraph "Target Sandbox (Container)"
MockAPI[OpenAI mock<br/>FastAPI :8000]
end

subgraph "LLM Backend (Local Host)"
Ollama[Ollama Server<br/>:11434]
end

Config --> AttackScript
AttackScript --> Suite
AttackScript --> Repo
Suite -->|POST /v1/chat/completions| MockAPI
MockAPI --> Ollama
Repo -->|reads manifests| MockAPI
Suite --> Reports
Repo --> Reports
```

## Prerequisites

* [`uv`](https://docs.astral.sh/uv/), Podman, and a running Ollama on port `11434`
(the same prerequisites as `exploitation/garak`)
* the model named in `config/config.toml`, pulled into Ollama
(`make -C ../../sandboxes/llm_local ollama-pull`)

## Running the Sandbox

```bash
make setup
```

Starts `sandboxes/llm_local`, which exposes the OpenAI-compatible mock on `:8000`.

## Configuration

`config/config.toml`:

```toml
[target]
sandbox = "llm_local"

[endpoint]
base_url = "http://localhost:8000/v1"
api_key = "sk-mock-key"
model = "gpt-oss:20b"
```

`attack.py` puts `base_url` and `api_key` into `OPENAI_BASE_URL` and `OPENAI_API_KEY` and
calls the CLI, so the run is reproducible by hand:

```bash
OPENAI_BASE_URL=http://localhost:8000/v1 OPENAI_API_KEY=sk-mock-key \
llmsectest --target openai:gpt-oss:20b \
--repo ../../sandboxes/llm_local \
--sarif-output reports/GenAI-Red-Team.sarif \
--render-sarif reports/GenAI-Red-Team.html
```

## Attack Workflow

```bash
make attack
```

**A non-zero exit means findings.** That is the expected outcome against a sandbox built to be
attacked, so treat it as the result rather than as a failed run. A probe LLMSecTest could not get an answer out of is recorded
**inconclusive** and never scored as a finding, so a broken target and a vulnerable one do
not look alike.

## Cleaning Up

```bash
make stop
```

## Files Overview

| File | Purpose |
|---|---|
| `attack.py` | sets the endpoint env vars and runs the CLI |
| `config/config.toml` | sandbox name, base URL, key, model |
| `Makefile` | `setup` / `attack` / `stop` / `all`, same shape as the other examples |
| `pyproject.toml` | pins `llmsectest[openai]` |
| `reports/` | SARIF and rendered HTML land here |

## OWASP Top 10 Coverage

Against this sandbox the run exercises **8 of 10** categories. LLMSecTest prints the map at
the end of every scan and names what it did **not** exercise and why, so a partial run never
reads as a full one.

| Category | How it is covered here |
|---|---|
| LLM01 Prompt Injection | marker-injection corpus plus built-in jailbreak prompts (`--redteam-set` runs the full JailbreakBench set) |
| LLM02 Sensitive Information Disclosure | four disclosure mechanisms against a seeded secret |
| LLM03 Supply Chain | `--repo` scans the sandbox's manifests; add `--osv` for known CVEs |
| LLM04 Data and Model Poisoning | not exercised here; needs `--model-scan <path>` over serialized model files |
| LLM05 Improper Output Handling | payloads checked for surviving unescaped into the response |
| LLM06 Excessive Agency | forged-authorization probes |
| LLM07 System Prompt Leakage | instruction-restatement corpus with de-obfuscation of encoded leaks |
| LLM08 Vector and Embedding Weaknesses | not exercised here; needs a RAG target, see below |
| LLM09 Misinformation | fabrication probes |
| LLM10 Unbounded Consumption | bounded resource-exhaustion probes |

**Natural next step:** point the same tool at `sandboxes/RAG_local` with
`--target app:<url> --app-canary <confidential string in a retrieved document>` and
`--app-rag-poison <marker a planted document emits>` to cover LLM08 as well.

## Two notes about this sandbox

Both were found by running it. Both would trip up the next person.

1. **The mock does not serve `GET /v1/models`.** It serves `POST /v1/chat/completions` and
`GET /health` only. So `llmsectest --preflight`, which health-checks a local server
through the models endpoint, reports a perfectly healthy sandbox as unreachable. It is
deliberately not used here.
2. **The mock enforces `Authorization: Bearer sk-mock-key`.** The key is not optional even
though the backend is local, so `OPENAI_API_KEY` has to be set to exactly that value.
63 changes: 63 additions & 0 deletions exploitation/llmsectest/attack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Run an LLMSecTest scan against the llm_local sandbox.

LLMSecTest is a pytest plugin. A scan is a test run. A failing security test is rendered as a
CVSS v4.0 scored OWASP finding in SARIF. This script sets the two
environment variables the OpenAI-compatible client reads and shells out to the CLI, so
what runs here is exactly what a reader can run by hand.

Two notes about this sandbox in particular, both found by running it:

* the mock serves ``POST /v1/chat/completions`` and ``GET /health`` and **not**
``GET /v1/models``, so ``llmsectest --preflight`` is deliberately not used here. It
would report a healthy sandbox as unreachable.
* the mock enforces ``Authorization: Bearer sk-mock-key``, so the key is not optional
even though the backend is local.

``--repo`` adds the white-box LLM03 supply-chain scan over the sandbox's own dependency
manifests, which is why this run covers a category a black-box scanner cannot reach.
"""

from __future__ import annotations

import os
import pathlib
import subprocess
import sys
import tomllib

HERE = pathlib.Path(__file__).parent
CONFIG = tomllib.loads((HERE / "config" / "config.toml").read_text())
REPORTS = HERE / "reports"
SANDBOX = HERE.parent.parent / "sandboxes" / CONFIG["target"]["sandbox"]


def main() -> int:
REPORTS.mkdir(exist_ok=True)
endpoint = CONFIG["endpoint"]

env = dict(os.environ)
env["OPENAI_BASE_URL"] = endpoint["base_url"]
env["OPENAI_API_KEY"] = endpoint["api_key"]

cmd = [
sys.executable, "-m", "llmsectest",
"--target", f"openai:{endpoint['model']}",
"--repo", str(SANDBOX),
"--sarif-output", str(REPORTS / "GenAI-Red-Team.sarif"),
"--render-sarif", str(REPORTS / "GenAI-Red-Team.html"),
"-q",
]
print("running:", " ".join(cmd))
completed = subprocess.run(cmd, env=env, cwd=HERE)

# A non-zero exit means the target was vulnerable. That is the expected outcome against
# a sandbox built to be attacked. Only a crash is a failure of the run. LLMSecTest keeps
# the two apart: a probe it could not get an answer out of is recorded inconclusive and
# is never scored as a finding.
print(f"\nscan exit={completed.returncode}; non-zero means findings were reported")
print(f"reports: {REPORTS}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
9 changes: 9 additions & 0 deletions exploitation/llmsectest/config/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[target]
sandbox = "llm_local"

# The sandbox's OpenAI-compatible mock. It enforces `Authorization: Bearer sk-mock-key`
# (sandboxes/llm_local/app/mocks/openai.py) and serves POST /v1/chat/completions only.
[endpoint]
base_url = "http://localhost:8000/v1"
api_key = "sk-mock-key"
model = "gpt-oss:20b"
19 changes: 19 additions & 0 deletions exploitation/llmsectest/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[project]
name = "llmsectest-exploitation"
version = "0.1.0"
description = "LLMSecTest exploitation setup for the GenAI Red Team Lab"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"llmsectest[openai]>=0.2.0",
]

[dependency-groups]
dev = [
"black>=24.0.0",
"isort>=5.0.0",
"mypy>=1.0.0",
]

[tool.mypy]
ignore_missing_imports = true
Empty file.