Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
Expand Down
26 changes: 16 additions & 10 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)

Expand Down
10 changes: 6 additions & 4 deletions internal/toolsets/vulnerability/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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)" +
Expand All @@ -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." +
Expand Down
14 changes: 7 additions & 7 deletions internal/toolsets/vulnerability/clusters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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{
Expand Down
10 changes: 7 additions & 3 deletions internal/toolsets/vulnerability/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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)" +
Expand All @@ -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." +
Expand Down
16 changes: 8 additions & 8 deletions internal/toolsets/vulnerability/deployments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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{
Expand Down
8 changes: 5 additions & 3 deletions internal/toolsets/vulnerability/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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)" +
Expand All @@ -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." +
Expand Down
12 changes: 6 additions & 6 deletions internal/toolsets/vulnerability/nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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{
Expand Down
Loading