@@ -2,63 +2,191 @@ 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+ // 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