@@ -2,63 +2,209 @@ package vulnerability
22
33import (
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+ type filterPlatformType string
24+
25+ const (
26+ filterPlatformNoFilter filterPlatformType = "NO_FILTER"
27+ filterPlatformUserWorkload filterPlatformType = "USER_WORKLOAD"
28+ filterPlatformPlatform filterPlatformType = "PLATFORM"
29+ )
30+
31+ // getDeploymentsForCVEInput defines the input parameters for get_deployments_for_cve tool.
32+ type getDeploymentsForCVEInput struct {
33+ CVEName string `json:"cveName"`
34+ FilterClusterID string `json:"filterClusterId,omitempty"`
35+ FilterNamespace string `json:"filterNamespace,omitempty"`
36+ FilterPlatform filterPlatformType `json:"filterPlatform,omitempty"`
37+ Offset int32 `json:"offset,omitempty"`
38+ Limit int32 `json:"limit,omitempty"`
39+ }
40+
41+ func (input * getDeploymentsForCVEInput ) validate () error {
42+ if input .CVEName == "" {
43+ return errors .New ("CVE name is required" )
44+ }
45+
46+ return nil
47+ }
48+
49+ // DeploymentResult contains deployment information.
50+ type DeploymentResult struct {
51+ Name string `json:"name"`
52+ Namespace string `json:"namespace"`
53+ ClusterID string `json:"clusterId"`
54+ ClusterName string `json:"clusterName"`
1455}
1556
16- // listClusterCVEsOutput defines the output structure for list_cluster_cves tool.
17- type listClusterCVEsOutput struct {
18- CVEs []string `json:"cves "`
57+ // getDeploymentsForCVEOutput defines the output structure for get_deployments_for_cve tool.
58+ type getDeploymentsForCVEOutput struct {
59+ Deployments []DeploymentResult `json:"deployments "`
1960}
2061
21- // listClusterCVEsTool implements the list_cluster_cves tool.
22- type listClusterCVEsTool struct {
23- name string
62+ // getDeploymentsForCVETool implements the get_deployments_for_cve tool.
63+ type getDeploymentsForCVETool struct {
64+ name string
65+ client * client.Client
2466}
2567
26- // NewListClusterCVEsTool creates a new list_cluster_cves tool.
27- func NewListClusterCVEsTool () toolsets.Tool {
28- return & listClusterCVEsTool {
29- name : "list_cluster_cves" ,
68+ // NewGetDeploymentsForCVETool creates a new get_deployments_for_cve tool.
69+ func NewGetDeploymentsForCVETool (c * client.Client ) toolsets.Tool {
70+ return & getDeploymentsForCVETool {
71+ name : "get_deployments_for_cve" ,
72+ client : c ,
3073 }
3174}
3275
3376// IsReadOnly returns true as this tool only reads data.
34- func (t * listClusterCVEsTool ) IsReadOnly () bool {
77+ func (t * getDeploymentsForCVETool ) IsReadOnly () bool {
3578 return true
3679}
3780
3881// GetName returns the tool name.
39- func (t * listClusterCVEsTool ) GetName () string {
82+ func (t * getDeploymentsForCVETool ) GetName () string {
4083 return t .name
4184}
4285
4386// GetTool returns the MCP Tool definition.
44- func (t * listClusterCVEsTool ) GetTool () * mcp.Tool {
87+ func (t * getDeploymentsForCVETool ) GetTool () * mcp.Tool {
4588 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" ,
89+ Name : t .name ,
90+ Description : "Get list of deployments affected by a specific CVE" ,
91+ InputSchema : getDeploymentsForCVEInputSchema () ,
4992 }
5093}
5194
52- // RegisterWith registers the list_cluster_cves tool handler with the MCP server.
53- func (t * listClusterCVEsTool ) RegisterWith (server * mcp.Server ) {
95+ // getDeploymentsForCVEInputSchema returns the JSON schema for input validation.
96+ func getDeploymentsForCVEInputSchema () * jsonschema.Schema {
97+ schema , err := jsonschema.For [getDeploymentsForCVEInput ](nil )
98+ if err != nil {
99+ logging .Fatal ("Could not get jsonschema for get_deployments_for_cve input" , err )
100+
101+ return nil
102+ }
103+
104+ // CVE name is required.
105+ schema .Required = []string {"cveName" }
106+
107+ schema .Properties ["cveName" ].Description = "CVE name to filter deployments (e.g., CVE-2021-44228)"
108+ schema .Properties ["filterClusterId" ].Description = "Optional cluster ID to filter deployments"
109+ schema .Properties ["filterNamespace" ].Description = "Optional namespace to filter deployments"
110+
111+ schema .Properties ["filterPlatform" ].Description =
112+ fmt .Sprintf ("Optional platform filter: %s=no filter, %s=user workload deployments, %s=platform deployments" ,
113+ filterPlatformNoFilter , filterPlatformUserWorkload , filterPlatformPlatform )
114+ schema .Properties ["filterPlatform" ].Default = toolsets .MustJSONMarshal (filterPlatformNoFilter )
115+ schema .Properties ["filterPlatform" ].Enum = []any {
116+ filterPlatformNoFilter ,
117+ filterPlatformUserWorkload ,
118+ filterPlatformPlatform ,
119+ }
120+
121+ schema .Properties ["offset" ].Description = "Pagination offset (default: 0)"
122+ schema .Properties ["offset" ].Default = toolsets .MustJSONMarshal (0 )
123+ schema .Properties ["limit" ].Minimum = jsonschema .Ptr (0.0 )
124+
125+ schema .Properties ["limit" ].Description = "Pagination limit: minimum: 1, maximum: 200 (default: 50)"
126+ schema .Properties ["limit" ].Default = toolsets .MustJSONMarshal (defaultLimit )
127+ schema .Properties ["limit" ].Minimum = jsonschema .Ptr (1.0 )
128+ schema .Properties ["limit" ].Maximum = jsonschema .Ptr (maximumLimit )
129+
130+ return schema
131+ }
132+
133+ // RegisterWith registers the get_deployments_for_cve tool handler with the MCP server.
134+ func (t * getDeploymentsForCVETool ) RegisterWith (server * mcp.Server ) {
54135 mcp .AddTool (server , t .GetTool (), t .handle )
55136}
56137
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" )
138+ // buildQuery builds query used to search deployments in StackRox Central.
139+ // We will quote values to have strict match. Without quote: CVE-2025-10, would match CVE-2025-101.
140+ func buildQuery (input getDeploymentsForCVEInput ) string {
141+ queryParts := []string {fmt .Sprintf ("CVE:%q" , input .CVEName )}
142+
143+ if input .FilterClusterID != "" {
144+ queryParts = append (queryParts , fmt .Sprintf ("Cluster ID:%q" , input .FilterClusterID ))
145+ }
146+
147+ if input .FilterNamespace != "" {
148+ queryParts = append (queryParts , fmt .Sprintf ("Namespace:%q" , input .FilterNamespace ))
149+ }
150+
151+ // Add platform filter if provided.
152+ switch input .FilterPlatform {
153+ case filterPlatformUserWorkload :
154+ queryParts = append (queryParts , "Platform Component:0" )
155+ case filterPlatformPlatform :
156+ queryParts = append (queryParts , "Platform Component:1" )
157+ case filterPlatformNoFilter :
158+ }
159+
160+ return strings .Join (queryParts , "+" )
161+ }
162+
163+ // handle is the handler for get_deployments_for_cve tool.
164+ func (t * getDeploymentsForCVETool ) handle (
165+ ctx context.Context ,
166+ req * mcp.CallToolRequest ,
167+ input getDeploymentsForCVEInput ,
168+ ) (* mcp.CallToolResult , * getDeploymentsForCVEOutput , error ) {
169+ err := input .validate ()
170+ if err != nil {
171+ return nil , nil , err
172+ }
173+
174+ conn , err := t .client .ReadyConn (ctx )
175+ if err != nil {
176+ return nil , nil , errors .Wrap (err , "unable to connect to server" )
177+ }
178+
179+ callCtx := auth .WithMCPRequestContext (ctx , req )
180+ deploymentClient := v1 .NewDeploymentServiceClient (conn )
181+
182+ listReq := & v1.RawQuery {
183+ Query : buildQuery (input ),
184+ Pagination : & v1.Pagination {
185+ Offset : input .Offset ,
186+ Limit : input .Limit ,
187+ },
188+ }
189+
190+ resp , err := deploymentClient .ListDeployments (callCtx , listReq )
191+ if err != nil {
192+ return nil , nil , client .NewError (err , "ListDeployments" )
193+ }
194+
195+ deployments := make ([]DeploymentResult , 0 , len (resp .GetDeployments ()))
196+ for _ , deployment := range resp .GetDeployments () {
197+ deployments = append (deployments , DeploymentResult {
198+ Name : deployment .GetName (),
199+ Namespace : deployment .GetNamespace (),
200+ ClusterID : deployment .GetClusterId (),
201+ ClusterName : deployment .GetCluster (),
202+ })
203+ }
204+
205+ output := & getDeploymentsForCVEOutput {
206+ Deployments : deployments ,
207+ }
208+
209+ return nil , output , nil
64210}
0 commit comments