diff --git a/adapters/v1/sidecar.go b/adapters/v1/sidecar.go index 8100d761..2e920a86 100644 --- a/adapters/v1/sidecar.go +++ b/adapters/v1/sidecar.go @@ -24,6 +24,7 @@ type SidecarSBOMAdapter struct { client sbomscanner.SBOMScannerClient maxImageSize int64 maxSBOMSize int + proxyRegistryMap map[string]string scanTimeout time.Duration scanEmbeddedSBOMs bool memoryLimit string @@ -45,11 +46,13 @@ func NewSidecarSBOMAdapter( maxSBOMSize int, scanEmbeddedSBOMs bool, memoryLimit string, + proxyRegistryMap map[string]string, ) *SidecarSBOMAdapter { return &SidecarSBOMAdapter{ client: client, maxImageSize: maxImageSize, maxSBOMSize: maxSBOMSize, + proxyRegistryMap: proxyRegistryMap, scanTimeout: scanTimeout, scanEmbeddedSBOMs: scanEmbeddedSBOMs, memoryLimit: memoryLimit, @@ -75,9 +78,12 @@ func (s *SidecarSBOMAdapter) CreateSBOM(ctx context.Context, name, imageID, imag } domainSBOM.Annotations[helpersv1.ImageTagMetadataKey] = imageTag + pullImageID := rewriteImageRef(imageID, s.proxyRegistryMap) + pullImageTag := rewriteImageRef(imageTag, s.proxyRegistryMap) + req := sbomscanner.ScanRequest{ - ImageID: imageID, - ImageTag: imageTag, + ImageID: pullImageID, + ImageTag: pullImageTag, Options: options, MaxImageSize: s.maxImageSize, MaxSBOMSize: int32(s.maxSBOMSize), diff --git a/adapters/v1/sidecar_test.go b/adapters/v1/sidecar_test.go index 26b8a6dc..928bae30 100644 --- a/adapters/v1/sidecar_test.go +++ b/adapters/v1/sidecar_test.go @@ -59,7 +59,7 @@ func TestSidecarSBOMAdapter_CreateSBOM_Success(t *testing.T) { }, } - adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi") + adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi", nil) sbom, err := adapter.CreateSBOM(context.Background(), "test-sbom", "", "nginx:latest", domain.RegistryOptions{}) require.NoError(t, err) @@ -81,7 +81,7 @@ func TestSidecarSBOMAdapter_CreateSBOM_TooLarge(t *testing.T) { }, } - adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi") + adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi", nil) sbom, err := adapter.CreateSBOM(context.Background(), "test-sbom", "", "large-image:latest", domain.RegistryOptions{}) require.NoError(t, err) @@ -100,7 +100,7 @@ func TestSidecarSBOMAdapter_CreateSBOM_CrashRetry(t *testing.T) { }, } - adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi") + adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi", nil) // First two attempts should return crash error (for retry) for i := 0; i < 2; i++ { @@ -122,6 +122,6 @@ func TestSidecarSBOMAdapter_Version(t *testing.T) { healthReady: true, } - adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi") + adapter := NewSidecarSBOMAdapter(mock, 5*time.Minute, 512*1024*1024, 20*1024*1024, false, "5Gi", nil) assert.Equal(t, "v0.100.0", adapter.Version()) } diff --git a/adapters/v1/syft.go b/adapters/v1/syft.go index 334a80b3..e84d4c83 100644 --- a/adapters/v1/syft.go +++ b/adapters/v1/syft.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "runtime" + "sort" "strings" "sync" "time" @@ -52,6 +53,7 @@ func gcpCredentials(ctx context.Context) (*image.RegistryCredentials, error) { type SyftAdapter struct { maxImageSize int64 maxSBOMSize int + proxyRegistryMap map[string]string pullMutex sync.Mutex scanTimeout time.Duration scanEmbeddedSBOMs bool @@ -62,15 +64,44 @@ const digestDelim = "@" var _ ports.SBOMCreator = (*SyftAdapter)(nil) // NewSyftAdapter initializes the SyftAdapter struct -func NewSyftAdapter(scanTimeout time.Duration, maxImageSize int64, maxSBOMSize int, scanEmbeddedSBOMs bool) *SyftAdapter { +func NewSyftAdapter(scanTimeout time.Duration, maxImageSize int64, maxSBOMSize int, scanEmbeddedSBOMs bool, proxyRegistryMap map[string]string) *SyftAdapter { return &SyftAdapter{ maxImageSize: maxImageSize, maxSBOMSize: maxSBOMSize, + proxyRegistryMap: proxyRegistryMap, scanTimeout: scanTimeout, scanEmbeddedSBOMs: scanEmbeddedSBOMs, } } +func rewriteImageRef(imageRef string, proxyMap map[string]string) string { + if len(proxyMap) == 0 { + return imageRef + } + keys := make([]string, 0, len(proxyMap)) + for k := range proxyMap { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) }) + for _, original := range keys { + proxy := strings.TrimRight(proxyMap[original], "/") + if proxy == "" { + continue + } + if strings.HasPrefix(imageRef, original+"/") { + return proxy + imageRef[len(original):] + } + // treat docker.io and index.docker.io as the same registry + if original == "docker.io" && strings.HasPrefix(imageRef, "index.docker.io/") { + return proxy + imageRef[len("index.docker.io"):] + } + if original == "index.docker.io" && strings.HasPrefix(imageRef, "docker.io/") { + return proxy + imageRef[len("docker.io"):] + } + } + return imageRef +} + func NormalizeImageID(imageID, imageTag string) string { // registry scanning doesn't provide imageID, so we use imageTag as a reference if imageID == "" { @@ -158,13 +189,15 @@ func (s *SyftAdapter) CreateSBOM(ctx context.Context, name, imageID, imageTag st logger.L().Debug("downloading image", helpers.String("imageID", imageID)) ctxWithSize := context.WithValue(context.Background(), image.MaxImageSize, s.maxImageSize) - src, err := syft.GetSource(ctxWithSize, imageID, syft.DefaultGetSourceConfig().WithRegistryOptions(®istryOptions).WithSources("registry")) + pullRef := rewriteImageRef(imageID, s.proxyRegistryMap) + src, err := syft.GetSource(ctxWithSize, pullRef, syft.DefaultGetSourceConfig().WithRegistryOptions(®istryOptions).WithSources("registry")) if err != nil && strings.Contains(err.Error(), "MANIFEST_UNKNOWN") { logger.L().Debug("got MANIFEST_UNKNOWN, retrying with imageTag", helpers.String("imageTag", imageTag), helpers.String("imageID", imageID)) - src, err = syft.GetSource(ctxWithSize, imageTag, syft.DefaultGetSourceConfig().WithRegistryOptions(®istryOptions).WithSources("registry")) + pullRef = rewriteImageRef(imageTag, s.proxyRegistryMap) + src, err = syft.GetSource(ctxWithSize, pullRef, syft.DefaultGetSourceConfig().WithRegistryOptions(®istryOptions).WithSources("registry")) } if err != nil && strings.Contains(err.Error(), "401 Unauthorized") { @@ -183,7 +216,7 @@ func (s *SyftAdapter) CreateSBOM(ctx context.Context, name, imageID, imageTag st logger.L().Debug("got 401, retrying without credentials", helpers.String("imageID", imageID)) registryOptions.Credentials = nil - src, err = syft.GetSource(ctxWithSize, imageID, syft.DefaultGetSourceConfig().WithRegistryOptions(®istryOptions).WithSources("registry")) + src, err = syft.GetSource(ctxWithSize, rewriteImageRef(imageID, s.proxyRegistryMap), syft.DefaultGetSourceConfig().WithRegistryOptions(®istryOptions).WithSources("registry")) } } diff --git a/adapters/v1/syft_test.go b/adapters/v1/syft_test.go index b3661146..4baa72b9 100644 --- a/adapters/v1/syft_test.go +++ b/adapters/v1/syft_test.go @@ -164,7 +164,7 @@ func Test_syftAdapter_CreateSBOM(t *testing.T) { if tt.scanTimeout > 0 { scanTimeout = tt.scanTimeout } - s := NewSyftAdapter(scanTimeout, maxImageSize, maxSBOMSize, tt.scanEmbeddedSBOMs) + s := NewSyftAdapter(scanTimeout, maxImageSize, maxSBOMSize, tt.scanEmbeddedSBOMs, nil) got, err := s.CreateSBOM(context.TODO(), "name", tt.imageID, tt.imageTag, tt.options) if (err != nil) != tt.wantErr { t.Errorf("CreateSBOM() error = %v, wantErr %v", err, tt.wantErr) @@ -207,7 +207,7 @@ func TestIsGCPRegistry(t *testing.T) { } func Test_syftAdapter_Version(t *testing.T) { - s := NewSyftAdapter(5*time.Minute, 512*1024*1024, 20*1024*1024, false) + s := NewSyftAdapter(5*time.Minute, 512*1024*1024, 20*1024*1024, false, nil) version := s.Version() assert.NotEqual(t, version, "") } @@ -225,7 +225,7 @@ func Test_syftAdapter_transformations(t *testing.T) { sbom := toSyftModel(d) // Convert to domain.sbom - s := NewSyftAdapter(5*time.Minute, 512*1024*1024, 20*1024*1024, false) + s := NewSyftAdapter(5*time.Minute, 512*1024*1024, 20*1024*1024, false, nil) domainSBOM, err := s.syftToDomain(*sbom) require.NoError(t, err) @@ -286,3 +286,90 @@ func TestNormalizeImageID(t *testing.T) { }) } } + +func TestRewriteImageRef(t *testing.T) { + tests := []struct { + name string + imageRef string + proxyMap map[string]string + want string + }{ + { + name: "nil map returns original", + imageRef: "docker.io/library/nginx:latest", + proxyMap: nil, + want: "docker.io/library/nginx:latest", + }, + { + name: "empty map returns original", + imageRef: "docker.io/library/nginx:latest", + proxyMap: map[string]string{}, + want: "docker.io/library/nginx:latest", + }, + { + name: "empty proxy value is skipped, returns original", + imageRef: "docker.io/library/nginx:latest", + proxyMap: map[string]string{"docker.io": ""}, + want: "docker.io/library/nginx:latest", + }, + { + name: "trailing slash in proxy value is stripped", + imageRef: "docker.io/library/nginx:latest", + proxyMap: map[string]string{"docker.io": "mirror.io/"}, + want: "mirror.io/library/nginx:latest", + }, + { + name: "basic rewrite", + imageRef: "docker.io/library/alpine:3.18", + proxyMap: map[string]string{"docker.io": "mirror.example.com"}, + want: "mirror.example.com/library/alpine:3.18", + }, + { + name: "index.docker.io in ref matched by docker.io key", + imageRef: "index.docker.io/library/alpine:latest", + proxyMap: map[string]string{"docker.io": "mirror.example.com"}, + want: "mirror.example.com/library/alpine:latest", + }, + { + name: "docker.io in ref matched by index.docker.io key", + imageRef: "docker.io/library/alpine:latest", + proxyMap: map[string]string{"index.docker.io": "mirror.example.com"}, + want: "mirror.example.com/library/alpine:latest", + }, + { + name: "digest ref is rewritten", + imageRef: "docker.io/library/alpine@sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501", + proxyMap: map[string]string{"docker.io": "mirror.example.com"}, + want: "mirror.example.com/library/alpine@sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501", + }, + { + name: "non-matching prefix returns original", + imageRef: "quay.io/kubescape/kubevuln:latest", + proxyMap: map[string]string{"docker.io": "mirror.example.com"}, + want: "quay.io/kubescape/kubevuln:latest", + }, + { + name: "longest prefix wins over shorter prefix", + imageRef: "docker.io/library/nginx:latest", + proxyMap: map[string]string{ + "docker.io": "generic-mirror.example.com", + "docker.io/library": "library-mirror.example.com", + }, + want: "library-mirror.example.com/nginx:latest", + }, + { + name: "multiple entries, correct one matches", + imageRef: "quay.io/kubescape/kubevuln:latest", + proxyMap: map[string]string{ + "docker.io": "mirror.example.com", + "quay.io": "quay-mirror.example.com", + }, + want: "quay-mirror.example.com/kubescape/kubevuln:latest", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, rewriteImageRef(tt.imageRef, tt.proxyMap)) + }) + } +} diff --git a/cmd/http/main.go b/cmd/http/main.go index c693438f..178c9ff4 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -76,13 +76,13 @@ func main() { if err != nil { logger.L().Warning("failed to connect to SBOM scanner sidecar, falling back to in-process Syft", helpers.Error(err)) - sbomAdapter = v1.NewSyftAdapter(c.ScanTimeout, c.MaxImageSize, c.MaxSBOMSize, c.ScanEmbeddedSboms) + sbomAdapter = v1.NewSyftAdapter(c.ScanTimeout, c.MaxImageSize, c.MaxSBOMSize, c.ScanEmbeddedSboms, c.ProxyRegistryMap) } else { memoryLimit := os.Getenv("SCANNER_MEMORY_LIMIT") - sbomAdapter = v1.NewSidecarSBOMAdapter(scannerClient, c.ScanTimeout, c.MaxImageSize, c.MaxSBOMSize, c.ScanEmbeddedSboms, memoryLimit) + sbomAdapter = v1.NewSidecarSBOMAdapter(scannerClient, c.ScanTimeout, c.MaxImageSize, c.MaxSBOMSize, c.ScanEmbeddedSboms, memoryLimit, c.ProxyRegistryMap) } } else { - sbomAdapter = v1.NewSyftAdapter(c.ScanTimeout, c.MaxImageSize, c.MaxSBOMSize, c.ScanEmbeddedSboms) + sbomAdapter = v1.NewSyftAdapter(c.ScanTimeout, c.MaxImageSize, c.MaxSBOMSize, c.ScanEmbeddedSboms, c.ProxyRegistryMap) } cveAdapter := v1.NewGrypeAdapter(c.ListingURL, c.UseDefaultMatchers) diff --git a/config/config.go b/config/config.go index 1b392e6e..09e15524 100644 --- a/config/config.go +++ b/config/config.go @@ -21,8 +21,9 @@ type Config struct { NodeSbomGeneration bool `mapstructure:"nodeSbomGeneration"` PartialRelevancy bool `mapstructure:"partialRelevancy"` ScanConcurrency int `mapstructure:"scanConcurrency"` - ScanEmbeddedSboms bool `mapstructure:"scanEmbeddedSBOMs"` - ScanTimeout time.Duration `mapstructure:"scanTimeout"` + ProxyRegistryMap map[string]string `mapstructure:"proxyRegistryMap"` + ScanEmbeddedSboms bool `mapstructure:"scanEmbeddedSBOMs"` + ScanTimeout time.Duration `mapstructure:"scanTimeout"` Storage bool `mapstructure:"storage"` StoreFilteredSbom bool `mapstructure:"storeFilteredSbom"` UseDefaultMatchers bool `mapstructure:"useDefaultMatchers"`