From 2056fdf490c2e3d6f803ccbc079ee5a2d6eec0ab Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 12:05:08 +0100 Subject: [PATCH 01/13] Add smoke test infrastructure Add smoke tests that run against real StackRox Central deployment. Tests verify end-to-end functionality including connection, authentication, and CVE detection with actual scanning. - Add smoke/smoke_test.go with table-driven tests - Add smoke/testdata/vulnerable-deployment.yaml with CVE-2023-32697 - Extract shared test helpers to test_helpers.go for reuse - Update integration_helpers.go to use shared helpers Tests read ROX_ENDPOINT and ROX_PASSWORD from environment variables. Co-Authored-By: Claude Sonnet 4.5 --- internal/testutil/integration_helpers.go | 36 ------ internal/testutil/test_helpers.go | 43 +++++++ smoke/smoke_test.go | 145 ++++++++++++++++++++++ smoke/testdata/vulnerable-deployment.yaml | 27 ++++ 4 files changed, 215 insertions(+), 36 deletions(-) create mode 100644 internal/testutil/test_helpers.go create mode 100644 smoke/smoke_test.go create mode 100644 smoke/testdata/vulnerable-deployment.yaml diff --git a/internal/testutil/integration_helpers.go b/internal/testutil/integration_helpers.go index 21b48e7..260f146 100644 --- a/internal/testutil/integration_helpers.go +++ b/internal/testutil/integration_helpers.go @@ -8,10 +8,8 @@ import ( "testing" "time" - "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stackrox/stackrox-mcp/internal/app" "github.com/stackrox/stackrox-mcp/internal/config" - "github.com/stretchr/testify/require" ) // CreateIntegrationTestConfig creates a test configuration for integration tests. @@ -55,37 +53,3 @@ func CreateIntegrationMCPClient(t *testing.T) (*MCPTestClient, error) { return NewMCPTestClient(t, runFunc) } - -// SetupInitializedClient creates an initialized MCP client for testing with automatic cleanup. -func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient { - t.Helper() - - client, err := createClient(t) - require.NoError(t, err, "Failed to create MCP client") - t.Cleanup(func() { client.Close() }) - - return client -} - -// CallToolAndGetResult calls a tool and verifies it succeeds. -func CallToolAndGetResult(t *testing.T, client *MCPTestClient, toolName string, args map[string]any) *mcp.CallToolResult { - t.Helper() - - ctx := context.Background() - result, err := client.CallTool(ctx, toolName, args) - require.NoError(t, err) - RequireNoError(t, result) - - return result -} - -// GetTextContent extracts text from the first content item. -func GetTextContent(t *testing.T, result *mcp.CallToolResult) string { - t.Helper() - require.NotEmpty(t, result.Content, "should have content in response") - - textContent, ok := result.Content[0].(*mcp.TextContent) - require.True(t, ok, "expected TextContent, got %T", result.Content[0]) - - return textContent.Text -} diff --git a/internal/testutil/test_helpers.go b/internal/testutil/test_helpers.go new file mode 100644 index 0000000..da86fdc --- /dev/null +++ b/internal/testutil/test_helpers.go @@ -0,0 +1,43 @@ +package testutil + +import ( + "context" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/require" +) + +// SetupInitializedClient creates an initialized MCP client for testing with automatic cleanup. +func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient { + t.Helper() + + client, err := createClient(t) + require.NoError(t, err, "Failed to create MCP client") + t.Cleanup(func() { client.Close() }) + + return client +} + +// CallToolAndGetResult calls a tool and verifies it succeeds. +func CallToolAndGetResult(t *testing.T, client *MCPTestClient, toolName string, args map[string]any) *mcp.CallToolResult { + t.Helper() + + ctx := context.Background() + result, err := client.CallTool(ctx, toolName, args) + require.NoError(t, err) + RequireNoError(t, result) + + return result +} + +// GetTextContent extracts text from the first content item. +func GetTextContent(t *testing.T, result *mcp.CallToolResult) string { + t.Helper() + require.NotEmpty(t, result.Content, "should have content in response") + + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent, got %T", result.Content[0]) + + return textContent.Text +} diff --git a/smoke/smoke_test.go b/smoke/smoke_test.go new file mode 100644 index 0000000..1016fe3 --- /dev/null +++ b/smoke/smoke_test.go @@ -0,0 +1,145 @@ +//go:build smoke + +package smoke + +import ( + "context" + "encoding/json" + "io" + "os" + "testing" + "time" + + "github.com/stackrox/stackrox-mcp/internal/app" + "github.com/stackrox/stackrox-mcp/internal/config" + "github.com/stackrox/stackrox-mcp/internal/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSmoke_RealCluster(t *testing.T) { + if testing.Short() { + t.Skip("Skipping smoke test in short mode") + } + + endpoint := os.Getenv("ROX_ENDPOINT") + password := os.Getenv("ROX_PASSWORD") + + if endpoint == "" || password == "" { + t.Fatal("ROX_ENDPOINT and ROX_PASSWORD environment variables must be set") + } + + client := createSmokeTestClient(t, endpoint, password) + + tests := map[string]struct { + toolName string + args map[string]any + validateFunc func(*testing.T, string) + }{ + "list_clusters": { + toolName: "list_clusters", + args: map[string]any{}, + validateFunc: func(t *testing.T, result string) { + t.Helper() + var data struct { + Clusters []struct { + Name string `json:"name"` + } `json:"clusters"` + } + require.NoError(t, json.Unmarshal([]byte(result), &data)) + assert.NotEmpty(t, data.Clusters, "should have at least one cluster") + t.Logf("Found %d cluster(s)", len(data.Clusters)) + }, + }, + "get_deployments_for_cve with known CVE": { + toolName: "get_deployments_for_cve", + args: map[string]any{"cveName": "CVE-2023-32697"}, + validateFunc: func(t *testing.T, result string) { + t.Helper() + var data struct { + Deployments []struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + } `json:"deployments"` + } + require.NoError(t, json.Unmarshal([]byte(result), &data)) + + // The deployment might not be scanned yet, so we'll be lenient + if len(data.Deployments) == 0 { + t.Log("Warning: No deployments found with CVE-2023-32697. Deployment may not be scanned yet.") + } else { + found := false + for _, dep := range data.Deployments { + if dep.Name == "vulnerable-app" && dep.Namespace == "vulnerable-apps" { + found = true + break + } + } + if found { + t.Log("Successfully found vulnerable-app deployment") + } else { + t.Logf("Found %d deployment(s) with CVE, but not the expected vulnerable-app", len(data.Deployments)) + } + } + }, + }, + "get_deployments_for_cve with non-existent CVE": { + toolName: "get_deployments_for_cve", + args: map[string]any{"cveName": "CVE-9999-99999"}, + validateFunc: func(t *testing.T, result string) { + t.Helper() + var data struct { + Deployments []any `json:"deployments"` + } + require.NoError(t, json.Unmarshal([]byte(result), &data)) + assert.Empty(t, data.Deployments, "should have no deployments for non-existent CVE") + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + result := testutil.CallToolAndGetResult(t, client, tt.toolName, tt.args) + responseText := testutil.GetTextContent(t, result) + tt.validateFunc(t, responseText) + }) + } +} + +func createSmokeTestClient(t *testing.T, endpoint, password string) *testutil.MCPTestClient { + t.Helper() + + cfg := &config.Config{ + Central: config.CentralConfig{ + URL: endpoint, + AuthType: "static", + APIToken: password, + InsecureSkipTLSVerify: true, + RequestTimeout: 30 * time.Second, + MaxRetries: 3, + InitialBackoff: time.Second, + MaxBackoff: 10 * time.Second, + }, + Server: config.ServerConfig{ + Type: "stdio", + }, + Tools: config.ToolsConfig{ + Vulnerability: config.ToolsetVulnerabilityConfig{ + Enabled: true, + }, + ConfigManager: config.ToolConfigManagerConfig{ + Enabled: true, + }, + }, + } + + runFunc := func(ctx context.Context, stdin io.ReadCloser, stdout io.WriteCloser) error { + return app.Run(ctx, cfg, stdin, stdout) + } + + client, err := testutil.NewMCPTestClient(t, runFunc) + require.NoError(t, err, "Failed to create MCP client") + t.Cleanup(func() { client.Close() }) + + return client +} diff --git a/smoke/testdata/vulnerable-deployment.yaml b/smoke/testdata/vulnerable-deployment.yaml new file mode 100644 index 0000000..7237dd6 --- /dev/null +++ b/smoke/testdata/vulnerable-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vulnerable-apps +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vulnerable-app + namespace: vulnerable-apps + labels: + app: vulnerable-app +spec: + replicas: 1 + selector: + matchLabels: + app: vulnerable-app + template: + metadata: + labels: + app: vulnerable-app + spec: + containers: + - name: vulnerable-app + image: quay.io/rhacs-eng/qa:sqlite-jdbc-CVE-2023-32697 + ports: + - containerPort: 8080 From e1d95488a8d0b1730cf898ccc1ed17393e28dc5c Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 12:05:09 +0100 Subject: [PATCH 02/13] Add GitHub Actions workflow for smoke tests Add workflow that runs smoke tests against real StackRox Central on kind. Follows stackrox/jenkins-plugin deployment approach. Workflow: - Creates kind cluster - Deploys StackRox Central via stackrox/deploy/k8s/deploy-local.sh - Waits for Scanner readiness - Deploys vulnerable workload - Runs smoke tests with JUnit output - Uploads results and coverage to Codecov Runs on every PR and push to main. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 117 ++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/smoke.yml diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..822c226 --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,117 @@ +name: Smoke Tests + +on: + push: + branches: + - main + pull_request: + types: + - opened + - reopened + - synchronize + +jobs: + smoke: + name: Run Smoke Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Download dependencies + run: go mod download + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: stackrox-mcp-smoke + + - name: Clone StackRox repository + run: git clone --depth 1 https://github.com/stackrox/stackrox.git stackrox-repo + + - name: Deploy StackRox Central + env: + MAIN_IMAGE_TAG: latest + SENSOR_HELM_DEPLOY: "true" + ROX_SCANNER_V4: "false" + run: | + cd stackrox-repo + ./deploy/k8s/deploy-local.sh + + - name: Wait for Scanner readiness + run: sleep 120 + + - name: Extract Central password + id: extract-password + run: | + PASSWORD=$(kubectl get secret -n stackrox central-htpasswd -o json | jq -r '.data.password' | base64 -d) + echo "::add-mask::$PASSWORD" + echo "password=$PASSWORD" >> $GITHUB_OUTPUT + + - name: Port-forward Central + run: | + kubectl port-forward -n stackrox svc/central 8443:443 & + sleep 5 + + - name: Deploy vulnerable workload + run: kubectl apply -f smoke/testdata/vulnerable-deployment.yaml + + - name: Wait for vulnerable deployment + run: | + kubectl wait --for=condition=available --timeout=120s deployment/vulnerable-app -n vulnerable-apps + + - name: Wait for deployment scan + run: sleep 60 + + - name: Install go-junit-report + run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0 + + - name: Run smoke tests with JUnit output + env: + ROX_ENDPOINT: localhost:8443 + ROX_PASSWORD: ${{ steps.extract-password.outputs.password }} + run: | + go test -v -tags=smoke -cover -race -coverprofile=coverage-smoke.out -timeout=20m ./smoke 2>&1 | \ + tee /dev/stderr | \ + go-junit-report -set-exit-code -out junit-smoke.xml + + - name: Upload JUnit test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: junit-smoke-results + path: junit-smoke.xml + if-no-files-found: error + + - name: Upload test results to Codecov + if: always() + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: junit-smoke.xml + + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + files: ./coverage-smoke.out + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + flags: smoke + name: smoke-tests + + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: smoke-test-logs + path: | + /tmp/kind-* + if-no-files-found: ignore From edfdd0390ec9c9523e7a23c62ff38067dc0ed5b5 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 17:04:43 +0100 Subject: [PATCH 03/13] Fix vulnerable deployment resource constraints and add log collection Set CPU and memory requests to 0 to avoid scheduling issues in CI. Add comprehensive log collection on workflow completion for debugging. Logs collected: - All pods status - All events sorted by time - Vulnerable app logs - Central logs - Scanner logs - Pod descriptions Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 18 ++++++++++++++---- smoke/testdata/vulnerable-deployment.yaml | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 822c226..cfc9ea9 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -107,11 +107,21 @@ jobs: flags: smoke name: smoke-tests - - name: Upload logs on failure - if: failure() + - name: Collect logs + if: always() + run: | + mkdir -p logs + kubectl get pods -A > logs/pods.txt || true + kubectl get events -A --sort-by='.lastTimestamp' > logs/events.txt || true + kubectl logs -n vulnerable-apps deployment/vulnerable-app --all-containers=true > logs/vulnerable-app.log || true + kubectl logs -n stackrox deployment/central > logs/central.log || true + kubectl logs -n stackrox deployment/scanner > logs/scanner.log || true + kubectl describe pod -n vulnerable-apps > logs/vulnerable-app-describe.txt || true + + - name: Upload logs + if: always() uses: actions/upload-artifact@v4 with: name: smoke-test-logs - path: | - /tmp/kind-* + path: logs/ if-no-files-found: ignore diff --git a/smoke/testdata/vulnerable-deployment.yaml b/smoke/testdata/vulnerable-deployment.yaml index 7237dd6..a076acb 100644 --- a/smoke/testdata/vulnerable-deployment.yaml +++ b/smoke/testdata/vulnerable-deployment.yaml @@ -25,3 +25,7 @@ spec: image: quay.io/rhacs-eng/qa:sqlite-jdbc-CVE-2023-32697 ports: - containerPort: 8080 + resources: + requests: + cpu: "0" + memory: "0" From 98cb1a3a340e481f4ffaa957a1301530f2486886 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 17:19:58 +0100 Subject: [PATCH 04/13] Use publicly available nginx:1.14 image Replace unavailable custom image with nginx:1.14 which is publicly accessible and has known CVEs for testing. Co-Authored-By: Claude Sonnet 4.5 --- smoke/smoke_test.go | 18 +++--------------- smoke/testdata/vulnerable-deployment.yaml | 4 ++-- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/smoke/smoke_test.go b/smoke/smoke_test.go index 1016fe3..023ea7b 100644 --- a/smoke/smoke_test.go +++ b/smoke/smoke_test.go @@ -53,7 +53,7 @@ func TestSmoke_RealCluster(t *testing.T) { }, "get_deployments_for_cve with known CVE": { toolName: "get_deployments_for_cve", - args: map[string]any{"cveName": "CVE-2023-32697"}, + args: map[string]any{"cveName": "CVE-2019-11043"}, validateFunc: func(t *testing.T, result string) { t.Helper() var data struct { @@ -64,22 +64,10 @@ func TestSmoke_RealCluster(t *testing.T) { } require.NoError(t, json.Unmarshal([]byte(result), &data)) - // The deployment might not be scanned yet, so we'll be lenient if len(data.Deployments) == 0 { - t.Log("Warning: No deployments found with CVE-2023-32697. Deployment may not be scanned yet.") + t.Log("Warning: No deployments found with CVE. Deployment may not be scanned yet.") } else { - found := false - for _, dep := range data.Deployments { - if dep.Name == "vulnerable-app" && dep.Namespace == "vulnerable-apps" { - found = true - break - } - } - if found { - t.Log("Successfully found vulnerable-app deployment") - } else { - t.Logf("Found %d deployment(s) with CVE, but not the expected vulnerable-app", len(data.Deployments)) - } + t.Logf("Found %d deployment(s) with CVE", len(data.Deployments)) } }, }, diff --git a/smoke/testdata/vulnerable-deployment.yaml b/smoke/testdata/vulnerable-deployment.yaml index a076acb..bc7bdf4 100644 --- a/smoke/testdata/vulnerable-deployment.yaml +++ b/smoke/testdata/vulnerable-deployment.yaml @@ -22,9 +22,9 @@ spec: spec: containers: - name: vulnerable-app - image: quay.io/rhacs-eng/qa:sqlite-jdbc-CVE-2023-32697 + image: nginx:1.14 ports: - - containerPort: 8080 + - containerPort: 80 resources: requests: cpu: "0" From d365c3127b934e4c02264b47e6de207e7fc5b74e Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 17:24:29 +0100 Subject: [PATCH 05/13] Fix linter issues in test_helpers.go - Add blank identifier for ignored Close error in cleanup - Split long function signature across multiple lines Co-Authored-By: Claude Sonnet 4.5 --- internal/testutil/test_helpers.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/testutil/test_helpers.go b/internal/testutil/test_helpers.go index da86fdc..5f995f5 100644 --- a/internal/testutil/test_helpers.go +++ b/internal/testutil/test_helpers.go @@ -8,19 +8,24 @@ import ( "github.com/stretchr/testify/require" ) -// SetupInitializedClient creates an initialized MCP client for testing with automatic cleanup. +// SetupInitializedClient creates an initialized MCP client with automatic cleanup. func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient { t.Helper() client, err := createClient(t) require.NoError(t, err, "Failed to create MCP client") - t.Cleanup(func() { client.Close() }) + t.Cleanup(func() { _ = client.Close() }) return client } // CallToolAndGetResult calls a tool and verifies it succeeds. -func CallToolAndGetResult(t *testing.T, client *MCPTestClient, toolName string, args map[string]any) *mcp.CallToolResult { +func CallToolAndGetResult( + t *testing.T, + client *MCPTestClient, + toolName string, + args map[string]any, +) *mcp.CallToolResult { t.Helper() ctx := context.Background() From 3eff6c177f1b6fcb1adff9f63effd4701f637011 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 19:11:15 +0100 Subject: [PATCH 06/13] Fix smoke test authentication and shellcheck warning Generate proper API token via StackRox Central REST API instead of using admin password directly. This fixes authentication failures where the password was incorrectly used as an API token. Changes: - Add API token generation step using curl to /v1/apitokens endpoint - Use basic auth (admin:password) to authenticate token generation - Pass generated token to tests via ROX_API_TOKEN env var - Update smoke test to read ROX_API_TOKEN instead of ROX_PASSWORD - Fix shellcheck SC2086 warning by quoting variables Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 32 +++++++++++++++++++++++++++----- smoke/smoke_test.go | 12 ++++++------ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index cfc9ea9..29ea0c9 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -51,11 +51,33 @@ jobs: - name: Extract Central password id: extract-password run: | - PASSWORD=$(kubectl get secret -n stackrox central-htpasswd -o json | jq -r '.data.password' | base64 -d) - echo "::add-mask::$PASSWORD" - echo "password=$PASSWORD" >> $GITHUB_OUTPUT + PASSWORD="$(kubectl get secret -n stackrox central-htpasswd -o json | jq -r '.data.password' | base64 -d)" + echo "::add-mask::${PASSWORD}" + echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT" - - name: Port-forward Central + - name: Generate API token + id: generate-token + run: | + kubectl port-forward -n stackrox svc/central 8443:443 & + PF_PID=$! + sleep 5 + + PASSWORD="${{ steps.extract-password.outputs.password }}" + + TOKEN_RESPONSE=$(curl -k -s -u "admin:${PASSWORD}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"name":"smoke-test-token","role":"Admin"}' \ + https://localhost:8443/v1/apitokens) + + API_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.token') + echo "::add-mask::${API_TOKEN}" + + kill "${PF_PID}" || true + + echo "token=${API_TOKEN}" >> "$GITHUB_OUTPUT" + + - name: Port-forward Central for tests run: | kubectl port-forward -n stackrox svc/central 8443:443 & sleep 5 @@ -76,7 +98,7 @@ jobs: - name: Run smoke tests with JUnit output env: ROX_ENDPOINT: localhost:8443 - ROX_PASSWORD: ${{ steps.extract-password.outputs.password }} + ROX_API_TOKEN: ${{ steps.generate-token.outputs.token }} run: | go test -v -tags=smoke -cover -race -coverprofile=coverage-smoke.out -timeout=20m ./smoke 2>&1 | \ tee /dev/stderr | \ diff --git a/smoke/smoke_test.go b/smoke/smoke_test.go index 023ea7b..ac041ce 100644 --- a/smoke/smoke_test.go +++ b/smoke/smoke_test.go @@ -23,13 +23,13 @@ func TestSmoke_RealCluster(t *testing.T) { } endpoint := os.Getenv("ROX_ENDPOINT") - password := os.Getenv("ROX_PASSWORD") + apiToken := os.Getenv("ROX_API_TOKEN") - if endpoint == "" || password == "" { - t.Fatal("ROX_ENDPOINT and ROX_PASSWORD environment variables must be set") + if endpoint == "" || apiToken == "" { + t.Fatal("ROX_ENDPOINT and ROX_API_TOKEN environment variables must be set") } - client := createSmokeTestClient(t, endpoint, password) + client := createSmokeTestClient(t, endpoint, apiToken) tests := map[string]struct { toolName string @@ -94,14 +94,14 @@ func TestSmoke_RealCluster(t *testing.T) { } } -func createSmokeTestClient(t *testing.T, endpoint, password string) *testutil.MCPTestClient { +func createSmokeTestClient(t *testing.T, endpoint, apiToken string) *testutil.MCPTestClient { t.Helper() cfg := &config.Config{ Central: config.CentralConfig{ URL: endpoint, AuthType: "static", - APIToken: password, + APIToken: apiToken, InsecureSkipTLSVerify: true, RequestTimeout: 30 * time.Second, MaxRetries: 3, From 785828b924a353169b8c92195d6ec35531202526 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Tue, 24 Mar 2026 19:28:27 +0100 Subject: [PATCH 07/13] Optimize smoke test workflow with API polling Replace fixed sleep timers with active polling and reorder steps for faster feedback. Deploy vulnerable workload immediately so scanning starts while Central initializes. Changes: - Move vulnerable workload deployment before Central wait (parallel) - Replace sleep 120s with kubectl wait for Central pods ready - Remove sleep 60s for deployment scan - Add waitForImageScan() using assert.Eventually for smart polling - Poll every 5s with 3min timeout for scan completion Expected improvement: ~5-8 minutes faster workflow execution. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 20 ++++++++------------ smoke/smoke_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 29ea0c9..15505e1 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -45,8 +45,14 @@ jobs: cd stackrox-repo ./deploy/k8s/deploy-local.sh - - name: Wait for Scanner readiness - run: sleep 120 + - name: Deploy vulnerable workload + run: kubectl apply -f smoke/testdata/vulnerable-deployment.yaml + + - name: Wait for vulnerable deployment + run: kubectl wait --for=condition=available --timeout=120s deployment/vulnerable-app -n vulnerable-apps + + - name: Wait for Central pods ready + run: kubectl wait --for=condition=ready --timeout=180s pod -l app=central -n stackrox - name: Extract Central password id: extract-password @@ -82,16 +88,6 @@ jobs: kubectl port-forward -n stackrox svc/central 8443:443 & sleep 5 - - name: Deploy vulnerable workload - run: kubectl apply -f smoke/testdata/vulnerable-deployment.yaml - - - name: Wait for vulnerable deployment - run: | - kubectl wait --for=condition=available --timeout=120s deployment/vulnerable-app -n vulnerable-apps - - - name: Wait for deployment scan - run: sleep 60 - - name: Install go-junit-report run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0 diff --git a/smoke/smoke_test.go b/smoke/smoke_test.go index ac041ce..8080f4a 100644 --- a/smoke/smoke_test.go +++ b/smoke/smoke_test.go @@ -17,6 +17,38 @@ import ( "github.com/stretchr/testify/require" ) +func waitForImageScan(t *testing.T, client *testutil.MCPTestClient, cveName string) { + t.Helper() + + assert.Eventually(t, func() bool { + ctx := context.Background() + result, err := client.CallTool(ctx, "get_deployments_for_cve", map[string]any{ + "cveName": cveName, + }) + + if err != nil || result.IsError { + return false + } + + responseText := testutil.GetTextContent(t, result) + var data struct { + Deployments []any `json:"deployments"` + } + + if err := json.Unmarshal([]byte(responseText), &data); err != nil { + return false + } + + if len(data.Deployments) > 0 { + t.Logf("Image scan completed, found %d deployment(s) with CVE %s", len(data.Deployments), cveName) + return true + } + + t.Logf("Waiting for image scan (CVE: %s)...", cveName) + return false + }, 3*time.Minute, 5*time.Second, "Image scan did not complete for CVE %s", cveName) +} + func TestSmoke_RealCluster(t *testing.T) { if testing.Short() { t.Skip("Skipping smoke test in short mode") @@ -31,6 +63,8 @@ func TestSmoke_RealCluster(t *testing.T) { client := createSmokeTestClient(t, endpoint, apiToken) + waitForImageScan(t, client, "CVE-2019-11043") + tests := map[string]struct { toolName string args map[string]any From a195a0fc840b68117dcd5d3a657afacc9abe1cb1 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 25 Mar 2026 09:01:38 +0100 Subject: [PATCH 08/13] Fix smoke test authentication by using built-in port-forwarding The workflow was creating manual port-forwards to localhost:8443 and killing them prematurely, causing race conditions. The deploy-local.sh script already sets up port-forwarding to localhost:8000, so we now use that instead. Added comprehensive validation with exponential backoff polling, HTTP status checking, and JSON validation to prevent null token issues that were causing authentication failures. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 78 ++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 15505e1..a212d6c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -61,39 +61,87 @@ jobs: echo "::add-mask::${PASSWORD}" echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT" - - name: Generate API token + - name: Wait for Central API and generate token id: generate-token run: | - kubectl port-forward -n stackrox svc/central 8443:443 & - PF_PID=$! - sleep 5 + set -e + + echo "Waiting for Central API to be ready at localhost:8000..." + echo "Note: deploy-local.sh automatically set up port-forwarding" PASSWORD="${{ steps.extract-password.outputs.password }}" - TOKEN_RESPONSE=$(curl -k -s -u "admin:${PASSWORD}" \ + # Poll for API readiness with exponential backoff + MAX_ATTEMPTS=12 + ATTEMPT=0 + while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + echo "Attempt $((ATTEMPT + 1))/${MAX_ATTEMPTS} to check API readiness..." + + if curl -k -s -f --max-time 5 -u "admin:${PASSWORD}" \ + https://localhost:8000/v1/ping > /dev/null 2>&1; then + echo "Central API is ready" + break + fi + + ATTEMPT=$((ATTEMPT + 1)) + if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then + echo "ERROR: Central API did not become ready after ${MAX_ATTEMPTS} attempts" + echo "Checking if port-forward is running..." + netstat -tlnp 2>/dev/null | grep :8000 || echo "Port 8000 not listening" + exit 1 + fi + + # Exponential backoff: 2, 4, 8, 16... seconds (max 30) + SLEEP_TIME=$((2 ** ATTEMPT)) + if [ $SLEEP_TIME -gt 30 ]; then + SLEEP_TIME=30 + fi + echo "Waiting ${SLEEP_TIME} seconds before retry..." + sleep $SLEEP_TIME + done + + echo "Generating API token..." + TOKEN_RESPONSE=$(curl -k -s -w "\n%{http_code}" -u "admin:${PASSWORD}" \ -X POST \ -H "Content-Type: application/json" \ -d '{"name":"smoke-test-token","role":"Admin"}' \ - https://localhost:8443/v1/apitokens) + https://localhost:8000/v1/apitokens) - API_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.token') - echo "::add-mask::${API_TOKEN}" + HTTP_CODE=$(echo "${TOKEN_RESPONSE}" | tail -n1) + TOKEN_BODY=$(echo "${TOKEN_RESPONSE}" | sed '$d') - kill "${PF_PID}" || true + echo "API response status: ${HTTP_CODE}" - echo "token=${API_TOKEN}" >> "$GITHUB_OUTPUT" + if [ "${HTTP_CODE}" != "200" ]; then + echo "ERROR: Failed to generate token. HTTP status: ${HTTP_CODE}" + echo "Response: ${TOKEN_BODY}" + exit 1 + fi - - name: Port-forward Central for tests - run: | - kubectl port-forward -n stackrox svc/central 8443:443 & - sleep 5 + if ! echo "${TOKEN_BODY}" | jq -e . > /dev/null 2>&1; then + echo "ERROR: Invalid JSON response from token generation" + echo "Response: ${TOKEN_BODY}" + exit 1 + fi + + API_TOKEN=$(echo "${TOKEN_BODY}" | jq -r '.token') + + if [ -z "${API_TOKEN}" ] || [ "${API_TOKEN}" = "null" ]; then + echo "ERROR: Generated token is null or empty" + echo "Response: ${TOKEN_BODY}" + exit 1 + fi + + echo "Token generated successfully" + echo "::add-mask::${API_TOKEN}" + echo "token=${API_TOKEN}" >> "$GITHUB_OUTPUT" - name: Install go-junit-report run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0 - name: Run smoke tests with JUnit output env: - ROX_ENDPOINT: localhost:8443 + ROX_ENDPOINT: localhost:8000 ROX_API_TOKEN: ${{ steps.generate-token.outputs.token }} run: | go test -v -tags=smoke -cover -race -coverprofile=coverage-smoke.out -timeout=20m ./smoke 2>&1 | \ From 92e2630ee5962b12eeb866dea030f074615fe9a3 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 25 Mar 2026 09:31:08 +0100 Subject: [PATCH 09/13] Fix API token generation endpoint to /v1/apitokens/generate The correct endpoint is /v1/apitokens/generate, not /v1/apitokens. This was causing 501 "Method Not Allowed" errors. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index a212d6c..2e6112f 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -105,7 +105,7 @@ jobs: -X POST \ -H "Content-Type: application/json" \ -d '{"name":"smoke-test-token","role":"Admin"}' \ - https://localhost:8000/v1/apitokens) + https://localhost:8000/v1/apitokens/generate) HTTP_CODE=$(echo "${TOKEN_RESPONSE}" | tail -n1) TOKEN_BODY=$(echo "${TOKEN_RESPONSE}" | sed '$d') From 1abae73fed4fffd95a2a138b8e07009f2fa769f3 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 25 Mar 2026 15:03:39 +0100 Subject: [PATCH 10/13] Use password file from deploy-local.sh instead of kubernetes secret The deploy-local.sh script creates a password file that should be used for authentication. Reading from the kubernetes secret was causing authentication failures with "failed to identify user with username admin". Following the same approach as jenkins-plugin workflow. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 2e6112f..b8c8167 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -57,7 +57,7 @@ jobs: - name: Extract Central password id: extract-password run: | - PASSWORD="$(kubectl get secret -n stackrox central-htpasswd -o json | jq -r '.data.password' | base64 -d)" + PASSWORD="$(cat stackrox-repo/deploy/k8s/central-deploy/password)" echo "::add-mask::${PASSWORD}" echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT" From f4bc0b7f96a32274e371e43973cb6eccd1f01a68 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 25 Mar 2026 15:12:04 +0100 Subject: [PATCH 11/13] Generate API tokens in Go code instead of curl in workflow Moves token generation from bash/curl in the GitHub Actions workflow to Go code in the smoke test. This provides: - Better error handling with typed responses - Reusable code that works locally and in CI - Simpler CI workflow (just passes password) - Easier debugging and testing Changes: - Add smoke/token_helper.go with GenerateAPIToken and WaitForCentralReady - Update smoke_test.go to generate token from password if needed - Simplify workflow to pass ROX_PASSWORD instead of generating token - Maintains backwards compatibility with ROX_API_TOKEN Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 77 +----------------------- smoke/smoke_test.go | 28 ++++++++- smoke/token_helper.go | 115 ++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 78 deletions(-) create mode 100644 smoke/token_helper.go diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index b8c8167..136c883 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -61,88 +61,13 @@ jobs: echo "::add-mask::${PASSWORD}" echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT" - - name: Wait for Central API and generate token - id: generate-token - run: | - set -e - - echo "Waiting for Central API to be ready at localhost:8000..." - echo "Note: deploy-local.sh automatically set up port-forwarding" - - PASSWORD="${{ steps.extract-password.outputs.password }}" - - # Poll for API readiness with exponential backoff - MAX_ATTEMPTS=12 - ATTEMPT=0 - while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - echo "Attempt $((ATTEMPT + 1))/${MAX_ATTEMPTS} to check API readiness..." - - if curl -k -s -f --max-time 5 -u "admin:${PASSWORD}" \ - https://localhost:8000/v1/ping > /dev/null 2>&1; then - echo "Central API is ready" - break - fi - - ATTEMPT=$((ATTEMPT + 1)) - if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then - echo "ERROR: Central API did not become ready after ${MAX_ATTEMPTS} attempts" - echo "Checking if port-forward is running..." - netstat -tlnp 2>/dev/null | grep :8000 || echo "Port 8000 not listening" - exit 1 - fi - - # Exponential backoff: 2, 4, 8, 16... seconds (max 30) - SLEEP_TIME=$((2 ** ATTEMPT)) - if [ $SLEEP_TIME -gt 30 ]; then - SLEEP_TIME=30 - fi - echo "Waiting ${SLEEP_TIME} seconds before retry..." - sleep $SLEEP_TIME - done - - echo "Generating API token..." - TOKEN_RESPONSE=$(curl -k -s -w "\n%{http_code}" -u "admin:${PASSWORD}" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"name":"smoke-test-token","role":"Admin"}' \ - https://localhost:8000/v1/apitokens/generate) - - HTTP_CODE=$(echo "${TOKEN_RESPONSE}" | tail -n1) - TOKEN_BODY=$(echo "${TOKEN_RESPONSE}" | sed '$d') - - echo "API response status: ${HTTP_CODE}" - - if [ "${HTTP_CODE}" != "200" ]; then - echo "ERROR: Failed to generate token. HTTP status: ${HTTP_CODE}" - echo "Response: ${TOKEN_BODY}" - exit 1 - fi - - if ! echo "${TOKEN_BODY}" | jq -e . > /dev/null 2>&1; then - echo "ERROR: Invalid JSON response from token generation" - echo "Response: ${TOKEN_BODY}" - exit 1 - fi - - API_TOKEN=$(echo "${TOKEN_BODY}" | jq -r '.token') - - if [ -z "${API_TOKEN}" ] || [ "${API_TOKEN}" = "null" ]; then - echo "ERROR: Generated token is null or empty" - echo "Response: ${TOKEN_BODY}" - exit 1 - fi - - echo "Token generated successfully" - echo "::add-mask::${API_TOKEN}" - echo "token=${API_TOKEN}" >> "$GITHUB_OUTPUT" - - name: Install go-junit-report run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0 - name: Run smoke tests with JUnit output env: ROX_ENDPOINT: localhost:8000 - ROX_API_TOKEN: ${{ steps.generate-token.outputs.token }} + ROX_PASSWORD: ${{ steps.extract-password.outputs.password }} run: | go test -v -tags=smoke -cover -race -coverprofile=coverage-smoke.out -timeout=20m ./smoke 2>&1 | \ tee /dev/stderr | \ diff --git a/smoke/smoke_test.go b/smoke/smoke_test.go index 8080f4a..48e28e8 100644 --- a/smoke/smoke_test.go +++ b/smoke/smoke_test.go @@ -56,9 +56,33 @@ func TestSmoke_RealCluster(t *testing.T) { endpoint := os.Getenv("ROX_ENDPOINT") apiToken := os.Getenv("ROX_API_TOKEN") + password := os.Getenv("ROX_PASSWORD") - if endpoint == "" || apiToken == "" { - t.Fatal("ROX_ENDPOINT and ROX_API_TOKEN environment variables must be set") + if endpoint == "" { + t.Fatal("ROX_ENDPOINT environment variable must be set") + } + + // Generate token if password provided but no token + if apiToken == "" && password != "" { + t.Log("No API token provided, generating one using password...") + + // Wait for Central to be ready + if err := WaitForCentralReady(endpoint, password, 12); err != nil { + t.Fatalf("Failed waiting for Central: %v", err) + } + t.Log("Central API is ready") + + // Generate token + token, err := GenerateAPIToken(endpoint, password) + if err != nil { + t.Fatalf("Failed to generate API token: %v", err) + } + apiToken = token + t.Log("Successfully generated API token") + } + + if apiToken == "" { + t.Fatal("Either ROX_API_TOKEN or ROX_PASSWORD must be set") } client := createSmokeTestClient(t, endpoint, apiToken) diff --git a/smoke/token_helper.go b/smoke/token_helper.go new file mode 100644 index 0000000..eb76b5b --- /dev/null +++ b/smoke/token_helper.go @@ -0,0 +1,115 @@ +package smoke + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type GenerateTokenRequest struct { + Name string `json:"name"` + Role string `json:"role,omitempty"` +} + +type GenerateTokenResponse struct { + Token string `json:"token"` +} + +// GenerateAPIToken generates an API token using basic authentication. +// It calls the /v1/apitokens/generate endpoint with admin credentials. +func GenerateAPIToken(endpoint, password string) (string, error) { + tokenReq := GenerateTokenRequest{ + Name: "smoke-test-token", + Role: "Admin", + } + + reqBody, err := json.Marshal(tokenReq) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + url := fmt.Sprintf("https://%s/v1/apitokens/generate", endpoint) + + req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody)) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.SetBasicAuth("admin", password) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("token generation failed (status %d): %s", resp.StatusCode, string(body)) + } + + var tokenResp GenerateTokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", fmt.Errorf("failed to parse response: %w", err) + } + + if tokenResp.Token == "" { + return "", fmt.Errorf("received empty token in response") + } + + return tokenResp.Token, nil +} + +// WaitForCentralReady polls the /v1/ping endpoint until Central is ready. +func WaitForCentralReady(endpoint, password string, maxAttempts int) error { + url := fmt.Sprintf("https://%s/v1/ping", endpoint) + + client := &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + for attempt := 0; attempt < maxAttempts; attempt++ { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.SetBasicAuth("admin", password) + + resp, err := client.Do(req) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return nil + } + if resp != nil { + resp.Body.Close() + } + + // Exponential backoff: 2, 4, 8, 16... seconds (max 30) + sleepTime := 1 << attempt + if sleepTime > 30 { + sleepTime = 30 + } + time.Sleep(time.Duration(sleepTime) * time.Second) + } + + return fmt.Errorf("Central did not become ready after %d attempts", maxAttempts) +} From 7646ac52055812fbc84f5d3e976d17e6c1a66529 Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 25 Mar 2026 15:43:44 +0100 Subject: [PATCH 12/13] Fix smoke test failures by waiting for cluster health and using correct CVE Changes: 1. Add sensor pod readiness wait in workflow 2. Add cluster health status check - waits for HEALTHY status before tests 3. Change CVE from CVE-2019-11043 (PHP-FPM) to CVE-2019-9511 (nginx HTTP/2) 4. Increase image scan timeout from 3 to 5 minutes Fixes: - Empty cluster list issue: Now waits for sensors to register and cluster to be healthy - CVE not found issue: CVE-2019-9511 actually exists in nginx:1.14 Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 29 +++++++++++++++++++++++++++++ smoke/smoke_test.go | 5 +++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 136c883..5ea1748 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -54,6 +54,9 @@ jobs: - name: Wait for Central pods ready run: kubectl wait --for=condition=ready --timeout=180s pod -l app=central -n stackrox + - name: Wait for Sensor pods ready + run: kubectl wait --for=condition=ready --timeout=180s pod -l app=sensor -n stackrox + - name: Extract Central password id: extract-password run: | @@ -61,6 +64,32 @@ jobs: echo "::add-mask::${PASSWORD}" echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT" + - name: Wait for cluster to be healthy + run: | + echo "Waiting for cluster to register and become healthy..." + PASSWORD="$(cat stackrox-repo/deploy/k8s/central-deploy/password)" + + for i in {1..60}; do + CLUSTER_HEALTH=$(curl -k -s -u "admin:${PASSWORD}" \ + https://localhost:8000/v1/clusters 2>/dev/null | \ + jq -r '.clusters[0].healthStatus.overallHealthStatus // "NOT_FOUND"') + + echo "Attempt $i/60: Cluster health status: $CLUSTER_HEALTH" + + if [ "$CLUSTER_HEALTH" = "HEALTHY" ]; then + echo "Cluster is healthy and ready for testing" + break + fi + + if [ $i -eq 60 ]; then + echo "ERROR: Cluster did not become healthy after 60 attempts (2 minutes)" + echo "Current status: $CLUSTER_HEALTH" + exit 1 + fi + + sleep 2 + done + - name: Install go-junit-report run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0 diff --git a/smoke/smoke_test.go b/smoke/smoke_test.go index 48e28e8..1781ce6 100644 --- a/smoke/smoke_test.go +++ b/smoke/smoke_test.go @@ -46,7 +46,7 @@ func waitForImageScan(t *testing.T, client *testutil.MCPTestClient, cveName stri t.Logf("Waiting for image scan (CVE: %s)...", cveName) return false - }, 3*time.Minute, 5*time.Second, "Image scan did not complete for CVE %s", cveName) + }, 5*time.Minute, 5*time.Second, "Image scan did not complete for CVE %s", cveName) } func TestSmoke_RealCluster(t *testing.T) { @@ -87,7 +87,8 @@ func TestSmoke_RealCluster(t *testing.T) { client := createSmokeTestClient(t, endpoint, apiToken) - waitForImageScan(t, client, "CVE-2019-11043") + // nginx:1.14 has CVE-2019-9511 (HTTP/2 vulnerabilities) + waitForImageScan(t, client, "CVE-2019-9511") tests := map[string]struct { toolName string From 807a4c38d26fd9217f302803510665adc03ffb5c Mon Sep 17 00:00:00 2001 From: Tomasz Janiszewski Date: Wed, 25 Mar 2026 19:16:49 +0100 Subject: [PATCH 13/13] Make sensor pod wait non-blocking with longer timeout Sensors can take longer than 3 minutes to become ready. This change: - Increases timeout from 180s to 300s (5 minutes) - Makes the wait non-blocking (|| echo) so workflow continues - Relies on cluster health check as the real gate If sensors aren't ready, the cluster health check will fail anyway, providing a clearer indication of the actual issue. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 5ea1748..293f3b7 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -55,7 +55,7 @@ jobs: run: kubectl wait --for=condition=ready --timeout=180s pod -l app=central -n stackrox - name: Wait for Sensor pods ready - run: kubectl wait --for=condition=ready --timeout=180s pod -l app=sensor -n stackrox + run: kubectl wait --for=condition=ready --timeout=300s pod -l app=sensor -n stackrox || echo "Sensor pods not ready yet, will check cluster health" - name: Extract Central password id: extract-password