Skip to content

Commit 70c8e5f

Browse files
committed
Add fetching of images for deployments
1 parent fc1e150 commit 70c8e5f

File tree

2 files changed

+360
-17
lines changed

2 files changed

+360
-17
lines changed

internal/toolsets/vulnerability/tools.go

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
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.
3233
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-
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

4042
func (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.
4951
type 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
172259
func (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

Comments
 (0)