diff --git a/README.md b/README.md index 84c2b48..1069d35 100644 --- a/README.md +++ b/README.md @@ -193,12 +193,22 @@ Once connected, interact with the tools using natural language: Can you list all the clusters secured by StackRox? ``` -#### Check for a specific CVE +#### Check for a specific vulnerability (CVE) ``` Is CVE-2021-44228 detected in any of my clusters? ``` -#### CVE analysis in specific namespace +#### Check for GitHub Security Advisory +``` +Is GHSA-jfh8-c2jp-5v3q detected in any of my deployments? +``` + +#### Check for Red Hat Security Advisory +``` +Check if RHSA-2026:1594 is present in nodes in the production-cluster +``` + +#### Vulnerability analysis in specific namespace ``` Check if CVE-2021-44228 is present in deployments in namespace "backend" ``` diff --git a/docs/architecture.md b/docs/architecture.md index b021951..698d3e7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -80,10 +80,10 @@ Central registry that manages all available toolsets and their tools. **Available Toolsets**: -1. **Vulnerability Toolset**: Query resources where CVEs are detected - - `get_deployments_for_cve`: Find deployments where CVE is detected - - `get_nodes_for_cve`: Find nodes where CVE is detected (aggregated by cluster and OS) - - `get_clusters_with_orchestrator_cve`: Find clusters where CVE is detected in orchestrator components +1. **Vulnerability Toolset**: Query resources where vulnerabilities are detected + - `get_deployments_for_cve`: Find deployments where vulnerability is detected + - `get_nodes_for_cve`: Find nodes where vulnerability is detected (aggregated by cluster and OS) + - `get_clusters_with_orchestrator_cve`: Find clusters where vulnerability is detected in orchestrator components 2. **Config Manager Toolset**: Manage cluster configurations - `list_clusters`: List all managed clusters with pagination @@ -210,21 +210,27 @@ All errors are converted to user-friendly messages with: ### Vulnerability Tools **get_deployments_for_cve** -- Query deployments where CVE is detected +- Query deployments where a vulnerability is detected +- Supports 24 different identifier formats ( CVE, GHSA, GO, PYSEC, RUSTSEC, ALAS, ALAS2, ALAS2023, RHSA, RHEA, RHBA, DRUPAL, ELSA, OESA, PHSA, MGASA, JLSEC, BELL, BIT, ECHO, MAL, MINI, TEMP, XSA - Optional filters: cluster, namespace, platform type -- Optional image enrichment (lists container images where CVE is detected) +- Optional image enrichment (lists container images where vulnerability is detected) - Pagination support for large result sets +- **Example identifiers**: `CVE-2021-44228`, `GHSA-jfh8-c2jp-5v3q`, `RHSA-2026:1594` **get_nodes_for_cve** -- Query nodes where CVE is detected +- Query nodes where a vulnerability is detected in OS packages +- Supports CVE, RHSA, RHEA, RHBA identifier formats - Results aggregated by cluster and OS image - Optional cluster filter - Streaming API for efficient processing +- **Example identifiers**: `CVE-2021-44228`, `RHSA-2026:1594` **get_clusters_with_orchestrator_cve** -- Query clusters where CVE is detected for orchestrator components +- Query clusters where a vulnerability is detected in Kubernetes orchestrator components +- Supports CVE, RHSA, RHEA, RHBA identifier formats - Optional cluster filter for verification - Sorted results for deterministic output +- **Example identifiers**: `CVE-2021-44228`, `RHSA-2026:1594` ### Config Management Tools @@ -237,8 +243,8 @@ All errors are converted to user-friendly messages with: All vulnerability tools use StackRox query syntax: -- **Field filters**: `CVE:"CVE-2021-44228"` -- **Multiple conditions**: `CVE:"CVE-2021"+Namespace:"default"` +- **Field filters**: `CVE:"RHSA-2026:1594"` (note: field is "CVE" but accepts all supported identifier formats) +- **Multiple conditions**: `CVE:"CVE-2021-44228"+Namespace:"default"` - **Exact matching**: Values quoted to prevent partial matches - **Platform filters**: `Platform Component:0` (user workload) or `Platform Component:1` (platform) diff --git a/internal/toolsets/vulnerability/clusters.go b/internal/toolsets/vulnerability/clusters.go index 0ed16d7..10585ba 100644 --- a/internal/toolsets/vulnerability/clusters.go +++ b/internal/toolsets/vulnerability/clusters.go @@ -25,7 +25,7 @@ type getClustersForCVEInput struct { func (input *getClustersForCVEInput) validate() error { if input.CVEName == "" { - return errors.New("CVE name is required") + return errors.New("vulnerability identifier is required (e.g., CVE-2021-44228, RHSA-2026:1594)") } if input.FilterClusterID != "" && input.FilterClusterName != "" { @@ -74,8 +74,9 @@ func (t *getClustersForCVETool) GetName() string { func (t *getClustersForCVETool) GetTool() *mcp.Tool { return &mcp.Tool{ Name: t.name, - Description: "Get list of clusters where a specified CVE is detected in Kubernetes orchestrator components" + - " (kube-apiserver, kubelet, etcd, etc.)." + + Description: "Get list of clusters where a specified vulnerability" + + " is detected in Kubernetes orchestrator components (kube-apiserver, kubelet, etcd, etc.)." + + " Supports CVE, RHSA, RHEA, RHBA identifiers." + " USAGE PATTERNS:" + " 1) When user asks 'Is CVE-X detected in my clusters?' (plural, general question):" + " Call ALL THREE CVE tools (get_clusters_with_orchestrator_cve, get_deployments_for_cve, get_nodes_for_cve)" + @@ -98,7 +99,8 @@ func getClustersForCVEInputSchema() *jsonschema.Schema { // CVE name is required. schema.Required = []string{"cveName"} - schema.Properties["cveName"].Description = "CVE name to filter clusters (e.g., CVE-2021-44228)" + schema.Properties["cveName"].Description = "Vulnerability identifier to filter clusters." + + " Supported formats: CVE, RHSA, RHEA, RHBA (e.g., CVE-2021-44228, RHSA-2026:1594)" schema.Properties["filterClusterId"].Description = "Optional cluster ID to verify if CVE is detected in a specific cluster." + " Cannot be used together with filterClusterName." + diff --git a/internal/toolsets/vulnerability/clusters_test.go b/internal/toolsets/vulnerability/clusters_test.go index a3ebec3..c36efc0 100644 --- a/internal/toolsets/vulnerability/clusters_test.go +++ b/internal/toolsets/vulnerability/clusters_test.go @@ -36,7 +36,7 @@ func TestGetClustersForCVETool_GetTool(t *testing.T) { require.NotNil(t, mcpTool) assert.Equal(t, "get_clusters_with_orchestrator_cve", mcpTool.Name) assert.Contains(t, mcpTool.Description, "clusters where") - assert.Contains(t, mcpTool.Description, "CVE is detected") + assert.Contains(t, mcpTool.Description, "vulnerability is detected") assert.NotNil(t, mcpTool.InputSchema) } @@ -73,12 +73,12 @@ func TestClusterInputValidate(t *testing.T) { "missing CVE name (empty string)": { input: getClustersForCVEInput{CVEName: ""}, expectError: true, - errorMsg: "CVE name is required", + errorMsg: "vulnerability identifier is required", }, "missing CVE name (zero value)": { input: getClustersForCVEInput{}, expectError: true, - errorMsg: "CVE name is required", + errorMsg: "vulnerability identifier is required", }, "both cluster ID and name provided": { input: getClustersForCVEInput{ @@ -141,7 +141,7 @@ func TestClusterHandle_MissingCVE(t *testing.T) { require.Error(t, err) assert.Nil(t, result) assert.Nil(t, output) - assert.Contains(t, err.Error(), "CVE name is required") + assert.Contains(t, err.Error(), "vulnerability identifier is required") } func TestClusterHandle_EmptyResults(t *testing.T) { @@ -157,7 +157,7 @@ func TestClusterHandle_EmptyResults(t *testing.T) { ctx := context.Background() req := &mcp.CallToolRequest{} input := getClustersForCVEInput{ - CVEName: "CVE-9999-99999", + CVEName: "CVE-2021-44228", } result, output, err := tool.handle(ctx, req, input) @@ -298,8 +298,8 @@ func TestClusterHandle_WithFilters(t *testing.T) { expectedQuery string }{ "CVE only": { - input: getClustersForCVEInput{CVEName: "CVE-2021-44228"}, - expectedQuery: `CVE:"CVE-2021-44228"`, + input: getClustersForCVEInput{CVEName: "RHSA-2026:1594"}, + expectedQuery: `CVE:"RHSA-2026:1594"`, }, "CVE with cluster": { input: getClustersForCVEInput{ diff --git a/internal/toolsets/vulnerability/deployments.go b/internal/toolsets/vulnerability/deployments.go index 9df02ed..5534910 100644 --- a/internal/toolsets/vulnerability/deployments.go +++ b/internal/toolsets/vulnerability/deployments.go @@ -43,7 +43,7 @@ type getDeploymentsForCVEInput struct { func (input *getDeploymentsForCVEInput) validate() error { if input.CVEName == "" { - return errors.New("CVE name is required") + return errors.New("vulnerability identifier is required (e.g., CVE-2021-44228, GHSA-xxxx-xxxx-xxxx, RHSA-2026:1594)") } if input.FilterClusterID != "" && input.FilterClusterName != "" { @@ -99,8 +99,9 @@ func (t *getDeploymentsForCVETool) GetName() string { func (t *getDeploymentsForCVETool) GetTool() *mcp.Tool { return &mcp.Tool{ Name: t.name, - Description: "Get list of deployments where a specified CVE" + + Description: "Get list of deployments where a specified vulnerability" + " is detected in application or platform container images." + + " Supports CVE, GHSA, and 22+ other vulnerability identifier formats." + " USAGE PATTERNS:" + " 1) When user asks 'Is CVE-X detected in my clusters?' (plural, general question):" + " Call ALL THREE CVE tools (get_clusters_with_orchestrator_cve, get_deployments_for_cve, get_nodes_for_cve)" + @@ -123,7 +124,10 @@ func getDeploymentsForCVEInputSchema() *jsonschema.Schema { // CVE name is required. schema.Required = []string{"cveName"} - schema.Properties["cveName"].Description = "CVE name to filter deployments (e.g., CVE-2021-44228)" + schema.Properties["cveName"].Description = "Vulnerability identifier to filter deployments." + + " Supported formats: CVE, GHSA, GO, PYSEC, RUSTSEC, ALAS, ALAS2, ALAS2023, RHSA, RHEA, RHBA," + + " DRUPAL, ELSA, OESA, PHSA, MGASA, JLSEC, BELL, BIT, ECHO, MAL, MINI, TEMP, XSA" + + " (e.g., CVE-2021-44228, GHSA-xxxx-xxxx-xxxx, RHSA-2026:1594)" schema.Properties["filterClusterId"].Description = "Optional cluster ID to filter deployments." + " Cannot be used together with filterClusterName." schema.Properties["filterClusterName"].Description = "Optional cluster name to filter deployments." + diff --git a/internal/toolsets/vulnerability/deployments_test.go b/internal/toolsets/vulnerability/deployments_test.go index e8bd453..cfaf7ea 100644 --- a/internal/toolsets/vulnerability/deployments_test.go +++ b/internal/toolsets/vulnerability/deployments_test.go @@ -80,12 +80,12 @@ func TestInputValidate(t *testing.T) { "missing CVE name (empty string)": { input: getDeploymentsForCVEInput{CVEName: ""}, expectError: true, - errorMsg: "CVE name is required", + errorMsg: "vulnerability identifier is required", }, "missing CVE name (zero value)": { input: getDeploymentsForCVEInput{}, expectError: true, - errorMsg: "CVE name is required", + errorMsg: "vulnerability identifier is required", }, "both cluster ID and name provided": { input: getDeploymentsForCVEInput{ @@ -195,7 +195,7 @@ func TestHandle_MissingCVE(t *testing.T) { require.Error(t, err) assert.Nil(t, result) assert.Nil(t, output) - assert.Contains(t, err.Error(), "CVE name is required") + assert.Contains(t, err.Error(), "vulnerability identifier is required") } func TestHandle_WithPagination(t *testing.T) { @@ -256,7 +256,7 @@ func TestHandle_EmptyResults(t *testing.T) { ctx := context.Background() req := &mcp.CallToolRequest{} input := getDeploymentsForCVEInput{ - CVEName: "CVE-9999-99999", + CVEName: "CVE-2021-44228", } result, output, err := tool.handle(ctx, req, input) @@ -308,8 +308,8 @@ func TestHandle_WithFilters(t *testing.T) { expectedQuery string }{ "CVE only": { - input: getDeploymentsForCVEInput{CVEName: "CVE-2021-44228"}, - expectedQuery: `CVE:"CVE-2021-44228"`, + input: getDeploymentsForCVEInput{CVEName: "GHSA-jfh8-c2jp-5v3q"}, + expectedQuery: `CVE:"GHSA-jfh8-c2jp-5v3q"`, }, "CVE with cluster": { input: getDeploymentsForCVEInput{ @@ -328,10 +328,10 @@ func TestHandle_WithFilters(t *testing.T) { }, "CVE with platform filter 1 (platform)": { input: getDeploymentsForCVEInput{ - CVEName: "CVE-2021-44228", + CVEName: "RHSA-2026:1594", FilterPlatform: filterPlatformPlatform, }, - expectedQuery: `CVE:"CVE-2021-44228"+Platform Component:1`, + expectedQuery: `CVE:"RHSA-2026:1594"+Platform Component:1`, }, "CVE with all filters": { input: getDeploymentsForCVEInput{ diff --git a/internal/toolsets/vulnerability/nodes.go b/internal/toolsets/vulnerability/nodes.go index a00c58f..63a0112 100644 --- a/internal/toolsets/vulnerability/nodes.go +++ b/internal/toolsets/vulnerability/nodes.go @@ -27,7 +27,7 @@ type getNodesForCVEInput struct { func (input *getNodesForCVEInput) validate() error { if input.CVEName == "" { - return errors.New("CVE name is required") + return errors.New("vulnerability identifier is required (e.g., CVE-2021-44228, RHSA-2026:1594)") } if input.FilterClusterID != "" && input.FilterClusterName != "" { @@ -78,8 +78,9 @@ func (t *getNodesForCVETool) GetName() string { func (t *getNodesForCVETool) GetTool() *mcp.Tool { return &mcp.Tool{ Name: t.name, - Description: "Get aggregated node groups where a specified CVE is detected" + + Description: "Get aggregated node groups where a specified vulnerability is detected" + " in node operating system packages, grouped by cluster and OS image." + + " Supports CVE, RHSA, RHEA, RHBA identifiers." + " USAGE PATTERNS:" + " 1) When user asks 'Is CVE-X detected in my clusters?' (plural, general question):" + " Call ALL THREE CVE tools (get_clusters_with_orchestrator_cve, get_deployments_for_cve, get_nodes_for_cve)" + @@ -102,7 +103,8 @@ func getNodesForCVEInputSchema() *jsonschema.Schema { // CVE name is required. schema.Required = []string{"cveName"} - schema.Properties["cveName"].Description = "CVE name to filter nodes (e.g., CVE-2020-26159)" + schema.Properties["cveName"].Description = "Vulnerability identifier to filter nodes." + + " Supported formats: CVE, RHSA, RHEA, RHBA (e.g., CVE-2021-44228, RHSA-2026:1594)" schema.Properties["filterClusterId"].Description = "Optional cluster ID to filter nodes." + " Cannot be used together with filterClusterName." schema.Properties["filterClusterName"].Description = "Optional cluster name to filter nodes." + diff --git a/internal/toolsets/vulnerability/nodes_test.go b/internal/toolsets/vulnerability/nodes_test.go index b59a4a5..80331a1 100644 --- a/internal/toolsets/vulnerability/nodes_test.go +++ b/internal/toolsets/vulnerability/nodes_test.go @@ -73,12 +73,12 @@ func TestNodeInputValidate(t *testing.T) { "missing CVE name (empty string)": { input: getNodesForCVEInput{CVEName: ""}, expectError: true, - errorMsg: "CVE name is required", + errorMsg: "vulnerability identifier is required", }, "missing CVE name (zero value)": { input: getNodesForCVEInput{}, expectError: true, - errorMsg: "CVE name is required", + errorMsg: "vulnerability identifier is required", }, "both cluster ID and name provided": { input: getNodesForCVEInput{ @@ -144,7 +144,7 @@ func TestNodeHandle_MissingCVE(t *testing.T) { require.Error(t, err) assert.Nil(t, result) assert.Nil(t, output) - assert.Contains(t, err.Error(), "CVE name is required") + assert.Contains(t, err.Error(), "vulnerability identifier is required") } func TestNodeHandle_EmptyResults(t *testing.T) { @@ -163,7 +163,7 @@ func TestNodeHandle_EmptyResults(t *testing.T) { ctx := context.Background() req := &mcp.CallToolRequest{} input := getNodesForCVEInput{ - CVEName: "CVE-9999-99999", + CVEName: "CVE-2021-44228", } result, output, err := tool.handle(ctx, req, input) @@ -328,8 +328,8 @@ func TestNodeHandle_WithFilters(t *testing.T) { expectedQuery string }{ "CVE only": { - input: getNodesForCVEInput{CVEName: "CVE-2021-44228"}, - expectedQuery: `CVE:"CVE-2021-44228"`, + input: getNodesForCVEInput{CVEName: "RHSA-2026:1594"}, + expectedQuery: `CVE:"RHSA-2026:1594"`, }, "CVE with cluster": { input: getNodesForCVEInput{