From 48058bcfa6ac89bd9e2d9ef9bb3c5231cec0b026 Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:51:35 -0600 Subject: [PATCH 01/12] rename SBOM generation http handler So that it is easier to differentiate between the the generation vs. scanning handler. --- central/image/service/http_handler.go | 26 +++++++++++----------- central/image/service/http_handler_test.go | 16 ++++++------- central/main.go | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/central/image/service/http_handler.go b/central/image/service/http_handler.go index ddb13ab8da39c..e9f91a3d815e1 100644 --- a/central/image/service/http_handler.go +++ b/central/image/service/http_handler.go @@ -29,7 +29,7 @@ import ( "google.golang.org/grpc/codes" ) -type sbomHttpHandler struct { +type sbomGenHttpHandler struct { integration integration.Set enricher enricher.ImageEnricher enricherV2 enricher.ImageEnricherV2 @@ -37,11 +37,11 @@ type sbomHttpHandler struct { riskManager manager.Manager } -var _ http.Handler = (*sbomHttpHandler)(nil) +var _ http.Handler = (*sbomGenHttpHandler)(nil) // SBOMHandler returns a handler for get sbom http request. -func SBOMHandler(integration integration.Set, enricher enricher.ImageEnricher, enricherV2 enricher.ImageEnricherV2, clusterSACHelper sachelper.ClusterSacHelper, riskManager manager.Manager) http.Handler { - return sbomHttpHandler{ +func SBOMGenHandler(integration integration.Set, enricher enricher.ImageEnricher, enricherV2 enricher.ImageEnricherV2, clusterSACHelper sachelper.ClusterSacHelper, riskManager manager.Manager) http.Handler { + return sbomGenHttpHandler{ integration: integration, enricher: enricher, enricherV2: enricherV2, @@ -50,7 +50,7 @@ func SBOMHandler(integration integration.Set, enricher enricher.ImageEnricher, e } } -func (h sbomHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h sbomGenHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -94,7 +94,7 @@ func (h sbomHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // enrichWithModelSwitch enriches an image by name, returning both V1 and V2 models based on the feature flag. -func (h sbomHttpHandler) enrichWithModelSwitch( +func (h sbomGenHttpHandler) enrichWithModelSwitch( ctx context.Context, enrichmentCtx enricher.EnrichmentContext, ci *storage.ContainerImage, @@ -121,7 +121,7 @@ func (h sbomHttpHandler) enrichWithModelSwitch( } // enrichImage enriches the image with the given name and based on the given enrichment context. -func (h sbomHttpHandler) enrichImage(ctx context.Context, enrichmentCtx enricher.EnrichmentContext, ci *storage.ContainerImage) (*storage.Image, bool, error) { +func (h sbomGenHttpHandler) enrichImage(ctx context.Context, enrichmentCtx enricher.EnrichmentContext, ci *storage.ContainerImage) (*storage.Image, bool, error) { // forcedEnrichment is set to true when enrichImage forces an enrichment. forcedEnrichment := false @@ -167,7 +167,7 @@ func errorOrNotScanned(enrichmentResult enricher.EnrichmentResult, err error) er } // getSBOM generates an SBOM for the specified parameters. -func (h sbomHttpHandler) getSBOM(ctx context.Context, params apiparams.SBOMRequestBody) ([]byte, error) { +func (h sbomGenHttpHandler) getSBOM(ctx context.Context, params apiparams.SBOMRequestBody) ([]byte, error) { enrichmentCtx := enricher.EnrichmentContext{ Delegable: true, FetchOpt: enricher.UseCachesIfPossible, @@ -268,7 +268,7 @@ func addForceToEnrichmentContext(enrichmentCtx *enricher.EnrichmentContext) { } // getScannerV4SBOMIntegration returns the SBOM interface of Scanner V4. -func (h sbomHttpHandler) getScannerV4SBOMIntegration() (scannerTypes.SBOMer, error) { +func (h sbomGenHttpHandler) getScannerV4SBOMIntegration() (scannerTypes.SBOMer, error) { scanners := h.integration.ScannerSet() for _, scanner := range scanners.GetAll() { if scanner.GetScanner().Type() == scannerTypes.ScannerV4 { @@ -281,12 +281,12 @@ func (h sbomHttpHandler) getScannerV4SBOMIntegration() (scannerTypes.SBOMer, err } // scannedByScannerV4 checks if image is scanned by Scanner V4. -func (h sbomHttpHandler) scannedByScannerV4(scan *storage.ImageScan) bool { +func (h sbomGenHttpHandler) scannedByScannerV4(scan *storage.ImageScan) bool { return scan.GetDataSource().GetId() == iiStore.DefaultScannerV4Integration.GetId() } // saveImage saves the image to Central's database. -func (h sbomHttpHandler) saveImage(img *storage.Image, imgV2 *storage.ImageV2) error { +func (h sbomGenHttpHandler) saveImage(img *storage.Image, imgV2 *storage.ImageV2) error { if features.FlattenImageData.Enabled() { return h.saveImageV2(imgV2) } @@ -294,7 +294,7 @@ func (h sbomHttpHandler) saveImage(img *storage.Image, imgV2 *storage.ImageV2) e } // saveImageV1 saves an Image V1 to Central's database. -func (h sbomHttpHandler) saveImageV1(img *storage.Image) error { +func (h sbomGenHttpHandler) saveImageV1(img *storage.Image) error { img.Id = utils.GetSHA(img) if img.GetId() == "" { return nil @@ -308,7 +308,7 @@ func (h sbomHttpHandler) saveImageV1(img *storage.Image) error { } // saveImageV2 saves an Image V2 to Central's database. -func (h sbomHttpHandler) saveImageV2(imgV2 *storage.ImageV2) error { +func (h sbomGenHttpHandler) saveImageV2(imgV2 *storage.ImageV2) error { if imgV2 == nil { return errors.New("nil images cannot be saved") } diff --git a/central/image/service/http_handler_test.go b/central/image/service/http_handler_test.go index aa3664cf315d9..7f0caf4d2c12f 100644 --- a/central/image/service/http_handler_test.go +++ b/central/image/service/http_handler_test.go @@ -56,7 +56,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/sbom", nil) recorder := httptest.NewRecorder() - handler := SBOMHandler(imageintegration.Set(), nil, nil, nil, nil) + handler := SBOMGenHandler(imageintegration.Set(), nil, nil, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -99,7 +99,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/sbom", bytes.NewReader(reqJson)) recorder := httptest.NewRecorder() - handler := SBOMHandler(imageintegration.Set(), mockEnricher, mockEnricherV2, nil, nil) + handler := SBOMGenHandler(imageintegration.Set(), mockEnricher, mockEnricherV2, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -154,7 +154,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/sbom", bytes.NewReader(reqJson)) recorder := httptest.NewRecorder() - handler := SBOMHandler(set, mockEnricher, mockEnricherV2, nil, nil) + handler := SBOMGenHandler(set, mockEnricher, mockEnricherV2, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -172,7 +172,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/sbom", bytes.NewBufferString(invalidJson)) recorder := httptest.NewRecorder() - handler := SBOMHandler(imageintegration.Set(), nil, nil, nil, nil) + handler := SBOMGenHandler(imageintegration.Set(), nil, nil, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -189,7 +189,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/sbom", bytes.NewReader(reqBody)) recorder := httptest.NewRecorder() - handler := SBOMHandler(imageintegration.Set(), nil, nil, nil, nil) + handler := SBOMGenHandler(imageintegration.Set(), nil, nil, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -205,7 +205,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/sbom", nil) recorder := httptest.NewRecorder() - handler := SBOMHandler(imageintegration.Set(), nil, nil, nil, nil) + handler := SBOMGenHandler(imageintegration.Set(), nil, nil, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -222,7 +222,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/sbom", bytes.NewReader(largeRequestBody)) recorder := httptest.NewRecorder() - handler := SBOMHandler(imageintegration.Set(), nil, nil, nil, nil) + handler := SBOMGenHandler(imageintegration.Set(), nil, nil, nil, nil) handler.ServeHTTP(recorder, req) res := recorder.Result() @@ -312,7 +312,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { assert.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/sbom", bytes.NewReader(reqJson)) recorder := httptest.NewRecorder() - handler := SBOMHandler(mockIntegrationSet, mockEnricher, mockEnricherV2, nil, mockRiskManager) + handler := SBOMGenHandler(mockIntegrationSet, mockEnricher, mockEnricherV2, nil, mockRiskManager) // Make the SBOM generation request. handler.ServeHTTP(recorder, req) diff --git a/central/main.go b/central/main.go index b8f3e33528401..d53f51de89ee9 100644 --- a/central/main.go +++ b/central/main.go @@ -802,7 +802,7 @@ func customRoutes() (customRoutes []routes.CustomRoute) { { Route: "/api/v1/images/sbom", Authorizer: user.With(permissions.Modify(resources.Image)), - ServerHandler: imageService.SBOMHandler(imageintegration.Set(), enrichment.ImageEnricherSingleton(), enrichment.ImageEnricherV2Singleton(), sachelper.NewClusterSacHelper(clusterDataStore.Singleton()), riskManager.Singleton()), + ServerHandler: imageService.SBOMGenHandler(imageintegration.Set(), enrichment.ImageEnricherSingleton(), enrichment.ImageEnricherV2Singleton(), sachelper.NewClusterSacHelper(clusterDataStore.Singleton()), riskManager.Singleton()), Compression: true, }, { From 0b983aff56e0bfa9c89030bc42a1f0d4216a7699 Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:59:42 -0600 Subject: [PATCH 02/12] SBOM Scanning rails - HTTP handler - Pass http request reader to Scanner V4 integration - Mock conversion of vuln report to Scan SBOM response --- .../image/service/sbom_scan_http_handler.go | 157 +++ .../service/sbom_scan_http_handler_test.go | 170 +++ central/main.go | 6 + generated/api/v1/sbom.pb.go | 227 ++++ generated/api/v1/sbom.swagger.json | 46 + generated/api/v1/sbom_vtproto.pb.go | 1017 +++++++++++++++++ pkg/env/image_scan.go | 3 + pkg/features/list.go | 3 + pkg/scanners/scannerv4/scannerv4.go | 69 ++ pkg/scanners/types/mocks/types.go | 31 + pkg/scanners/types/types.go | 7 +- proto/api/v1/sbom.proto | 30 + 12 files changed, 1765 insertions(+), 1 deletion(-) create mode 100644 central/image/service/sbom_scan_http_handler.go create mode 100644 central/image/service/sbom_scan_http_handler_test.go create mode 100644 generated/api/v1/sbom.pb.go create mode 100644 generated/api/v1/sbom.swagger.json create mode 100644 generated/api/v1/sbom_vtproto.pb.go create mode 100644 proto/api/v1/sbom.proto diff --git a/central/image/service/sbom_scan_http_handler.go b/central/image/service/sbom_scan_http_handler.go new file mode 100644 index 0000000000000..3b854e7ac1df4 --- /dev/null +++ b/central/image/service/sbom_scan_http_handler.go @@ -0,0 +1,157 @@ +package service + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + + "github.com/pkg/errors" + v1 "github.com/stackrox/rox/generated/api/v1" + "github.com/stackrox/rox/generated/storage" + "github.com/stackrox/rox/pkg/env" + "github.com/stackrox/rox/pkg/features" + "github.com/stackrox/rox/pkg/httputil" + "github.com/stackrox/rox/pkg/images/integration" + "github.com/stackrox/rox/pkg/ioutils" + scannerTypes "github.com/stackrox/rox/pkg/scanners/types" + "github.com/stackrox/rox/pkg/set" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/protojson" +) + +var ( + supportedMediaTypes = set.NewFrozenStringSet( + "text/spdx+json", // Used by Sigstore/Cosign, not IANA registered. + "application/spdx+json", // IANA registered type for SPDX JSON. + ) +) + +type sbomScanHttpHandler struct { + integrations integration.Set +} + +var _ http.Handler = (*sbomScanHttpHandler)(nil) + +func SBOMScanHandler(integrations integration.Set) http.Handler { + return sbomScanHttpHandler{ + integrations: integrations, + } +} + +func (s sbomScanHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Verify Scanner V4 is enabled. + if !features.ScannerV4.Enabled() { + httputil.WriteGRPCStyleError(w, codes.Unimplemented, errors.New("Scanner V4 is disabled.")) + return + } + + if !features.SBOMScanning.Enabled() { + httputil.WriteGRPCStyleError(w, codes.Unimplemented, errors.New("SBOM Scanning is disabled.")) + return + + } + + // Only POST requests are supported. + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // Validate the media type is supported. + contentType := r.Header.Get("Content-Type") + err := s.validateMediaType(contentType) + if err != nil { + httputil.WriteGRPCStyleError(w, codes.InvalidArgument, fmt.Errorf("validating media type: %w", err)) + return + } + + // Enforce maximum uncompressed request size to prevent excessive memory usage. + // MaxBytesReader returns an error if the request body exceeds the limit. + maxReqSizeBytes := env.SBOMScanMaxReqSizeBytes.IntegerSetting() + limitedBody := http.MaxBytesReader(w, r.Body, int64(maxReqSizeBytes)) + + // Add cancellation safety to prevent partial/corrupted data on interruption. + // InterruptibleReader: Ensures clean termination without partial reads. + body, interrupt := ioutils.NewInterruptibleReader(limitedBody) + defer interrupt() + + // ContextBoundReader: Ensures reads fail fast when request context is canceled. + // This prevents hanging reads on connection interruption + readCtx, cancel := context.WithCancel(r.Context()) + defer cancel() + body = ioutils.NewContextBoundReader(readCtx, body) + + sbomScanResponse, err := s.scanSBOM(body, contentType) + if err != nil { + // Check if error is due to request body exceeding size limit. + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + httputil.WriteGRPCStyleError(w, codes.InvalidArgument, fmt.Errorf("request body exceeds maximum size of %d bytes", maxReqSizeBytes)) + return + } + httputil.WriteGRPCStyleError(w, codes.Internal, fmt.Errorf("scanning SBOM: %w", err)) + return + } + + // Serialize the scan result to JSON using protojson for proper protobuf handling. + // protojson handles protobuf-specific types (enums, oneof, etc.) correctly. + jsonBytes, err := protojson.MarshalOptions{Multiline: true}.Marshal(sbomScanResponse) + if err != nil { + httputil.WriteGRPCStyleError(w, codes.Internal, fmt.Errorf("serializing SBOM scan response: %w", err)) + return + } + + // Set response headers and write JSON response. + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(jsonBytes); err != nil { + log.Warnw("writing SBOM scan response: %v", err) + return + } +} + +// scanSBOM will request a scan of the SBOM from Scanner V4. +func (s sbomScanHttpHandler) scanSBOM(limitedReader io.Reader, contentType string) (*v1.SBOMScanResponse, error) { + // Get reference to Scanner V4. + scannerV4, dataSource, err := s.getScannerV4Integration() + if err != nil { + return nil, fmt.Errorf("getting Scanner V4 integration: %w", err) + } + + // Scan the SBOM. + sbomScanResponse, err := scannerV4.ScanSBOM(limitedReader, contentType) + if err != nil { + return nil, fmt.Errorf("scanning sbom: %w", err) + } + // Set the scan DataSource used to do the scan. + if sbomScanResponse.GetScan() != nil { + sbomScanResponse.GetScan().DataSource = dataSource + } + + return sbomScanResponse, nil +} + +// getScannerV4Integration returns the SBOM interface of Scanner V4. +func (s sbomScanHttpHandler) getScannerV4Integration() (scannerTypes.SBOMer, *storage.DataSource, error) { + scanners := s.integrations.ScannerSet() + for _, scanner := range scanners.GetAll() { + if scanner.GetScanner().Type() == scannerTypes.ScannerV4 { + if scannerv4, ok := scanner.GetScanner().(scannerTypes.SBOMer); ok { + return scannerv4, scanner.DataSource(), nil + } + } + } + return nil, nil, errors.New("Scanner V4 integration not found") +} + +// validateMediaType validates the media type from the content type header is supported. +func (s sbomScanHttpHandler) validateMediaType(contentType string) error { + // Strip any parameters (e.g., charset) from the media type + mediaType := strings.TrimSpace(strings.Split(contentType, ";")[0]) + if !supportedMediaTypes.Contains(mediaType) { + return fmt.Errorf("unsupported media type %q, supported types %v", mediaType, supportedMediaTypes.AsSlice()) + } + + return nil +} diff --git a/central/image/service/sbom_scan_http_handler_test.go b/central/image/service/sbom_scan_http_handler_test.go new file mode 100644 index 0000000000000..f168a6c615c59 --- /dev/null +++ b/central/image/service/sbom_scan_http_handler_test.go @@ -0,0 +1,170 @@ +package service + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pkg/errors" + "github.com/stackrox/rox/central/imageintegration" + v1 "github.com/stackrox/rox/generated/api/v1" + "github.com/stackrox/rox/generated/storage" + "github.com/stackrox/rox/pkg/features" + intergrationMocks "github.com/stackrox/rox/pkg/images/integration/mocks" + scannerMocks "github.com/stackrox/rox/pkg/scanners/mocks" + scannerTypes "github.com/stackrox/rox/pkg/scanners/types" + scannerTypesMocks "github.com/stackrox/rox/pkg/scanners/types/mocks" + "github.com/stackrox/rox/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestScanSBOMHttpHandler_ServeHTTP(t *testing.T) { + t.Run("scanner v4 disabled", func(t *testing.T) { + testutils.MustUpdateFeature(t, features.ScannerV4, false) + + req := httptest.NewRequest(http.MethodGet, "/sbom", nil) + recorder := httptest.NewRecorder() + + handler := SBOMScanHandler(imageintegration.Set()) + handler.ServeHTTP(recorder, req) + + res := recorder.Result() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + err = res.Body.Close() + assert.NoError(t, err) + assert.Equal(t, http.StatusNotImplemented, res.StatusCode) + assert.Contains(t, string(body), "Scanner V4 is disabled") + }) + + t.Run("invalid request method", func(t *testing.T) { + testutils.MustUpdateFeature(t, features.ScannerV4, true) + + req := httptest.NewRequest(http.MethodGet, "/sbom", nil) + recorder := httptest.NewRecorder() + + handler := SBOMScanHandler(imageintegration.Set()) + handler.ServeHTTP(recorder, req) + + res := recorder.Result() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + err = res.Body.Close() + assert.NoError(t, err) + assert.Equal(t, http.StatusNotImplemented, res.StatusCode) + assert.Contains(t, string(body), "SBOM Scanning is disabled") + }) + + t.Run("invalid media type", func(t *testing.T) { + testutils.MustUpdateFeature(t, features.ScannerV4, true) + testutils.MustUpdateFeature(t, features.SBOMScanning, true) + + req := httptest.NewRequest(http.MethodPost, "/sbom", nil) + req.Header.Add("Content-Type", "wrong") + recorder := httptest.NewRecorder() + + handler := SBOMScanHandler(imageintegration.Set()) + handler.ServeHTTP(recorder, req) + + res := recorder.Result() + err := res.Body.Close() + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("scanner v4 integration missing", func(t *testing.T) { + testutils.MustUpdateFeature(t, features.ScannerV4, true) + testutils.MustUpdateFeature(t, features.SBOMScanning, true) + + req := httptest.NewRequest(http.MethodPost, "/sbom", nil) + req.Header.Add("Content-Type", supportedMediaTypes.AsSlice()[0]) + recorder := httptest.NewRecorder() + + handler := SBOMScanHandler(imageintegration.Set()) + handler.ServeHTTP(recorder, req) + + res := recorder.Result() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + err = res.Body.Close() + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "integration") + }) + + t.Run("scanner v4 scan error", func(t *testing.T) { + testutils.MustUpdateFeature(t, features.ScannerV4, true) + testutils.MustUpdateFeature(t, features.SBOMScanning, true) + + req := httptest.NewRequest(http.MethodPost, "/sbom", nil) + req.Header.Add("Content-Type", supportedMediaTypes.AsSlice()[0]) + recorder := httptest.NewRecorder() + + ctrl := gomock.NewController(t) + + mockSBOMScanner := scannerTypesMocks.NewMockScannerSBOMer(ctrl) + mockSBOMScanner.EXPECT().ScanSBOM(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")) + mockSBOMScanner.EXPECT().Type().Return(scannerTypes.ScannerV4) + + mockImageScannerWithDS := scannerTypesMocks.NewMockImageScannerWithDataSource(ctrl) + mockImageScannerWithDS.EXPECT().GetScanner().Return(mockSBOMScanner).AnyTimes() + mockImageScannerWithDS.EXPECT().DataSource().Return(&storage.DataSource{}).AnyTimes() + + mockScannerSet := scannerMocks.NewMockSet(ctrl) + mockScannerSet.EXPECT().GetAll().Return([]scannerTypes.ImageScannerWithDataSource{mockImageScannerWithDS}) + + mockIntegrationSet := intergrationMocks.NewMockSet(ctrl) + mockIntegrationSet.EXPECT().ScannerSet().Return(mockScannerSet) + + handler := SBOMScanHandler(mockIntegrationSet) + handler.ServeHTTP(recorder, req) + + res := recorder.Result() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + err = res.Body.Close() + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Contains(t, string(body), "scanning sbom") + }) + + t.Run("valid scan", func(t *testing.T) { + testutils.MustUpdateFeature(t, features.ScannerV4, true) + testutils.MustUpdateFeature(t, features.SBOMScanning, true) + + req := httptest.NewRequest(http.MethodPost, "/sbom", nil) + req.Header.Add("Content-Type", supportedMediaTypes.AsSlice()[0]) + recorder := httptest.NewRecorder() + + ctrl := gomock.NewController(t) + + mockSBOMScanner := scannerTypesMocks.NewMockScannerSBOMer(ctrl) + mockSBOMScanner.EXPECT().ScanSBOM(gomock.Any(), gomock.Any()).Return(&v1.SBOMScanResponse{Id: "fake-sbom-id"}, nil) + mockSBOMScanner.EXPECT().Type().Return(scannerTypes.ScannerV4) + + mockImageScannerWithDS := scannerTypesMocks.NewMockImageScannerWithDataSource(ctrl) + mockImageScannerWithDS.EXPECT().GetScanner().Return(mockSBOMScanner).AnyTimes() + mockImageScannerWithDS.EXPECT().DataSource().Return(&storage.DataSource{}).AnyTimes() + + mockScannerSet := scannerMocks.NewMockSet(ctrl) + mockScannerSet.EXPECT().GetAll().Return([]scannerTypes.ImageScannerWithDataSource{mockImageScannerWithDS}) + + mockIntegrationSet := intergrationMocks.NewMockSet(ctrl) + mockIntegrationSet.EXPECT().ScannerSet().Return(mockScannerSet) + + handler := SBOMScanHandler(mockIntegrationSet) + handler.ServeHTTP(recorder, req) + + res := recorder.Result() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + err = res.Body.Close() + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, string(body), "fake-sbom-id") + }) + +} diff --git a/central/main.go b/central/main.go index d53f51de89ee9..ad3eb49247e03 100644 --- a/central/main.go +++ b/central/main.go @@ -805,6 +805,12 @@ func customRoutes() (customRoutes []routes.CustomRoute) { ServerHandler: imageService.SBOMGenHandler(imageintegration.Set(), enrichment.ImageEnricherSingleton(), enrichment.ImageEnricherV2Singleton(), sachelper.NewClusterSacHelper(clusterDataStore.Singleton()), riskManager.Singleton()), Compression: true, }, + { + Route: "/api/v1/sboms/scan", + Authorizer: user.With(permissions.Modify(resources.Image)), + ServerHandler: imageService.SBOMScanHandler(imageintegration.Set()), + Compression: true, + }, { Route: "/api/splunk/ta/vulnmgmt", Authorizer: user.With(permissions.View(resources.Image), permissions.View(resources.Deployment)), diff --git a/generated/api/v1/sbom.pb.go b/generated/api/v1/sbom.pb.go new file mode 100644 index 0000000000000..13f5c284cde97 --- /dev/null +++ b/generated/api/v1/sbom.pb.go @@ -0,0 +1,227 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.32.1 +// source: api/v1/sbom.proto + +package v1 + +import ( + storage "github.com/stackrox/rox/generated/storage" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// SBOMScanResponse wraps metadata and scan results for an SBOM. +// The fields are intended to be similar to those from `storage.Node` and `storage.Image` +// so that parsing scans are familiar to users. +// +// There is no service defined for this response as it is handled +// by a custom route (non-gRPC). +// +// next available tag: 3 +type SBOMScanResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Scan *SBOMScanResponse_SBOMScan `protobuf:"bytes,2,opt,name=scan,proto3" json:"scan,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SBOMScanResponse) Reset() { + *x = SBOMScanResponse{} + mi := &file_api_v1_sbom_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SBOMScanResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SBOMScanResponse) ProtoMessage() {} + +func (x *SBOMScanResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_sbom_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SBOMScanResponse.ProtoReflect.Descriptor instead. +func (*SBOMScanResponse) Descriptor() ([]byte, []int) { + return file_api_v1_sbom_proto_rawDescGZIP(), []int{0} +} + +func (x *SBOMScanResponse) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *SBOMScanResponse) GetScan() *SBOMScanResponse_SBOMScan { + if x != nil { + return x.Scan + } + return nil +} + +// next available tag: 5 +type SBOMScanResponse_SBOMScan struct { + state protoimpl.MessageState `protogen:"open.v1"` + ScanTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=scan_time,json=scanTime,proto3" json:"scan_time,omitempty"` + Components []*storage.EmbeddedImageScanComponent `protobuf:"bytes,2,rep,name=components,proto3" json:"components,omitempty"` + OperatingSystem string `protobuf:"bytes,3,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` + DataSource *storage.DataSource `protobuf:"bytes,4,opt,name=data_source,json=dataSource,proto3" json:"data_source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SBOMScanResponse_SBOMScan) Reset() { + *x = SBOMScanResponse_SBOMScan{} + mi := &file_api_v1_sbom_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SBOMScanResponse_SBOMScan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SBOMScanResponse_SBOMScan) ProtoMessage() {} + +func (x *SBOMScanResponse_SBOMScan) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_sbom_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SBOMScanResponse_SBOMScan.ProtoReflect.Descriptor instead. +func (*SBOMScanResponse_SBOMScan) Descriptor() ([]byte, []int) { + return file_api_v1_sbom_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *SBOMScanResponse_SBOMScan) GetScanTime() *timestamppb.Timestamp { + if x != nil { + return x.ScanTime + } + return nil +} + +func (x *SBOMScanResponse_SBOMScan) GetComponents() []*storage.EmbeddedImageScanComponent { + if x != nil { + return x.Components + } + return nil +} + +func (x *SBOMScanResponse_SBOMScan) GetOperatingSystem() string { + if x != nil { + return x.OperatingSystem + } + return "" +} + +func (x *SBOMScanResponse_SBOMScan) GetDataSource() *storage.DataSource { + if x != nil { + return x.DataSource + } + return nil +} + +var File_api_v1_sbom_proto protoreflect.FileDescriptor + +const file_api_v1_sbom_proto_rawDesc = "" + + "\n" + + "\x11api/v1/sbom.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13storage/image.proto\"\xc1\x02\n" + + "\x10SBOMScanResponse\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x121\n" + + "\x04scan\x18\x02 \x01(\v2\x1d.v1.SBOMScanResponse.SBOMScanR\x04scan\x1a\xe9\x01\n" + + "\bSBOMScan\x127\n" + + "\tscan_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\bscanTime\x12C\n" + + "\n" + + "components\x18\x02 \x03(\v2#.storage.EmbeddedImageScanComponentR\n" + + "components\x12)\n" + + "\x10operating_system\x18\x03 \x01(\tR\x0foperatingSystem\x124\n" + + "\vdata_source\x18\x04 \x01(\v2\x13.storage.DataSourceR\n" + + "dataSourceB'\n" + + "\x18io.stackrox.proto.api.v1Z\v./api/v1;v1b\x06proto3" + +var ( + file_api_v1_sbom_proto_rawDescOnce sync.Once + file_api_v1_sbom_proto_rawDescData []byte +) + +func file_api_v1_sbom_proto_rawDescGZIP() []byte { + file_api_v1_sbom_proto_rawDescOnce.Do(func() { + file_api_v1_sbom_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_sbom_proto_rawDesc), len(file_api_v1_sbom_proto_rawDesc))) + }) + return file_api_v1_sbom_proto_rawDescData +} + +var file_api_v1_sbom_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_api_v1_sbom_proto_goTypes = []any{ + (*SBOMScanResponse)(nil), // 0: v1.SBOMScanResponse + (*SBOMScanResponse_SBOMScan)(nil), // 1: v1.SBOMScanResponse.SBOMScan + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp + (*storage.EmbeddedImageScanComponent)(nil), // 3: storage.EmbeddedImageScanComponent + (*storage.DataSource)(nil), // 4: storage.DataSource +} +var file_api_v1_sbom_proto_depIdxs = []int32{ + 1, // 0: v1.SBOMScanResponse.scan:type_name -> v1.SBOMScanResponse.SBOMScan + 2, // 1: v1.SBOMScanResponse.SBOMScan.scan_time:type_name -> google.protobuf.Timestamp + 3, // 2: v1.SBOMScanResponse.SBOMScan.components:type_name -> storage.EmbeddedImageScanComponent + 4, // 3: v1.SBOMScanResponse.SBOMScan.data_source:type_name -> storage.DataSource + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_api_v1_sbom_proto_init() } +func file_api_v1_sbom_proto_init() { + if File_api_v1_sbom_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_sbom_proto_rawDesc), len(file_api_v1_sbom_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_api_v1_sbom_proto_goTypes, + DependencyIndexes: file_api_v1_sbom_proto_depIdxs, + MessageInfos: file_api_v1_sbom_proto_msgTypes, + }.Build() + File_api_v1_sbom_proto = out.File + file_api_v1_sbom_proto_goTypes = nil + file_api_v1_sbom_proto_depIdxs = nil +} diff --git a/generated/api/v1/sbom.swagger.json b/generated/api/v1/sbom.swagger.json new file mode 100644 index 0000000000000..7010259cff871 --- /dev/null +++ b/generated/api/v1/sbom.swagger.json @@ -0,0 +1,46 @@ +{ + "swagger": "2.0", + "info": { + "title": "api/v1/sbom.proto", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "googlerpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." + } + }, + "additionalProperties": {}, + "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(&foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := &pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := &pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": ,\n \"lastName\": \n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" + } + } +} diff --git a/generated/api/v1/sbom_vtproto.pb.go b/generated/api/v1/sbom_vtproto.pb.go new file mode 100644 index 0000000000000..6ae52a6016e0b --- /dev/null +++ b/generated/api/v1/sbom_vtproto.pb.go @@ -0,0 +1,1017 @@ +// Code generated by protoc-gen-go-vtproto. DO NOT EDIT. +// protoc-gen-go-vtproto version: v0.6.1-0.20240409071808-615f978279ca +// source: api/v1/sbom.proto + +package v1 + +import ( + fmt "fmt" + protohelpers "github.com/planetscale/vtprotobuf/protohelpers" + timestamppb1 "github.com/planetscale/vtprotobuf/types/known/timestamppb" + storage "github.com/stackrox/rox/generated/storage" + proto "google.golang.org/protobuf/proto" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + io "io" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +func (m *SBOMScanResponse_SBOMScan) CloneVT() *SBOMScanResponse_SBOMScan { + if m == nil { + return (*SBOMScanResponse_SBOMScan)(nil) + } + r := new(SBOMScanResponse_SBOMScan) + r.ScanTime = (*timestamppb.Timestamp)((*timestamppb1.Timestamp)(m.ScanTime).CloneVT()) + r.OperatingSystem = m.OperatingSystem + if rhs := m.Components; rhs != nil { + tmpContainer := make([]*storage.EmbeddedImageScanComponent, len(rhs)) + for k, v := range rhs { + if vtpb, ok := interface{}(v).(interface { + CloneVT() *storage.EmbeddedImageScanComponent + }); ok { + tmpContainer[k] = vtpb.CloneVT() + } else { + tmpContainer[k] = proto.Clone(v).(*storage.EmbeddedImageScanComponent) + } + } + r.Components = tmpContainer + } + if rhs := m.DataSource; rhs != nil { + if vtpb, ok := interface{}(rhs).(interface{ CloneVT() *storage.DataSource }); ok { + r.DataSource = vtpb.CloneVT() + } else { + r.DataSource = proto.Clone(rhs).(*storage.DataSource) + } + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *SBOMScanResponse_SBOMScan) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *SBOMScanResponse) CloneVT() *SBOMScanResponse { + if m == nil { + return (*SBOMScanResponse)(nil) + } + r := new(SBOMScanResponse) + r.Id = m.Id + r.Scan = m.Scan.CloneVT() + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *SBOMScanResponse) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (this *SBOMScanResponse_SBOMScan) EqualVT(that *SBOMScanResponse_SBOMScan) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if !(*timestamppb1.Timestamp)(this.ScanTime).EqualVT((*timestamppb1.Timestamp)(that.ScanTime)) { + return false + } + if len(this.Components) != len(that.Components) { + return false + } + for i, vx := range this.Components { + vy := that.Components[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &storage.EmbeddedImageScanComponent{} + } + if q == nil { + q = &storage.EmbeddedImageScanComponent{} + } + if equal, ok := interface{}(p).(interface { + EqualVT(*storage.EmbeddedImageScanComponent) bool + }); ok { + if !equal.EqualVT(q) { + return false + } + } else if !proto.Equal(p, q) { + return false + } + } + } + if this.OperatingSystem != that.OperatingSystem { + return false + } + if equal, ok := interface{}(this.DataSource).(interface { + EqualVT(*storage.DataSource) bool + }); ok { + if !equal.EqualVT(that.DataSource) { + return false + } + } else if !proto.Equal(this.DataSource, that.DataSource) { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *SBOMScanResponse_SBOMScan) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*SBOMScanResponse_SBOMScan) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *SBOMScanResponse) EqualVT(that *SBOMScanResponse) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Id != that.Id { + return false + } + if !this.Scan.EqualVT(that.Scan) { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *SBOMScanResponse) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*SBOMScanResponse) + if !ok { + return false + } + return this.EqualVT(that) +} +func (m *SBOMScanResponse_SBOMScan) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *SBOMScanResponse_SBOMScan) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *SBOMScanResponse_SBOMScan) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.DataSource != nil { + if vtmsg, ok := interface{}(m.DataSource).(interface { + MarshalToSizedBufferVT([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.DataSource) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x22 + } + if len(m.OperatingSystem) > 0 { + i -= len(m.OperatingSystem) + copy(dAtA[i:], m.OperatingSystem) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.OperatingSystem))) + i-- + dAtA[i] = 0x1a + } + if len(m.Components) > 0 { + for iNdEx := len(m.Components) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.Components[iNdEx]).(interface { + MarshalToSizedBufferVT([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.Components[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x12 + } + } + if m.ScanTime != nil { + size, err := (*timestamppb1.Timestamp)(m.ScanTime).MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *SBOMScanResponse) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *SBOMScanResponse) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *SBOMScanResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Scan != nil { + size, err := m.Scan.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x12 + } + if len(m.Id) > 0 { + i -= len(m.Id) + copy(dAtA[i:], m.Id) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Id))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *SBOMScanResponse_SBOMScan) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ScanTime != nil { + l = (*timestamppb1.Timestamp)(m.ScanTime).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.Components) > 0 { + for _, e := range m.Components { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + l = len(m.OperatingSystem) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.DataSource != nil { + if size, ok := interface{}(m.DataSource).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.DataSource) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *SBOMScanResponse) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Id) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Scan != nil { + l = m.Scan.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *SBOMScanResponse_SBOMScan) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SBOMScanResponse_SBOMScan: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SBOMScanResponse_SBOMScan: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ScanTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ScanTime == nil { + m.ScanTime = ×tamppb.Timestamp{} + } + if err := (*timestamppb1.Timestamp)(m.ScanTime).UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Components", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Components = append(m.Components, &storage.EmbeddedImageScanComponent{}) + if unmarshal, ok := interface{}(m.Components[len(m.Components)-1]).(interface { + UnmarshalVT([]byte) error + }); ok { + if err := unmarshal.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + } else { + if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.Components[len(m.Components)-1]); err != nil { + return err + } + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field OperatingSystem", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.OperatingSystem = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DataSource", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.DataSource == nil { + m.DataSource = &storage.DataSource{} + } + if unmarshal, ok := interface{}(m.DataSource).(interface { + UnmarshalVT([]byte) error + }); ok { + if err := unmarshal.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + } else { + if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.DataSource); err != nil { + return err + } + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *SBOMScanResponse) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SBOMScanResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SBOMScanResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Id = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Scan", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Scan == nil { + m.Scan = &SBOMScanResponse_SBOMScan{} + } + if err := m.Scan.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *SBOMScanResponse_SBOMScan) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SBOMScanResponse_SBOMScan: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SBOMScanResponse_SBOMScan: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ScanTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ScanTime == nil { + m.ScanTime = ×tamppb.Timestamp{} + } + if err := (*timestamppb1.Timestamp)(m.ScanTime).UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Components", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Components = append(m.Components, &storage.EmbeddedImageScanComponent{}) + if unmarshal, ok := interface{}(m.Components[len(m.Components)-1]).(interface { + UnmarshalVTUnsafe([]byte) error + }); ok { + if err := unmarshal.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { + return err + } + } else { + if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.Components[len(m.Components)-1]); err != nil { + return err + } + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field OperatingSystem", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.OperatingSystem = stringValue + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DataSource", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.DataSource == nil { + m.DataSource = &storage.DataSource{} + } + if unmarshal, ok := interface{}(m.DataSource).(interface { + UnmarshalVTUnsafe([]byte) error + }); ok { + if err := unmarshal.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { + return err + } + } else { + if err := proto.Unmarshal(dAtA[iNdEx:postIndex], m.DataSource); err != nil { + return err + } + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *SBOMScanResponse) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SBOMScanResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SBOMScanResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.Id = stringValue + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Scan", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Scan == nil { + m.Scan = &SBOMScanResponse_SBOMScan{} + } + if err := m.Scan.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/pkg/env/image_scan.go b/pkg/env/image_scan.go index e6d23f355cc88..8b4dc02d52e05 100644 --- a/pkg/env/image_scan.go +++ b/pkg/env/image_scan.go @@ -8,4 +8,7 @@ var ( // SBOMGenerationMaxReqSizeBytes defines the maximum allowed size of an SBOM generation API request. SBOMGenerationMaxReqSizeBytes = RegisterIntegerSetting("ROX_SBOM_GEN_MAX_REQ_SIZE_BYTES", 100*1024) + + // SBOMScanMaxReqSizeBytes defines the maximum allowed size of an SBOM scan API request (100 MB). + SBOMScanMaxReqSizeBytes = RegisterIntegerSetting("ROX_SBOM_SCAN_MAX_REQ_SIZE_BYTES", 100*1024*1024) ) diff --git a/pkg/features/list.go b/pkg/features/list.go index a83570db3b2b1..93a2ce4ba692f 100644 --- a/pkg/features/list.go +++ b/pkg/features/list.go @@ -170,4 +170,7 @@ var ( // ScannerV4StoreExternalIndexReports enables storing index reports from delegated scans to Central's Scanner V4 Indexer. ScannerV4StoreExternalIndexReports = registerFeature("Enables storing index reports from delegated scans to Central's Scanner V4 Indexer", "ROX_SCANNER_V4_STORE_EXTERNAL_INDEX_REPORTS", enabled) + + // SBOMScanning enables matching vulnerabilities to components found in Red Hat produced SBOMs. + SBOMScanning = registerFeature("Enables matching vulnerabilities to components found in Red Hat produced SBOMs", "ROX_SBOM_SCANNING") ) diff --git a/pkg/scanners/scannerv4/scannerv4.go b/pkg/scanners/scannerv4/scannerv4.go index b970e5c0b43f0..547ca93fe23fe 100644 --- a/pkg/scanners/scannerv4/scannerv4.go +++ b/pkg/scanners/scannerv4/scannerv4.go @@ -3,6 +3,7 @@ package scannerv4 import ( "context" "fmt" + "io" "time" "github.com/google/go-containerregistry/pkg/authn" @@ -132,6 +133,74 @@ func (s *scannerv4) GetSBOM(image *storage.Image) ([]byte, bool, error) { return sbom, found, err } +// ScanSBOM scans an SBOM, the contentType (which would include media type, optionally version, etc.) +// will be passed to the scanner to assist in parsing. +func (s *scannerv4) ScanSBOM(sbomReader io.Reader, contentType string) (*v1.SBOMScanResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), scanTimeout) + defer cancel() + + var scannerVersion pkgscanner.Version + + // TODO(ROX-30570): START Remove + // Read all data from the SBOM reader and throw it away (testing purposes only) + dataB, err := io.ReadAll(sbomReader) + if err != nil { + return nil, fmt.Errorf("reading sbom data: %w", err) + } + log.Debugf("Scanning SBOM: %s", dataB) + _ = ctx + // Create a fake vuln report for testing purposes + vr := &v4.VulnerabilityReport{ + HashId: "fake HashId", + Vulnerabilities: map[string]*v4.VulnerabilityReport_Vulnerability{ + "v1": {Name: "Fake Vuln #1"}, + "v2": {Name: "Fake Vuln #2"}, + }, + PackageVulnerabilities: map[string]*v4.StringList{ + "p1": {Values: []string{"v1", "v2"}}, + }, + Contents: &v4.Contents{ + Packages: map[string]*v4.Package{ + "p1": {Name: "Fake Package #1"}, + }, + }, + } + // TODO(ROX-30570): END Remove + + // TODO(ROX-30570): Replace with actual scanner client call + // vr, err := s.scannerClient.ScanSBOM(ctx, sbomReader, contentType, client.Version(&scannerVersion)) + // if err != nil { + // return nil, fmt.Errorf("scanning sbom: %w", err) + // } + + scannerVersionStr, err := scannerVersion.Encode() + if err != nil { + log.Warnf("Failed to encode Scanner version: %v", err) + } + + return &v1.SBOMScanResponse{ + Id: vr.GetHashId(), + Scan: sbomScan(vr, scannerVersionStr), + }, nil +} + +func sbomScan(vr *v4.VulnerabilityReport, scannerVersionStr string) *v1.SBOMScanResponse_SBOMScan { + imageScan := imageScan(nil, vr, scannerVersionStr) + + for _, c := range imageScan.GetComponents() { + for _, v := range c.GetVulns() { + // With SBOMs we will not always know what the component represents. + v.VulnerabilityType = storage.EmbeddedVulnerability_UNKNOWN_VULNERABILITY + } + } + + return &v1.SBOMScanResponse_SBOMScan{ + ScanTime: imageScan.GetScanTime(), + Components: imageScan.GetComponents(), + OperatingSystem: imageScan.GetOperatingSystem(), + } +} + func (s *scannerv4) GetScan(image *storage.Image) (*storage.ImageScan, error) { if image.GetMetadata() == nil { return nil, nil diff --git a/pkg/scanners/types/mocks/types.go b/pkg/scanners/types/mocks/types.go index c10fe4d80ff4b..1df97419db544 100644 --- a/pkg/scanners/types/mocks/types.go +++ b/pkg/scanners/types/mocks/types.go @@ -10,6 +10,7 @@ package mocks import ( + io "io" reflect "reflect" v1 "github.com/stackrox/rox/generated/api/v1" @@ -185,6 +186,21 @@ func (mr *MockSBOMerMockRecorder) GetSBOM(image any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSBOM", reflect.TypeOf((*MockSBOMer)(nil).GetSBOM), image) } +// ScanSBOM mocks base method. +func (m *MockSBOMer) ScanSBOM(reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ScanSBOM", reader, mediatype) + ret0, _ := ret[0].(*v1.SBOMScanResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ScanSBOM indicates an expected call of ScanSBOM. +func (mr *MockSBOMerMockRecorder) ScanSBOM(reader, mediatype any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockSBOMer)(nil).ScanSBOM), reader, mediatype) +} + // MockScannerSBOMer is a mock of ScannerSBOMer interface. type MockScannerSBOMer struct { ctrl *gomock.Controller @@ -297,6 +313,21 @@ func (mr *MockScannerSBOMerMockRecorder) Name() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockScannerSBOMer)(nil).Name)) } +// ScanSBOM mocks base method. +func (m *MockScannerSBOMer) ScanSBOM(reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ScanSBOM", reader, mediatype) + ret0, _ := ret[0].(*v1.SBOMScanResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ScanSBOM indicates an expected call of ScanSBOM. +func (mr *MockScannerSBOMerMockRecorder) ScanSBOM(reader, mediatype any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockScannerSBOMer)(nil).ScanSBOM), reader, mediatype) +} + // Test mocks base method. func (m *MockScannerSBOMer) Test() error { m.ctrl.T.Helper() diff --git a/pkg/scanners/types/types.go b/pkg/scanners/types/types.go index cbf4ea2566eaa..cbcebfd5d78c0 100644 --- a/pkg/scanners/types/types.go +++ b/pkg/scanners/types/types.go @@ -1,6 +1,8 @@ package types import ( + "io" + v1 "github.com/stackrox/rox/generated/api/v1" v4 "github.com/stackrox/rox/generated/internalapi/scanner/v4" "github.com/stackrox/rox/generated/storage" @@ -34,8 +36,11 @@ type Scanner interface { // SBOM is the interface that contains the StackRox SBOM methods type SBOMer interface { - // GetSBOM to get sbom for an image + // GetSBOM to get SBOM for an image. GetSBOM(image *storage.Image) ([]byte, bool, error) + + // ScanSBOM to match vulnerabilities to components found in an SBOM. + ScanSBOM(reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) } // ScannerSBOMer represents a Scanner with SBOM generation capabilities. This diff --git a/proto/api/v1/sbom.proto b/proto/api/v1/sbom.proto new file mode 100644 index 0000000000000..9049b6ec492d7 --- /dev/null +++ b/proto/api/v1/sbom.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package v1; + +import "google/protobuf/timestamp.proto"; +import "storage/image.proto"; + +option go_package = "./api/v1;v1"; +option java_package = "io.stackrox.proto.api.v1"; + +// SBOMScanResponse wraps metadata and scan results for an SBOM. +// The fields are intended to be similar to those from `storage.Node` and `storage.Image` +// so that parsing scans are familiar to users. +// +// There is no service defined for this response as it is handled +// by a custom route (non-gRPC). +// +// next available tag: 3 +message SBOMScanResponse { + string id = 1; + + // next available tag: 5 + message SBOMScan { + google.protobuf.Timestamp scan_time = 1; + repeated storage.EmbeddedImageScanComponent components = 2; + string operating_system = 3; + storage.DataSource data_source = 4; + } + SBOMScan scan = 2; +} From 52d5ef127f00f6d85c9c58db797e360fed26a804 Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:23:38 -0600 Subject: [PATCH 03/12] populate scanner version --- generated/api/v1/sbom.pb.go | 36 ++++++---- generated/api/v1/sbom_vtproto.pb.go | 101 +++++++++++++++++++++++++--- pkg/scanners/scannerv4/scannerv4.go | 2 + proto/api/v1/sbom.proto | 16 +++-- 4 files changed, 126 insertions(+), 29 deletions(-) diff --git a/generated/api/v1/sbom.pb.go b/generated/api/v1/sbom.pb.go index 13f5c284cde97..86ba57f09424a 100644 --- a/generated/api/v1/sbom.pb.go +++ b/generated/api/v1/sbom.pb.go @@ -24,7 +24,8 @@ const ( ) // SBOMScanResponse wraps metadata and scan results for an SBOM. -// The fields are intended to be similar to those from `storage.Node` and `storage.Image` +// +// Fields must be JSON marshal/unmarshal compatible with `storage.Image` // so that parsing scans are familiar to users. // // There is no service defined for this response as it is handled @@ -83,13 +84,14 @@ func (x *SBOMScanResponse) GetScan() *SBOMScanResponse_SBOMScan { return nil } -// next available tag: 5 +// next available tag: 6 type SBOMScanResponse_SBOMScan struct { state protoimpl.MessageState `protogen:"open.v1"` - ScanTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=scan_time,json=scanTime,proto3" json:"scan_time,omitempty"` - Components []*storage.EmbeddedImageScanComponent `protobuf:"bytes,2,rep,name=components,proto3" json:"components,omitempty"` - OperatingSystem string `protobuf:"bytes,3,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` - DataSource *storage.DataSource `protobuf:"bytes,4,opt,name=data_source,json=dataSource,proto3" json:"data_source,omitempty"` + ScannerVersion string `protobuf:"bytes,1,opt,name=scanner_version,json=scannerVersion,proto3" json:"scanner_version,omitempty"` + ScanTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=scan_time,json=scanTime,proto3" json:"scan_time,omitempty"` + Components []*storage.EmbeddedImageScanComponent `protobuf:"bytes,3,rep,name=components,proto3" json:"components,omitempty"` + OperatingSystem string `protobuf:"bytes,4,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` + DataSource *storage.DataSource `protobuf:"bytes,5,opt,name=data_source,json=dataSource,proto3" json:"data_source,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -124,6 +126,13 @@ func (*SBOMScanResponse_SBOMScan) Descriptor() ([]byte, []int) { return file_api_v1_sbom_proto_rawDescGZIP(), []int{0, 0} } +func (x *SBOMScanResponse_SBOMScan) GetScannerVersion() string { + if x != nil { + return x.ScannerVersion + } + return "" +} + func (x *SBOMScanResponse_SBOMScan) GetScanTime() *timestamppb.Timestamp { if x != nil { return x.ScanTime @@ -156,17 +165,18 @@ var File_api_v1_sbom_proto protoreflect.FileDescriptor const file_api_v1_sbom_proto_rawDesc = "" + "\n" + - "\x11api/v1/sbom.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13storage/image.proto\"\xc1\x02\n" + + "\x11api/v1/sbom.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13storage/image.proto\"\xea\x02\n" + "\x10SBOMScanResponse\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x121\n" + - "\x04scan\x18\x02 \x01(\v2\x1d.v1.SBOMScanResponse.SBOMScanR\x04scan\x1a\xe9\x01\n" + - "\bSBOMScan\x127\n" + - "\tscan_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\bscanTime\x12C\n" + + "\x04scan\x18\x02 \x01(\v2\x1d.v1.SBOMScanResponse.SBOMScanR\x04scan\x1a\x92\x02\n" + + "\bSBOMScan\x12'\n" + + "\x0fscanner_version\x18\x01 \x01(\tR\x0escannerVersion\x127\n" + + "\tscan_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bscanTime\x12C\n" + "\n" + - "components\x18\x02 \x03(\v2#.storage.EmbeddedImageScanComponentR\n" + + "components\x18\x03 \x03(\v2#.storage.EmbeddedImageScanComponentR\n" + "components\x12)\n" + - "\x10operating_system\x18\x03 \x01(\tR\x0foperatingSystem\x124\n" + - "\vdata_source\x18\x04 \x01(\v2\x13.storage.DataSourceR\n" + + "\x10operating_system\x18\x04 \x01(\tR\x0foperatingSystem\x124\n" + + "\vdata_source\x18\x05 \x01(\v2\x13.storage.DataSourceR\n" + "dataSourceB'\n" + "\x18io.stackrox.proto.api.v1Z\v./api/v1;v1b\x06proto3" diff --git a/generated/api/v1/sbom_vtproto.pb.go b/generated/api/v1/sbom_vtproto.pb.go index 6ae52a6016e0b..fe0191f484ed4 100644 --- a/generated/api/v1/sbom_vtproto.pb.go +++ b/generated/api/v1/sbom_vtproto.pb.go @@ -28,6 +28,7 @@ func (m *SBOMScanResponse_SBOMScan) CloneVT() *SBOMScanResponse_SBOMScan { return (*SBOMScanResponse_SBOMScan)(nil) } r := new(SBOMScanResponse_SBOMScan) + r.ScannerVersion = m.ScannerVersion r.ScanTime = (*timestamppb.Timestamp)((*timestamppb1.Timestamp)(m.ScanTime).CloneVT()) r.OperatingSystem = m.OperatingSystem if rhs := m.Components; rhs != nil { @@ -85,6 +86,9 @@ func (this *SBOMScanResponse_SBOMScan) EqualVT(that *SBOMScanResponse_SBOMScan) } else if this == nil || that == nil { return false } + if this.ScannerVersion != that.ScannerVersion { + return false + } if !(*timestamppb1.Timestamp)(this.ScanTime).EqualVT((*timestamppb1.Timestamp)(that.ScanTime)) { return false } @@ -205,14 +209,14 @@ func (m *SBOMScanResponse_SBOMScan) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) } i-- - dAtA[i] = 0x22 + dAtA[i] = 0x2a } if len(m.OperatingSystem) > 0 { i -= len(m.OperatingSystem) copy(dAtA[i:], m.OperatingSystem) i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.OperatingSystem))) i-- - dAtA[i] = 0x1a + dAtA[i] = 0x22 } if len(m.Components) > 0 { for iNdEx := len(m.Components) - 1; iNdEx >= 0; iNdEx-- { @@ -235,7 +239,7 @@ func (m *SBOMScanResponse_SBOMScan) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) } i-- - dAtA[i] = 0x12 + dAtA[i] = 0x1a } } if m.ScanTime != nil { @@ -246,6 +250,13 @@ func (m *SBOMScanResponse_SBOMScan) MarshalToSizedBufferVT(dAtA []byte) (int, er i -= size i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- + dAtA[i] = 0x12 + } + if len(m.ScannerVersion) > 0 { + i -= len(m.ScannerVersion) + copy(dAtA[i:], m.ScannerVersion) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.ScannerVersion))) + i-- dAtA[i] = 0xa } return len(dAtA) - i, nil @@ -307,6 +318,10 @@ func (m *SBOMScanResponse_SBOMScan) SizeVT() (n int) { } var l int _ = l + l = len(m.ScannerVersion) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } if m.ScanTime != nil { l = (*timestamppb1.Timestamp)(m.ScanTime).SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) @@ -389,6 +404,38 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVT(dAtA []byte) error { } switch fieldNum { case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ScannerVersion", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ScannerVersion = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ScanTime", wireType) } @@ -424,7 +471,7 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex - case 2: + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Components", wireType) } @@ -466,7 +513,7 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVT(dAtA []byte) error { } } iNdEx = postIndex - case 3: + case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field OperatingSystem", wireType) } @@ -498,7 +545,7 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVT(dAtA []byte) error { } m.OperatingSystem = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 4: + case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DataSource", wireType) } @@ -713,6 +760,42 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVTUnsafe(dAtA []byte) error { } switch fieldNum { case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ScannerVersion", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.ScannerVersion = stringValue + iNdEx = postIndex + case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ScanTime", wireType) } @@ -748,7 +831,7 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVTUnsafe(dAtA []byte) error { return err } iNdEx = postIndex - case 2: + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Components", wireType) } @@ -790,7 +873,7 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVTUnsafe(dAtA []byte) error { } } iNdEx = postIndex - case 3: + case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field OperatingSystem", wireType) } @@ -826,7 +909,7 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVTUnsafe(dAtA []byte) error { } m.OperatingSystem = stringValue iNdEx = postIndex - case 4: + case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DataSource", wireType) } diff --git a/pkg/scanners/scannerv4/scannerv4.go b/pkg/scanners/scannerv4/scannerv4.go index 547ca93fe23fe..af1d118c6f0c8 100644 --- a/pkg/scanners/scannerv4/scannerv4.go +++ b/pkg/scanners/scannerv4/scannerv4.go @@ -140,6 +140,7 @@ func (s *scannerv4) ScanSBOM(sbomReader io.Reader, contentType string) (*v1.SBOM defer cancel() var scannerVersion pkgscanner.Version + scannerVersion.Matcher = "v7" // TODO(ROX-30570): START Remove // Read all data from the SBOM reader and throw it away (testing purposes only) @@ -195,6 +196,7 @@ func sbomScan(vr *v4.VulnerabilityReport, scannerVersionStr string) *v1.SBOMScan } return &v1.SBOMScanResponse_SBOMScan{ + ScannerVersion: imageScan.GetScannerVersion(), ScanTime: imageScan.GetScanTime(), Components: imageScan.GetComponents(), OperatingSystem: imageScan.GetOperatingSystem(), diff --git a/proto/api/v1/sbom.proto b/proto/api/v1/sbom.proto index 9049b6ec492d7..629b09208a32a 100644 --- a/proto/api/v1/sbom.proto +++ b/proto/api/v1/sbom.proto @@ -9,8 +9,9 @@ option go_package = "./api/v1;v1"; option java_package = "io.stackrox.proto.api.v1"; // SBOMScanResponse wraps metadata and scan results for an SBOM. -// The fields are intended to be similar to those from `storage.Node` and `storage.Image` -// so that parsing scans are familiar to users. +// +// Components must be JSON marshal/unmarshall compatible with `storage.Image` +// so that output formatting via roxctl is consistent. // // There is no service defined for this response as it is handled // by a custom route (non-gRPC). @@ -19,12 +20,13 @@ option java_package = "io.stackrox.proto.api.v1"; message SBOMScanResponse { string id = 1; - // next available tag: 5 + // next available tag: 6 message SBOMScan { - google.protobuf.Timestamp scan_time = 1; - repeated storage.EmbeddedImageScanComponent components = 2; - string operating_system = 3; - storage.DataSource data_source = 4; + string scanner_version = 1; + google.protobuf.Timestamp scan_time = 2; + repeated storage.EmbeddedImageScanComponent components = 3; + string operating_system = 4; + storage.DataSource data_source = 5; } SBOMScan scan = 2; } From 44a7ec4d869133871a3c1a29771b124360bcd973 Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:28:52 -0600 Subject: [PATCH 04/12] add more fields to fake vuln report --- pkg/scanners/scannerv4/scannerv4.go | 101 +++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/pkg/scanners/scannerv4/scannerv4.go b/pkg/scanners/scannerv4/scannerv4.go index af1d118c6f0c8..916301605e2b6 100644 --- a/pkg/scanners/scannerv4/scannerv4.go +++ b/pkg/scanners/scannerv4/scannerv4.go @@ -148,24 +148,10 @@ func (s *scannerv4) ScanSBOM(sbomReader io.Reader, contentType string) (*v1.SBOM if err != nil { return nil, fmt.Errorf("reading sbom data: %w", err) } - log.Debugf("Scanning SBOM: %s", dataB) + log.Debugf("Scanned SBOM: %s", dataB) _ = ctx // Create a fake vuln report for testing purposes - vr := &v4.VulnerabilityReport{ - HashId: "fake HashId", - Vulnerabilities: map[string]*v4.VulnerabilityReport_Vulnerability{ - "v1": {Name: "Fake Vuln #1"}, - "v2": {Name: "Fake Vuln #2"}, - }, - PackageVulnerabilities: map[string]*v4.StringList{ - "p1": {Values: []string{"v1", "v2"}}, - }, - Contents: &v4.Contents{ - Packages: map[string]*v4.Package{ - "p1": {Name: "Fake Package #1"}, - }, - }, - } + vr := fakeVulnReport() // TODO(ROX-30570): END Remove // TODO(ROX-30570): Replace with actual scanner client call @@ -185,6 +171,89 @@ func (s *scannerv4) ScanSBOM(sbomReader io.Reader, contentType string) (*v1.SBOM }, nil } +// fakeVulnReport generates a fake vuln report for testing purposes. +// +// TODO(ROX-30570): REMOVE +func fakeVulnReport() *v4.VulnerabilityReport { + return &v4.VulnerabilityReport{ + HashId: "fake HashId", + Vulnerabilities: map[string]*v4.VulnerabilityReport_Vulnerability{ + "v1": { + Name: "Fake Vuln #1", + NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_CRITICAL, + CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ + { + V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ + Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N", + }, + Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, + Url: "https://nvd.nist.gov/vuln/detail/CVE-5678-1234", + }, + }, + }, + "v2": { + Name: "Fake Vuln #2", + NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_IMPORTANT, + CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ + { + V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ + Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N", + }, + Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, + Url: "https://nvd.nist.gov/vuln/detail/CVE-5678-1234", + }, + }, + }, + "v3": { + Name: "Fake Vuln #3", + NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_MODERATE, + + CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ + { + V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ + BaseScore: 8.2, + Vector: "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:H", + }, + Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_RED_HAT, + Url: "https://access.redhat.com/security/cve/CVE-1234-567", + }, + { + V2: &v4.VulnerabilityReport_Vulnerability_CVSS_V2{ + BaseScore: 6.4, + Vector: "AV:N/AC:M/Au:M/C:C/I:N/A:P", + }, + Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, + Url: "https://nvd.nist.gov/vuln/detail/CVE-1234-567", + }, + }, + }, + "v4": { + Name: "Fake Vuln #4", + NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_LOW, + CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ + { + V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ + Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N", + }, + Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, + Url: "https://nvd.nist.gov/vuln/detail/CVE-5678-1234", + }, + }, + }, + }, + PackageVulnerabilities: map[string]*v4.StringList{ + "p1": {Values: []string{"v1", "v2", "v3", "v4"}}, + "p2": {Values: []string{"v3", "v4"}}, + }, + Contents: &v4.Contents{ + Packages: map[string]*v4.Package{ + "p1": {Name: "Fake Package #1", Version: "v1.0.0"}, + "p2": {Name: "Fake Package #2", Version: "v2.3.4"}, + }, + }, + } +} + func sbomScan(vr *v4.VulnerabilityReport, scannerVersionStr string) *v1.SBOMScanResponse_SBOMScan { imageScan := imageScan(nil, vr, scannerVersionStr) From 5b85fff1afac46e6de317cbb16df561ba151595e Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:36:32 -0600 Subject: [PATCH 05/12] remove extra whitespace in sbom proto comment --- generated/api/v1/sbom.pb.go | 4 ++-- proto/api/v1/sbom.proto | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/api/v1/sbom.pb.go b/generated/api/v1/sbom.pb.go index 86ba57f09424a..b4767f80b4b89 100644 --- a/generated/api/v1/sbom.pb.go +++ b/generated/api/v1/sbom.pb.go @@ -25,8 +25,8 @@ const ( // SBOMScanResponse wraps metadata and scan results for an SBOM. // -// Fields must be JSON marshal/unmarshal compatible with `storage.Image` -// so that parsing scans are familiar to users. +// Components must be JSON marshal/unmarshall compatible with `storage.Image` +// so that output formatting via roxctl is consistent. // // There is no service defined for this response as it is handled // by a custom route (non-gRPC). diff --git a/proto/api/v1/sbom.proto b/proto/api/v1/sbom.proto index 629b09208a32a..3ea262abcca57 100644 --- a/proto/api/v1/sbom.proto +++ b/proto/api/v1/sbom.proto @@ -9,7 +9,7 @@ option go_package = "./api/v1;v1"; option java_package = "io.stackrox.proto.api.v1"; // SBOMScanResponse wraps metadata and scan results for an SBOM. -// +// // Components must be JSON marshal/unmarshall compatible with `storage.Image` // so that output formatting via roxctl is consistent. // From 8962c1e8d18894bbf3eb9ae93d6b90935ac96304 Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:44:07 -0600 Subject: [PATCH 06/12] review updates - made 'getScannerV4SBOMIntegration' reusable from SBOM gen and scan - now pass request context down to scannerv4 client - read size exceeds limit from error directly - removed operating system variable from the sbom response type --- central/image/service/http_handler.go | 23 +++-- .../image/service/sbom_scan_http_handler.go | 20 ++--- .../service/sbom_scan_http_handler_test.go | 4 +- generated/api/v1/sbom.pb.go | 33 +++----- generated/api/v1/sbom_vtproto.pb.go | 83 ------------------- pkg/scanners/scannerv4/scannerv4.go | 12 ++- pkg/scanners/types/mocks/types.go | 17 ++-- pkg/scanners/types/types.go | 3 +- proto/api/v1/sbom.proto | 5 +- 9 files changed, 52 insertions(+), 148 deletions(-) diff --git a/central/image/service/http_handler.go b/central/image/service/http_handler.go index e9f91a3d815e1..367a9d50149b7 100644 --- a/central/image/service/http_handler.go +++ b/central/image/service/http_handler.go @@ -24,6 +24,7 @@ import ( "github.com/stackrox/rox/pkg/images/types" "github.com/stackrox/rox/pkg/images/utils" "github.com/stackrox/rox/pkg/logging" + "github.com/stackrox/rox/pkg/scanners" scannerTypes "github.com/stackrox/rox/pkg/scanners/types" "github.com/stackrox/rox/pkg/zip" "google.golang.org/grpc/codes" @@ -269,15 +270,8 @@ func addForceToEnrichmentContext(enrichmentCtx *enricher.EnrichmentContext) { // getScannerV4SBOMIntegration returns the SBOM interface of Scanner V4. func (h sbomGenHttpHandler) getScannerV4SBOMIntegration() (scannerTypes.SBOMer, error) { - scanners := h.integration.ScannerSet() - for _, scanner := range scanners.GetAll() { - if scanner.GetScanner().Type() == scannerTypes.ScannerV4 { - if scannerv4, ok := scanner.GetScanner().(scannerTypes.SBOMer); ok { - return scannerv4, nil - } - } - } - return nil, errors.New("Scanner V4 integration not found") + sbomer, _, err := getScannerV4SBOMIntegration(h.integration.ScannerSet()) + return sbomer, err } // scannedByScannerV4 checks if image is scanned by Scanner V4. @@ -328,3 +322,14 @@ func (h sbomGenHttpHandler) saveImageV2(imgV2 *storage.ImageV2) error { } return nil } + +func getScannerV4SBOMIntegration(scanners scanners.Set) (scannerTypes.SBOMer, *storage.DataSource, error) { + for _, scanner := range scanners.GetAll() { + if scanner.GetScanner().Type() == scannerTypes.ScannerV4 { + if scannerv4, ok := scanner.GetScanner().(scannerTypes.SBOMer); ok { + return scannerv4, scanner.DataSource(), nil + } + } + } + return nil, nil, errors.New("Scanner V4 integration not found") +} diff --git a/central/image/service/sbom_scan_http_handler.go b/central/image/service/sbom_scan_http_handler.go index 3b854e7ac1df4..4402acd0cde3f 100644 --- a/central/image/service/sbom_scan_http_handler.go +++ b/central/image/service/sbom_scan_http_handler.go @@ -50,7 +50,6 @@ func (s sbomScanHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !features.SBOMScanning.Enabled() { httputil.WriteGRPCStyleError(w, codes.Unimplemented, errors.New("SBOM Scanning is disabled.")) return - } // Only POST requests are supported. @@ -83,12 +82,12 @@ func (s sbomScanHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer cancel() body = ioutils.NewContextBoundReader(readCtx, body) - sbomScanResponse, err := s.scanSBOM(body, contentType) + sbomScanResponse, err := s.scanSBOM(readCtx, body, contentType) if err != nil { // Check if error is due to request body exceeding size limit. var maxBytesErr *http.MaxBytesError if errors.As(err, &maxBytesErr) { - httputil.WriteGRPCStyleError(w, codes.InvalidArgument, fmt.Errorf("request body exceeds maximum size of %d bytes", maxReqSizeBytes)) + httputil.WriteGRPCStyleError(w, codes.InvalidArgument, fmt.Errorf("request body exceeds maximum size of %d bytes", maxBytesErr.Limit)) return } httputil.WriteGRPCStyleError(w, codes.Internal, fmt.Errorf("scanning SBOM: %w", err)) @@ -112,7 +111,7 @@ func (s sbomScanHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // scanSBOM will request a scan of the SBOM from Scanner V4. -func (s sbomScanHttpHandler) scanSBOM(limitedReader io.Reader, contentType string) (*v1.SBOMScanResponse, error) { +func (s sbomScanHttpHandler) scanSBOM(ctx context.Context, limitedReader io.Reader, contentType string) (*v1.SBOMScanResponse, error) { // Get reference to Scanner V4. scannerV4, dataSource, err := s.getScannerV4Integration() if err != nil { @@ -120,7 +119,7 @@ func (s sbomScanHttpHandler) scanSBOM(limitedReader io.Reader, contentType strin } // Scan the SBOM. - sbomScanResponse, err := scannerV4.ScanSBOM(limitedReader, contentType) + sbomScanResponse, err := scannerV4.ScanSBOM(ctx, limitedReader, contentType) if err != nil { return nil, fmt.Errorf("scanning sbom: %w", err) } @@ -134,15 +133,8 @@ func (s sbomScanHttpHandler) scanSBOM(limitedReader io.Reader, contentType strin // getScannerV4Integration returns the SBOM interface of Scanner V4. func (s sbomScanHttpHandler) getScannerV4Integration() (scannerTypes.SBOMer, *storage.DataSource, error) { - scanners := s.integrations.ScannerSet() - for _, scanner := range scanners.GetAll() { - if scanner.GetScanner().Type() == scannerTypes.ScannerV4 { - if scannerv4, ok := scanner.GetScanner().(scannerTypes.SBOMer); ok { - return scannerv4, scanner.DataSource(), nil - } - } - } - return nil, nil, errors.New("Scanner V4 integration not found") + sbomer, dataSource, err := getScannerV4SBOMIntegration(s.integrations.ScannerSet()) + return sbomer, dataSource, err } // validateMediaType validates the media type from the content type header is supported. diff --git a/central/image/service/sbom_scan_http_handler_test.go b/central/image/service/sbom_scan_http_handler_test.go index f168a6c615c59..f895204ff9a36 100644 --- a/central/image/service/sbom_scan_http_handler_test.go +++ b/central/image/service/sbom_scan_http_handler_test.go @@ -106,7 +106,7 @@ func TestScanSBOMHttpHandler_ServeHTTP(t *testing.T) { ctrl := gomock.NewController(t) mockSBOMScanner := scannerTypesMocks.NewMockScannerSBOMer(ctrl) - mockSBOMScanner.EXPECT().ScanSBOM(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")) + mockSBOMScanner.EXPECT().ScanSBOM(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")) mockSBOMScanner.EXPECT().Type().Return(scannerTypes.ScannerV4) mockImageScannerWithDS := scannerTypesMocks.NewMockImageScannerWithDataSource(ctrl) @@ -142,7 +142,7 @@ func TestScanSBOMHttpHandler_ServeHTTP(t *testing.T) { ctrl := gomock.NewController(t) mockSBOMScanner := scannerTypesMocks.NewMockScannerSBOMer(ctrl) - mockSBOMScanner.EXPECT().ScanSBOM(gomock.Any(), gomock.Any()).Return(&v1.SBOMScanResponse{Id: "fake-sbom-id"}, nil) + mockSBOMScanner.EXPECT().ScanSBOM(gomock.Any(), gomock.Any(), gomock.Any()).Return(&v1.SBOMScanResponse{Id: "fake-sbom-id"}, nil) mockSBOMScanner.EXPECT().Type().Return(scannerTypes.ScannerV4) mockImageScannerWithDS := scannerTypesMocks.NewMockImageScannerWithDataSource(ctrl) diff --git a/generated/api/v1/sbom.pb.go b/generated/api/v1/sbom.pb.go index b4767f80b4b89..5857f099a3d19 100644 --- a/generated/api/v1/sbom.pb.go +++ b/generated/api/v1/sbom.pb.go @@ -84,16 +84,15 @@ func (x *SBOMScanResponse) GetScan() *SBOMScanResponse_SBOMScan { return nil } -// next available tag: 6 +// next available tag: 5 type SBOMScanResponse_SBOMScan struct { - state protoimpl.MessageState `protogen:"open.v1"` - ScannerVersion string `protobuf:"bytes,1,opt,name=scanner_version,json=scannerVersion,proto3" json:"scanner_version,omitempty"` - ScanTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=scan_time,json=scanTime,proto3" json:"scan_time,omitempty"` - Components []*storage.EmbeddedImageScanComponent `protobuf:"bytes,3,rep,name=components,proto3" json:"components,omitempty"` - OperatingSystem string `protobuf:"bytes,4,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` - DataSource *storage.DataSource `protobuf:"bytes,5,opt,name=data_source,json=dataSource,proto3" json:"data_source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ScannerVersion string `protobuf:"bytes,1,opt,name=scanner_version,json=scannerVersion,proto3" json:"scanner_version,omitempty"` + ScanTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=scan_time,json=scanTime,proto3" json:"scan_time,omitempty"` + Components []*storage.EmbeddedImageScanComponent `protobuf:"bytes,3,rep,name=components,proto3" json:"components,omitempty"` + DataSource *storage.DataSource `protobuf:"bytes,4,opt,name=data_source,json=dataSource,proto3" json:"data_source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SBOMScanResponse_SBOMScan) Reset() { @@ -147,13 +146,6 @@ func (x *SBOMScanResponse_SBOMScan) GetComponents() []*storage.EmbeddedImageScan return nil } -func (x *SBOMScanResponse_SBOMScan) GetOperatingSystem() string { - if x != nil { - return x.OperatingSystem - } - return "" -} - func (x *SBOMScanResponse_SBOMScan) GetDataSource() *storage.DataSource { if x != nil { return x.DataSource @@ -165,18 +157,17 @@ var File_api_v1_sbom_proto protoreflect.FileDescriptor const file_api_v1_sbom_proto_rawDesc = "" + "\n" + - "\x11api/v1/sbom.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13storage/image.proto\"\xea\x02\n" + + "\x11api/v1/sbom.proto\x12\x02v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13storage/image.proto\"\xbf\x02\n" + "\x10SBOMScanResponse\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x121\n" + - "\x04scan\x18\x02 \x01(\v2\x1d.v1.SBOMScanResponse.SBOMScanR\x04scan\x1a\x92\x02\n" + + "\x04scan\x18\x02 \x01(\v2\x1d.v1.SBOMScanResponse.SBOMScanR\x04scan\x1a\xe7\x01\n" + "\bSBOMScan\x12'\n" + "\x0fscanner_version\x18\x01 \x01(\tR\x0escannerVersion\x127\n" + "\tscan_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bscanTime\x12C\n" + "\n" + "components\x18\x03 \x03(\v2#.storage.EmbeddedImageScanComponentR\n" + - "components\x12)\n" + - "\x10operating_system\x18\x04 \x01(\tR\x0foperatingSystem\x124\n" + - "\vdata_source\x18\x05 \x01(\v2\x13.storage.DataSourceR\n" + + "components\x124\n" + + "\vdata_source\x18\x04 \x01(\v2\x13.storage.DataSourceR\n" + "dataSourceB'\n" + "\x18io.stackrox.proto.api.v1Z\v./api/v1;v1b\x06proto3" diff --git a/generated/api/v1/sbom_vtproto.pb.go b/generated/api/v1/sbom_vtproto.pb.go index fe0191f484ed4..e09cbbdab5119 100644 --- a/generated/api/v1/sbom_vtproto.pb.go +++ b/generated/api/v1/sbom_vtproto.pb.go @@ -30,7 +30,6 @@ func (m *SBOMScanResponse_SBOMScan) CloneVT() *SBOMScanResponse_SBOMScan { r := new(SBOMScanResponse_SBOMScan) r.ScannerVersion = m.ScannerVersion r.ScanTime = (*timestamppb.Timestamp)((*timestamppb1.Timestamp)(m.ScanTime).CloneVT()) - r.OperatingSystem = m.OperatingSystem if rhs := m.Components; rhs != nil { tmpContainer := make([]*storage.EmbeddedImageScanComponent, len(rhs)) for k, v := range rhs { @@ -115,9 +114,6 @@ func (this *SBOMScanResponse_SBOMScan) EqualVT(that *SBOMScanResponse_SBOMScan) } } } - if this.OperatingSystem != that.OperatingSystem { - return false - } if equal, ok := interface{}(this.DataSource).(interface { EqualVT(*storage.DataSource) bool }); ok { @@ -209,13 +205,6 @@ func (m *SBOMScanResponse_SBOMScan) MarshalToSizedBufferVT(dAtA []byte) (int, er i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) } i-- - dAtA[i] = 0x2a - } - if len(m.OperatingSystem) > 0 { - i -= len(m.OperatingSystem) - copy(dAtA[i:], m.OperatingSystem) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.OperatingSystem))) - i-- dAtA[i] = 0x22 } if len(m.Components) > 0 { @@ -338,10 +327,6 @@ func (m *SBOMScanResponse_SBOMScan) SizeVT() (n int) { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } } - l = len(m.OperatingSystem) - if l > 0 { - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } if m.DataSource != nil { if size, ok := interface{}(m.DataSource).(interface { SizeVT() int @@ -514,38 +499,6 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVT(dAtA []byte) error { } iNdEx = postIndex case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field OperatingSystem", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.OperatingSystem = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DataSource", wireType) } @@ -874,42 +827,6 @@ func (m *SBOMScanResponse_SBOMScan) UnmarshalVTUnsafe(dAtA []byte) error { } iNdEx = postIndex case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field OperatingSystem", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - var stringValue string - if intStringLen > 0 { - stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) - } - m.OperatingSystem = stringValue - iNdEx = postIndex - case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field DataSource", wireType) } diff --git a/pkg/scanners/scannerv4/scannerv4.go b/pkg/scanners/scannerv4/scannerv4.go index 916301605e2b6..f0bd00bcb82bb 100644 --- a/pkg/scanners/scannerv4/scannerv4.go +++ b/pkg/scanners/scannerv4/scannerv4.go @@ -135,8 +135,8 @@ func (s *scannerv4) GetSBOM(image *storage.Image) ([]byte, bool, error) { // ScanSBOM scans an SBOM, the contentType (which would include media type, optionally version, etc.) // will be passed to the scanner to assist in parsing. -func (s *scannerv4) ScanSBOM(sbomReader io.Reader, contentType string) (*v1.SBOMScanResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), scanTimeout) +func (s *scannerv4) ScanSBOM(ctx context.Context, sbomReader io.Reader, contentType string) (*v1.SBOMScanResponse, error) { + ctx, cancel := context.WithTimeout(ctx, scanTimeout) defer cancel() var scannerVersion pkgscanner.Version @@ -149,7 +149,6 @@ func (s *scannerv4) ScanSBOM(sbomReader io.Reader, contentType string) (*v1.SBOM return nil, fmt.Errorf("reading sbom data: %w", err) } log.Debugf("Scanned SBOM: %s", dataB) - _ = ctx // Create a fake vuln report for testing purposes vr := fakeVulnReport() // TODO(ROX-30570): END Remove @@ -265,10 +264,9 @@ func sbomScan(vr *v4.VulnerabilityReport, scannerVersionStr string) *v1.SBOMScan } return &v1.SBOMScanResponse_SBOMScan{ - ScannerVersion: imageScan.GetScannerVersion(), - ScanTime: imageScan.GetScanTime(), - Components: imageScan.GetComponents(), - OperatingSystem: imageScan.GetOperatingSystem(), + ScannerVersion: imageScan.GetScannerVersion(), + ScanTime: imageScan.GetScanTime(), + Components: imageScan.GetComponents(), } } diff --git a/pkg/scanners/types/mocks/types.go b/pkg/scanners/types/mocks/types.go index 1df97419db544..96bf2d432c494 100644 --- a/pkg/scanners/types/mocks/types.go +++ b/pkg/scanners/types/mocks/types.go @@ -10,6 +10,7 @@ package mocks import ( + context "context" io "io" reflect "reflect" @@ -187,18 +188,18 @@ func (mr *MockSBOMerMockRecorder) GetSBOM(image any) *gomock.Call { } // ScanSBOM mocks base method. -func (m *MockSBOMer) ScanSBOM(reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) { +func (m *MockSBOMer) ScanSBOM(ctx context.Context, reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ScanSBOM", reader, mediatype) + ret := m.ctrl.Call(m, "ScanSBOM", ctx, reader, mediatype) ret0, _ := ret[0].(*v1.SBOMScanResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ScanSBOM indicates an expected call of ScanSBOM. -func (mr *MockSBOMerMockRecorder) ScanSBOM(reader, mediatype any) *gomock.Call { +func (mr *MockSBOMerMockRecorder) ScanSBOM(ctx, reader, mediatype any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockSBOMer)(nil).ScanSBOM), reader, mediatype) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockSBOMer)(nil).ScanSBOM), ctx, reader, mediatype) } // MockScannerSBOMer is a mock of ScannerSBOMer interface. @@ -314,18 +315,18 @@ func (mr *MockScannerSBOMerMockRecorder) Name() *gomock.Call { } // ScanSBOM mocks base method. -func (m *MockScannerSBOMer) ScanSBOM(reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) { +func (m *MockScannerSBOMer) ScanSBOM(ctx context.Context, reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ScanSBOM", reader, mediatype) + ret := m.ctrl.Call(m, "ScanSBOM", ctx, reader, mediatype) ret0, _ := ret[0].(*v1.SBOMScanResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ScanSBOM indicates an expected call of ScanSBOM. -func (mr *MockScannerSBOMerMockRecorder) ScanSBOM(reader, mediatype any) *gomock.Call { +func (mr *MockScannerSBOMerMockRecorder) ScanSBOM(ctx, reader, mediatype any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockScannerSBOMer)(nil).ScanSBOM), reader, mediatype) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockScannerSBOMer)(nil).ScanSBOM), ctx, reader, mediatype) } // Test mocks base method. diff --git a/pkg/scanners/types/types.go b/pkg/scanners/types/types.go index cbcebfd5d78c0..a023445d0f8e5 100644 --- a/pkg/scanners/types/types.go +++ b/pkg/scanners/types/types.go @@ -1,6 +1,7 @@ package types import ( + "context" "io" v1 "github.com/stackrox/rox/generated/api/v1" @@ -40,7 +41,7 @@ type SBOMer interface { GetSBOM(image *storage.Image) ([]byte, bool, error) // ScanSBOM to match vulnerabilities to components found in an SBOM. - ScanSBOM(reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) + ScanSBOM(ctx context.Context, reader io.Reader, mediatype string) (*v1.SBOMScanResponse, error) } // ScannerSBOMer represents a Scanner with SBOM generation capabilities. This diff --git a/proto/api/v1/sbom.proto b/proto/api/v1/sbom.proto index 3ea262abcca57..70d833612db49 100644 --- a/proto/api/v1/sbom.proto +++ b/proto/api/v1/sbom.proto @@ -20,13 +20,12 @@ option java_package = "io.stackrox.proto.api.v1"; message SBOMScanResponse { string id = 1; - // next available tag: 6 + // next available tag: 5 message SBOMScan { string scanner_version = 1; google.protobuf.Timestamp scan_time = 2; repeated storage.EmbeddedImageScanComponent components = 3; - string operating_system = 4; - storage.DataSource data_source = 5; + storage.DataSource data_source = 4; } SBOMScan scan = 2; } From 01e65a6778d3226f34e8fad3e2de8fcf6d9725f4 Mon Sep 17 00:00:00 2001 From: David Caravello <119438707+dcaravel@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:23:58 -0600 Subject: [PATCH 07/12] fix style and unit test failures --- central/image/service/http_handler_test.go | 2 ++ pkg/scanners/scannerv4/scannerv4.go | 1 + 2 files changed, 3 insertions(+) diff --git a/central/image/service/http_handler_test.go b/central/image/service/http_handler_test.go index 7f0caf4d2c12f..76ebabcdc1ba0 100644 --- a/central/image/service/http_handler_test.go +++ b/central/image/service/http_handler_test.go @@ -142,6 +142,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { scanner.EXPECT().GetSBOM(gomock.Any()).DoAndReturn(getFakeSBOM).AnyTimes() set.EXPECT().ScannerSet().Return(scannerSet).AnyTimes() fsr.EXPECT().GetScanner().Return(scanner).AnyTimes() + fsr.EXPECT().DataSource().Return(nil).AnyTimes() scannerSet.EXPECT().GetAll().Return([]scannerTypes.ImageScannerWithDataSource{fsr}).AnyTimes() reqBody := &apiparams.SBOMRequestBody{ @@ -280,6 +281,7 @@ func TestHttpHandler_ServeHTTP(t *testing.T) { mockImageScannerWithDS := scannerTypesMocks.NewMockImageScannerWithDataSource(ctrl) mockImageScannerWithDS.EXPECT().GetScanner().Return(mockScanner).AnyTimes() + mockImageScannerWithDS.EXPECT().DataSource().Return(nil).AnyTimes() mockScannerSet := scannerMocks.NewMockSet(ctrl) mockScannerSet.EXPECT().GetAll().Return([]scannerTypes.ImageScannerWithDataSource{mockImageScannerWithDS}).AnyTimes() diff --git a/pkg/scanners/scannerv4/scannerv4.go b/pkg/scanners/scannerv4/scannerv4.go index f0bd00bcb82bb..7e8cdc12c8880 100644 --- a/pkg/scanners/scannerv4/scannerv4.go +++ b/pkg/scanners/scannerv4/scannerv4.go @@ -144,6 +144,7 @@ func (s *scannerv4) ScanSBOM(ctx context.Context, sbomReader io.Reader, contentT // TODO(ROX-30570): START Remove // Read all data from the SBOM reader and throw it away (testing purposes only) + _ = ctx dataB, err := io.ReadAll(sbomReader) if err != nil { return nil, fmt.Errorf("reading sbom data: %w", err) From 9bff2aaa4957d4ed4d37fe99ada579302e198ff5 Mon Sep 17 00:00:00 2001 From: Brad Lugo Date: Sun, 25 Jan 2026 23:52:10 -0800 Subject: [PATCH 08/12] ROX-32846: Add repository-to-CPE mapping API to indexer Adds a new RPC, GetRepositoryToCPEMapping, to the Indexer service that returns the Red Hat repository-to-CPE mapping used for RHEL package vulnerability matching. This mapping will be needed by the matcher's ScanSBOM API to enrich RHEL packages with CPE information during SBOM vulnerability scanning. --- .../scanner/v4/indexer_service.pb.go | 206 +++- .../scanner/v4/indexer_service_grpc.pb.go | 50 +- .../scanner/v4/indexer_service_vtproto.pb.go | 1012 ++++++++++++++++- pkg/scannerv4/client/client.go | 32 + pkg/scannerv4/client/mocks/client.go | 16 + pkg/scannerv4/repositorytocpe/mappingfile.go | 29 + .../scanner/v4/indexer_service.proto | 14 + scanner/indexer/indexer.go | 27 + scanner/indexer/mocks/indexer.go | 16 + scanner/indexer/remote.go | 9 + scanner/indexer/repositorytocpeupdater.go | 86 ++ scanner/internal/httputil/updater.go | 105 ++ scanner/services/indexer.go | 24 + 13 files changed, 1559 insertions(+), 67 deletions(-) create mode 100644 pkg/scannerv4/repositorytocpe/mappingfile.go create mode 100644 scanner/indexer/repositorytocpeupdater.go create mode 100644 scanner/internal/httputil/updater.go diff --git a/generated/internalapi/scanner/v4/indexer_service.pb.go b/generated/internalapi/scanner/v4/indexer_service.pb.go index 6f6d399a3467c..602037144b185 100644 --- a/generated/internalapi/scanner/v4/indexer_service.pb.go +++ b/generated/internalapi/scanner/v4/indexer_service.pb.go @@ -487,6 +487,131 @@ func (x *StoreIndexReportResponse) GetStatus() string { return "" } +type GetRepositoryToCPEMappingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRepositoryToCPEMappingRequest) Reset() { + *x = GetRepositoryToCPEMappingRequest{} + mi := &file_internalapi_scanner_v4_indexer_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRepositoryToCPEMappingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRepositoryToCPEMappingRequest) ProtoMessage() {} + +func (x *GetRepositoryToCPEMappingRequest) ProtoReflect() protoreflect.Message { + mi := &file_internalapi_scanner_v4_indexer_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRepositoryToCPEMappingRequest.ProtoReflect.Descriptor instead. +func (*GetRepositoryToCPEMappingRequest) Descriptor() ([]byte, []int) { + return file_internalapi_scanner_v4_indexer_service_proto_rawDescGZIP(), []int{8} +} + +type RepositoryCPEInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cpes []string `protobuf:"bytes,1,rep,name=cpes,proto3" json:"cpes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RepositoryCPEInfo) Reset() { + *x = RepositoryCPEInfo{} + mi := &file_internalapi_scanner_v4_indexer_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RepositoryCPEInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryCPEInfo) ProtoMessage() {} + +func (x *RepositoryCPEInfo) ProtoReflect() protoreflect.Message { + mi := &file_internalapi_scanner_v4_indexer_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryCPEInfo.ProtoReflect.Descriptor instead. +func (*RepositoryCPEInfo) Descriptor() ([]byte, []int) { + return file_internalapi_scanner_v4_indexer_service_proto_rawDescGZIP(), []int{9} +} + +func (x *RepositoryCPEInfo) GetCpes() []string { + if x != nil { + return x.Cpes + } + return nil +} + +type GetRepositoryToCPEMappingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Maps repository names to their CPE information. + Mapping map[string]*RepositoryCPEInfo `protobuf:"bytes,1,rep,name=mapping,proto3" json:"mapping,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRepositoryToCPEMappingResponse) Reset() { + *x = GetRepositoryToCPEMappingResponse{} + mi := &file_internalapi_scanner_v4_indexer_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRepositoryToCPEMappingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRepositoryToCPEMappingResponse) ProtoMessage() {} + +func (x *GetRepositoryToCPEMappingResponse) ProtoReflect() protoreflect.Message { + mi := &file_internalapi_scanner_v4_indexer_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRepositoryToCPEMappingResponse.ProtoReflect.Descriptor instead. +func (*GetRepositoryToCPEMappingResponse) Descriptor() ([]byte, []int) { + return file_internalapi_scanner_v4_indexer_service_proto_rawDescGZIP(), []int{10} +} + +func (x *GetRepositoryToCPEMappingResponse) GetMapping() map[string]*RepositoryCPEInfo { + if x != nil { + return x.Mapping + } + return nil +} + var File_internalapi_scanner_v4_indexer_service_proto protoreflect.FileDescriptor const file_internalapi_scanner_v4_indexer_service_proto_rawDesc = "" + @@ -518,13 +643,22 @@ const file_internalapi_scanner_v4_indexer_service_proto_rawDesc = "" + "\x0findexer_version\x18\x02 \x01(\tR\x0eindexerVersion\x120\n" + "\bcontents\x18\x03 \x01(\v2\x14.scanner.v4.ContentsR\bcontents\"2\n" + "\x18StoreIndexReportResponse\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status2\xc1\x03\n" + + "\x06status\x18\x01 \x01(\tR\x06status\"\"\n" + + " GetRepositoryToCPEMappingRequest\"'\n" + + "\x11RepositoryCPEInfo\x12\x12\n" + + "\x04cpes\x18\x01 \x03(\tR\x04cpes\"\xd4\x01\n" + + "!GetRepositoryToCPEMappingResponse\x12T\n" + + "\amapping\x18\x01 \x03(\v2:.scanner.v4.GetRepositoryToCPEMappingResponse.MappingEntryR\amapping\x1aY\n" + + "\fMappingEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x123\n" + + "\x05value\x18\x02 \x01(\v2\x1d.scanner.v4.RepositoryCPEInfoR\x05value:\x028\x012\xbb\x04\n" + "\aIndexer\x12R\n" + "\x11CreateIndexReport\x12$.scanner.v4.CreateIndexReportRequest\x1a\x17.scanner.v4.IndexReport\x12L\n" + "\x0eGetIndexReport\x12!.scanner.v4.GetIndexReportRequest\x1a\x17.scanner.v4.IndexReport\x12\\\n" + "\x16GetOrCreateIndexReport\x12).scanner.v4.GetOrCreateIndexReportRequest\x1a\x17.scanner.v4.IndexReport\x12W\n" + "\x0eHasIndexReport\x12!.scanner.v4.HasIndexReportRequest\x1a\".scanner.v4.HasIndexReportResponse\x12]\n" + - "\x10StoreIndexReport\x12#.scanner.v4.StoreIndexReportRequest\x1a$.scanner.v4.StoreIndexReportResponseB\x1dZ\x1b./internalapi/scanner/v4;v4b\x06proto3" + "\x10StoreIndexReport\x12#.scanner.v4.StoreIndexReportRequest\x1a$.scanner.v4.StoreIndexReportResponse\x12x\n" + + "\x19GetRepositoryToCPEMapping\x12,.scanner.v4.GetRepositoryToCPEMappingRequest\x1a-.scanner.v4.GetRepositoryToCPEMappingResponseB\x1dZ\x1b./internalapi/scanner/v4;v4b\x06proto3" var ( file_internalapi_scanner_v4_indexer_service_proto_rawDescOnce sync.Once @@ -538,38 +672,46 @@ func file_internalapi_scanner_v4_indexer_service_proto_rawDescGZIP() []byte { return file_internalapi_scanner_v4_indexer_service_proto_rawDescData } -var file_internalapi_scanner_v4_indexer_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_internalapi_scanner_v4_indexer_service_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_internalapi_scanner_v4_indexer_service_proto_goTypes = []any{ - (*ContainerImageLocator)(nil), // 0: scanner.v4.ContainerImageLocator - (*CreateIndexReportRequest)(nil), // 1: scanner.v4.CreateIndexReportRequest - (*GetIndexReportRequest)(nil), // 2: scanner.v4.GetIndexReportRequest - (*HasIndexReportRequest)(nil), // 3: scanner.v4.HasIndexReportRequest - (*HasIndexReportResponse)(nil), // 4: scanner.v4.HasIndexReportResponse - (*GetOrCreateIndexReportRequest)(nil), // 5: scanner.v4.GetOrCreateIndexReportRequest - (*StoreIndexReportRequest)(nil), // 6: scanner.v4.StoreIndexReportRequest - (*StoreIndexReportResponse)(nil), // 7: scanner.v4.StoreIndexReportResponse - (*Contents)(nil), // 8: scanner.v4.Contents - (*IndexReport)(nil), // 9: scanner.v4.IndexReport + (*ContainerImageLocator)(nil), // 0: scanner.v4.ContainerImageLocator + (*CreateIndexReportRequest)(nil), // 1: scanner.v4.CreateIndexReportRequest + (*GetIndexReportRequest)(nil), // 2: scanner.v4.GetIndexReportRequest + (*HasIndexReportRequest)(nil), // 3: scanner.v4.HasIndexReportRequest + (*HasIndexReportResponse)(nil), // 4: scanner.v4.HasIndexReportResponse + (*GetOrCreateIndexReportRequest)(nil), // 5: scanner.v4.GetOrCreateIndexReportRequest + (*StoreIndexReportRequest)(nil), // 6: scanner.v4.StoreIndexReportRequest + (*StoreIndexReportResponse)(nil), // 7: scanner.v4.StoreIndexReportResponse + (*GetRepositoryToCPEMappingRequest)(nil), // 8: scanner.v4.GetRepositoryToCPEMappingRequest + (*RepositoryCPEInfo)(nil), // 9: scanner.v4.RepositoryCPEInfo + (*GetRepositoryToCPEMappingResponse)(nil), // 10: scanner.v4.GetRepositoryToCPEMappingResponse + nil, // 11: scanner.v4.GetRepositoryToCPEMappingResponse.MappingEntry + (*Contents)(nil), // 12: scanner.v4.Contents + (*IndexReport)(nil), // 13: scanner.v4.IndexReport } var file_internalapi_scanner_v4_indexer_service_proto_depIdxs = []int32{ - 0, // 0: scanner.v4.CreateIndexReportRequest.container_image:type_name -> scanner.v4.ContainerImageLocator - 0, // 1: scanner.v4.GetOrCreateIndexReportRequest.container_image:type_name -> scanner.v4.ContainerImageLocator - 8, // 2: scanner.v4.StoreIndexReportRequest.contents:type_name -> scanner.v4.Contents - 1, // 3: scanner.v4.Indexer.CreateIndexReport:input_type -> scanner.v4.CreateIndexReportRequest - 2, // 4: scanner.v4.Indexer.GetIndexReport:input_type -> scanner.v4.GetIndexReportRequest - 5, // 5: scanner.v4.Indexer.GetOrCreateIndexReport:input_type -> scanner.v4.GetOrCreateIndexReportRequest - 3, // 6: scanner.v4.Indexer.HasIndexReport:input_type -> scanner.v4.HasIndexReportRequest - 6, // 7: scanner.v4.Indexer.StoreIndexReport:input_type -> scanner.v4.StoreIndexReportRequest - 9, // 8: scanner.v4.Indexer.CreateIndexReport:output_type -> scanner.v4.IndexReport - 9, // 9: scanner.v4.Indexer.GetIndexReport:output_type -> scanner.v4.IndexReport - 9, // 10: scanner.v4.Indexer.GetOrCreateIndexReport:output_type -> scanner.v4.IndexReport - 4, // 11: scanner.v4.Indexer.HasIndexReport:output_type -> scanner.v4.HasIndexReportResponse - 7, // 12: scanner.v4.Indexer.StoreIndexReport:output_type -> scanner.v4.StoreIndexReportResponse - 8, // [8:13] is the sub-list for method output_type - 3, // [3:8] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 0, // 0: scanner.v4.CreateIndexReportRequest.container_image:type_name -> scanner.v4.ContainerImageLocator + 0, // 1: scanner.v4.GetOrCreateIndexReportRequest.container_image:type_name -> scanner.v4.ContainerImageLocator + 12, // 2: scanner.v4.StoreIndexReportRequest.contents:type_name -> scanner.v4.Contents + 11, // 3: scanner.v4.GetRepositoryToCPEMappingResponse.mapping:type_name -> scanner.v4.GetRepositoryToCPEMappingResponse.MappingEntry + 9, // 4: scanner.v4.GetRepositoryToCPEMappingResponse.MappingEntry.value:type_name -> scanner.v4.RepositoryCPEInfo + 1, // 5: scanner.v4.Indexer.CreateIndexReport:input_type -> scanner.v4.CreateIndexReportRequest + 2, // 6: scanner.v4.Indexer.GetIndexReport:input_type -> scanner.v4.GetIndexReportRequest + 5, // 7: scanner.v4.Indexer.GetOrCreateIndexReport:input_type -> scanner.v4.GetOrCreateIndexReportRequest + 3, // 8: scanner.v4.Indexer.HasIndexReport:input_type -> scanner.v4.HasIndexReportRequest + 6, // 9: scanner.v4.Indexer.StoreIndexReport:input_type -> scanner.v4.StoreIndexReportRequest + 8, // 10: scanner.v4.Indexer.GetRepositoryToCPEMapping:input_type -> scanner.v4.GetRepositoryToCPEMappingRequest + 13, // 11: scanner.v4.Indexer.CreateIndexReport:output_type -> scanner.v4.IndexReport + 13, // 12: scanner.v4.Indexer.GetIndexReport:output_type -> scanner.v4.IndexReport + 13, // 13: scanner.v4.Indexer.GetOrCreateIndexReport:output_type -> scanner.v4.IndexReport + 4, // 14: scanner.v4.Indexer.HasIndexReport:output_type -> scanner.v4.HasIndexReportResponse + 7, // 15: scanner.v4.Indexer.StoreIndexReport:output_type -> scanner.v4.StoreIndexReportResponse + 10, // 16: scanner.v4.Indexer.GetRepositoryToCPEMapping:output_type -> scanner.v4.GetRepositoryToCPEMappingResponse + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_internalapi_scanner_v4_indexer_service_proto_init() } @@ -591,7 +733,7 @@ func file_internalapi_scanner_v4_indexer_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_internalapi_scanner_v4_indexer_service_proto_rawDesc), len(file_internalapi_scanner_v4_indexer_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/generated/internalapi/scanner/v4/indexer_service_grpc.pb.go b/generated/internalapi/scanner/v4/indexer_service_grpc.pb.go index 0ef641a53720a..639141f18b4aa 100644 --- a/generated/internalapi/scanner/v4/indexer_service_grpc.pb.go +++ b/generated/internalapi/scanner/v4/indexer_service_grpc.pb.go @@ -23,11 +23,12 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Indexer_CreateIndexReport_FullMethodName = "/scanner.v4.Indexer/CreateIndexReport" - Indexer_GetIndexReport_FullMethodName = "/scanner.v4.Indexer/GetIndexReport" - Indexer_GetOrCreateIndexReport_FullMethodName = "/scanner.v4.Indexer/GetOrCreateIndexReport" - Indexer_HasIndexReport_FullMethodName = "/scanner.v4.Indexer/HasIndexReport" - Indexer_StoreIndexReport_FullMethodName = "/scanner.v4.Indexer/StoreIndexReport" + Indexer_CreateIndexReport_FullMethodName = "/scanner.v4.Indexer/CreateIndexReport" + Indexer_GetIndexReport_FullMethodName = "/scanner.v4.Indexer/GetIndexReport" + Indexer_GetOrCreateIndexReport_FullMethodName = "/scanner.v4.Indexer/GetOrCreateIndexReport" + Indexer_HasIndexReport_FullMethodName = "/scanner.v4.Indexer/HasIndexReport" + Indexer_StoreIndexReport_FullMethodName = "/scanner.v4.Indexer/StoreIndexReport" + Indexer_GetRepositoryToCPEMapping_FullMethodName = "/scanner.v4.Indexer/GetRepositoryToCPEMapping" ) // IndexerClient is the client API for Indexer service. @@ -48,6 +49,8 @@ type IndexerClient interface { HasIndexReport(ctx context.Context, in *HasIndexReportRequest, opts ...grpc.CallOption) (*HasIndexReportResponse, error) // StoreIndexReport stores an external index report to the datastore. StoreIndexReport(ctx context.Context, in *StoreIndexReportRequest, opts ...grpc.CallOption) (*StoreIndexReportResponse, error) + // GetRepositoryToCPEMapping returns the repository-to-CPE mapping used for RHEL package matching. + GetRepositoryToCPEMapping(ctx context.Context, in *GetRepositoryToCPEMappingRequest, opts ...grpc.CallOption) (*GetRepositoryToCPEMappingResponse, error) } type indexerClient struct { @@ -108,6 +111,16 @@ func (c *indexerClient) StoreIndexReport(ctx context.Context, in *StoreIndexRepo return out, nil } +func (c *indexerClient) GetRepositoryToCPEMapping(ctx context.Context, in *GetRepositoryToCPEMappingRequest, opts ...grpc.CallOption) (*GetRepositoryToCPEMappingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRepositoryToCPEMappingResponse) + err := c.cc.Invoke(ctx, Indexer_GetRepositoryToCPEMapping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // IndexerServer is the server API for Indexer service. // All implementations should embed UnimplementedIndexerServer // for forward compatibility. @@ -126,6 +139,8 @@ type IndexerServer interface { HasIndexReport(context.Context, *HasIndexReportRequest) (*HasIndexReportResponse, error) // StoreIndexReport stores an external index report to the datastore. StoreIndexReport(context.Context, *StoreIndexReportRequest) (*StoreIndexReportResponse, error) + // GetRepositoryToCPEMapping returns the repository-to-CPE mapping used for RHEL package matching. + GetRepositoryToCPEMapping(context.Context, *GetRepositoryToCPEMappingRequest) (*GetRepositoryToCPEMappingResponse, error) } // UnimplementedIndexerServer should be embedded to have @@ -150,6 +165,9 @@ func (UnimplementedIndexerServer) HasIndexReport(context.Context, *HasIndexRepor func (UnimplementedIndexerServer) StoreIndexReport(context.Context, *StoreIndexReportRequest) (*StoreIndexReportResponse, error) { return nil, status.Error(codes.Unimplemented, "method StoreIndexReport not implemented") } +func (UnimplementedIndexerServer) GetRepositoryToCPEMapping(context.Context, *GetRepositoryToCPEMappingRequest) (*GetRepositoryToCPEMappingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRepositoryToCPEMapping not implemented") +} func (UnimplementedIndexerServer) testEmbeddedByValue() {} // UnsafeIndexerServer may be embedded to opt out of forward compatibility for this service. @@ -260,6 +278,24 @@ func _Indexer_StoreIndexReport_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Indexer_GetRepositoryToCPEMapping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRepositoryToCPEMappingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IndexerServer).GetRepositoryToCPEMapping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Indexer_GetRepositoryToCPEMapping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IndexerServer).GetRepositoryToCPEMapping(ctx, req.(*GetRepositoryToCPEMappingRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Indexer_ServiceDesc is the grpc.ServiceDesc for Indexer service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -287,6 +323,10 @@ var Indexer_ServiceDesc = grpc.ServiceDesc{ MethodName: "StoreIndexReport", Handler: _Indexer_StoreIndexReport_Handler, }, + { + MethodName: "GetRepositoryToCPEMapping", + Handler: _Indexer_GetRepositoryToCPEMapping_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "internalapi/scanner/v4/indexer_service.proto", diff --git a/generated/internalapi/scanner/v4/indexer_service_vtproto.pb.go b/generated/internalapi/scanner/v4/indexer_service_vtproto.pb.go index 9ab517bbd58e1..8e485738914a2 100644 --- a/generated/internalapi/scanner/v4/indexer_service_vtproto.pb.go +++ b/generated/internalapi/scanner/v4/indexer_service_vtproto.pb.go @@ -190,6 +190,66 @@ func (m *StoreIndexReportResponse) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *GetRepositoryToCPEMappingRequest) CloneVT() *GetRepositoryToCPEMappingRequest { + if m == nil { + return (*GetRepositoryToCPEMappingRequest)(nil) + } + r := new(GetRepositoryToCPEMappingRequest) + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *GetRepositoryToCPEMappingRequest) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *RepositoryCPEInfo) CloneVT() *RepositoryCPEInfo { + if m == nil { + return (*RepositoryCPEInfo)(nil) + } + r := new(RepositoryCPEInfo) + if rhs := m.Cpes; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.Cpes = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RepositoryCPEInfo) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *GetRepositoryToCPEMappingResponse) CloneVT() *GetRepositoryToCPEMappingResponse { + if m == nil { + return (*GetRepositoryToCPEMappingResponse)(nil) + } + r := new(GetRepositoryToCPEMappingResponse) + if rhs := m.Mapping; rhs != nil { + tmpContainer := make(map[string]*RepositoryCPEInfo, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Mapping = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *GetRepositoryToCPEMappingResponse) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (this *ContainerImageLocator) EqualVT(that *ContainerImageLocator) bool { if this == that { return true @@ -434,6 +494,83 @@ func (this *StoreIndexReportResponse) EqualMessageVT(thatMsg proto.Message) bool } return this.EqualVT(that) } +func (this *GetRepositoryToCPEMappingRequest) EqualVT(that *GetRepositoryToCPEMappingRequest) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *GetRepositoryToCPEMappingRequest) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*GetRepositoryToCPEMappingRequest) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *RepositoryCPEInfo) EqualVT(that *RepositoryCPEInfo) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if len(this.Cpes) != len(that.Cpes) { + return false + } + for i, vx := range this.Cpes { + vy := that.Cpes[i] + if vx != vy { + return false + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RepositoryCPEInfo) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RepositoryCPEInfo) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *GetRepositoryToCPEMappingResponse) EqualVT(that *GetRepositoryToCPEMappingResponse) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if len(this.Mapping) != len(that.Mapping) { + return false + } + for i, vx := range this.Mapping { + vy, ok := that.Mapping[i] + if !ok { + return false + } + if p, q := vx, vy; p != q { + if p == nil { + p = &RepositoryCPEInfo{} + } + if q == nil { + q = &RepositoryCPEInfo{} + } + if !p.EqualVT(q) { + return false + } + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *GetRepositoryToCPEMappingResponse) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*GetRepositoryToCPEMappingResponse) + if !ok { + return false + } + return this.EqualVT(that) +} func (m *ContainerImageLocator) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -872,6 +1009,136 @@ func (m *StoreIndexReportResponse) MarshalToSizedBufferVT(dAtA []byte) (int, err return len(dAtA) - i, nil } +func (m *GetRepositoryToCPEMappingRequest) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetRepositoryToCPEMappingRequest) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *GetRepositoryToCPEMappingRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + return len(dAtA) - i, nil +} + +func (m *RepositoryCPEInfo) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RepositoryCPEInfo) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *RepositoryCPEInfo) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Cpes) > 0 { + for iNdEx := len(m.Cpes) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Cpes[iNdEx]) + copy(dAtA[i:], m.Cpes[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Cpes[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *GetRepositoryToCPEMappingResponse) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetRepositoryToCPEMappingResponse) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *GetRepositoryToCPEMappingResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Mapping) > 0 { + for k := range m.Mapping { + v := m.Mapping[k] + baseI := i + size, err := v.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x12 + i -= len(k) + copy(dAtA[i:], k) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(k))) + i-- + dAtA[i] = 0xa + i = protohelpers.EncodeVarint(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func (m *ContainerImageLocator) SizeVT() (n int) { if m == nil { return 0 @@ -1039,6 +1306,55 @@ func (m *StoreIndexReportResponse) SizeVT() (n int) { return n } +func (m *GetRepositoryToCPEMappingRequest) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += len(m.unknownFields) + return n +} + +func (m *RepositoryCPEInfo) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Cpes) > 0 { + for _, s := range m.Cpes { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *GetRepositoryToCPEMappingResponse) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Mapping) > 0 { + for k, v := range m.Mapping { + _ = k + _ = v + l = 0 + if v != nil { + l = v.SizeVT() + } + l += 1 + protohelpers.SizeOfVarint(uint64(l)) + mapEntrySize := 1 + len(k) + protohelpers.SizeOfVarint(uint64(len(k))) + l + n += mapEntrySize + 1 + protohelpers.SizeOfVarint(uint64(mapEntrySize)) + } + } + n += len(m.unknownFields) + return n +} + func (m *ContainerImageLocator) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1945,7 +2261,7 @@ func (m *StoreIndexReportResponse) UnmarshalVT(dAtA []byte) error { } return nil } -func (m *ContainerImageLocator) UnmarshalVTUnsafe(dAtA []byte) error { +func (m *GetRepositoryToCPEMappingRequest) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -1968,44 +2284,358 @@ func (m *ContainerImageLocator) UnmarshalVTUnsafe(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: ContainerImageLocator: wiretype end group for non-group") + return fmt.Errorf("proto: GetRepositoryToCPEMappingRequest: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: ContainerImageLocator: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: GetRepositoryToCPEMappingRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protohelpers.ErrInvalidLength + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err } - postIndex := iNdEx + intStringLen - if postIndex < 0 { + if (skippy < 0) || (iNdEx+skippy) < 0 { return protohelpers.ErrInvalidLength } - if postIndex > l { + if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } - var stringValue string - if intStringLen > 0 { + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RepositoryCPEInfo) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RepositoryCPEInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RepositoryCPEInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Cpes", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Cpes = append(m.Cpes, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetRepositoryToCPEMappingResponse) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetRepositoryToCPEMappingResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetRepositoryToCPEMappingResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Mapping", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Mapping == nil { + m.Mapping = make(map[string]*RepositoryCPEInfo) + } + var mapkey string + var mapvalue *RepositoryCPEInfo + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return protohelpers.ErrInvalidLength + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return protohelpers.ErrInvalidLength + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapmsglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapmsglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if mapmsglen < 0 { + return protohelpers.ErrInvalidLength + } + postmsgIndex := iNdEx + mapmsglen + if postmsgIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postmsgIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = &RepositoryCPEInfo{} + if err := mapvalue.UnmarshalVT(dAtA[iNdEx:postmsgIndex]); err != nil { + return err + } + iNdEx = postmsgIndex + } else { + iNdEx = entryPreIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.Mapping[mapkey] = mapvalue + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ContainerImageLocator) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ContainerImageLocator: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ContainerImageLocator: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Url", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) } m.Url = stringValue @@ -2891,3 +3521,325 @@ func (m *StoreIndexReportResponse) UnmarshalVTUnsafe(dAtA []byte) error { } return nil } +func (m *GetRepositoryToCPEMappingRequest) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetRepositoryToCPEMappingRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetRepositoryToCPEMappingRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RepositoryCPEInfo) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RepositoryCPEInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RepositoryCPEInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Cpes", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.Cpes = append(m.Cpes, stringValue) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetRepositoryToCPEMappingResponse) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetRepositoryToCPEMappingResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetRepositoryToCPEMappingResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Mapping", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Mapping == nil { + m.Mapping = make(map[string]*RepositoryCPEInfo) + } + var mapkey string + var mapvalue *RepositoryCPEInfo + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return protohelpers.ErrInvalidLength + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return protohelpers.ErrInvalidLength + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + if intStringLenmapkey == 0 { + mapkey = "" + } else { + mapkey = unsafe.String(&dAtA[iNdEx], intStringLenmapkey) + } + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapmsglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapmsglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if mapmsglen < 0 { + return protohelpers.ErrInvalidLength + } + postmsgIndex := iNdEx + mapmsglen + if postmsgIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postmsgIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = &RepositoryCPEInfo{} + if err := mapvalue.UnmarshalVTUnsafe(dAtA[iNdEx:postmsgIndex]); err != nil { + return err + } + iNdEx = postmsgIndex + } else { + iNdEx = entryPreIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.Mapping[mapkey] = mapvalue + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/pkg/scannerv4/client/client.go b/pkg/scannerv4/client/client.go index 8f28691514f86..dfef9e04c13df 100644 --- a/pkg/scannerv4/client/client.go +++ b/pkg/scannerv4/client/client.go @@ -17,6 +17,7 @@ import ( "github.com/stackrox/rox/pkg/errorhelpers" "github.com/stackrox/rox/pkg/protocompat" "github.com/stackrox/rox/pkg/scannerv4" + "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" "github.com/stackrox/rox/pkg/utils" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -98,6 +99,9 @@ type Scanner interface { // if ref already exists in its datastore. StoreImageIndex(ctx context.Context, ref name.Digest, indexerVersion string, contents *v4.Contents, callOpts ...CallOption) error + // GetRepositoryToCPEMapping returns the repository-to-CPE mapping from the indexer. + GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) + // Close cleans up any resources used by the implementation. Close() error } @@ -453,6 +457,34 @@ func (c *gRPCScanner) StoreImageIndex(ctx context.Context, ref name.Digest, inde return nil } +// GetRepositoryToCPEMapping calls the Indexer's gRPC endpoint GetRepositoryToCPEMapping. +func (c *gRPCScanner) GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) { + if c.indexer == nil { + return nil, errIndexerNotConfigured + } + + ctx = zlog.ContextWithValues(ctx, "component", "scanner/client", "method", "GetRepositoryToCPEMapping") + + var resp *v4.GetRepositoryToCPEMappingResponse + err := retryWithBackoff(ctx, defaultBackoff(), "indexer.GetRepositoryToCPEMapping", func() error { + var err error + resp, err = c.indexer.GetRepositoryToCPEMapping(ctx, &v4.GetRepositoryToCPEMappingRequest{}) + return err + }) + if err != nil { + return nil, fmt.Errorf("getting repository-to-CPE mapping: %w", err) + } + + // Convert proto response to MappingFile. + data := make(map[string]repositorytocpe.Repo, len(resp.GetMapping())) + for repo, info := range resp.GetMapping() { + data[repo] = repositorytocpe.Repo{CPEs: info.GetCpes()} + } + + zlog.Debug(ctx).Int("entries", len(data)).Msg("received repo-to-CPE mapping") + return &repositorytocpe.MappingFile{Data: data}, nil +} + func getImageManifestID(ref name.Digest) string { return fmt.Sprintf("/v4/containerimage/%s", ref.DigestStr()) } diff --git a/pkg/scannerv4/client/mocks/client.go b/pkg/scannerv4/client/mocks/client.go index 94a261bced767..98930263f3d44 100644 --- a/pkg/scannerv4/client/mocks/client.go +++ b/pkg/scannerv4/client/mocks/client.go @@ -17,6 +17,7 @@ import ( name "github.com/google/go-containerregistry/pkg/name" v4 "github.com/stackrox/rox/generated/internalapi/scanner/v4" client "github.com/stackrox/rox/pkg/scannerv4/client" + repositorytocpe "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" gomock "go.uber.org/mock/gomock" ) @@ -119,6 +120,21 @@ func (mr *MockScannerMockRecorder) GetOrCreateImageIndex(ctx, ref, auth, opt any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrCreateImageIndex", reflect.TypeOf((*MockScanner)(nil).GetOrCreateImageIndex), varargs...) } +// GetRepositoryToCPEMapping mocks base method. +func (m *MockScanner) GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRepositoryToCPEMapping", ctx) + ret0, _ := ret[0].(*repositorytocpe.MappingFile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRepositoryToCPEMapping indicates an expected call of GetRepositoryToCPEMapping. +func (mr *MockScannerMockRecorder) GetRepositoryToCPEMapping(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryToCPEMapping", reflect.TypeOf((*MockScanner)(nil).GetRepositoryToCPEMapping), ctx) +} + // GetSBOM mocks base method. func (m *MockScanner) GetSBOM(ctx context.Context, arg1 string, ref name.Digest, uri string, callOpts ...client.CallOption) ([]byte, bool, error) { m.ctrl.T.Helper() diff --git a/pkg/scannerv4/repositorytocpe/mappingfile.go b/pkg/scannerv4/repositorytocpe/mappingfile.go new file mode 100644 index 0000000000000..78eede01eac93 --- /dev/null +++ b/pkg/scannerv4/repositorytocpe/mappingfile.go @@ -0,0 +1,29 @@ +package repositorytocpe + +import "log/slog" + +// MappingFile is a data struct for mapping between repositories and CPEs. +// This is used to map RHEL repositories to their corresponding CPEs for +// vulnerability matching. +// Largely based on https://github.com/quay/claircore/blob/v1.5.48/rhel/repositoryscanner.go#L291-L311. +type MappingFile struct { + Data map[string]Repo `json:"data"` +} + +// Repo holds CPE information for a given repository. +type Repo struct { + CPEs []string `json:"cpes"` +} + +// GetCPEs returns the CPEs for the given repository ID. +// Returns nil and false if the repository is not found. +func (m *MappingFile) GetCPEs(repoid string) ([]string, bool) { + if m == nil { + return nil, false + } + if repo, ok := m.Data[repoid]; ok { + return repo.CPEs, true + } + slog.Debug("repository not present in mapping file", "repository", repoid) + return nil, false +} diff --git a/proto/internalapi/scanner/v4/indexer_service.proto b/proto/internalapi/scanner/v4/indexer_service.proto index 14ef3021f0e33..01188c636a19d 100644 --- a/proto/internalapi/scanner/v4/indexer_service.proto +++ b/proto/internalapi/scanner/v4/indexer_service.proto @@ -56,6 +56,17 @@ message StoreIndexReportResponse { string status = 1; } +message GetRepositoryToCPEMappingRequest {} + +message RepositoryCPEInfo { + repeated string cpes = 1; +} + +message GetRepositoryToCPEMappingResponse { + // Maps repository names to their CPE information. + map mapping = 1; +} + // Indexer service creates manifests and store index reports. service Indexer { // CreateIndexReport creates an index report for the specified resource and returns the report. @@ -74,4 +85,7 @@ service Indexer { // StoreIndexReport stores an external index report to the datastore. rpc StoreIndexReport(StoreIndexReportRequest) returns (StoreIndexReportResponse); + + // GetRepositoryToCPEMapping returns the repository-to-CPE mapping used for RHEL package matching. + rpc GetRepositoryToCPEMapping(GetRepositoryToCPEMappingRequest) returns (GetRepositoryToCPEMappingResponse); } diff --git a/scanner/indexer/indexer.go b/scanner/indexer/indexer.go index 1294b39866e07..e141e93244d7b 100644 --- a/scanner/indexer/indexer.go +++ b/scanner/indexer/indexer.go @@ -42,6 +42,7 @@ import ( "github.com/stackrox/rox/pkg/env" "github.com/stackrox/rox/pkg/features" "github.com/stackrox/rox/pkg/httputil/proxy" + "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" "github.com/stackrox/rox/pkg/utils" pkgversion "github.com/stackrox/rox/pkg/version" "github.com/stackrox/rox/scanner/config" @@ -150,6 +151,7 @@ type Indexer interface { ReportGetter ReportStorer IndexContainerImage(context.Context, string, string, ...Option) (*claircore.IndexReport, error) + GetRepositoryToCPEMapping(context.Context) (*repositorytocpe.MappingFile, error) Close(context.Context) error Ready(context.Context) error } @@ -167,6 +169,8 @@ type localIndexer struct { manifestManager *manifest.Manager deleteIntervalStart int64 deleteIntervalDuration int64 + + repositoryToCPEUpdater *RepositoryToCPEUpdater } // NewIndexer creates a new indexer. @@ -284,6 +288,19 @@ func NewIndexer(ctx context.Context, cfg config.IndexerConfig) (Indexer, error) deleteIntervalDuration = minManifestDeleteDuration } + var repo2cpeUpdater *RepositoryToCPEUpdater + if cfg.RepositoryToCPEURL != "" { + repo2cpeUpdater, err = NewUpdater(ctx, client, cfg.RepositoryToCPEURL, cfg.RepositoryToCPEFile) + if err != nil { + if features.SBOMScanning.Enabled() { + return nil, fmt.Errorf("creating repository-to-cpe updater: %w", err) + } + zlog.Debug(ctx).Err(err).Msg("failed to create repository-to-cpe updater") + } + } else if features.SBOMScanning.Enabled() { + zlog.Warn(ctx).Msg("unconfigured repository_to_cpe_url will lead to inaccurate SBOM scanning results") + } + success = true return &localIndexer{ libIndex: indexer, @@ -297,6 +314,8 @@ func NewIndexer(ctx context.Context, cfg config.IndexerConfig) (Indexer, error) manifestManager: manifestManager, deleteIntervalStart: int64(deleteIntervalStart.Seconds()), deleteIntervalDuration: int64(deleteIntervalDuration.Seconds()), + + repositoryToCPEUpdater: repo2cpeUpdater, }, nil } @@ -384,6 +403,14 @@ func (i *localIndexer) Ready(ctx context.Context) error { return nil } +// GetRepositoryToCPEMapping returns the repository-to-CPE mapping file. +func (i *localIndexer) GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) { + if i.repositoryToCPEUpdater == nil { + return nil, errors.New("unsupported repository_to_cpe_url configuration") + } + return i.repositoryToCPEUpdater.Get(ctx) +} + // IndexContainerImage creates a ClairCore index report for a given container // image. The manifest is populated with layers from the image specified by a // URL. This method performs a partial content request on each layer to generate diff --git a/scanner/indexer/mocks/indexer.go b/scanner/indexer/mocks/indexer.go index 3a8c7739d6dca..6fc99b73664fd 100644 --- a/scanner/indexer/mocks/indexer.go +++ b/scanner/indexer/mocks/indexer.go @@ -14,6 +14,7 @@ import ( reflect "reflect" claircore "github.com/quay/claircore" + repositorytocpe "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" indexer "github.com/stackrox/rox/scanner/indexer" gomock "go.uber.org/mock/gomock" ) @@ -151,6 +152,21 @@ func (mr *MockIndexerMockRecorder) GetIndexReport(arg0, arg1, arg2 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndexReport", reflect.TypeOf((*MockIndexer)(nil).GetIndexReport), arg0, arg1, arg2) } +// GetRepositoryToCPEMapping mocks base method. +func (m *MockIndexer) GetRepositoryToCPEMapping(arg0 context.Context) (*repositorytocpe.MappingFile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRepositoryToCPEMapping", arg0) + ret0, _ := ret[0].(*repositorytocpe.MappingFile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRepositoryToCPEMapping indicates an expected call of GetRepositoryToCPEMapping. +func (mr *MockIndexerMockRecorder) GetRepositoryToCPEMapping(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryToCPEMapping", reflect.TypeOf((*MockIndexer)(nil).GetRepositoryToCPEMapping), arg0) +} + // IndexContainerImage mocks base method. func (m *MockIndexer) IndexContainerImage(arg0 context.Context, arg1, arg2 string, arg3 ...indexer.Option) (*claircore.IndexReport, error) { m.ctrl.T.Helper() diff --git a/scanner/indexer/remote.go b/scanner/indexer/remote.go index 09be20315d585..3dfa482b98634 100644 --- a/scanner/indexer/remote.go +++ b/scanner/indexer/remote.go @@ -7,11 +7,13 @@ import ( "github.com/quay/zlog" "github.com/stackrox/rox/pkg/scannerv4/client" "github.com/stackrox/rox/pkg/scannerv4/mappers" + "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" ) // RemoteIndexer represents the interface offered by remote indexers. type RemoteIndexer interface { ReportGetter + GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) Close(context.Context) error } @@ -60,3 +62,10 @@ func (r *remoteIndexer) GetIndexReport(ctx context.Context, hashID string, _ boo ir.Err = resp.GetErr() return ir, true, nil } + +// GetRepositoryToCPEMapping fetches the repository-to-CPE mapping from the remote indexer. +func (r *remoteIndexer) GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) { + ctx = zlog.ContextWithValues(ctx, "component", "scanner/backend/remoteIndexer.GetRepositoryToCPEMapping") + zlog.Info(ctx).Msg("fetching repo-to-CPE mapping from remote indexer") + return r.indexer.GetRepositoryToCPEMapping(ctx) +} diff --git a/scanner/indexer/repositorytocpeupdater.go b/scanner/indexer/repositorytocpeupdater.go new file mode 100644 index 0000000000000..1a37447037c9b --- /dev/null +++ b/scanner/indexer/repositorytocpeupdater.go @@ -0,0 +1,86 @@ +package indexer + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "os" + + "github.com/quay/claircore/rhel" + "github.com/quay/zlog" + "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" + "github.com/stackrox/rox/pkg/utils" + "github.com/stackrox/rox/scanner/internal/httputil" +) + +// RepositoryToCPEUpdater wraps the httputil.Updater and provides repository-to-CPE mapping +// functionality with periodic refresh support. +type RepositoryToCPEUpdater struct { + updater *httputil.Updater + client *http.Client +} + +// NewUpdater creates a new RepositoryToCPEUpdater for the repository-to-CPE mapping. +// It optionally loads initial data from a file and sets up periodic refresh from the URL. +func NewUpdater(ctx context.Context, client *http.Client, url, filePath string) (*RepositoryToCPEUpdater, error) { + ctx = zlog.ContextWithValues(ctx, "component", "scanner/repositorytocpe.NewUpdater") + + var initValue *repositorytocpe.MappingFile + + // If a file is configured, load it as the initial value. + if filePath != "" { + f, err := os.Open(filePath) + if err == nil { + initValue = &repositorytocpe.MappingFile{} + if err := json.NewDecoder(f).Decode(initValue); err != nil { + zlog.Warn(ctx).Err(err).Msg("failed to decode initial repo-to-CPE mapping file") + initValue = nil + } + defer utils.IgnoreError(f.Close) + } else { + zlog.Warn(ctx).Err(err).Str("file", filePath).Msg("failed to open repo-to-CPE mapping file") + } + } + + if initValue == nil { + initValue = &repositorytocpe.MappingFile{} + } + + // Use default URL if not specified. + if url == "" { + url = rhel.DefaultRepo2CPEMappingURL + } + + updater := httputil.NewUpdater(url, initValue) + + u := &RepositoryToCPEUpdater{ + updater: updater, + client: client, + } + + // Trigger initial fetch if no file data was loaded. + if len(initValue.Data) == 0 { + if _, err := updater.Get(ctx, client); err != nil { + zlog.Warn(ctx).Err(err).Msg("failed to fetch initial repo-to-CPE mapping") + } + } + + return u, nil +} + +// Get returns the current repository-to-CPE mapping. +// This may trigger a refresh if the periodic update interval has elapsed. +func (u *RepositoryToCPEUpdater) Get(ctx context.Context) (*repositorytocpe.MappingFile, error) { + v, err := u.updater.Get(ctx, u.client) + if err != nil && v == nil { + return nil, err + } + + mf, ok := v.(*repositorytocpe.MappingFile) + if !ok || mf == nil { + return nil, errors.New("unable to get repository-to-CPE mapping") + } + + return mf, nil +} diff --git a/scanner/internal/httputil/updater.go b/scanner/internal/httputil/updater.go new file mode 100644 index 0000000000000..14a85eefb0251 --- /dev/null +++ b/scanner/internal/httputil/updater.go @@ -0,0 +1,105 @@ +package httputil + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "reflect" + "sync/atomic" + "time" + + "github.com/stackrox/rox/pkg/concurrency" + "github.com/stackrox/rox/pkg/sync" + "github.com/stackrox/rox/pkg/utils" + "golang.org/x/time/rate" +) + +// Interval is how often we attempt to update the mapping file. +var interval = rate.Every(24 * time.Hour) + +// Updater returns a value that's periodically updated. +// Largely based on https://github.com/quay/claircore/blob/v1.5.48/rhel/internal/common/updater.go. +type Updater struct { + url string + typ reflect.Type + value atomic.Value + reqRate *rate.Limiter + mu sync.RWMutex // protects lastModified + lastModified string +} + +// NewUpdater returns an Updater holding a value of the type passed as "init", +// periodically updated from the endpoint "url." +// +// To omit an initial value, use a typed nil pointer. +func NewUpdater(url string, init any) *Updater { + u := Updater{ + url: url, + typ: reflect.TypeOf(init).Elem(), + reqRate: rate.NewLimiter(interval, 1), + } + u.value.Store(init) + return &u +} + +// Get returns a pointer to the current copy of the value. The Get call may be +// hijacked to update the value from the configured endpoint. +func (u *Updater) Get(ctx context.Context, c *http.Client) (any, error) { + var err error + if u.url != "" && u.reqRate.Allow() { + slog.DebugContext(ctx, "got unlucky, updating mapping file") + err = u.Fetch(ctx, c) + if err != nil { + slog.ErrorContext(ctx, "error updating mapping file", "reason", err) + } + } + + return u.value.Load(), err +} + +// Fetch attempts to perform an atomic update of the mapping file. +// +// Fetch is safe to call concurrently. +func (u *Updater) Fetch(ctx context.Context, c *http.Client) error { + log := slog.With("url", u.url) + log.DebugContext(ctx, "attempting fetch of mapping file") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil) + if err != nil { + return err + } + concurrency.WithRLock(&u.mu, func() { + if u.lastModified != "" { + req.Header.Set("if-modified-since", u.lastModified) + } + }) + + resp, err := c.Do(req) + if err != nil { + return err + } + defer utils.IgnoreError(resp.Body.Close) + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + log.DebugContext(ctx, "response not modified; no update necessary", "since", u.lastModified) + return nil + default: + return fmt.Errorf("received status code %d querying mapping url", resp.StatusCode) + } + + v := reflect.New(u.typ).Interface() + if err := json.NewDecoder(resp.Body).Decode(v); err != nil { + return fmt.Errorf("failed to decode mapping file: %w", err) + } + + concurrency.WithRLock(&u.mu, func() { + u.lastModified = resp.Header.Get("last-modified") + }) + // atomic store of mapping file + u.value.Store(v) + log.DebugContext(ctx, "atomic update of local mapping file complete") + return nil +} diff --git a/scanner/services/indexer.go b/scanner/services/indexer.go index 2fff750a912da..1ec4f5666705c 100644 --- a/scanner/services/indexer.go +++ b/scanner/services/indexer.go @@ -35,6 +35,9 @@ var indexerAuth = perrpc.FromMap(map[authz.Authorizer][]string{ or.Or(idcheck.CentralOnly()): { v4.Indexer_StoreIndexReport_FullMethodName, }, + or.Or(idcheck.ScannerV4MatcherOnly()): { + v4.Indexer_GetRepositoryToCPEMapping_FullMethodName, + }, }) type indexerService struct { @@ -218,6 +221,27 @@ func (s *indexerService) StoreIndexReport(ctx context.Context, req *v4.StoreInde return resp, nil } +func (s *indexerService) GetRepositoryToCPEMapping(ctx context.Context, _ *v4.GetRepositoryToCPEMappingRequest) (*v4.GetRepositoryToCPEMappingResponse, error) { + ctx = zlog.ContextWithValues(ctx, "component", "scanner/service/indexer.GetRepositoryToCPEMapping") + zlog.Info(ctx).Msg("getting repository-to-CPE mapping") + + mf, err := s.indexer.GetRepositoryToCPEMapping(ctx) + if err != nil { + zlog.Error(ctx).Err(err).Msg("failed to get repository-to-CPE mapping") + return nil, err + } + + // Convert to proto format. + result := make(map[string]*v4.RepositoryCPEInfo, len(mf.Data)) + for repo, info := range mf.Data { + if len(info.CPEs) > 0 { + result[repo] = &v4.RepositoryCPEInfo{Cpes: info.CPEs} + } + } + + return &v4.GetRepositoryToCPEMappingResponse{Mapping: result}, nil +} + // RegisterServiceServer registers this service with the given gRPC Server. func (s *indexerService) RegisterServiceServer(grpcServer *grpc.Server) { v4.RegisterIndexerServer(grpcServer, s) From 6a387010a936da1fcd8cf60a2fdc370deba73619 Mon Sep 17 00:00:00 2001 From: Brad Lugo Date: Mon, 26 Jan 2026 15:43:57 -0800 Subject: [PATCH 09/12] ROX-30588: Update claircore --- compliance/node/index/indexer.go | 27 +++++++++++++++++++-------- go.mod | 8 +++++--- go.sum | 19 ++++++++----------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/compliance/node/index/indexer.go b/compliance/node/index/indexer.go index 521d079abde2c..d58e99a26689e 100644 --- a/compliance/node/index/indexer.go +++ b/compliance/node/index/indexer.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "os" + "path/filepath" "strconv" "strings" "time" @@ -155,11 +156,6 @@ func (l *localNodeIndexer) GetIntervals() *utils.NodeScanIntervals { // IndexNode indexes a node at the configured host path mount. func (l *localNodeIndexer) IndexNode(ctx context.Context) (*v4.IndexReport, error) { - // claircore no longer returns an error if the host path does not exist. - if _, err := os.Stat(l.cfg.HostPath); err != nil { - return nil, errors.Wrapf(err, "host path %q does not exist", l.cfg.HostPath) - } - layer, err := layer(ctx, layerDigest, l.cfg.HostPath) if err != nil { return nil, err @@ -194,15 +190,30 @@ func (l *localNodeIndexer) IndexNode(ctx context.Context) (*v4.IndexReport, erro } func layer(ctx context.Context, digest string, hostPath string) (*claircore.Layer, error) { - log.Debugf("Realizing mount path: %s", hostPath) + // claircore no longer returns an error if the host path is empty. + if hostPath == "" { + return nil, errors.New("no URI provided: empty host path") + } + + // claircore now requires an absolute path for the file URI + p, err := filepath.Abs(hostPath) + if err != nil { + return nil, errors.New(fmt.Sprintf("could not generate absolute path for host path: %q", hostPath)) + } + + // claircore no longer returns an error if the host path does not exist. + if _, err = os.Stat(p); err != nil { + return nil, errors.Wrapf(err, "host path %q does not exist", p) + } + log.Debugf("Realizing mount path: %s", p) desc := &claircore.LayerDescription{ Digest: digest, - URI: hostPath, + URI: fmt.Sprintf("file://%s", p), MediaType: layerMediaType, } l := &claircore.Layer{} - err := l.Init(ctx, desc, nil) + err = l.Init(ctx, desc, nil) return l, errors.Wrap(err, "failed to init layer") } diff --git a/go.mod b/go.mod index f54cbff7ddfc8..a945e11676afc 100644 --- a/go.mod +++ b/go.mod @@ -227,7 +227,7 @@ require ( github.com/alibabacloud-go/tea-utils v1.4.5 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect github.com/aliyun/credentials-go v1.3.2 // indirect - github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/anchore/go-struct-converter v0.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -454,7 +454,7 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect - github.com/spdx/tools-golang v0.5.5 // indirect + github.com/spdx/tools-golang v0.5.7 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -522,7 +522,7 @@ require ( modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.40.1 // indirect + modernc.org/sqlite v1.42.2 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect @@ -593,3 +593,5 @@ replace ( github.com/heroku/docker-registry-client => github.com/stackrox/docker-registry-client v0.2.0 github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 ) + +replace github.com/quay/claircore => github.com/BradLugo/claircore v0.0.0-20260126184054-889f4577ab1d diff --git a/go.sum b/go.sum index d8f90b1acd21d..54d94f9da43aa 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BradLugo/claircore v0.0.0-20260126184054-889f4577ab1d h1:NgTFZPYnMXHBs1i94eTB8rZVfFOBdiTzJblbDLA8WAo= +github.com/BradLugo/claircore v0.0.0-20260126184054-889f4577ab1d/go.mod h1:l5g6S1eO2R8vAUhex9rXSQD7eR4Xb1Yo0x+DNMXL30U= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= @@ -259,8 +261,8 @@ github.com/aliyun/credentials-go v1.3.2 h1:L4WppI9rctC8PdlMgyTkF8bBsy9pyKQEzBD1b github.com/aliyun/credentials-go v1.3.2/go.mod h1:tlpz4uys4Rn7Ik4/piGRrTbXy2uLKvePgQJJduE+Y5c= github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA= github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= +github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= @@ -1356,8 +1358,6 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/quay/claircore v1.5.44 h1:brG93+ZvX4IZJtFLBTB/p8mTtKSvX7knzgfTkBoME0Y= -github.com/quay/claircore v1.5.44/go.mod h1:iOpZqd2tErng1hTypZy0pBWP8xFT3Iwj1Rr/iiWL4Sg= github.com/quay/claircore/toolkit v1.0.0/go.mod h1:3ELtgf92x7o1JCTSKVOAqhcnCTXc4s5qiGaEDx62i20= github.com/quay/claircore/toolkit v1.4.0 h1:ygHG1pLAOTSk7r2Wmo/UbIz9WHJb+K3hhQnNIvLrBSQ= github.com/quay/claircore/toolkit v1.4.0/go.mod h1:0cQXEt/BIYSxo/Wq6ItyAJOJzdvD6ty1wPZJ9xR3b6E= @@ -1468,9 +1468,8 @@ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrel github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= -github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= +github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= +github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -1548,7 +1547,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -2416,8 +2414,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= -modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74= +modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= @@ -2454,7 +2452,6 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/ sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= From aad901cf66710e6a5e870892a48f2153cd06004e Mon Sep 17 00:00:00 2001 From: Brad Lugo Date: Tue, 27 Jan 2026 13:21:39 -0800 Subject: [PATCH 10/12] chore: move Matcher's SBOMer to the service layer To prepare for the SBOMer ScanSBOM API. We'll need to pass in a RemoteIndexer, which currently lives in the service layer. --- pkg/scanners/types/types.go | 2 +- scanner/matcher/matcher.go | 15 --------------- scanner/matcher/mocks/matcher.go | 16 ---------------- scanner/services/matcher.go | 7 +++++-- scanner/services/matcher_test.go | 16 +--------------- 5 files changed, 7 insertions(+), 49 deletions(-) diff --git a/pkg/scanners/types/types.go b/pkg/scanners/types/types.go index a023445d0f8e5..3cd0d2cf7bb5d 100644 --- a/pkg/scanners/types/types.go +++ b/pkg/scanners/types/types.go @@ -35,7 +35,7 @@ type Scanner interface { GetVulnDefinitionsInfo() (*v1.VulnDefinitionsInfo, error) } -// SBOM is the interface that contains the StackRox SBOM methods +// SBOMer is the interface that contains the StackRox SBOM methods type SBOMer interface { // GetSBOM to get SBOM for an image. GetSBOM(image *storage.Image) ([]byte, bool, error) diff --git a/scanner/matcher/matcher.go b/scanner/matcher/matcher.go index c3c888ba06a6c..9dffbf46ae357 100644 --- a/scanner/matcher/matcher.go +++ b/scanner/matcher/matcher.go @@ -38,7 +38,6 @@ import ( "github.com/stackrox/rox/scanner/enricher/nvd" "github.com/stackrox/rox/scanner/internal/httputil" "github.com/stackrox/rox/scanner/matcher/updater/vuln" - "github.com/stackrox/rox/scanner/sbom" ) // matcherNames specifies the ClairCore matchers to use. @@ -75,7 +74,6 @@ type Matcher interface { GetVulnerabilities(ctx context.Context, ir *claircore.IndexReport) (*claircore.VulnerabilityReport, error) GetLastVulnerabilityUpdate(ctx context.Context) (time.Time, error) GetKnownDistributions(ctx context.Context) []claircore.Distribution - GetSBOM(ctx context.Context, ir *claircore.IndexReport, opts *sbom.Options) ([]byte, error) Ready(ctx context.Context) error Initialized(ctx context.Context) error Close(ctx context.Context) error @@ -88,7 +86,6 @@ type matcherImpl struct { pool *pgxpool.Pool vulnUpdater *vuln.Updater - sbomer *sbom.SBOMer readyWithVulns bool } @@ -193,13 +190,6 @@ func NewMatcher(ctx context.Context, cfg config.MatcherConfig) (Matcher, error) return nil, fmt.Errorf("creating vuln updater: %w", err) } - // SBOM generation capabilities are only avail via the matcher. - // SBOMs may optionally include vulnerabilities which aligns SBOM - // generation to matcher capabilities and reduces the complexity - // of routing requests differently based on if a user chooses to - // include vulnerabilities vs. not. - sbomer := sbom.NewSBOMer() - // Start the vulnerability updater. go func() { if err := vulnUpdater.Start(); err != nil { @@ -214,7 +204,6 @@ func NewMatcher(ctx context.Context, cfg config.MatcherConfig) (Matcher, error) pool: pool, vulnUpdater: vulnUpdater, - sbomer: sbomer, readyWithVulns: cfg.Readiness == config.ReadinessVulnerability, }, nil @@ -234,10 +223,6 @@ func (m *matcherImpl) GetKnownDistributions(_ context.Context) []claircore.Distr return m.vulnUpdater.KnownDistributions() } -func (m *matcherImpl) GetSBOM(ctx context.Context, ir *claircore.IndexReport, opts *sbom.Options) ([]byte, error) { - return m.sbomer.GetSBOM(ctx, ir, opts) -} - // Close closes the matcher. func (m *matcherImpl) Close(ctx context.Context) error { ctx = zlog.ContextWithValues(ctx, "component", "scanner/backend/matcher.Close") diff --git a/scanner/matcher/mocks/matcher.go b/scanner/matcher/mocks/matcher.go index 0aa24fda37f25..3a0d5e00dfe1c 100644 --- a/scanner/matcher/mocks/matcher.go +++ b/scanner/matcher/mocks/matcher.go @@ -15,7 +15,6 @@ import ( time "time" claircore "github.com/quay/claircore" - sbom "github.com/stackrox/rox/scanner/sbom" gomock "go.uber.org/mock/gomock" ) @@ -86,21 +85,6 @@ func (mr *MockMatcherMockRecorder) GetLastVulnerabilityUpdate(ctx any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastVulnerabilityUpdate", reflect.TypeOf((*MockMatcher)(nil).GetLastVulnerabilityUpdate), ctx) } -// GetSBOM mocks base method. -func (m *MockMatcher) GetSBOM(ctx context.Context, ir *claircore.IndexReport, opts *sbom.Options) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSBOM", ctx, ir, opts) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSBOM indicates an expected call of GetSBOM. -func (mr *MockMatcherMockRecorder) GetSBOM(ctx, ir, opts any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSBOM", reflect.TypeOf((*MockMatcher)(nil).GetSBOM), ctx, ir, opts) -} - // GetVulnerabilities mocks base method. func (m *MockMatcher) GetVulnerabilities(ctx context.Context, ir *claircore.IndexReport) (*claircore.VulnerabilityReport, error) { m.ctrl.T.Helper() diff --git a/scanner/services/matcher.go b/scanner/services/matcher.go index b6e4be37ea599..64a04fbd2571f 100644 --- a/scanner/services/matcher.go +++ b/scanner/services/matcher.go @@ -41,6 +41,8 @@ type matcherService struct { indexer indexer.ReportGetter // matcher is used to match vulnerabilities with index contents. matcher matcher.Matcher + // sbomer is used to generate SBOMs from index reports. + sbomer *sbom.SBOMer // disableEmptyContents allows the vulnerability matching API to reject requests with empty contents. disableEmptyContents bool // anonymousAuthEnabled specifies if the service should allow for traffic from anonymous users. @@ -53,6 +55,7 @@ func NewMatcherService(matcher matcher.Matcher, indexer indexer.ReportGetter) *m return &matcherService{ matcher: matcher, indexer: indexer, + sbomer: sbom.NewSBOMer(), disableEmptyContents: indexer == nil, anonymousAuthEnabled: env.ScannerV4AnonymousAuth.BooleanSetting(), } @@ -199,7 +202,7 @@ func (s *matcherService) GetSBOM(ctx context.Context, req *v4.GetSBOMRequest) (* return nil, err } - sbom, err := s.matcher.GetSBOM(ctx, ir, &sbom.Options{ + sbomBytes, err := s.sbomer.GetSBOM(ctx, ir, &sbom.Options{ Name: req.GetId(), Namespace: req.GetUri(), Comment: fmt.Sprintf("Generated for '%s'", req.GetName()), @@ -209,5 +212,5 @@ func (s *matcherService) GetSBOM(ctx context.Context, req *v4.GetSBOMRequest) (* return nil, err } - return &v4.GetSBOMResponse{Sbom: sbom}, nil + return &v4.GetSBOMResponse{Sbom: sbomBytes}, nil } diff --git a/scanner/services/matcher_test.go b/scanner/services/matcher_test.go index 27f2e31554a31..bdf15c5fddfcc 100644 --- a/scanner/services/matcher_test.go +++ b/scanner/services/matcher_test.go @@ -354,21 +354,7 @@ func (s *matcherServiceTestSuite) Test_matcherService_GetSBOM() { }) - s.Run("error when sbom generation fails", func() { - s.matcherMock.EXPECT().GetSBOM(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("broken")) - srv := NewMatcherService(s.matcherMock, nil) - _, err := srv.GetSBOM(s.ctx, &v4.GetSBOMRequest{ - Id: "id", - Name: "name", - Uri: "uri", - Contents: &v4.Contents{}, - }) - s.ErrorContains(err, "broken") - }) - s.Run("success", func() { - fakeSbomB := []byte("fake sbom") - s.matcherMock.EXPECT().GetSBOM(gomock.Any(), gomock.Any(), gomock.Any()).Return(fakeSbomB, nil) srv := NewMatcherService(s.matcherMock, nil) res, err := srv.GetSBOM(s.ctx, &v4.GetSBOMRequest{ Id: "id", @@ -377,6 +363,6 @@ func (s *matcherServiceTestSuite) Test_matcherService_GetSBOM() { Contents: &v4.Contents{}, }) s.Require().NoError(err) - s.Equal(res.GetSbom(), fakeSbomB) + s.NotEmpty(res.GetSbom()) }) } From 0c13a12bd11bcd118aae8c68e13b611538e5a13c Mon Sep 17 00:00:00 2001 From: Brad Lugo Date: Sun, 25 Jan 2026 23:52:10 -0800 Subject: [PATCH 11/12] feat(matcher): add ScanSBOM API Add ScanSBOM RPC to the Matcher service that decodes an SBOM, parses its components, and matches vulnerabilities. --- .../central/v1/token_service.pb.go | 4 +- .../scanner/v4/matcher_service.pb.go | 155 +++- .../scanner/v4/matcher_service_grpc.pb.go | 40 + .../scanner/v4/matcher_service_vtproto.pb.go | 737 ++++++++++++++++-- go.mod | 2 +- .../scanner/v4/matcher_service.proto | 15 + scanner/cmd/scanner/main.go | 13 +- scanner/indexer/indexer.go | 8 + scanner/matcher/mocks/matcher.go | 15 + scanner/sbom/sbom.go | 111 ++- scanner/sbom/sbom_test.go | 6 +- scanner/services/matcher.go | 59 +- scanner/services/validators/validators.go | 21 + 13 files changed, 1078 insertions(+), 108 deletions(-) diff --git a/generated/internalapi/central/v1/token_service.pb.go b/generated/internalapi/central/v1/token_service.pb.go index 355fa7fc46727..d976570f83085 100644 --- a/generated/internalapi/central/v1/token_service.pb.go +++ b/generated/internalapi/central/v1/token_service.pb.go @@ -307,8 +307,8 @@ var file_internalapi_central_v1_token_service_proto_enumTypes = make([]protoimpl var file_internalapi_central_v1_token_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_internalapi_central_v1_token_service_proto_goTypes = []any{ (Access)(0), // 0: central.v1.Access - (*GenerateTokenForPermissionsAndScopeRequest)(nil), // 1: central.v1.GenerateTokenForPermissionsAndScopeRequest - (*ClusterScope)(nil), // 2: central.v1.ClusterScope + (*GenerateTokenForPermissionsAndScopeRequest)(nil), // 1: central.v1.GenerateTokenForPermissionsAndScopeRequest + (*ClusterScope)(nil), // 2: central.v1.ClusterScope (*GenerateTokenForPermissionsAndScopeResponse)(nil), // 3: central.v1.GenerateTokenForPermissionsAndScopeResponse nil, // 4: central.v1.GenerateTokenForPermissionsAndScopeRequest.PermissionsEntry (*durationpb.Duration)(nil), // 5: google.protobuf.Duration diff --git a/generated/internalapi/scanner/v4/matcher_service.pb.go b/generated/internalapi/scanner/v4/matcher_service.pb.go index 1fd75d74db221..cfa767be8bed7 100644 --- a/generated/internalapi/scanner/v4/matcher_service.pb.go +++ b/generated/internalapi/scanner/v4/matcher_service.pb.go @@ -238,6 +238,105 @@ func (x *Metadata) GetLastVulnerabilityUpdate() *timestamppb.Timestamp { return nil } +type ScanSBOMRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Raw SBOM content. + Sbom []byte `protobuf:"bytes,1,opt,name=sbom,proto3" json:"sbom,omitempty"` + // Media type of the SBOM (e.g., "application/spdx+json"). + MediaType string `protobuf:"bytes,2,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScanSBOMRequest) Reset() { + *x = ScanSBOMRequest{} + mi := &file_internalapi_scanner_v4_matcher_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScanSBOMRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanSBOMRequest) ProtoMessage() {} + +func (x *ScanSBOMRequest) ProtoReflect() protoreflect.Message { + mi := &file_internalapi_scanner_v4_matcher_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanSBOMRequest.ProtoReflect.Descriptor instead. +func (*ScanSBOMRequest) Descriptor() ([]byte, []int) { + return file_internalapi_scanner_v4_matcher_service_proto_rawDescGZIP(), []int{4} +} + +func (x *ScanSBOMRequest) GetSbom() []byte { + if x != nil { + return x.Sbom + } + return nil +} + +func (x *ScanSBOMRequest) GetMediaType() string { + if x != nil { + return x.MediaType + } + return "" +} + +type ScanSBOMResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Vulnerability report including the parsed contents from the SBOM. + VulnerabilityReport *VulnerabilityReport `protobuf:"bytes,1,opt,name=vulnerability_report,json=vulnerabilityReport,proto3" json:"vulnerability_report,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScanSBOMResponse) Reset() { + *x = ScanSBOMResponse{} + mi := &file_internalapi_scanner_v4_matcher_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScanSBOMResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanSBOMResponse) ProtoMessage() {} + +func (x *ScanSBOMResponse) ProtoReflect() protoreflect.Message { + mi := &file_internalapi_scanner_v4_matcher_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanSBOMResponse.ProtoReflect.Descriptor instead. +func (*ScanSBOMResponse) Descriptor() ([]byte, []int) { + return file_internalapi_scanner_v4_matcher_service_proto_rawDescGZIP(), []int{5} +} + +func (x *ScanSBOMResponse) GetVulnerabilityReport() *VulnerabilityReport { + if x != nil { + return x.VulnerabilityReport + } + return nil +} + var File_internalapi_scanner_v4_matcher_service_proto protoreflect.FileDescriptor const file_internalapi_scanner_v4_matcher_service_proto_rawDesc = "" + @@ -255,11 +354,18 @@ const file_internalapi_scanner_v4_matcher_service_proto_rawDesc = "" + "\x0fGetSBOMResponse\x12\x12\n" + "\x04sbom\x18\x01 \x01(\fR\x04sbom\"`\n" + "\bMetadata\x12T\n" + - "\x17LastVulnerabilityUpdate\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x17LastVulnerabilityUpdate2\xe8\x01\n" + + "\x17LastVulnerabilityUpdate\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x17LastVulnerabilityUpdate\"D\n" + + "\x0fScanSBOMRequest\x12\x12\n" + + "\x04sbom\x18\x01 \x01(\fR\x04sbom\x12\x1d\n" + + "\n" + + "media_type\x18\x02 \x01(\tR\tmediaType\"f\n" + + "\x10ScanSBOMResponse\x12R\n" + + "\x14vulnerability_report\x18\x01 \x01(\v2\x1f.scanner.v4.VulnerabilityReportR\x13vulnerabilityReport2\xaf\x02\n" + "\aMatcher\x12\\\n" + "\x12GetVulnerabilities\x12%.scanner.v4.GetVulnerabilitiesRequest\x1a\x1f.scanner.v4.VulnerabilityReport\x12;\n" + "\vGetMetadata\x12\x16.google.protobuf.Empty\x1a\x14.scanner.v4.Metadata\x12B\n" + - "\aGetSBOM\x12\x1a.scanner.v4.GetSBOMRequest\x1a\x1b.scanner.v4.GetSBOMResponseB\x1dZ\x1b./internalapi/scanner/v4;v4b\x06proto3" + "\aGetSBOM\x12\x1a.scanner.v4.GetSBOMRequest\x1a\x1b.scanner.v4.GetSBOMResponse\x12E\n" + + "\bScanSBOM\x12\x1b.scanner.v4.ScanSBOMRequest\x1a\x1c.scanner.v4.ScanSBOMResponseB\x1dZ\x1b./internalapi/scanner/v4;v4b\x06proto3" var ( file_internalapi_scanner_v4_matcher_service_proto_rawDescOnce sync.Once @@ -273,32 +379,37 @@ func file_internalapi_scanner_v4_matcher_service_proto_rawDescGZIP() []byte { return file_internalapi_scanner_v4_matcher_service_proto_rawDescData } -var file_internalapi_scanner_v4_matcher_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_internalapi_scanner_v4_matcher_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_internalapi_scanner_v4_matcher_service_proto_goTypes = []any{ (*GetVulnerabilitiesRequest)(nil), // 0: scanner.v4.GetVulnerabilitiesRequest (*GetSBOMRequest)(nil), // 1: scanner.v4.GetSBOMRequest (*GetSBOMResponse)(nil), // 2: scanner.v4.GetSBOMResponse (*Metadata)(nil), // 3: scanner.v4.Metadata - (*Contents)(nil), // 4: scanner.v4.Contents - (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 6: google.protobuf.Empty - (*VulnerabilityReport)(nil), // 7: scanner.v4.VulnerabilityReport + (*ScanSBOMRequest)(nil), // 4: scanner.v4.ScanSBOMRequest + (*ScanSBOMResponse)(nil), // 5: scanner.v4.ScanSBOMResponse + (*Contents)(nil), // 6: scanner.v4.Contents + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*VulnerabilityReport)(nil), // 8: scanner.v4.VulnerabilityReport + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty } var file_internalapi_scanner_v4_matcher_service_proto_depIdxs = []int32{ - 4, // 0: scanner.v4.GetVulnerabilitiesRequest.contents:type_name -> scanner.v4.Contents - 4, // 1: scanner.v4.GetSBOMRequest.contents:type_name -> scanner.v4.Contents - 5, // 2: scanner.v4.Metadata.LastVulnerabilityUpdate:type_name -> google.protobuf.Timestamp - 0, // 3: scanner.v4.Matcher.GetVulnerabilities:input_type -> scanner.v4.GetVulnerabilitiesRequest - 6, // 4: scanner.v4.Matcher.GetMetadata:input_type -> google.protobuf.Empty - 1, // 5: scanner.v4.Matcher.GetSBOM:input_type -> scanner.v4.GetSBOMRequest - 7, // 6: scanner.v4.Matcher.GetVulnerabilities:output_type -> scanner.v4.VulnerabilityReport - 3, // 7: scanner.v4.Matcher.GetMetadata:output_type -> scanner.v4.Metadata - 2, // 8: scanner.v4.Matcher.GetSBOM:output_type -> scanner.v4.GetSBOMResponse - 6, // [6:9] is the sub-list for method output_type - 3, // [3:6] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 6, // 0: scanner.v4.GetVulnerabilitiesRequest.contents:type_name -> scanner.v4.Contents + 6, // 1: scanner.v4.GetSBOMRequest.contents:type_name -> scanner.v4.Contents + 7, // 2: scanner.v4.Metadata.LastVulnerabilityUpdate:type_name -> google.protobuf.Timestamp + 8, // 3: scanner.v4.ScanSBOMResponse.vulnerability_report:type_name -> scanner.v4.VulnerabilityReport + 0, // 4: scanner.v4.Matcher.GetVulnerabilities:input_type -> scanner.v4.GetVulnerabilitiesRequest + 9, // 5: scanner.v4.Matcher.GetMetadata:input_type -> google.protobuf.Empty + 1, // 6: scanner.v4.Matcher.GetSBOM:input_type -> scanner.v4.GetSBOMRequest + 4, // 7: scanner.v4.Matcher.ScanSBOM:input_type -> scanner.v4.ScanSBOMRequest + 8, // 8: scanner.v4.Matcher.GetVulnerabilities:output_type -> scanner.v4.VulnerabilityReport + 3, // 9: scanner.v4.Matcher.GetMetadata:output_type -> scanner.v4.Metadata + 2, // 10: scanner.v4.Matcher.GetSBOM:output_type -> scanner.v4.GetSBOMResponse + 5, // 11: scanner.v4.Matcher.ScanSBOM:output_type -> scanner.v4.ScanSBOMResponse + 8, // [8:12] is the sub-list for method output_type + 4, // [4:8] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_internalapi_scanner_v4_matcher_service_proto_init() } @@ -314,7 +425,7 @@ func file_internalapi_scanner_v4_matcher_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_internalapi_scanner_v4_matcher_service_proto_rawDesc), len(file_internalapi_scanner_v4_matcher_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/generated/internalapi/scanner/v4/matcher_service_grpc.pb.go b/generated/internalapi/scanner/v4/matcher_service_grpc.pb.go index ed97bbd08c40b..6bbcc7a97b755 100644 --- a/generated/internalapi/scanner/v4/matcher_service_grpc.pb.go +++ b/generated/internalapi/scanner/v4/matcher_service_grpc.pb.go @@ -23,6 +23,7 @@ const ( Matcher_GetVulnerabilities_FullMethodName = "/scanner.v4.Matcher/GetVulnerabilities" Matcher_GetMetadata_FullMethodName = "/scanner.v4.Matcher/GetMetadata" Matcher_GetSBOM_FullMethodName = "/scanner.v4.Matcher/GetSBOM" + Matcher_ScanSBOM_FullMethodName = "/scanner.v4.Matcher/ScanSBOM" ) // MatcherClient is the client API for Matcher service. @@ -37,6 +38,8 @@ type MatcherClient interface { GetMetadata(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Metadata, error) // GetSBOM returns an SBOM for a previously indexed manifest. GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (*GetSBOMResponse, error) + // ScanSBOM decodes an SBOM and returns a VulnerabilityReport with matched vulnerabilities. + ScanSBOM(ctx context.Context, in *ScanSBOMRequest, opts ...grpc.CallOption) (*ScanSBOMResponse, error) } type matcherClient struct { @@ -77,6 +80,16 @@ func (c *matcherClient) GetSBOM(ctx context.Context, in *GetSBOMRequest, opts .. return out, nil } +func (c *matcherClient) ScanSBOM(ctx context.Context, in *ScanSBOMRequest, opts ...grpc.CallOption) (*ScanSBOMResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ScanSBOMResponse) + err := c.cc.Invoke(ctx, Matcher_ScanSBOM_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // MatcherServer is the server API for Matcher service. // All implementations should embed UnimplementedMatcherServer // for forward compatibility. @@ -89,6 +102,8 @@ type MatcherServer interface { GetMetadata(context.Context, *emptypb.Empty) (*Metadata, error) // GetSBOM returns an SBOM for a previously indexed manifest. GetSBOM(context.Context, *GetSBOMRequest) (*GetSBOMResponse, error) + // ScanSBOM decodes an SBOM and returns a VulnerabilityReport with matched vulnerabilities. + ScanSBOM(context.Context, *ScanSBOMRequest) (*ScanSBOMResponse, error) } // UnimplementedMatcherServer should be embedded to have @@ -107,6 +122,9 @@ func (UnimplementedMatcherServer) GetMetadata(context.Context, *emptypb.Empty) ( func (UnimplementedMatcherServer) GetSBOM(context.Context, *GetSBOMRequest) (*GetSBOMResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSBOM not implemented") } +func (UnimplementedMatcherServer) ScanSBOM(context.Context, *ScanSBOMRequest) (*ScanSBOMResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ScanSBOM not implemented") +} func (UnimplementedMatcherServer) testEmbeddedByValue() {} // UnsafeMatcherServer may be embedded to opt out of forward compatibility for this service. @@ -181,6 +199,24 @@ func _Matcher_GetSBOM_Handler(srv interface{}, ctx context.Context, dec func(int return interceptor(ctx, in, info, handler) } +func _Matcher_ScanSBOM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ScanSBOMRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MatcherServer).ScanSBOM(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Matcher_ScanSBOM_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MatcherServer).ScanSBOM(ctx, req.(*ScanSBOMRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Matcher_ServiceDesc is the grpc.ServiceDesc for Matcher service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -200,6 +236,10 @@ var Matcher_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetSBOM", Handler: _Matcher_GetSBOM_Handler, }, + { + MethodName: "ScanSBOM", + Handler: _Matcher_ScanSBOM_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "internalapi/scanner/v4/matcher_service.proto", diff --git a/generated/internalapi/scanner/v4/matcher_service_vtproto.pb.go b/generated/internalapi/scanner/v4/matcher_service_vtproto.pb.go index 508922531543e..1121b356f93ea 100644 --- a/generated/internalapi/scanner/v4/matcher_service_vtproto.pb.go +++ b/generated/internalapi/scanner/v4/matcher_service_vtproto.pb.go @@ -98,6 +98,45 @@ func (m *Metadata) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *ScanSBOMRequest) CloneVT() *ScanSBOMRequest { + if m == nil { + return (*ScanSBOMRequest)(nil) + } + r := new(ScanSBOMRequest) + r.MediaType = m.MediaType + if rhs := m.Sbom; rhs != nil { + tmpBytes := make([]byte, len(rhs)) + copy(tmpBytes, rhs) + r.Sbom = tmpBytes + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *ScanSBOMRequest) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *ScanSBOMResponse) CloneVT() *ScanSBOMResponse { + if m == nil { + return (*ScanSBOMResponse)(nil) + } + r := new(ScanSBOMResponse) + r.VulnerabilityReport = m.VulnerabilityReport.CloneVT() + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *ScanSBOMResponse) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (this *GetVulnerabilitiesRequest) EqualVT(that *GetVulnerabilitiesRequest) bool { if this == that { return true @@ -186,6 +225,47 @@ func (this *Metadata) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *ScanSBOMRequest) EqualVT(that *ScanSBOMRequest) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if string(this.Sbom) != string(that.Sbom) { + return false + } + if this.MediaType != that.MediaType { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *ScanSBOMRequest) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*ScanSBOMRequest) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *ScanSBOMResponse) EqualVT(that *ScanSBOMResponse) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if !this.VulnerabilityReport.EqualVT(that.VulnerabilityReport) { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *ScanSBOMResponse) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*ScanSBOMResponse) + if !ok { + return false + } + return this.EqualVT(that) +} func (m *GetVulnerabilitiesRequest) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -383,6 +463,96 @@ func (m *Metadata) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *ScanSBOMRequest) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ScanSBOMRequest) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *ScanSBOMRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.MediaType) > 0 { + i -= len(m.MediaType) + copy(dAtA[i:], m.MediaType) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.MediaType))) + i-- + dAtA[i] = 0x12 + } + if len(m.Sbom) > 0 { + i -= len(m.Sbom) + copy(dAtA[i:], m.Sbom) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Sbom))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *ScanSBOMResponse) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ScanSBOMResponse) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *ScanSBOMResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.VulnerabilityReport != nil { + size, err := m.VulnerabilityReport.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *GetVulnerabilitiesRequest) SizeVT() (n int) { if m == nil { return 0 @@ -455,6 +625,38 @@ func (m *Metadata) SizeVT() (n int) { return n } +func (m *ScanSBOMRequest) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Sbom) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.MediaType) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *ScanSBOMResponse) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.VulnerabilityReport != nil { + l = m.VulnerabilityReport.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + func (m *GetVulnerabilitiesRequest) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -929,7 +1131,7 @@ func (m *Metadata) UnmarshalVT(dAtA []byte) error { } return nil } -func (m *GetVulnerabilitiesRequest) UnmarshalVTUnsafe(dAtA []byte) error { +func (m *ScanSBOMRequest) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -952,17 +1154,17 @@ func (m *GetVulnerabilitiesRequest) UnmarshalVTUnsafe(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: GetVulnerabilitiesRequest: wiretype end group for non-group") + return fmt.Errorf("proto: ScanSBOMRequest: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: GetVulnerabilitiesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: ScanSBOMRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field HashId", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Sbom", wireType) } - var stringLen uint64 + var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return protohelpers.ErrIntOverflow @@ -972,33 +1174,31 @@ func (m *GetVulnerabilitiesRequest) UnmarshalVTUnsafe(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + byteLen |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { + if byteLen < 0 { return protohelpers.ErrInvalidLength } - postIndex := iNdEx + intStringLen + postIndex := iNdEx + byteLen if postIndex < 0 { return protohelpers.ErrInvalidLength } if postIndex > l { return io.ErrUnexpectedEOF } - var stringValue string - if intStringLen > 0 { - stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + m.Sbom = append(m.Sbom[:0], dAtA[iNdEx:postIndex]...) + if m.Sbom == nil { + m.Sbom = []byte{} } - m.HashId = stringValue iNdEx = postIndex case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Contents", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field MediaType", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return protohelpers.ErrIntOverflow @@ -1008,27 +1208,23 @@ func (m *GetVulnerabilitiesRequest) UnmarshalVTUnsafe(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return protohelpers.ErrInvalidLength } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return protohelpers.ErrInvalidLength } if postIndex > l { return io.ErrUnexpectedEOF } - if m.Contents == nil { - m.Contents = &Contents{} - } - if err := m.Contents.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { - return err - } + m.MediaType = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex @@ -1052,7 +1248,7 @@ func (m *GetVulnerabilitiesRequest) UnmarshalVTUnsafe(dAtA []byte) error { } return nil } -func (m *GetSBOMRequest) UnmarshalVTUnsafe(dAtA []byte) error { +func (m *ScanSBOMResponse) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -1075,17 +1271,17 @@ func (m *GetSBOMRequest) UnmarshalVTUnsafe(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: GetSBOMRequest: wiretype end group for non-group") + return fmt.Errorf("proto: ScanSBOMResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: GetSBOMRequest: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: ScanSBOMResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field VulnerabilityReport", wireType) } - var stringLen uint64 + var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return protohelpers.ErrIntOverflow @@ -1095,59 +1291,269 @@ func (m *GetSBOMRequest) UnmarshalVTUnsafe(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + msglen |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { + if msglen < 0 { return protohelpers.ErrInvalidLength } - postIndex := iNdEx + intStringLen + postIndex := iNdEx + msglen if postIndex < 0 { return protohelpers.ErrInvalidLength } if postIndex > l { return io.ErrUnexpectedEOF } - var stringValue string - if intStringLen > 0 { - stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) - } - m.Id = stringValue - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + if m.VulnerabilityReport == nil { + m.VulnerabilityReport = &VulnerabilityReport{} } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } + if err := m.VulnerabilityReport.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protohelpers.ErrInvalidLength + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err } - postIndex := iNdEx + intStringLen - if postIndex < 0 { + if (skippy < 0) || (iNdEx+skippy) < 0 { return protohelpers.ErrInvalidLength } - if postIndex > l { + if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } - var stringValue string + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetVulnerabilitiesRequest) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetVulnerabilitiesRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetVulnerabilitiesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field HashId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.HashId = stringValue + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Contents", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Contents == nil { + m.Contents = &Contents{} + } + if err := m.Contents.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetSBOMRequest) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetSBOMRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetSBOMRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.Id = stringValue + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string if intStringLen > 0 { stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) } @@ -1416,3 +1822,208 @@ func (m *Metadata) UnmarshalVTUnsafe(dAtA []byte) error { } return nil } +func (m *ScanSBOMRequest) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ScanSBOMRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ScanSBOMRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Sbom", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Sbom = dAtA[iNdEx:postIndex] + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MediaType", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var stringValue string + if intStringLen > 0 { + stringValue = unsafe.String(&dAtA[iNdEx], intStringLen) + } + m.MediaType = stringValue + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ScanSBOMResponse) UnmarshalVTUnsafe(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ScanSBOMResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ScanSBOMResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field VulnerabilityReport", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.VulnerabilityReport == nil { + m.VulnerabilityReport = &VulnerabilityReport{} + } + if err := m.VulnerabilityReport.UnmarshalVTUnsafe(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/go.mod b/go.mod index a945e11676afc..98f9f54f28c21 100644 --- a/go.mod +++ b/go.mod @@ -100,6 +100,7 @@ require ( github.com/openshift/runtime-utils v0.0.0-20230921210328-7bdb5b9c177b github.com/operator-framework/helm-operator-plugins v0.0.0-00010101000000-000000000000 github.com/owenrumney/go-sarif/v2 v2.3.3 + github.com/package-url/packageurl-go v0.1.3 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca @@ -426,7 +427,6 @@ require ( github.com/openshift-online/ocm-api-model/model v0.0.444 // indirect github.com/openshift/custom-resource-status v1.1.2 // indirect github.com/operator-framework/operator-lib v0.17.0 // indirect - github.com/package-url/packageurl-go v0.1.3 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect diff --git a/proto/internalapi/scanner/v4/matcher_service.proto b/proto/internalapi/scanner/v4/matcher_service.proto index bdf10a198daaf..9ebaee985928c 100644 --- a/proto/internalapi/scanner/v4/matcher_service.proto +++ b/proto/internalapi/scanner/v4/matcher_service.proto @@ -36,6 +36,18 @@ message Metadata { google.protobuf.Timestamp LastVulnerabilityUpdate = 1; } +message ScanSBOMRequest { + // Raw SBOM content. + bytes sbom = 1; + // Media type of the SBOM (e.g., "application/spdx+json"). + string media_type = 2; +} + +message ScanSBOMResponse { + // Vulnerability report including the parsed contents from the SBOM. + VulnerabilityReport vulnerability_report = 1; +} + // Matcher finds vulnerabilities in index reports. service Matcher { // GetVulnerabilities returns a VulnerabilityReport for a previously indexed manifest. @@ -46,4 +58,7 @@ service Matcher { // GetSBOM returns an SBOM for a previously indexed manifest. rpc GetSBOM(GetSBOMRequest) returns (GetSBOMResponse); + + // ScanSBOM decodes an SBOM and returns a VulnerabilityReport with matched vulnerabilities. + rpc ScanSBOM(ScanSBOMRequest) returns (ScanSBOMResponse); } diff --git a/scanner/cmd/scanner/main.go b/scanner/cmd/scanner/main.go index 0c20a36975dfa..3900363d08891 100644 --- a/scanner/cmd/scanner/main.go +++ b/scanner/cmd/scanner/main.go @@ -253,14 +253,13 @@ func (b *Backends) APIServices() []grpc.APIService { srvs = append(srvs, services.NewIndexerService(b.Indexer)) } if b.Matcher != nil { - // Set the index report getter to the remote indexer if available, otherwise the - // local indexer. A nil getter is ok, see implementation. - var getter indexer.ReportGetter - getter = b.RemoteIndexer - if getter == nil { - getter = b.Indexer + // Set the report provider to the remote indexer if available, otherwise the + // local indexer. A nil provider is ok, see implementation. + var provider indexer.ReportProvider + if b.RemoteIndexer != nil { + provider = b.RemoteIndexer } - srvs = append(srvs, services.NewMatcherService(b.Matcher, getter)) + srvs = append(srvs, services.NewMatcherService(b.Matcher, provider)) } return srvs } diff --git a/scanner/indexer/indexer.go b/scanner/indexer/indexer.go index e141e93244d7b..4a689ce965d69 100644 --- a/scanner/indexer/indexer.go +++ b/scanner/indexer/indexer.go @@ -50,6 +50,7 @@ import ( "github.com/stackrox/rox/scanner/indexer/manifest" "github.com/stackrox/rox/scanner/internal/httputil" "github.com/stackrox/rox/scanner/internal/version" + "github.com/stackrox/rox/scanner/sbom" ) var ( @@ -139,6 +140,13 @@ type ReportGetter interface { GetIndexReport(context.Context, string, bool) (*claircore.IndexReport, bool, error) } +// ReportProvider combines ReportGetter with repository-to-CPE mapping functionality (sbom.RepositoryToCPEProvider). +// Both Indexer and RemoteIndexer implement this interface. +type ReportProvider interface { + ReportGetter + sbom.RepositoryToCPEProvider +} + // ReportStorer stores a claircore.IndexReport. type ReportStorer interface { StoreIndexReport(ctx context.Context, hashID string, indexerVersion string, report *claircore.IndexReport) (string, error) diff --git a/scanner/matcher/mocks/matcher.go b/scanner/matcher/mocks/matcher.go index 3a0d5e00dfe1c..6a5ad3b3dd96c 100644 --- a/scanner/matcher/mocks/matcher.go +++ b/scanner/matcher/mocks/matcher.go @@ -127,3 +127,18 @@ func (mr *MockMatcherMockRecorder) Ready(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ready", reflect.TypeOf((*MockMatcher)(nil).Ready), ctx) } + +// ScanSBOM mocks base method. +func (m *MockMatcher) ScanSBOM(ctx context.Context, sbomBytes []byte, mediaType string) (*claircore.VulnerabilityReport, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ScanSBOM", ctx, sbomBytes, mediaType) + ret0, _ := ret[0].(*claircore.VulnerabilityReport) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ScanSBOM indicates an expected call of ScanSBOM. +func (mr *MockMatcherMockRecorder) ScanSBOM(ctx, sbomBytes, mediaType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockMatcher)(nil).ScanSBOM), ctx, sbomBytes, mediaType) +} diff --git a/scanner/sbom/sbom.go b/scanner/sbom/sbom.go index 85bd96bf3f48e..58b2dafab4efb 100644 --- a/scanner/sbom/sbom.go +++ b/scanner/sbom/sbom.go @@ -4,26 +4,106 @@ import ( "bytes" "context" "fmt" + "strings" + "github.com/package-url/packageurl-go" "github.com/pkg/errors" "github.com/quay/claircore" + "github.com/quay/claircore/purl" + "github.com/quay/claircore/python" + "github.com/quay/claircore/rhel" + "github.com/quay/claircore/sbom" "github.com/quay/claircore/sbom/spdx" + "github.com/quay/zlog" + "github.com/stackrox/rox/pkg/features" + "github.com/stackrox/rox/pkg/scannerv4/repositorytocpe" "github.com/stackrox/rox/scanner/internal/version" ) -type SBOMer struct { -} +// Supported media types for SBOM encoding/decoding. +const ( + MediaTypeSPDXJSON = "application/spdx+json" + MediaTypeSPDXText = "text/spdx+json" +) +// Options contains options for SBOM generation. type Options struct { Name string Namespace string Comment string } -func NewSBOMer() *SBOMer { - return &SBOMer{} +// RepositoryToCPEProvider provides repository-to-CPE mappings. +// Both Indexer and RemoteIndexer implement this interface. +type RepositoryToCPEProvider interface { + GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) +} + +// SBOMer handles both encoding (generation) and decoding (parsing) of SBOMs. +type SBOMer struct { + decoder sbom.Decoder } +// NewSBOMer creates a new SBOMer with the given repository-to-CPE provider. +// The provider is used to fetch CPE information for RPM packages during SBOM decoding. +func NewSBOMer(repo2cpeProvider RepositoryToCPEProvider) *SBOMer { + reg := purl.NewRegistry() + reg.RegisterPurlType(python.PURLType, purl.NoneNamespace, python.ParsePURL) + if repo2cpeProvider != nil { + reg.RegisterPurlType(rhel.PURLType, rhel.PURLNamespace, rhel.ParseRPMPURL, repo2CPETransformer(repo2cpeProvider)) + } else if features.SBOMScanning.Enabled() { + zlog.Warn(context.Background()).Msg("no repositoryToCPE provider configured") + } + + // Only support SPDX decoding. + decoder := spdx.NewDefaultDecoder(spdx.WithDecoderPURLConverter(reg)) + + return &SBOMer{decoder} +} + +// repo2CPETransformer creates a TransformerFunc that adds repository CPEs to RPM PURLs. +// It looks up the repository_id qualifier and adds the repository_cpes qualifier +// with the corresponding CPEs from the provider. +func repo2CPETransformer(provider RepositoryToCPEProvider) purl.TransformerFunc { + return func(ctx context.Context, p *packageurl.PackageURL) error { + if provider == nil { + return nil + } + + qualifiersMap := p.Qualifiers.Map() + repoID, ok := qualifiersMap[rhel.PURLRepositoryID] + if !ok { + return nil + } + + repo2cpe, err := provider.GetRepositoryToCPEMapping(ctx) + if err != nil || repo2cpe == nil { + // Best effort - continue without CPE enrichment. + return nil + } + + cpes, ok := repo2cpe.GetCPEs(repoID) + if !ok || len(cpes) == 0 { + zlog.Debug(ctx).Msgf("could not find repoid \"%s\" in cpe mapping", repoID) + // Repository not in mapping. + return nil + } + + // Add the repository_cpes qualifier as a comma-separated list. + // Don't overwrite if it's already set. + if _, exists := qualifiersMap[rhel.PURLRepositoryCPEs]; exists { + zlog.Debug(ctx).Msgf("found extra CPEs (not recorded): %s", strings.Join(cpes, ", ")) + return nil + } + + qualifiersMap[rhel.PURLRepositoryCPEs] = strings.Join(cpes, ",") + p.Qualifiers = packageurl.QualifiersFromMap(qualifiersMap) + + return nil + } +} + +// GetSBOM encodes an IndexReport into an SBOM. func (s *SBOMer) GetSBOM(ctx context.Context, ir *claircore.IndexReport, opts *Options) ([]byte, error) { if ir == nil { return nil, errors.New("index report is required") @@ -50,3 +130,26 @@ func (s *SBOMer) GetSBOM(ctx context.Context, ir *claircore.IndexReport, opts *O return b.Bytes(), nil } + +// Decode decodes an SBOM into an IndexReport. +// The mediaType specifies the format of the SBOM (e.g., "application/spdx+json"). +func (s *SBOMer) Decode(ctx context.Context, sbomData []byte, mediaType string) (*claircore.IndexReport, error) { + switch mediaType { + case MediaTypeSPDXJSON, MediaTypeSPDXText: + return s.decodeSPDX(ctx, sbomData) + default: + return nil, fmt.Errorf("unsupported media type: %s", mediaType) + } +} + +// decodeSPDX decodes an SPDX JSON SBOM into an IndexReport. +func (s *SBOMer) decodeSPDX(ctx context.Context, sbomData []byte) (*claircore.IndexReport, error) { + reader := bytes.NewReader(sbomData) + + ir, err := s.decoder.Decode(ctx, reader) + if err != nil { + return nil, fmt.Errorf("decoding SPDX SBOM: %w", err) + } + + return ir, nil +} diff --git a/scanner/sbom/sbom_test.go b/scanner/sbom/sbom_test.go index ab3530ea28b24..83c30c7513eac 100644 --- a/scanner/sbom/sbom_test.go +++ b/scanner/sbom/sbom_test.go @@ -10,7 +10,7 @@ import ( func TestGetSBOM(t *testing.T) { t.Run("error on nil index report", func(t *testing.T) { - s := NewSBOMer() + s := NewSBOMer(nil) _, err := s.GetSBOM(context.Background(), nil, nil) assert.ErrorContains(t, err, "index report is required") @@ -18,7 +18,7 @@ func TestGetSBOM(t *testing.T) { t.Run("error on nil opts", func(t *testing.T) { ir := &claircore.IndexReport{} - s := NewSBOMer() + s := NewSBOMer(nil) _, err := s.GetSBOM(context.Background(), ir, nil) assert.ErrorContains(t, err, "opts is required") @@ -26,7 +26,7 @@ func TestGetSBOM(t *testing.T) { t.Run("success", func(t *testing.T) { ir := &claircore.IndexReport{} - s := NewSBOMer() + s := NewSBOMer(nil) sbom, err := s.GetSBOM(context.Background(), ir, &Options{}) assert.NoError(t, err) diff --git a/scanner/services/matcher.go b/scanner/services/matcher.go index 64a04fbd2571f..0a69f01b9f24c 100644 --- a/scanner/services/matcher.go +++ b/scanner/services/matcher.go @@ -31,14 +31,15 @@ var matcherAuth = perrpc.FromMap(map[authz.Authorizer][]string{ v4.Matcher_GetVulnerabilities_FullMethodName, v4.Matcher_GetMetadata_FullMethodName, v4.Matcher_GetSBOM_FullMethodName, + v4.Matcher_ScanSBOM_FullMethodName, }, }) // matcherService represents a vulnerability matcher gRPC service. type matcherService struct { v4.UnimplementedMatcherServer - // indexer is used to retrieve index reports. - indexer indexer.ReportGetter + // indexer is used to retrieve index reports and provide repo-to-CPE mappings for SBOM decoding. + indexer indexer.ReportProvider // matcher is used to match vulnerabilities with index contents. matcher matcher.Matcher // sbomer is used to generate SBOMs from index reports. @@ -49,13 +50,14 @@ type matcherService struct { anonymousAuthEnabled bool } -// NewMatcherService creates a new vulnerability matcher gRPC service, to enable -// empty content in enrich requests, pass a non-nil indexer. -func NewMatcherService(matcher matcher.Matcher, indexer indexer.ReportGetter) *matcherService { +// NewMatcherService creates a new vulnerability matcher gRPC service. +// The indexer is used to retrieve index reports and provide repo-to-CPE mappings for SBOM decoding. +// To enable empty content in enrich requests, pass a non-nil indexer. +func NewMatcherService(matcher matcher.Matcher, indexer indexer.ReportProvider) *matcherService { return &matcherService{ matcher: matcher, indexer: indexer, - sbomer: sbom.NewSBOMer(), + sbomer: sbom.NewSBOMer(indexer), disableEmptyContents: indexer == nil, anonymousAuthEnabled: env.ScannerV4AnonymousAuth.BooleanSetting(), } @@ -214,3 +216,48 @@ func (s *matcherService) GetSBOM(ctx context.Context, req *v4.GetSBOMRequest) (* return &v4.GetSBOMResponse{Sbom: sbomBytes}, nil } + +func (s *matcherService) ScanSBOM(ctx context.Context, req *v4.ScanSBOMRequest) (*v4.ScanSBOMResponse, error) { + ctx = zlog.ContextWithValues(ctx, + "component", "scanner/service/matcher.ScanSBOM", + "media_type", req.GetMediaType(), + ) + + if err := validators.ValidateScanSBOMRequest(req); err != nil { + return nil, errox.InvalidArgs.CausedBy(err) + } + if err := s.matcher.Initialized(ctx); err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "the matcher is not initialized: %v", err) + } + + zlog.Info(ctx). + Int("sbom_size", len(req.GetSbom())). + Msg("scanning SBOM") + + ir, err := s.sbomer.Decode(ctx, req.GetSbom(), req.GetMediaType()) + if err != nil { + zlog.Error(ctx).Err(err).Msg("decoding SBOM failed") + return nil, fmt.Errorf("decoding SBOM: %w", err) + } + + zlog.Debug(ctx). + Int("packages", len(ir.Packages)). + Int("distributions", len(ir.Distributions)). + Int("repositories", len(ir.Repositories)). + Msg("decoded SBOM") + + ccReport, err := s.matcher.GetVulnerabilities(ctx, ir) + if err != nil { + zlog.Error(ctx).Err(err).Msg("matching vulnerabilities failed") + return nil, fmt.Errorf("matching vulnerabilities: %w", err) + } + + report, err := mappers.ToProtoV4VulnerabilityReport(ctx, ccReport) + if err != nil { + zlog.Error(ctx).Err(err).Msg("internal error: converting to v4.VulnerabilityReport") + return nil, fmt.Errorf("converting vulnerability report: %w", err) + } + report.Notes = s.notes(ctx, report) + + return &v4.ScanSBOMResponse{VulnerabilityReport: report}, nil +} diff --git a/scanner/services/validators/validators.go b/scanner/services/validators/validators.go index d72118d42da81..eb57840aa7a9e 100644 --- a/scanner/services/validators/validators.go +++ b/scanner/services/validators/validators.go @@ -11,6 +11,7 @@ import ( "github.com/quay/claircore/toolkit/types/cpe" v4 "github.com/stackrox/rox/generated/internalapi/scanner/v4" "github.com/stackrox/rox/pkg/errox" + "github.com/stackrox/rox/scanner/sbom" ) // hasIDAndCPE is the common interface offered by some proto definitions related @@ -94,6 +95,26 @@ func ValidateGetSBOMRequest(req *v4.GetSBOMRequest) error { return nil } +var supportedSBOMMediaTypes = map[string]struct{}{ + sbom.MediaTypeSPDXJSON: {}, + sbom.MediaTypeSPDXText: {}, +} + +// ValidateScanSBOMRequest validates a ScanSBOMRequest. +func ValidateScanSBOMRequest(req *v4.ScanSBOMRequest) error { + if req == nil { + return errox.InvalidArgs.New("empty request") + } + if len(req.GetSbom()) == 0 { + return errox.InvalidArgs.New("sbom is required") + } + mediaType := strings.TrimSpace(strings.Split(req.GetMediaType(), ";")[0]) + if _, ok := supportedSBOMMediaTypes[mediaType]; !ok { + return errox.InvalidArgs.Newf("unsupported media type: %q", req.GetMediaType()) + } + return nil +} + func validateContents(contents *v4.Contents) error { if contents == nil { return nil From d8c20d888325e8b7cd626e6a3de42f826b3903cd Mon Sep 17 00:00:00 2001 From: Brad Lugo Date: Sun, 25 Jan 2026 23:52:10 -0800 Subject: [PATCH 12/12] feat: add ScanSBOM scanner client method --- pkg/scanners/scannerv4/scannerv4.go | 106 ++------------------------- pkg/scannerv4/client/client.go | 33 +++++++++ pkg/scannerv4/client/mocks/client.go | 20 +++++ 3 files changed, 60 insertions(+), 99 deletions(-) diff --git a/pkg/scanners/scannerv4/scannerv4.go b/pkg/scanners/scannerv4/scannerv4.go index 7e8cdc12c8880..cacecd18c1c7e 100644 --- a/pkg/scanners/scannerv4/scannerv4.go +++ b/pkg/scanners/scannerv4/scannerv4.go @@ -139,26 +139,17 @@ func (s *scannerv4) ScanSBOM(ctx context.Context, sbomReader io.Reader, contentT ctx, cancel := context.WithTimeout(ctx, scanTimeout) defer cancel() + sbomBytes, err := io.ReadAll(sbomReader) + if err != nil { + return nil, fmt.Errorf("reading sbom: %w", err) + } + var scannerVersion pkgscanner.Version - scannerVersion.Matcher = "v7" - // TODO(ROX-30570): START Remove - // Read all data from the SBOM reader and throw it away (testing purposes only) - _ = ctx - dataB, err := io.ReadAll(sbomReader) + vr, err := s.scannerClient.ScanSBOM(ctx, sbomBytes, contentType, client.Version(&scannerVersion)) if err != nil { - return nil, fmt.Errorf("reading sbom data: %w", err) + return nil, fmt.Errorf("scanning sbom: %w", err) } - log.Debugf("Scanned SBOM: %s", dataB) - // Create a fake vuln report for testing purposes - vr := fakeVulnReport() - // TODO(ROX-30570): END Remove - - // TODO(ROX-30570): Replace with actual scanner client call - // vr, err := s.scannerClient.ScanSBOM(ctx, sbomReader, contentType, client.Version(&scannerVersion)) - // if err != nil { - // return nil, fmt.Errorf("scanning sbom: %w", err) - // } scannerVersionStr, err := scannerVersion.Encode() if err != nil { @@ -171,89 +162,6 @@ func (s *scannerv4) ScanSBOM(ctx context.Context, sbomReader io.Reader, contentT }, nil } -// fakeVulnReport generates a fake vuln report for testing purposes. -// -// TODO(ROX-30570): REMOVE -func fakeVulnReport() *v4.VulnerabilityReport { - return &v4.VulnerabilityReport{ - HashId: "fake HashId", - Vulnerabilities: map[string]*v4.VulnerabilityReport_Vulnerability{ - "v1": { - Name: "Fake Vuln #1", - NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_CRITICAL, - CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ - { - V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ - Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N", - }, - Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, - Url: "https://nvd.nist.gov/vuln/detail/CVE-5678-1234", - }, - }, - }, - "v2": { - Name: "Fake Vuln #2", - NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_IMPORTANT, - CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ - { - V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ - Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N", - }, - Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, - Url: "https://nvd.nist.gov/vuln/detail/CVE-5678-1234", - }, - }, - }, - "v3": { - Name: "Fake Vuln #3", - NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_MODERATE, - - CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ - { - V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ - BaseScore: 8.2, - Vector: "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:H", - }, - Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_RED_HAT, - Url: "https://access.redhat.com/security/cve/CVE-1234-567", - }, - { - V2: &v4.VulnerabilityReport_Vulnerability_CVSS_V2{ - BaseScore: 6.4, - Vector: "AV:N/AC:M/Au:M/C:C/I:N/A:P", - }, - Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, - Url: "https://nvd.nist.gov/vuln/detail/CVE-1234-567", - }, - }, - }, - "v4": { - Name: "Fake Vuln #4", - NormalizedSeverity: v4.VulnerabilityReport_Vulnerability_SEVERITY_LOW, - CvssMetrics: []*v4.VulnerabilityReport_Vulnerability_CVSS{ - { - V3: &v4.VulnerabilityReport_Vulnerability_CVSS_V3{ - Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N", - }, - Source: v4.VulnerabilityReport_Vulnerability_CVSS_SOURCE_NVD, - Url: "https://nvd.nist.gov/vuln/detail/CVE-5678-1234", - }, - }, - }, - }, - PackageVulnerabilities: map[string]*v4.StringList{ - "p1": {Values: []string{"v1", "v2", "v3", "v4"}}, - "p2": {Values: []string{"v3", "v4"}}, - }, - Contents: &v4.Contents{ - Packages: map[string]*v4.Package{ - "p1": {Name: "Fake Package #1", Version: "v1.0.0"}, - "p2": {Name: "Fake Package #2", Version: "v2.3.4"}, - }, - }, - } -} - func sbomScan(vr *v4.VulnerabilityReport, scannerVersionStr string) *v1.SBOMScanResponse_SBOMScan { imageScan := imageScan(nil, vr, scannerVersionStr) diff --git a/pkg/scannerv4/client/client.go b/pkg/scannerv4/client/client.go index dfef9e04c13df..bdfb343ed8268 100644 --- a/pkg/scannerv4/client/client.go +++ b/pkg/scannerv4/client/client.go @@ -102,6 +102,9 @@ type Scanner interface { // GetRepositoryToCPEMapping returns the repository-to-CPE mapping from the indexer. GetRepositoryToCPEMapping(ctx context.Context) (*repositorytocpe.MappingFile, error) + // ScanSBOM decodes an SBOM and returns a vulnerability report. + ScanSBOM(ctx context.Context, sbom []byte, mediaType string, callOpts ...CallOption) (*v4.VulnerabilityReport, error) + // Close cleans up any resources used by the implementation. Close() error } @@ -485,6 +488,36 @@ func (c *gRPCScanner) GetRepositoryToCPEMapping(ctx context.Context) (*repositor return &repositorytocpe.MappingFile{Data: data}, nil } +// ScanSBOM calls the Matcher's gRPC endpoint ScanSBOM to decode an SBOM and return vulnerabilities. +func (c *gRPCScanner) ScanSBOM(ctx context.Context, sbom []byte, mediaType string, callOpts ...CallOption) (*v4.VulnerabilityReport, error) { + if c.matcher == nil { + return nil, errMatcherNotConfigured + } + + ctx = zlog.ContextWithValues(ctx, "component", "scanner/client", "method", "ScanSBOM") + + options := makeCallOptions(callOpts...) + + req := &v4.ScanSBOMRequest{ + Sbom: sbom, + MediaType: mediaType, + } + var resp *v4.ScanSBOMResponse + var responseMetadata metadata.MD + err := retryWithBackoff(ctx, defaultBackoff(), "matcher.ScanSBOM", func() error { + var err error + resp, err = c.matcher.ScanSBOM(ctx, req, grpc.Header(&responseMetadata)) + return err + }) + if err != nil { + return nil, fmt.Errorf("scan SBOM: %w", err) + } + + setMatcherVersion(options, responseMetadata) + + return resp.GetVulnerabilityReport(), nil +} + func getImageManifestID(ref name.Digest) string { return fmt.Sprintf("/v4/containerimage/%s", ref.DigestStr()) } diff --git a/pkg/scannerv4/client/mocks/client.go b/pkg/scannerv4/client/mocks/client.go index 98930263f3d44..195ac74b17bd3 100644 --- a/pkg/scannerv4/client/mocks/client.go +++ b/pkg/scannerv4/client/mocks/client.go @@ -196,6 +196,26 @@ func (mr *MockScannerMockRecorder) IndexAndScanImage(arg0, arg1, arg2, arg3 any, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexAndScanImage", reflect.TypeOf((*MockScanner)(nil).IndexAndScanImage), varargs...) } +// ScanSBOM mocks base method. +func (m *MockScanner) ScanSBOM(ctx context.Context, sbom []byte, mediaType string, callOpts ...client.CallOption) (*v4.VulnerabilityReport, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, sbom, mediaType} + for _, a := range callOpts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ScanSBOM", varargs...) + ret0, _ := ret[0].(*v4.VulnerabilityReport) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ScanSBOM indicates an expected call of ScanSBOM. +func (mr *MockScannerMockRecorder) ScanSBOM(ctx, sbom, mediaType any, callOpts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, sbom, mediaType}, callOpts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanSBOM", reflect.TypeOf((*MockScanner)(nil).ScanSBOM), varargs...) +} + // StoreImageIndex mocks base method. func (m *MockScanner) StoreImageIndex(ctx context.Context, ref name.Digest, indexerVersion string, contents *v4.Contents, callOpts ...client.CallOption) error { m.ctrl.T.Helper()