Skip to content

Commit 4f5f3e0

Browse files
committed
Add get_deployment_for_cve tool
1 parent 2a34dfe commit 4f5f3e0

File tree

6 files changed

+498
-48
lines changed

6 files changed

+498
-48
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
# Claude Code
77
.claude/
8+
/.mcp.json
89

910
# Test output
1011
/*.out

cmd/stackrox-mcp/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
func getToolsets(cfg *config.Config, c *client.Client) []toolsets.Toolset {
2323
return []toolsets.Toolset{
2424
toolsetConfig.NewToolset(cfg, c),
25-
toolsetVulnerability.NewToolset(cfg),
25+
toolsetVulnerability.NewToolset(cfg, c),
2626
}
2727
}
2828

internal/toolsets/vulnerability/tools.go

Lines changed: 157 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,191 @@ package vulnerability
22

33
import (
44
"context"
5-
"errors"
5+
"fmt"
6+
"strings"
67

8+
"github.com/google/jsonschema-go/jsonschema"
79
"github.com/modelcontextprotocol/go-sdk/mcp"
10+
"github.com/pkg/errors"
11+
v1 "github.com/stackrox/rox/generated/api/v1"
12+
"github.com/stackrox/stackrox-mcp/internal/client"
13+
"github.com/stackrox/stackrox-mcp/internal/client/auth"
14+
"github.com/stackrox/stackrox-mcp/internal/logging"
815
"github.com/stackrox/stackrox-mcp/internal/toolsets"
916
)
1017

11-
// listClusterCVEsInput defines the input parameters for list_cluster_cves tool.
12-
type listClusterCVEsInput struct {
13-
ClusterID string `json:"clusterId,omitempty"`
18+
const (
19+
defaultLimit = 50
20+
maximumLimit = 200.0
21+
)
22+
23+
// getDeploymentsForCVEInput defines the input parameters for get_deployments_for_cve tool.
24+
type getDeploymentsForCVEInput struct {
25+
CVEName string `json:"cveName"`
26+
ClusterID string `json:"clusterId,omitempty"`
27+
Namespace string `json:"namespace,omitempty"`
28+
PlatformFilter *int32 `json:"platformFilter,omitempty"`
29+
Offset int32 `json:"offset,omitempty"`
30+
Limit int32 `json:"limit,omitempty"`
31+
}
32+
33+
func (input *getDeploymentsForCVEInput) validate() error {
34+
if input.CVEName == "" {
35+
return errors.New("CVE name is required")
36+
}
37+
38+
return nil
39+
}
40+
41+
// DeploymentResult contains deployment information.
42+
type DeploymentResult struct {
43+
Name string `json:"name"`
44+
Namespace string `json:"namespace"`
45+
ClusterID string `json:"clusterId"`
46+
ClusterName string `json:"clusterName"`
1447
}
1548

16-
// listClusterCVEsOutput defines the output structure for list_cluster_cves tool.
17-
type listClusterCVEsOutput struct {
18-
CVEs []string `json:"cves"`
49+
// getDeploymentsForCVEOutput defines the output structure for get_deployments_for_cve tool.
50+
type getDeploymentsForCVEOutput struct {
51+
Deployments []DeploymentResult `json:"deployments"`
1952
}
2053

21-
// listClusterCVEsTool implements the list_cluster_cves tool.
22-
type listClusterCVEsTool struct {
23-
name string
54+
// getDeploymentsForCVETool implements the get_deployments_for_cve tool.
55+
type getDeploymentsForCVETool struct {
56+
name string
57+
client *client.Client
2458
}
2559

26-
// NewListClusterCVEsTool creates a new list_cluster_cves tool.
27-
func NewListClusterCVEsTool() toolsets.Tool {
28-
return &listClusterCVEsTool{
29-
name: "list_cluster_cves",
60+
// NewGetDeploymentsForCVETool creates a new get_deployments_for_cve tool.
61+
func NewGetDeploymentsForCVETool(c *client.Client) toolsets.Tool {
62+
return &getDeploymentsForCVETool{
63+
name: "get_deployments_for_cve",
64+
client: c,
3065
}
3166
}
3267

3368
// IsReadOnly returns true as this tool only reads data.
34-
func (t *listClusterCVEsTool) IsReadOnly() bool {
69+
func (t *getDeploymentsForCVETool) IsReadOnly() bool {
3570
return true
3671
}
3772

3873
// GetName returns the tool name.
39-
func (t *listClusterCVEsTool) GetName() string {
74+
func (t *getDeploymentsForCVETool) GetName() string {
4075
return t.name
4176
}
4277

4378
// GetTool returns the MCP Tool definition.
44-
func (t *listClusterCVEsTool) GetTool() *mcp.Tool {
79+
func (t *getDeploymentsForCVETool) GetTool() *mcp.Tool {
4580
return &mcp.Tool{
46-
Name: t.name,
47-
//nolint:lll
48-
Description: "List CVEs affecting a specific cluster or all clusters in StackRox Central with CVE names, scores, affected images, and deployments",
81+
Name: t.name,
82+
Description: "Get list of deployments affected by a specific CVE",
83+
InputSchema: getDeploymentsForCVEInputSchema(),
84+
}
85+
}
86+
87+
// getDeploymentsForCVEInputSchema returns the JSON schema for input validation.
88+
func getDeploymentsForCVEInputSchema() *jsonschema.Schema {
89+
schema, err := jsonschema.For[getDeploymentsForCVEInput](nil)
90+
if err != nil {
91+
logging.Fatal("Could not get jsonschema for get_deployments_for_cve input", err)
92+
93+
return nil
4994
}
95+
96+
// CVE name is required.
97+
schema.Required = []string{"cveName"}
98+
99+
schema.Properties["cveName"].Description = "CVE name to filter deployments (e.g., CVE-2021-44228)"
100+
schema.Properties["clusterId"].Description = "Optional cluster ID to filter deployments"
101+
schema.Properties["namespace"].Description = "Optional namespace to filter deployments"
102+
103+
schema.Properties["platformFilter"].Description =
104+
"Optional platform filter: 0=non-platform deployments, 1=platform deployments"
105+
schema.Properties["platformFilter"].Enum = []any{0, 1}
106+
107+
schema.Properties["offset"].Description = "Pagination offset (default: 0)"
108+
schema.Properties["offset"].Default = toolsets.MustJSONMarshal(0)
109+
schema.Properties["limit"].Minimum = jsonschema.Ptr(0.0)
110+
111+
schema.Properties["limit"].Description = "Pagination limit: minimum: 1, maximum: 200 (default: 50)"
112+
schema.Properties["limit"].Default = toolsets.MustJSONMarshal(defaultLimit)
113+
schema.Properties["limit"].Minimum = jsonschema.Ptr(1.0)
114+
schema.Properties["limit"].Maximum = jsonschema.Ptr(maximumLimit)
115+
116+
return schema
50117
}
51118

52-
// RegisterWith registers the list_cluster_cves tool handler with the MCP server.
53-
func (t *listClusterCVEsTool) RegisterWith(server *mcp.Server) {
119+
// RegisterWith registers the get_deployments_for_cve tool handler with the MCP server.
120+
func (t *getDeploymentsForCVETool) RegisterWith(server *mcp.Server) {
54121
mcp.AddTool(server, t.GetTool(), t.handle)
55122
}
56123

57-
// handle is the placeholder handler for list_cluster_cves tool.
58-
func (t *listClusterCVEsTool) handle(
59-
_ context.Context,
60-
_ *mcp.CallToolRequest,
61-
_ listClusterCVEsInput,
62-
) (*mcp.CallToolResult, *listClusterCVEsOutput, error) {
63-
return nil, nil, errors.New("list_cluster_cves tool is not yet implemented")
124+
// buildQuery builds query used to search deployments in StackRox Central.
125+
// We will quote values to have strict match. Without quote: CVE-2025-10, would match CVE-2025-101.
126+
func buildQuery(input getDeploymentsForCVEInput) string {
127+
queryParts := []string{fmt.Sprintf("CVE:%q", input.CVEName)}
128+
129+
if input.ClusterID != "" {
130+
queryParts = append(queryParts, fmt.Sprintf("Cluster ID:%q", input.ClusterID))
131+
}
132+
133+
if input.Namespace != "" {
134+
queryParts = append(queryParts, fmt.Sprintf("Namespace:%q", input.Namespace))
135+
}
136+
137+
// Add platform filter if provided.
138+
if input.PlatformFilter != nil {
139+
queryParts = append(queryParts, fmt.Sprintf("Platform Component:%d", *input.PlatformFilter))
140+
}
141+
142+
return strings.Join(queryParts, "+")
143+
}
144+
145+
// handle is the handler for get_deployments_for_cve tool.
146+
func (t *getDeploymentsForCVETool) handle(
147+
ctx context.Context,
148+
req *mcp.CallToolRequest,
149+
input getDeploymentsForCVEInput,
150+
) (*mcp.CallToolResult, *getDeploymentsForCVEOutput, error) {
151+
err := input.validate()
152+
if err != nil {
153+
return nil, nil, err
154+
}
155+
156+
conn, err := t.client.ReadyConn(ctx)
157+
if err != nil {
158+
return nil, nil, errors.Wrap(err, "unable to connect to server")
159+
}
160+
161+
callCtx := auth.WithMCPRequestContext(ctx, req)
162+
deploymentClient := v1.NewDeploymentServiceClient(conn)
163+
164+
listReq := &v1.RawQuery{
165+
Query: buildQuery(input),
166+
Pagination: &v1.Pagination{
167+
Offset: input.Offset,
168+
Limit: input.Limit,
169+
},
170+
}
171+
172+
resp, err := deploymentClient.ListDeployments(callCtx, listReq)
173+
if err != nil {
174+
return nil, nil, client.NewError(err, "ListDeployments")
175+
}
176+
177+
deployments := make([]DeploymentResult, 0, len(resp.GetDeployments()))
178+
for _, deployment := range resp.GetDeployments() {
179+
deployments = append(deployments, DeploymentResult{
180+
Name: deployment.GetName(),
181+
Namespace: deployment.GetNamespace(),
182+
ClusterID: deployment.GetClusterId(),
183+
ClusterName: deployment.GetCluster(),
184+
})
185+
}
186+
187+
output := &getDeploymentsForCVEOutput{
188+
Deployments: deployments,
189+
}
190+
191+
return nil, output, nil
64192
}

0 commit comments

Comments
 (0)