Skip to content
Open
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
20 changes: 2 additions & 18 deletions pkg/leeway/cache/remote/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <artifact>.sbom.<ext>
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
Expand Down Expand Up @@ -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 <artifact>.sbom.<ext>
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
Expand Down
32 changes: 32 additions & 0 deletions pkg/leeway/cache/types.go
Original file line number Diff line number Diff line change
@@ -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: <artifact>.<extension> 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:
Expand Down Expand Up @@ -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
Expand Down
35 changes: 12 additions & 23 deletions pkg/leeway/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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)))
Expand All @@ -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.
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
59 changes: 57 additions & 2 deletions pkg/leeway/signing/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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")
}
}
Loading