diff --git a/pkg/leeway/cache/remote/s3.go b/pkg/leeway/cache/remote/s3.go index df5f21ad..33ce113b 100644 --- a/pkg/leeway/cache/remote/s3.go +++ b/pkg/leeway/cache/remote/s3.go @@ -1446,23 +1446,11 @@ func (s *S3Cache) uploadProvenanceBundle(ctx context.Context, packageName, artif }).Debug("Successfully uploaded provenance bundle to remote cache") } -// SBOM file extensions - must match pkg/leeway/sbom.go constants -const ( - sbomBaseFilename = "sbom" - sbomCycloneDXFileExtension = ".cdx.json" - sbomSPDXFileExtension = ".spdx.json" - sbomSyftFileExtension = ".json" -) - // uploadSBOMFiles uploads SBOM files to S3 with retry logic. // This is a non-blocking operation - failures are logged but don't fail the build. // SBOM files are stored alongside artifacts as .sbom. func (s *S3Cache) uploadSBOMFiles(ctx context.Context, packageName, artifactKey, localPath string) { - sbomExtensions := []string{ - "." + sbomBaseFilename + sbomCycloneDXFileExtension, - "." + sbomBaseFilename + sbomSPDXFileExtension, - "." + sbomBaseFilename + sbomSyftFileExtension, - } + sbomExtensions := cache.SBOMSidecarExtensions() for _, ext := range sbomExtensions { sbomPath := localPath + ext @@ -1510,11 +1498,7 @@ func (s *S3Cache) uploadSBOMFiles(ctx context.Context, packageName, artifactKey, // This is a best-effort operation - missing SBOMs are expected for older artifacts. // SBOM files are stored alongside artifacts as .sbom. func (s *S3Cache) downloadSBOMFiles(ctx context.Context, packageName, artifactKey, localPath string) { - sbomExtensions := []string{ - "." + sbomBaseFilename + sbomCycloneDXFileExtension, - "." + sbomBaseFilename + sbomSPDXFileExtension, - "." + sbomBaseFilename + sbomSyftFileExtension, - } + sbomExtensions := cache.SBOMSidecarExtensions() for _, ext := range sbomExtensions { sbomPath := localPath + ext diff --git a/pkg/leeway/cache/types.go b/pkg/leeway/cache/types.go index 826589a3..f284bd75 100644 --- a/pkg/leeway/cache/types.go +++ b/pkg/leeway/cache/types.go @@ -1,5 +1,12 @@ // Package cache provides local and remote caching capabilities for build artifacts. // +// SBOM Sidecar Files: +// SBOM (Software Bill of Materials) files are stored alongside artifacts as sidecar files. +// The naming convention is: . where extension is one of: +// - .sbom.cdx.json (CycloneDX format) +// - .sbom.spdx.json (SPDX format) +// - .sbom.json (Syft native format) +// // SLSA Verification Behavior: // The cache system supports SLSA (Supply-chain Levels for Software Artifacts) verification // for enhanced security. The behavior is controlled by the SLSAConfig.RequireAttestation field: @@ -27,6 +34,31 @@ import ( "context" ) +// SBOM file format constants +const ( + // SBOMBaseFilename is the base filename for SBOM files (e.g., "sbom" in "artifact.sbom.cdx.json") + SBOMBaseFilename = "sbom" + + // SBOMCycloneDXFileExtension is the extension of the CycloneDX SBOM file + SBOMCycloneDXFileExtension = ".cdx.json" + + // SBOMSPDXFileExtension is the extension of the SPDX SBOM file + SBOMSPDXFileExtension = ".spdx.json" + + // SBOMSyftFileExtension is the extension of the Syft SBOM file + SBOMSyftFileExtension = ".json" +) + +// SBOMSidecarExtensions returns all SBOM sidecar file extensions. +// These are the extensions used for SBOM files stored alongside artifacts. +func SBOMSidecarExtensions() []string { + return []string{ + "." + SBOMBaseFilename + SBOMCycloneDXFileExtension, // .sbom.cdx.json + "." + SBOMBaseFilename + SBOMSPDXFileExtension, // .sbom.spdx.json + "." + SBOMBaseFilename + SBOMSyftFileExtension, // .sbom.json + } +} + // Package represents a build package that can be cached type Package interface { // Version returns a unique identifier for the package diff --git a/pkg/leeway/sbom.go b/pkg/leeway/sbom.go index 54bed9b2..96bf4a33 100644 --- a/pkg/leeway/sbom.go +++ b/pkg/leeway/sbom.go @@ -25,6 +25,7 @@ import ( "github.com/anchore/syft/syft/format/syftjson" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" + "github.com/gitpod-io/leeway/pkg/leeway/cache" "github.com/google/uuid" log "github.com/sirupsen/logrus" "golang.org/x/xerrors" @@ -39,18 +40,6 @@ const ( // EnvvarVulnReportsDir names the environment variable we take the vulnerability reports directory location from EnvvarVulnReportsDir = "LEEWAY_VULN_REPORTS_DIR" - - // SBOM file format constants - sbomBaseFilename = "sbom" - - // sbomCycloneDXFileExtension is the extension of the CycloneDX SBOM file we store in the archived build artifacts - sbomCycloneDXFileExtension = ".cdx.json" - - // sbomSPDXFileExtension is the extension of the SPDX SBOM file we store in the archived build artifacts - sbomSPDXFileExtension = ".spdx.json" - - // sbomSyftFileExtension is the extension of the Syft SBOM file we store in the archived build artifacts - sbomSyftFileExtension = ".json" ) // WorkspaceSBOM configures SBOM generation for a workspace @@ -362,14 +351,14 @@ func writeSBOMToCache(buildctx *buildContext, p *Package, builddir string) (err } // Normalize CycloneDX - cycloneDXPath := artifactPath + "." + sbomBaseFilename + sbomCycloneDXFileExtension + cycloneDXPath := artifactPath + "." + cache.SBOMBaseFilename + cache.SBOMCycloneDXFileExtension if err := normalizeCycloneDX(cycloneDXPath, timestamp); err != nil { buildctx.Reporter.PackageBuildLog(p, true, []byte(fmt.Sprintf("Warning: failed to normalize CycloneDX SBOM: %v\n", err))) } // Normalize SPDX - spdxPath := artifactPath + "." + sbomBaseFilename + sbomSPDXFileExtension + spdxPath := artifactPath + "." + cache.SBOMBaseFilename + cache.SBOMSPDXFileExtension if err := normalizeSPDX(spdxPath, timestamp); err != nil { buildctx.Reporter.PackageBuildLog(p, true, []byte(fmt.Sprintf("Warning: failed to normalize SPDX SBOM: %v\n", err))) @@ -392,21 +381,21 @@ func getSBOMEncoder(format string) (encoder sbom.FormatEncoder, filename string, if err != nil { return nil, "", xerrors.Errorf("failed to create CycloneDX encoder: %w", err) } - fileExtension = sbomCycloneDXFileExtension + fileExtension = cache.SBOMCycloneDXFileExtension case "spdx": encoder, err = spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig()) if err != nil { return nil, "", xerrors.Errorf("failed to create SPDX encoder: %w", err) } - fileExtension = sbomSPDXFileExtension + fileExtension = cache.SBOMSPDXFileExtension case "syft": encoder = syftjson.NewFormatEncoder() - fileExtension = sbomSyftFileExtension + fileExtension = cache.SBOMSyftFileExtension default: return nil, "", xerrors.Errorf("unsupported SBOM format: %s", format) } - return encoder, sbomBaseFilename + fileExtension, nil + return encoder, cache.SBOMBaseFilename + fileExtension, nil } // writeFileHandler returns a handler function for AccessSBOMInCachedArchive that writes to a file. @@ -442,11 +431,11 @@ func ValidateSBOMFormat(format string) (bool, []string) { func GetSBOMFileExtension(format string) string { switch format { case "cyclonedx": - return sbomCycloneDXFileExtension + return cache.SBOMCycloneDXFileExtension case "spdx": - return sbomSPDXFileExtension + return cache.SBOMSPDXFileExtension case "syft": - return sbomSyftFileExtension + return cache.SBOMSyftFileExtension default: return ".json" } @@ -474,7 +463,7 @@ func AccessSBOMInCachedArchive(fn string, format string, handler func(sbomFile i } // Try reading from separate SBOM file first (new format) - sbomExt := "." + sbomBaseFilename + GetSBOMFileExtension(format) + sbomExt := "." + cache.SBOMBaseFilename + GetSBOMFileExtension(format) sbomPath := fn + sbomExt if _, statErr := os.Stat(sbomPath); statErr == nil { @@ -497,7 +486,7 @@ func AccessSBOMInCachedArchive(fn string, format string, handler func(sbomFile i // accessSBOMInTarArchive extracts an SBOM file from inside a tar.gz archive (legacy format). func accessSBOMInTarArchive(fn string, format string, handler func(sbomFile io.Reader) error) error { - sbomFilename := sbomBaseFilename + GetSBOMFileExtension(format) + sbomFilename := cache.SBOMBaseFilename + GetSBOMFileExtension(format) f, err := os.Open(fn) if err != nil { diff --git a/pkg/leeway/signing/upload.go b/pkg/leeway/signing/upload.go index 90d65e82..5d722858 100644 --- a/pkg/leeway/signing/upload.go +++ b/pkg/leeway/signing/upload.go @@ -108,12 +108,15 @@ func (u *ArtifactUploader) UploadArtifactWithAttestation(ctx context.Context, ar } if attestationExists { - // Both artifact and attestation exist, skip both uploads + // Both artifact and attestation exist, but still check SBOM files log.WithFields(log.Fields{ "artifact": artifactPath, "artifact_key": artifactKey, "att_key": attestationKey, - }).Info("Skipping attestation upload (artifact and attestation already exist)") + }).Info("Skipping artifact and attestation upload (already exist)") + + // Still upload SBOM files if missing + u.uploadSBOMFiles(ctx, artifactPath, artifactKey) return nil } @@ -133,6 +136,9 @@ func (u *ArtifactUploader) UploadArtifactWithAttestation(ctx context.Context, ar "artifact_key": artifactKey, "att_key": attestationKey, }).Info("Successfully uploaded attestation") + + // Also upload SBOM files if missing + u.uploadSBOMFiles(ctx, artifactPath, artifactKey) return nil } @@ -161,5 +167,54 @@ func (u *ArtifactUploader) UploadArtifactWithAttestation(ctx context.Context, ar "att_key": attestationKey, }).Info("Successfully uploaded attestation file") + // Upload SBOM files if they exist (non-blocking - failures are logged but don't fail the upload) + u.uploadSBOMFiles(ctx, artifactPath, artifactKey) + return nil } + +// uploadSBOMFiles uploads SBOM sidecar files alongside the artifact. +// This is a non-blocking operation - failures are logged but don't fail the upload. +func (u *ArtifactUploader) uploadSBOMFiles(ctx context.Context, artifactPath, artifactKey string) { + sbomExtensions := cache.SBOMSidecarExtensions() + + for _, ext := range sbomExtensions { + sbomPath := artifactPath + ext + sbomKey := artifactKey + ext + + // Check if SBOM file exists locally + if _, err := os.Stat(sbomPath); os.IsNotExist(err) { + log.WithFields(log.Fields{ + "path": sbomPath, + }).Debug("SBOM file not found locally, skipping upload") + continue + } + + // Check if SBOM already exists in remote cache + exists, err := u.remoteCache.HasFile(ctx, sbomKey) + if err != nil { + log.WithError(err).WithField("key", sbomKey).Warn("Failed to check if SBOM exists, will attempt upload") + exists = false + } + + if exists { + log.WithFields(log.Fields{ + "key": sbomKey, + }).Debug("SBOM file already exists in remote cache, skipping upload") + continue + } + + // Upload SBOM file + if err := u.remoteCache.UploadFile(ctx, sbomPath, sbomKey); err != nil { + log.WithError(err).WithFields(log.Fields{ + "key": sbomKey, + "path": sbomPath, + }).Warn("Failed to upload SBOM file to remote cache") + continue + } + + log.WithFields(log.Fields{ + "key": sbomKey, + }).Info("Successfully uploaded SBOM file to remote cache") + } +}