Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions central/image/service/http_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,25 @@ 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"
)

type sbomHttpHandler struct {
type sbomGenHttpHandler struct {
integration integration.Set
enricher enricher.ImageEnricher
enricherV2 enricher.ImageEnricherV2
clusterSACHelper sachelper.ClusterSacHelper
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,
Expand All @@ -50,7 +51,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
Expand Down Expand Up @@ -94,7 +95,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,
Expand All @@ -121,7 +122,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

Expand Down Expand Up @@ -167,7 +168,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,
Expand Down Expand Up @@ -268,33 +269,26 @@ func addForceToEnrichmentContext(enrichmentCtx *enricher.EnrichmentContext) {
}

// getScannerV4SBOMIntegration returns the SBOM interface of Scanner V4.
func (h sbomHttpHandler) 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")
func (h sbomGenHttpHandler) getScannerV4SBOMIntegration() (scannerTypes.SBOMer, error) {
sbomer, _, err := getScannerV4SBOMIntegration(h.integration.ScannerSet())
return 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)
}
return h.saveImageV1(img)
}

// 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
Expand All @@ -308,7 +302,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")
}
Expand All @@ -328,3 +322,14 @@ func (h sbomHttpHandler) 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")
}
18 changes: 10 additions & 8 deletions central/image/service/http_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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{
Expand All @@ -154,7 +155,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()
Expand All @@ -172,7 +173,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()
Expand All @@ -189,7 +190,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()
Expand All @@ -205,7 +206,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()
Expand All @@ -222,7 +223,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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -312,7 +314,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)
Expand Down
149 changes: 149 additions & 0 deletions central/image/service/sbom_scan_http_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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(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", maxBytesErr.Limit))
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(ctx context.Context, 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(ctx, 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) {
sbomer, dataSource, err := getScannerV4SBOMIntegration(s.integrations.ScannerSet())
return sbomer, dataSource, err
}

// 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
}
Loading
Loading