diff --git a/.github/workflows/wiremock-test.yml b/.github/workflows/wiremock-test.yml new file mode 100644 index 0000000..46e09c7 --- /dev/null +++ b/.github/workflows/wiremock-test.yml @@ -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 diff --git a/.gitignore b/.gitignore index bba521f..0e487cd 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/Makefile b/Makefile index 4f1bf40..9e70fbf 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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) diff --git a/e2e-tests/README.md b/e2e-tests/README.md index 2accae0..d01b255 100644 --- a/e2e-tests/README.md +++ b/e2e-tests/README.md @@ -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 @@ -39,9 +38,6 @@ Create `.env` file: # Required: GCP Project for Vertex AI (Claude agent) ANTHROPIC_VERTEX_PROJECT_ID= -# Required: StackRox Central API Token -STACKROX_MCP__CENTRAL__API_TOKEN= - # Required: OpenAI API Key (for LLM judge) OPENAI_API_KEY= @@ -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 @@ -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 @@ -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 diff --git a/e2e-tests/mcpchecker/eval.yaml b/e2e-tests/mcpchecker/eval.yaml index 7634627..6c14da2 100644 --- a/e2e-tests/mcpchecker/eval.yaml +++ b/e2e-tests/mcpchecker/eval.yaml @@ -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 @@ -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 @@ -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 diff --git a/e2e-tests/mcpchecker/mcp-config-mock.yaml b/e2e-tests/mcpchecker/mcp-config-mock.yaml new file mode 100644 index 0000000..29189f5 --- /dev/null +++ b/e2e-tests/mcpchecker/mcp-config-mock.yaml @@ -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 diff --git a/e2e-tests/mcpchecker/tasks/cve-detected-workloads.yaml b/e2e-tests/mcpchecker/tasks/cve-detected-workloads.yaml index ca9ca01..bc5bac6 100644 --- a/e2e-tests/mcpchecker/tasks/cve-detected-workloads.yaml +++ b/e2e-tests/mcpchecker/tasks/cve-detected-workloads.yaml @@ -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)." diff --git a/e2e-tests/mcpchecker/tasks/cve-log4shell.yaml b/e2e-tests/mcpchecker/tasks/cve-log4shell.yaml new file mode 100644 index 0000000..aa76c12 --- /dev/null +++ b/e2e-tests/mcpchecker/tasks/cve-log4shell.yaml @@ -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. diff --git a/e2e-tests/mcpchecker/tasks/cve-multiple.yaml b/e2e-tests/mcpchecker/tasks/cve-multiple.yaml new file mode 100644 index 0000000..ef726ea --- /dev/null +++ b/e2e-tests/mcpchecker/tasks/cve-multiple.yaml @@ -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." diff --git a/e2e-tests/mcpchecker/tasks/rhsa-not-supported.yaml b/e2e-tests/mcpchecker/tasks/rhsa-not-supported.yaml new file mode 100644 index 0000000..de48984 --- /dev/null +++ b/e2e-tests/mcpchecker/tasks/rhsa-not-supported.yaml @@ -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." diff --git a/e2e-tests/scripts/run-tests.sh b/e2e-tests/scripts/run-tests.sh index fc13de7..bbed336 100755 --- a/e2e-tests/scripts/run-tests.sh +++ b/e2e-tests/scripts/run-tests.sh @@ -3,26 +3,47 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" E2E_DIR="$(dirname "$SCRIPT_DIR")" +ROOT_DIR="$(dirname "$E2E_DIR")" + +# Cleanup function +WIREMOCK_WAS_STARTED=false +cleanup() { + if [ "$WIREMOCK_WAS_STARTED" = true ]; then + echo "Stopping WireMock..." + cd "$ROOT_DIR" + make mock-stop > /dev/null 2>&1 || true + fi +} +trap cleanup EXIT echo "══════════════════════════════════════════════════════════" echo " StackRox MCP E2E Testing with mcpchecker" +echo " Mode: mock (WireMock)" echo "══════════════════════════════════════════════════════════" echo "" # Load environment variables if [ -f "$E2E_DIR/.env" ]; then echo "Loading environment variables from .env..." - set -a && source .env && set +a -else - echo "Warning: .env file not found" + # shellcheck source=/dev/null + set -a && source "$E2E_DIR/.env" && set +a fi -if [ -z "$STACKROX_MCP__CENTRAL__API_TOKEN" ]; then - echo "Error: STACKROX_MCP__CENTRAL__API_TOKEN is not set" - echo "Please set it in .env file or export it in your environment" - exit 1 +# Check if WireMock is already running +if ! curl -skf https://localhost:8081/__admin/mappings > /dev/null 2>&1; then + echo "Starting WireMock mock service..." + cd "$ROOT_DIR" + make mock-start + WIREMOCK_WAS_STARTED=true +else + echo "WireMock already running on port 8081" fi +# Set environment variables for mock mode +export STACKROX_MCP__CENTRAL__URL="localhost:8081" +export STACKROX_MCP__CENTRAL__API_TOKEN="test-token-admin" +export STACKROX_MCP__CENTRAL__INSECURE_SKIP_TLS_VERIFY="true" + # Check OpenAI API key for judge if [ -z "$OPENAI_API_KEY" ]; then echo "Warning: OPENAI_API_KEY is not set (needed for LLM judge)" @@ -46,6 +67,7 @@ export JUDGE_API_KEY="${JUDGE_API_KEY:-$OPENAI_API_KEY}" export JUDGE_MODEL_NAME="${JUDGE_MODEL_NAME:-gpt-5-nano}" echo "Configuration:" +echo " Central URL: $STACKROX_MCP__CENTRAL__URL (WireMock)" echo " Judge: $JUDGE_MODEL_NAME (OpenAI)" echo " MCP Server: stackrox-mcp (via go run)" echo "" @@ -55,7 +77,9 @@ cd "$E2E_DIR/mcpchecker" echo "Running mcpchecker tests..." echo "" -"$E2E_DIR/bin/mcpchecker" check eval.yaml +EVAL_FILE="eval.yaml" +echo "Using eval file: $EVAL_FILE" +"$E2E_DIR/bin/mcpchecker" check "$EVAL_FILE" EXIT_CODE=$? diff --git a/e2e-tests/scripts/smoke-test-mock.sh b/e2e-tests/scripts/smoke-test-mock.sh new file mode 100755 index 0000000..12bbd90 --- /dev/null +++ b/e2e-tests/scripts/smoke-test-mock.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +E2E_DIR="$(dirname "$SCRIPT_DIR")" +ROOT_DIR="$(dirname "$E2E_DIR")" + +echo "══════════════════════════════════════════════════════════" +echo " WireMock Integration Smoke Test" +echo "══════════════════════════════════════════════════════════" +echo "" + +# Start WireMock +echo "1. Starting WireMock..." +cd "$ROOT_DIR" +make mock-stop > /dev/null 2>&1 || true +make mock-start + +# Wait for WireMock to be ready +echo "" +echo "2. Waiting for WireMock to be ready..." +for i in {1..10}; do + if nc -z localhost 8081 2>/dev/null; then + echo "✓ WireMock is ready" + break + fi + sleep 1 +done + +# Test MCP server can connect +echo "" +echo "3. Testing MCP server connection..." +cd "$ROOT_DIR" + +# Run MCP server and test a simple tool call +timeout 10 bash -c ' +export STACKROX_MCP__CENTRAL__URL=localhost:8081 +export STACKROX_MCP__CENTRAL__API_TOKEN=test-token-admin +export STACKROX_MCP__CENTRAL__INSECURE_SKIP_TLS_VERIFY=true + +# Start MCP server in background +go run ./cmd/stackrox-mcp --config e2e-tests/stackrox-mcp-e2e-config.yaml 2>&1 | grep -m1 "Tools registration complete" && echo "✓ MCP server started successfully" +' || echo "✗ MCP server failed to start" + +# Test with grpcurl +echo "" +echo "4. Testing WireMock responses..." + +# Test auth (should accept test-token-admin) +AUTH_RESULT=$(grpcurl -insecure -H "Authorization: Bearer test-token-admin" \ + -d '{}' localhost:8081 v1.ClustersService/GetClusters 2>&1 || true) + +if echo "$AUTH_RESULT" | grep -q "clusters"; then + echo "✓ Authentication works" +else + echo "✗ Authentication failed" + echo "$AUTH_RESULT" +fi + +# Test CVE query +CVE_RESULT=$(grpcurl -insecure -H "Authorization: Bearer test-token-admin" \ + -d '{"query": "CVE:\"CVE-2021-44228\""}' \ + localhost:8081 v1.DeploymentService/ListDeployments 2>&1 || true) + +if echo "$CVE_RESULT" | grep -q "deployments"; then + echo "✓ CVE query returns data" +else + echo "✗ CVE query failed" + echo "$CVE_RESULT" +fi + +# Cleanup +echo "" +echo "5. Cleaning up..." +cd "$ROOT_DIR" +make mock-stop + +echo "" +echo "══════════════════════════════════════════════════════════" +echo " Smoke Test Complete!" +echo "══════════════════════════════════════════════════════════" diff --git a/e2e-tests/tools/go.mod b/e2e-tests/tools/go.mod index 7f5d2ba..03a867e 100644 --- a/e2e-tests/tools/go.mod +++ b/e2e-tests/tools/go.mod @@ -2,20 +2,30 @@ module github.com/stackrox/stackrox-mcp/e2e-tests go 1.25.5 -require github.com/mcpchecker/mcpchecker v0.0.4 +require ( + github.com/fullstorydev/grpcurl v1.9.3 + github.com/mcpchecker/mcpchecker v0.0.4 +) require ( + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c // indirect github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/genmcp/gen-mcp v0.2.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.24.2 // indirect @@ -40,6 +50,7 @@ require ( github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-openapi/validate v0.25.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/certificate-transparency-go v1.3.2 // indirect github.com/google/go-containerregistry v0.20.7 // indirect github.com/google/jsonschema-go v0.4.2 // indirect @@ -49,6 +60,7 @@ require ( github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jhump/protoreflect v1.17.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -57,6 +69,7 @@ require ( github.com/openai/openai-go/v2 v2.7.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect @@ -67,6 +80,7 @@ require ( github.com/sigstore/timestamp-authority/v2 v2.0.4 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.3.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect diff --git a/e2e-tests/tools/go.sum b/e2e-tests/tools/go.sum index 4f446c7..5e36cec 100644 --- a/e2e-tests/tools/go.sum +++ b/e2e-tests/tools/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= @@ -68,6 +70,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -76,6 +80,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= @@ -92,10 +98,20 @@ github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c h1:g349iS+CtAvba7i github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c/go.mod h1:mCGGmWkOQvEuLdIRfPIpXViBfpWto4AhwtJlAvo62SQ= github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea h1:ALRwvjsSP53QmnN3Bcj0NpR8SsFLnskny/EIMebAk1c= github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk= +github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE= github.com/genmcp/gen-mcp v0.2.3 h1:nnHbUinrCV/W4Ki0DGQaZTfrfQEcpNzJ4N2WgFfqCUk= github.com/genmcp/gen-mcp v0.2.3/go.mod h1:OOeVwjEfMoQ+P7K5tHb25EbcmA79iII4wGqZkt2bvKA= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= @@ -232,6 +248,8 @@ github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7P github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -268,6 +286,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -310,6 +330,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= diff --git a/e2e-tests/tools/tools.go b/e2e-tests/tools/tools.go index dba1934..bd46238 100644 --- a/e2e-tests/tools/tools.go +++ b/e2e-tests/tools/tools.go @@ -4,5 +4,6 @@ package tools import ( + _ "github.com/fullstorydev/grpcurl/cmd/grpcurl" _ "github.com/mcpchecker/mcpchecker/cmd/mcpchecker" ) diff --git a/scripts/download-wiremock.sh b/scripts/download-wiremock.sh new file mode 100755 index 0000000..53ede54 --- /dev/null +++ b/scripts/download-wiremock.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +WIREMOCK_VERSION="3.9.1" +GRPC_EXTENSION_VERSION="0.8.0" +WIREMOCK_DIR="wiremock/lib" + +mkdir -p "$WIREMOCK_DIR" + +echo "Downloading WireMock standalone JAR..." +curl -L "https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/${WIREMOCK_VERSION}/wiremock-standalone-${WIREMOCK_VERSION}.jar" \ + -o "$WIREMOCK_DIR/wiremock-standalone.jar" + +echo "Downloading WireMock gRPC extension..." +curl -L "https://repo1.maven.org/maven2/org/wiremock/wiremock-grpc-extension-standalone/${GRPC_EXTENSION_VERSION}/wiremock-grpc-extension-standalone-${GRPC_EXTENSION_VERSION}.jar" \ + -o "$WIREMOCK_DIR/wiremock-grpc-extension.jar" + +echo "✓ WireMock JARs downloaded to $WIREMOCK_DIR" diff --git a/scripts/generate-proto-descriptors.sh b/scripts/generate-proto-descriptors.sh new file mode 100755 index 0000000..5b4aac9 --- /dev/null +++ b/scripts/generate-proto-descriptors.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +DESCRIPTOR_DIR="wiremock/proto/descriptors" +ROX_PROTO_PATH="wiremock/proto/stackrox" +GOOGLEAPIS_PATH="wiremock/proto/googleapis" +GRPC_DIR="wiremock/grpc" + +mkdir -p "$DESCRIPTOR_DIR" "$GRPC_DIR" + +# Ensure proto files are present +if [ ! -d "$ROX_PROTO_PATH" ]; then + echo "Proto files not found. Running setup..." + ./scripts/setup-proto-files.sh +fi + +# Use PROTOC_BIN if set (from Makefile), otherwise use system protoc +PROTOC_CMD="${PROTOC_BIN:-protoc}" + +if ! command -v "$PROTOC_CMD" &> /dev/null; then + echo "Error: protoc is not installed" + echo "Install with: make proto-install" + echo "Or install manually from: https://grpc.io/docs/protoc-installation/" + exit 1 +fi + +echo "Generating proto descriptors with $PROTOC_CMD..." + +"$PROTOC_CMD" \ + --descriptor_set_out="$DESCRIPTOR_DIR/stackrox.dsc" \ + --include_imports \ + --proto_path="$ROX_PROTO_PATH" \ + --proto_path="$GOOGLEAPIS_PATH" \ + api/v1/deployment_service.proto \ + api/v1/image_service.proto \ + api/v1/node_service.proto \ + api/v1/cluster_service.proto + +cp "$DESCRIPTOR_DIR/stackrox.dsc" "$GRPC_DIR/" + +echo "✓ Proto descriptors generated at $DESCRIPTOR_DIR/stackrox.dsc" diff --git a/scripts/setup-proto-files.sh b/scripts/setup-proto-files.sh new file mode 100755 index 0000000..ff92e2c --- /dev/null +++ b/scripts/setup-proto-files.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +echo "Setting up proto files from go modules..." + +# Ensure go modules are downloaded +go mod download + +# Discover rox module location using go list +ROX_DIR=$(go list -f '{{.Dir}}' -m github.com/stackrox/rox) + +if [ -z "$ROX_DIR" ]; then + echo "Error: github.com/stackrox/rox module not found" + echo "Run: go mod download" + exit 1 +fi + +echo "Using proto files from: $ROX_DIR" + +# Create target directories +mkdir -p wiremock/proto/stackrox wiremock/proto/googleapis + +# Copy proto files from rox module +# Note: Files from go mod cache are read-only, so we copy and chmod +cp -r "$ROX_DIR/proto/"* wiremock/proto/stackrox/ +cp -r "$ROX_DIR/third_party/googleapis/"* wiremock/proto/googleapis/ + +# Copy scanner protos from scanner module (following stackrox pattern) +SCANNER_DIR=$(go list -f '{{.Dir}}' -m github.com/stackrox/scanner) +if [ -n "$SCANNER_DIR" ] && [ -d "$SCANNER_DIR/proto/scanner" ]; then + echo "Using scanner proto files from: $SCANNER_DIR" + cp -r "$SCANNER_DIR/proto/scanner" wiremock/proto/stackrox/ +fi + +echo "✓ Proto files copied from go mod cache" +echo "Next: ./scripts/generate-proto-descriptors.sh" diff --git a/scripts/smoke-test-wiremock.sh b/scripts/smoke-test-wiremock.sh new file mode 100755 index 0000000..073a188 --- /dev/null +++ b/scripts/smoke-test-wiremock.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +echo "=== WireMock Smoke Test ===" + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Create temp directory for test artifacts +TEMP_DIR=$(mktemp -d) +echo "Using temp directory: $TEMP_DIR" + +cleanup() { + echo "" + echo "Cleaning up..." + "$PROJECT_ROOT/scripts/stop-mock-central.sh" 2>/dev/null || true + rm -rf "$TEMP_DIR" +} + +trap cleanup EXIT + +run_test() { + local test_name="$1" + local test_command="$2" + + echo -n "Testing: $test_name... " + if eval "$test_command" > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC}" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo -e "${RED}✗${NC}" + TESTS_FAILED=$((TESTS_FAILED + 1)) + return 1 + fi +} + +echo "" +echo "Setup..." + +if [ ! -f "$PROJECT_ROOT/wiremock/lib/wiremock-standalone.jar" ]; then + "$PROJECT_ROOT/scripts/download-wiremock.sh" +fi + +if [ ! -f "$PROJECT_ROOT/wiremock/proto/descriptors/stackrox.dsc" ]; then + "$PROJECT_ROOT/scripts/generate-proto-descriptors.sh" +fi + +echo "" +echo "Starting WireMock..." +"$PROJECT_ROOT/scripts/start-mock-central.sh" + +echo "" +run_test "WireMock is running" "make -C '$PROJECT_ROOT' mock-status | grep -q 'running'" || true +run_test "Admin API responds" "curl -skf https://localhost:8081/__admin/mappings > /dev/null" || true +run_test "Rejects missing auth" "curl -sk -X POST -H 'Content-Type: application/json' -d '{}' https://localhost:8081/v1.DeploymentService/ListDeployments | grep -q '\"code\":16'" || true +run_test "Returns CVE-2021-44228 data" "curl -skf -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer test-token-admin' -d '{\"query\":{\"query\":\"CVE:\\\"CVE-2021-44228\\\"\"}}' https://localhost:8081/v1.DeploymentService/ListDeployments | grep -q 'dep-004'" || true +run_test "Returns empty for unknown CVE" "curl -skf -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer test-token-admin' -d '{}' https://localhost:8081/v1.DeploymentService/ListDeployments | grep -q '\"deployments\": \[\]'" || true + +echo "" +echo "Testing MCP integration..." + +if [ ! -f "$PROJECT_ROOT/stackrox-mcp" ]; then + make -C "$PROJECT_ROOT" build > /dev/null 2>&1 +fi + +export STACKROX_MCP__SERVER__TYPE=stdio +export STACKROX_MCP__CENTRAL__URL=localhost:8081 +export STACKROX_MCP__CENTRAL__AUTH_TYPE=static +export STACKROX_MCP__CENTRAL__API_TOKEN=test-token-admin +export STACKROX_MCP__CENTRAL__INSECURE_SKIP_TLS_VERIFY=true +export STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED=true + +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' > "$TEMP_DIR/input.json" + +timeout 3 "$PROJECT_ROOT/stackrox-mcp" < "$TEMP_DIR/input.json" > "$TEMP_DIR/stdout.log" 2>"$TEMP_DIR/stderr.log" || true + +run_test "MCP starts with WireMock" "grep -q 'Starting StackRox MCP server' '$TEMP_DIR/stderr.log'" || true +run_test "MCP registers tools" "grep -q 'get_deployments_for_cve' '$TEMP_DIR/stderr.log'" || true + +echo "" +if [ $TESTS_FAILED -eq 0 ]; then + echo -e "${GREEN}✓ All $TESTS_PASSED tests passed${NC}" + exit 0 +else + echo -e "${RED}✗ $TESTS_FAILED/$((TESTS_PASSED + TESTS_FAILED)) tests failed${NC}" + exit 1 +fi diff --git a/scripts/start-mock-central.sh b/scripts/start-mock-central.sh new file mode 100755 index 0000000..e8ab5c3 --- /dev/null +++ b/scripts/start-mock-central.sh @@ -0,0 +1,87 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +WIREMOCK_DIR="$PROJECT_ROOT/wiremock" +PID_FILE="$WIREMOCK_DIR/wiremock.pid" +LOG_FILE="$WIREMOCK_DIR/wiremock.log" + +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p "$PID" > /dev/null 2>&1; then + echo "WireMock is already running (PID: $PID)" + exit 0 + fi + rm "$PID_FILE" +fi + +if [ ! -f "$WIREMOCK_DIR/lib/wiremock-standalone.jar" ]; then + "$PROJECT_ROOT/scripts/download-wiremock.sh" +fi + +if [ ! -f "$WIREMOCK_DIR/proto/descriptors/stackrox.dsc" ]; then + "$PROJECT_ROOT/scripts/generate-proto-descriptors.sh" +fi + +# Create __files symlink if needed (WireMock expects this) +if [ ! -L "$WIREMOCK_DIR/__files" ]; then + cd "$WIREMOCK_DIR" + ln -s fixtures __files + cd "$PROJECT_ROOT" +fi + +echo "Starting WireMock with TLS..." + +# Use subshell to avoid having to cd back +( +cd "$WIREMOCK_DIR" +java -cp "lib/wiremock-standalone.jar:lib/wiremock-grpc-extension.jar" \ + wiremock.Run \ + --port 8080 \ + --https-port 8081 \ + --https-keystore certs/keystore.jks \ + --keystore-password wiremock \ + --key-manager-password wiremock \ + --keystore-type JKS \ + --global-response-templating \ + --verbose \ + --root-dir . \ + > wiremock.log 2>&1 & + +WIREMOCK_PID=$! +echo $WIREMOCK_PID > wiremock.pid +) + +# Wait for WireMock to be ready +echo "Waiting for WireMock to be ready..." +MAX_WAIT=30 +WAITED=0 +while [ $WAITED -lt $MAX_WAIT ]; do + if curl -skf https://localhost:8081/__admin/mappings > /dev/null 2>&1; then + break + fi + sleep 1 + WAITED=$((WAITED + 1)) +done + +if [ $WAITED -eq $MAX_WAIT ]; then + echo "✗ WireMock failed to start within ${MAX_WAIT}s. Check $LOG_FILE" + exit 1 +fi + +# Read PID from file (written inside subshell) +if [ -f "$PID_FILE" ]; then + WIREMOCK_PID=$(cat "$PID_FILE") + if ps -p "$WIREMOCK_PID" > /dev/null 2>&1; then + echo "✓ WireMock started (PID: $WIREMOCK_PID) on https://localhost:8081" + else + echo "✗ Failed to start WireMock. Check $LOG_FILE" + rm "$PID_FILE" + exit 1 + fi +else + echo "✗ Failed to start WireMock. PID file not created. Check $LOG_FILE" + exit 1 +fi diff --git a/scripts/stop-mock-central.sh b/scripts/stop-mock-central.sh new file mode 100755 index 0000000..7cf5f8a --- /dev/null +++ b/scripts/stop-mock-central.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +PID_FILE="wiremock/wiremock.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "WireMock is not running" + exit 0 +fi + +PID=$(cat "$PID_FILE") + +if ps -p "$PID" > /dev/null 2>&1; then + kill "$PID" + for i in {1..10}; do + if ! ps -p "$PID" > /dev/null 2>&1; then + break + fi + sleep 1 + done + if ps -p "$PID" > /dev/null 2>&1; then + kill -9 "$PID" + fi + echo "✓ WireMock stopped" +else + echo "WireMock process not found" +fi + +rm "$PID_FILE" diff --git a/wiremock/README.md b/wiremock/README.md new file mode 100644 index 0000000..57744ab --- /dev/null +++ b/wiremock/README.md @@ -0,0 +1,41 @@ +# WireMock Mock StackRox Central + +WireMock standalone mock service for StackRox Central. Docs: [WireMock](https://wiremock.org/docs/), [gRPC Extension](https://github.com/wiremock/wiremock-grpc-extension) + +## Quick Start + +```bash +make mock-download # Download WireMock JARs +make proto-setup # Setup proto files from github.com/stackrox/rox +./scripts/generate-proto-descriptors.sh # Generate proto descriptors +make mock-start # Start on port 8081 +``` + +## Usage + +Commands: `make mock-start|stop|restart|status|logs|test` + +Connect MCP server to `localhost:8081` with `STACKROX_MCP__CENTRAL__API_TOKEN=test-token-admin` and `INSECURE_SKIP_TLS_VERIFY=true` + +## Test Data + +**Auth:** Any `test-token-*` accepted (e.g., `test-token-admin`) + +**CVE Queries:** +- `CVE-2021-44228` → 3 deployments (log4j) +- `CVE-2021-31805` → 3 deployments, 2 clusters +- `CVE-2016-1000031` → 2 deployments, 1 cluster +- Other CVEs → 1 deployment +- No CVE → all clusters (3) + +## Adding Scenarios + +1. Add fixture to `fixtures/` (see `fixtures/README.md`) +2. Add mapping to `mappings/` ([docs](https://wiremock.org/docs/request-matching/)) +3. `make mock-restart` + +## Troubleshooting + +- Check logs: `cat wiremock/wiremock.log` +- Missing proto descriptors: `./scripts/generate-proto-descriptors.sh` +- Debug requests: `curl http://localhost:8081/__admin/requests` ([Admin API docs](https://wiremock.org/docs/api/)) diff --git a/wiremock/certs/keystore.jks b/wiremock/certs/keystore.jks new file mode 100644 index 0000000..29d6d8e Binary files /dev/null and b/wiremock/certs/keystore.jks differ diff --git a/wiremock/fixtures/README.md b/wiremock/fixtures/README.md new file mode 100644 index 0000000..0164214 --- /dev/null +++ b/wiremock/fixtures/README.md @@ -0,0 +1,203 @@ +# WireMock Fixtures + +This directory contains JSON fixture files that WireMock uses to respond to gRPC requests. + +## Directory Structure + +``` +fixtures/ +├── deployments/ # DeploymentService responses +├── images/ # ImageService responses +├── nodes/ # NodeService responses +└── clusters/ # ClustersService responses +``` + +## How Fixtures Work + +1. **WireMock mappings** (in `../mappings/`) define request matching rules +2. **Fixtures** (in this directory) provide the response data +3. Mappings reference fixtures via `bodyFileName` field + +Example mapping: +```json +{ + "request": { + "urlPath": "/v1.DeploymentService/ListDeployments", + "bodyPatterns": [ + {"matchesJsonPath": "$.query[?(@.query =~ /.*CVE-2021-44228.*/)]"} + ] + }, + "response": { + "bodyFileName": "deployments/log4j_cve.json" + } +} +``` + +## Adding New Test Scenarios + +### Step 1: Create a Fixture File + +Create a new JSON file with realistic response data: + +```bash +# Example: Create a fixture for a specific CVE +cat > deployments/my_cve_scenario.json <