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,15 @@ 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+ Name string `json:"name"`
54+ Namespace string `json:"namespace"`
55+ ClusterID string `json:"clusterId"`
56+ ClusterName string `json:"clusterName"`
57+ AffectedImages []string `json:"affectedImages,omitempty"`
58+ ImageFetchError string `json:"imageFetchError,omitempty"`
5459}
5560
5661// getDeploymentsForCVEOutput defines the output structure for get_deployments_for_cve tool.
@@ -118,6 +123,11 @@ func getDeploymentsForCVEInputSchema() *jsonschema.Schema {
118123 filterPlatformPlatform ,
119124 }
120125
126+ schema .Properties ["includeAffectedImages" ].Description =
127+ "Whether to include affected image names for each deployment.\n " +
128+ "WARNING: This may significantly increase response time."
129+ schema .Properties ["includeAffectedImages" ].Default = toolsets .MustJSONMarshal (false )
130+
121131 schema .Properties ["cursor" ].Description = "Cursor for next page provided by server"
122132
123133 return schema
@@ -168,7 +178,84 @@ func getCursor(input *getDeploymentsForCVEInput) (*cursor.Cursor, error) {
168178 return currCursor , nil
169179}
170180
181+ const defaultMaxFetchImageConcurrency = 10
182+
183+ // deploymentEnricher handles parallel enrichment of deployments with image data.
184+ type deploymentEnricher struct {
185+ imageClient v1.ImageServiceClient
186+ cveName string
187+ semaphore chan struct {}
188+ wg sync.WaitGroup
189+ }
190+
191+ // newDeploymentEnricher creates a new enricher with max concurrency limit.
192+ func newDeploymentEnricher (
193+ imageClient v1.ImageServiceClient ,
194+ cveName string ,
195+ maxConcurrency int ,
196+ ) * deploymentEnricher {
197+ return & deploymentEnricher {
198+ imageClient : imageClient ,
199+ cveName : cveName ,
200+ semaphore : make (chan struct {}, maxConcurrency ),
201+ }
202+ }
203+
204+ // enrich enriches a single deployment result with image data in a goroutine.
205+ // Must be called before wait().
206+ func (e * deploymentEnricher ) enrich (
207+ ctx context.Context ,
208+ deployment * DeploymentResult ,
209+ ) {
210+ e .wg .Go (func () {
211+ e .semaphore <- struct {}{}
212+
213+ defer func () { <- e .semaphore }()
214+
215+ // Enrich the result in-place.
216+ images , err := fetchImagesForDeployment (ctx , e .imageClient , deployment , e .cveName )
217+ if err != nil {
218+ deployment .ImageFetchError = err .Error ()
219+
220+ return
221+ }
222+
223+ deployment .AffectedImages = images
224+ })
225+ }
226+
227+ // wait waits for all enrichment workers to complete.
228+ func (e * deploymentEnricher ) wait () {
229+ e .wg .Wait ()
230+ }
231+
232+ // fetchImagesForDeployment fetches images for a single deployment.
233+ // It queries the images API filtered by CVE and Deployment ID.
234+ func fetchImagesForDeployment (
235+ ctx context.Context ,
236+ imageClient v1.ImageServiceClient ,
237+ deployment * DeploymentResult ,
238+ cveName string ,
239+ ) ([]string , error ) {
240+ query := fmt .Sprintf ("CVE:%q+Deployment ID:%q" , cveName , deployment .ID )
241+
242+ resp , err := imageClient .ListImages (ctx , & v1.RawQuery {Query : query })
243+ if err != nil {
244+ return nil , errors .Wrapf (err , "failed to fetch images for deployment %q in namespace %q" ,
245+ deployment .Name , deployment .Namespace )
246+ }
247+
248+ images := make ([]string , 0 , len (resp .GetImages ()))
249+ for _ , img := range resp .GetImages () {
250+ images = append (images , img .GetName ())
251+ }
252+
253+ return images , nil
254+ }
255+
171256// handle is the handler for get_deployments_for_cve tool.
257+ //
258+ //nolint:funlen
172259func (t * getDeploymentsForCVETool ) handle (
173260 ctx context.Context ,
174261 req * mcp.CallToolRequest ,
@@ -205,14 +292,28 @@ func (t *getDeploymentsForCVETool) handle(
205292 return nil , nil , client .NewError (err , "ListDeployments" )
206293 }
207294
208- deployments := make ([]DeploymentResult , 0 , len (resp .GetDeployments ()))
209- for _ , deployment := range resp .GetDeployments () {
210- deployments = append (deployments , DeploymentResult {
295+ rawDeployments := resp .GetDeployments ()
296+
297+ deployments := make ([]DeploymentResult , len (rawDeployments ))
298+ for i , deployment := range rawDeployments {
299+ deployments [i ] = DeploymentResult {
300+ ID : deployment .GetId (),
211301 Name : deployment .GetName (),
212302 Namespace : deployment .GetNamespace (),
213303 ClusterID : deployment .GetClusterId (),
214304 ClusterName : deployment .GetCluster (),
215- })
305+ }
306+ }
307+
308+ if input .IncludeAffectedImages {
309+ imageClient := v1 .NewImageServiceClient (conn )
310+ enricher := newDeploymentEnricher (imageClient , input .CVEName , defaultMaxFetchImageConcurrency )
311+
312+ for i := range deployments {
313+ enricher .enrich (callCtx , & deployments [i ])
314+ }
315+
316+ enricher .wait ()
216317 }
217318
218319 // We always fetch limit+1 - if we do not have one additional element we can end paging.
0 commit comments