44 "context"
55 "fmt"
66 "strings"
7+ "sync"
78
89 "github.com/google/jsonschema-go/jsonschema"
910 "github.com/modelcontextprotocol/go-sdk/mcp"
@@ -30,11 +31,12 @@ const (
3031
3132// getDeploymentsForCVEInput defines the input parameters for get_deployments_for_cve tool.
3233type 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- Cursor string `json:"cursor,omitempty"`
34+ CVEName string `json:"cveName"`
35+ FilterClusterID string `json:"filterClusterId,omitempty"`
36+ FilterNamespace string `json:"filterNamespace,omitempty"`
37+ FilterPlatform filterPlatformType `json:"filterPlatform,omitempty"`
38+ IncludeAffectedImages bool `json:"includeAffectedImages,omitempty"`
39+ Cursor string `json:"cursor,omitempty"`
3840}
3941
4042func (input * getDeploymentsForCVEInput ) validate () error {
@@ -45,12 +47,16 @@ func (input *getDeploymentsForCVEInput) validate() error {
4547 return nil
4648}
4749
48- // DeploymentResult contains deployment information.
50+ // DeploymentResult contains deployment information with optional image data .
4951type DeploymentResult struct {
50- Name string `json:"name"`
51- Namespace string `json:"namespace"`
52- ClusterID string `json:"clusterId"`
53- ClusterName string `json:"clusterName"`
52+ id string
53+
54+ Name string `json:"name"`
55+ Namespace string `json:"namespace"`
56+ ClusterID string `json:"clusterId"`
57+ ClusterName string `json:"clusterName"`
58+ AffectedImages []string `json:"affectedImages,omitempty"`
59+ ImageFetchError string `json:"imageFetchError,omitempty"`
5460}
5561
5662// getDeploymentsForCVEOutput defines the output structure for get_deployments_for_cve tool.
@@ -118,6 +124,11 @@ func getDeploymentsForCVEInputSchema() *jsonschema.Schema {
118124 filterPlatformPlatform ,
119125 }
120126
127+ schema .Properties ["includeAffectedImages" ].Description =
128+ "Whether to include affected image names for each deployment.\n " +
129+ "WARNING: This may significantly increase response time."
130+ schema .Properties ["includeAffectedImages" ].Default = toolsets .MustJSONMarshal (false )
131+
121132 schema .Properties ["cursor" ].Description = "Cursor for next page provided by server"
122133
123134 return schema
@@ -168,7 +179,84 @@ func getCursor(input *getDeploymentsForCVEInput) (*cursor.Cursor, error) {
168179 return currCursor , nil
169180}
170181
182+ const defaultMaxFetchImageConcurrency = 10
183+
184+ // deploymentEnricher handles parallel enrichment of deployments with image data.
185+ type deploymentEnricher struct {
186+ imageClient v1.ImageServiceClient
187+ cveName string
188+ semaphore chan struct {}
189+ wg sync.WaitGroup
190+ }
191+
192+ // newDeploymentEnricher creates a new enricher with max concurrency limit.
193+ func newDeploymentEnricher (
194+ imageClient v1.ImageServiceClient ,
195+ cveName string ,
196+ maxConcurrency int ,
197+ ) * deploymentEnricher {
198+ return & deploymentEnricher {
199+ imageClient : imageClient ,
200+ cveName : cveName ,
201+ semaphore : make (chan struct {}, maxConcurrency ),
202+ }
203+ }
204+
205+ // enrich enriches a single deployment result with image data in a goroutine.
206+ // Must be called before wait().
207+ func (e * deploymentEnricher ) enrich (
208+ ctx context.Context ,
209+ deployment * DeploymentResult ,
210+ ) {
211+ e .wg .Go (func () {
212+ e .semaphore <- struct {}{}
213+
214+ defer func () { <- e .semaphore }()
215+
216+ // Enrich the result in-place.
217+ images , err := fetchImagesForDeployment (ctx , e .imageClient , deployment , e .cveName )
218+ if err != nil {
219+ deployment .ImageFetchError = err .Error ()
220+
221+ return
222+ }
223+
224+ deployment .AffectedImages = images
225+ })
226+ }
227+
228+ // wait waits for all enrichment workers to complete.
229+ func (e * deploymentEnricher ) wait () {
230+ e .wg .Wait ()
231+ }
232+
233+ // fetchImagesForDeployment fetches images for a single deployment.
234+ // It queries the images API filtered by CVE and Deployment ID.
235+ func fetchImagesForDeployment (
236+ ctx context.Context ,
237+ imageClient v1.ImageServiceClient ,
238+ deployment * DeploymentResult ,
239+ cveName string ,
240+ ) ([]string , error ) {
241+ query := fmt .Sprintf ("CVE:%q+Deployment ID:%q" , cveName , deployment .id )
242+
243+ resp , err := imageClient .ListImages (ctx , & v1.RawQuery {Query : query })
244+ if err != nil {
245+ return nil , errors .Wrapf (err , "failed to fetch images for deployment %q in namespace %q" ,
246+ deployment .Name , deployment .Namespace )
247+ }
248+
249+ images := make ([]string , 0 , len (resp .GetImages ()))
250+ for _ , img := range resp .GetImages () {
251+ images = append (images , img .GetName ())
252+ }
253+
254+ return images , nil
255+ }
256+
171257// handle is the handler for get_deployments_for_cve tool.
258+ //
259+ //nolint:funlen
172260func (t * getDeploymentsForCVETool ) handle (
173261 ctx context.Context ,
174262 req * mcp.CallToolRequest ,
@@ -205,14 +293,28 @@ func (t *getDeploymentsForCVETool) handle(
205293 return nil , nil , client .NewError (err , "ListDeployments" )
206294 }
207295
208- deployments := make ([]DeploymentResult , 0 , len (resp .GetDeployments ()))
209- for _ , deployment := range resp .GetDeployments () {
210- deployments = append (deployments , DeploymentResult {
296+ rawDeployments := resp .GetDeployments ()
297+
298+ deployments := make ([]DeploymentResult , len (rawDeployments ))
299+ for i , deployment := range rawDeployments {
300+ deployments [i ] = DeploymentResult {
301+ id : deployment .GetId (),
211302 Name : deployment .GetName (),
212303 Namespace : deployment .GetNamespace (),
213304 ClusterID : deployment .GetClusterId (),
214305 ClusterName : deployment .GetCluster (),
215- })
306+ }
307+ }
308+
309+ if input .IncludeAffectedImages {
310+ imageClient := v1 .NewImageServiceClient (conn )
311+ enricher := newDeploymentEnricher (imageClient , input .CVEName , defaultMaxFetchImageConcurrency )
312+
313+ for i := range deployments {
314+ enricher .enrich (callCtx , & deployments [i ])
315+ }
316+
317+ enricher .wait ()
216318 }
217319
218320 // We always fetch limit+1 - if we do not have one additional element we can end paging.
0 commit comments