Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
48 changes: 48 additions & 0 deletions .github/workflows/wiremock-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: WireMock Smoke Test

on:
push:
branches:
- main
pull_request:
types:
- opened
- reopened
- synchronize

jobs:
wiremock-smoke-test:
name: WireMock Smoke Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '11'

- name: Download Go dependencies
run: go mod download

- name: Setup proto files from go mod cache
run: ./scripts/setup-proto-files.sh

- name: Run smoke test
run: ./scripts/smoke-test-wiremock.sh

- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: wiremock-logs
path: wiremock/wiremock.log
if-no-files-found: ignore
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@
# Lint output
/report.xml

# Proto tools
/.proto/

# E2E tests
/e2e-tests/.env
/e2e-tests/mcp-reports/
/e2e-tests/bin/
/e2e-tests/**/*-out.json

# WireMock
/wiremock/lib/*.jar
/wiremock/*.pid
/wiremock/*.log
/wiremock/__files
/wiremock/proto/
/wiremock/grpc/
101 changes: 99 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ test: ## Run unit tests
e2e-smoke-test: ## Run E2E smoke test (build and verify mcpchecker)
@cd e2e-tests && ./scripts/smoke-test.sh

.PHONY: e2e-test
e2e-test: ## Run E2E tests
.PHONY: e2e-test mock-start proto-generate
e2e-test: ## Run E2E tests (uses WireMock)
@cd e2e-tests && ./scripts/run-tests.sh

.PHONY: test-coverage-and-junit
Expand Down Expand Up @@ -91,9 +91,106 @@ lint: ## Run golangci-lint
go install -v "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6"
golangci-lint run

##############
## Protobuf ##
##############

# Protoc version and paths
PROTOC_VERSION := 32.1
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
PROTOC_OS = linux
endif
ifeq ($(UNAME_S),Darwin)
PROTOC_OS = osx
endif
PROTOC_ARCH=$(shell case $$(uname -m) in (arm64|aarch64) echo aarch_64 ;; (s390x) echo s390_64 ;; (*) uname -m ;; esac)

PROTO_PRIVATE_DIR := .proto
PROTOC_DIR := $(PROTO_PRIVATE_DIR)/protoc-$(PROTOC_OS)-$(PROTOC_ARCH)-$(PROTOC_VERSION)
PROTOC := $(PROTOC_DIR)/bin/protoc
PROTOC_ZIP := protoc-$(PROTOC_VERSION)-$(PROTOC_OS)-$(PROTOC_ARCH).zip
PROTOC_DOWNLOADS_DIR := $(PROTO_PRIVATE_DIR)/.downloads
PROTOC_FILE := $(PROTOC_DOWNLOADS_DIR)/$(PROTOC_ZIP)

$(PROTOC_DOWNLOADS_DIR):
@mkdir -p "$@"

$(PROTOC_FILE): $(PROTOC_DOWNLOADS_DIR)
@echo "Downloading protoc $(PROTOC_VERSION)..."
@curl -fSL -o "$@" "https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/$(PROTOC_ZIP)"

$(PROTOC): $(PROTOC_FILE)
@echo "Installing protoc to $(PROTOC_DIR)..."
@mkdir -p "$(PROTOC_DIR)"
@unzip -q -o -d "$(PROTOC_DIR)" "$(PROTOC_FILE)"
@test -x "$@"
@echo "✓ protoc $(PROTOC_VERSION) installed"

.PHONY: proto-install
proto-install: $(PROTOC) ## Install protoc locally

.PHONY: proto-setup
proto-setup: ## Setup proto files from go mod cache
@./scripts/setup-proto-files.sh

.PHONY: proto-generate
proto-generate: $(PROTOC) ## Generate proto descriptors for WireMock
@PROTOC_BIN=$(PROTOC) ./scripts/generate-proto-descriptors.sh

.PHONY: proto-clean
proto-clean: ## Clean generated proto files
@rm -rf wiremock/proto/ wiremock/grpc/

.PHONY: proto-check
proto-check: ## Verify proto setup is correct
@if [ ! -f wiremock/proto/descriptors/stackrox.dsc ]; then \
echo "❌ Proto descriptors not found"; \
echo "Run: make proto-generate"; \
exit 1; \
fi
@echo "✓ Proto descriptors present"

.PHONY: mock-download
mock-download: ## Download WireMock JARs
@./scripts/download-wiremock.sh

.PHONY: mock-start
mock-start: proto-check ## Start WireMock mock Central locally
@./scripts/start-mock-central.sh

.PHONY: mock-stop
mock-stop: ## Stop WireMock mock Central
@./scripts/stop-mock-central.sh

.PHONY: mock-logs
mock-logs: ## View WireMock logs
@tail -f wiremock/wiremock.log

.PHONY: mock-restart
mock-restart: mock-stop mock-start ## Restart WireMock

.PHONY: mock-status
mock-status: ## Check WireMock status
@if [ -f wiremock/wiremock.pid ]; then \
PID=$$(cat wiremock/wiremock.pid); \
if ps -p $$PID > /dev/null 2>&1; then \
echo "WireMock is running (PID: $$PID)"; \
else \
echo "WireMock PID file exists but process not running"; \
fi \
else \
echo "WireMock is not running"; \
fi

.PHONY: mock-test
mock-test: ## Run WireMock smoke tests
@./scripts/smoke-test-wiremock.sh

.PHONY: clean
clean: ## Clean build artifacts and coverage files
$(GOCLEAN)
rm -f $(BINARY_NAME)
rm -f $(COVERAGE_OUT)
rm -f $(LINT_OUT)
rm -rf $(PROTO_PRIVATE_DIR)
45 changes: 27 additions & 18 deletions e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ This is useful for CI and quickly checking that everything compiles.
- Go 1.25+
- Google Cloud Project with Vertex AI enabled (for Claude agent)
- OpenAI API Key (for LLM judge)
- StackRox API Token

## Setup

Expand All @@ -39,9 +38,6 @@ Create `.env` file:
# Required: GCP Project for Vertex AI (Claude agent)
ANTHROPIC_VERTEX_PROJECT_ID=<GCP Project ID>

# Required: StackRox Central API Token
STACKROX_MCP__CENTRAL__API_TOKEN=<StackRox API Token>

# Required: OpenAI API Key (for LLM judge)
OPENAI_API_KEY=<OpenAI API Key>

Expand All @@ -52,12 +48,22 @@ CLOUD_ML_REGION=us-east5
JUDGE_MODEL_NAME=gpt-5-nano
```

Note: No StackRox API token required - tests use WireMock mock service.

## Running Tests

Run tests against the WireMock mock service:

```bash
./scripts/run-tests.sh
```

The test suite:
- Starts WireMock automatically on localhost:8081
- Uses deterministic test fixtures
- Requires no StackRox API tokens
- Fast and reliable for development and CI

Results are saved to `mcpchecker/mcpchecker-stackrox-mcp-e2e-out.json`.

### View Results
Expand All @@ -72,21 +78,24 @@ jq '[.[] | .callHistory.ToolCalls[]? | {name: .request.Params.name, arguments: .

## Test Cases

| Test | Description | Tool |
|------|-------------|------|
| `list-clusters` | List all clusters | `list_clusters` |
| `cve-detected-workloads` | CVE detected in deployments | `get_deployments_for_cve` |
| `cve-detected-clusters` | CVE detected in clusters | `get_clusters_with_orchestrator_cve` |
| `cve-nonexistent` | Handle non-existent CVE | `get_clusters_with_orchestrator_cve` |
| `cve-cluster-does-exist` | CVE with cluster filter | `get_clusters_with_orchestrator_cve` |
| `cve-cluster-does-not-exist` | CVE with cluster filter | `get_clusters_with_orchestrator_cve` |
| `cve-clusters-general` | General CVE query | `get_clusters_with_orchestrator_cve` |
| `cve-cluster-list` | CVE across clusters | `get_clusters_with_orchestrator_cve` |
| Test | Description | Tool | Eval Coverage |
|------|-------------|------|---------------|
| `list-clusters` | List all clusters | `list_clusters` | - |
| `cve-detected-workloads` | CVE detected in deployments | `get_deployments_for_cve` | Eval 1 |
| `cve-detected-clusters` | CVE detected in clusters | `get_clusters_with_orchestrator_cve` | Eval 1 |
| `cve-nonexistent` | Handle non-existent CVE | `get_clusters_with_orchestrator_cve` | Eval 2 |
| `cve-cluster-does-exist` | CVE with cluster filter | `get_clusters_with_orchestrator_cve` | Eval 4 |
| `cve-cluster-does-not-exist` | CVE with non-existent cluster | `list_clusters` | - |
| `cve-clusters-general` | General CVE query | `get_clusters_with_orchestrator_cve` | Eval 1 |
| `cve-cluster-list` | CVE across clusters | `get_clusters_with_orchestrator_cve` | - |
| `cve-log4shell` | Well-known CVE (log4shell) | `get_deployments_for_cve` | Eval 3 |
| `cve-multiple` | Multiple CVEs in one prompt | `get_deployments_for_cve` | Eval 5 |
| `rhsa-not-supported` | RHSA detection (should fail) | None | Eval 7 |

## Configuration

- **`mcpchecker/eval.yaml`**: Main test configuration, agent settings, assertions
- **`mcpchecker/mcp-config.yaml`**: MCP server configuration
- **`mcpchecker/eval.yaml`**: Test configuration, agent settings, assertions
- **`mcpchecker/mcp-config-mock.yaml`**: MCP server configuration for WireMock
- **`mcpchecker/tasks/*.yaml`**: Individual test task definitions

## How It Works
Expand All @@ -103,8 +112,8 @@ mcpchecker uses a proxy architecture to intercept MCP tool calls:
## Troubleshooting

**Tests fail - no tools called**
- Verify StackRox Central is accessible
- Check API token permissions
- Verify WireMock is running: `make mock-status`
- Check WireMock logs: `make mock-logs`

**Build errors**
```bash
Expand Down
38 changes: 36 additions & 2 deletions e2e-tests/mcpchecker/eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ config:
model: "claude-sonnet-4-5"
llmJudge:
env:
typeKey: JUDGE_TYPE
baseUrlKey: JUDGE_BASE_URL
apiKeyKey: JUDGE_API_KEY
modelNameKey: JUDGE_MODEL_NAME
mcpConfigFile: mcp-config.yaml
mcpConfigFile: mcp-config-mock.yaml
taskSets:
# Assertion Fields Explained:
# - toolsUsed: List of tools that MUST be called at least once
Expand Down Expand Up @@ -77,13 +78,14 @@ config:
maxToolCalls: 4

# Test 6: CVE with specific cluster filter (does not exist)
# Claude does comprehensive checking even when cluster doesn't exist
- path: tasks/cve-cluster-does-not-exist.yaml
assertions:
toolsUsed:
- server: stackrox-mcp
toolPattern: "list_clusters"
minToolCalls: 1
maxToolCalls: 4
maxToolCalls: 5

# Test 7: CVE detected in clusters - general
- path: tasks/cve-clusters-general.yaml
Expand All @@ -106,3 +108,35 @@ config:
cveName: "CVE-2024-52577"
minToolCalls: 1
maxToolCalls: 5

# Test 9: Log4shell (well-known CVE)
- path: tasks/cve-log4shell.yaml
assertions:
toolsUsed:
- server: stackrox-mcp
toolPattern: "get_deployments_for_cve"
argumentsMatch:
cveName: "CVE-2021-44228"
minToolCalls: 1
maxToolCalls: 3

# Test 10: Multiple CVEs in one prompt
- path: tasks/cve-multiple.yaml
assertions:
toolsUsed:
- server: stackrox-mcp
toolPattern: "get_deployments_for_cve"
argumentsMatch:
cveName: "CVE-2021-31805"
- server: stackrox-mcp
toolPattern: "get_deployments_for_cve"
argumentsMatch:
cveName: "CVE-2016-1000031"
minToolCalls: 2
maxToolCalls: 6

# Test 11: RHSA detection (should fail gracefully)
- path: tasks/rhsa-not-supported.yaml
assertions:
minToolCalls: 0
maxToolCalls: 1
21 changes: 21 additions & 0 deletions e2e-tests/mcpchecker/mcp-config-mock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
mcpServers:
stackrox-mcp:
command: go
args:
- run
- ../../cmd/stackrox-mcp/...
- --config
- ../stackrox-mcp-e2e-config.yaml
# Explicit environment overrides for mock mode
# mcpchecker doesn't support proper environment inheritance,
# so we need to explicitly set all required variables here
env:
STACKROX_MCP__CENTRAL__URL: "localhost:8081"
STACKROX_MCP__CENTRAL__API_TOKEN: "test-token-admin"
STACKROX_MCP__CENTRAL__INSECURE_SKIP_TLS_VERIFY: "true"
STACKROX_MCP__CENTRAL__AUTH_TYPE: "static"
STACKROX_MCP__SERVER__TYPE: "stdio"
STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED: "true"
STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED: "true"
# Auto-approve all tools
enableAllTools: true
2 changes: 1 addition & 1 deletion e2e-tests/mcpchecker/tasks/cve-detected-workloads.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ steps:
prompt:
inline: "Is this CVE-2021-31805 detected in my workloads?"
verify:
contains: "A response indicating whether CVE-2021-31805 is detected in workloads/deployments, with specific deployment names if detected, or a statement that no deployments have this CVE."
contains: "CVE-2021-31805 is detected in deployments: struts-app (staging-cluster/applications) and legacy-portal (development-cluster/production)."
9 changes: 9 additions & 0 deletions e2e-tests/mcpchecker/tasks/cve-log4shell.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: Task
metadata:
name: "cve-log4shell"
difficulty: easy
steps:
prompt:
inline: "Is log4shell CVE detected in my cluster fleet?"
verify:
contains: The commonly used name "log4shell" is resolved into the exact CVE number (CVE-2021-44228). And it's detected in a few deployments.
9 changes: 9 additions & 0 deletions e2e-tests/mcpchecker/tasks/cve-multiple.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: Task
metadata:
name: "cve-multiple"
difficulty: medium
steps:
prompt:
inline: "Are CVE-2021-31805 and CVE-2016-1000031 detected in my cluster fleet?"
verify:
contains: "Both CVEs (CVE-2021-31805 and CVE-2016-1000031), along with the cluster names, deployments, and namespaces where they are detected."
9 changes: 9 additions & 0 deletions e2e-tests/mcpchecker/tasks/rhsa-not-supported.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: Task
metadata:
name: "rhsa-not-supported"
difficulty: easy
steps:
prompt:
inline: "Is RHSA-2024:5137 affecting my clusters?"
verify:
contains: "Information that RHSA-2024:5137 has been detected, along with the cluster where it is detected."
Loading
Loading