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
347 changes: 347 additions & 0 deletions pkg/leeway/build_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,52 @@ CMD ["echo", "test"]`
t.Fatal(err)
}

// Initialize git repository for SBOM timestamp normalization
{
gitInit := exec.Command("git", "init")
gitInit.Dir = tmpDir
gitInit.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitInit.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// Configure git user for commits
gitConfigName := exec.Command("git", "config", "user.name", "Test User")
gitConfigName.Dir = tmpDir
gitConfigName.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitConfigName.Run(); err != nil {
t.Fatalf("Failed to configure git user.name: %v", err)
}

gitConfigEmail := exec.Command("git", "config", "user.email", "test@example.com")
gitConfigEmail.Dir = tmpDir
gitConfigEmail.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitConfigEmail.Run(); err != nil {
t.Fatalf("Failed to configure git user.email: %v", err)
}
}

// Create initial git commit for SBOM timestamp
gitAdd := exec.Command("git", "add", ".")
gitAdd.Dir = tmpDir
gitAdd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitAdd.Run(); err != nil {
t.Fatalf("Failed to git add: %v", err)
}

// Use fixed timestamp for deterministic git commit
gitCommit := exec.Command("git", "commit", "-m", "initial")
gitCommit.Dir = tmpDir
gitCommit.Env = append(os.Environ(),
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_CONFIG_SYSTEM=/dev/null",
"GIT_AUTHOR_DATE=2021-01-01T00:00:00Z",
"GIT_COMMITTER_DATE=2021-01-01T00:00:00Z",
)
if err := gitCommit.Run(); err != nil {
t.Fatalf("Failed to git commit: %v", err)
}

// Load workspace
workspace, err := FindWorkspace(tmpDir, Arguments{}, "", "")
if err != nil {
Expand Down Expand Up @@ -1196,3 +1242,304 @@ RUN echo "test content" > /test.txt
})
}
}


// TestDockerPackage_SBOM_OCI_Integration verifies SBOM generation works with OCI layout export.
// Tests two scenarios:
// 1. SBOM with Docker daemon (exportToCache=false) - traditional path
// 2. SBOM with OCI layout (exportToCache=true) - should scan oci-archive:image.tar
//
// This test validates the fix for the issue where SBOM generation fails with OCI layout
// because it tries to inspect the Docker daemon instead of scanning the OCI archive.
func TestDockerPackage_SBOM_OCI_Integration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}

// Ensure Docker is available
if err := exec.Command("docker", "version").Run(); err != nil {
t.Skip("Docker not available, skipping integration test")
}

tests := []struct {
name string
exportToCache bool
description string
}{
{
name: "sbom_with_docker_daemon",
exportToCache: false,
description: "SBOM generation from Docker daemon (traditional path)",
},
{
name: "sbom_with_oci_layout",
exportToCache: true,
description: "SBOM generation from OCI layout (oci-archive:image.tar)",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Logf("Testing: %s", tt.description)

// Create docker-container builder for OCI export if needed
if tt.exportToCache {
builderName := "leeway-sbom-test-builder"
createBuilder := exec.Command("docker", "buildx", "create", "--name", builderName, "--driver", "docker-container", "--bootstrap")
if err := createBuilder.Run(); err != nil {
t.Logf("Builder creation failed (might already exist): %v", err)
}
defer func() {
removeBuilder := exec.Command("docker", "buildx", "rm", builderName)
_ = removeBuilder.Run()
}()

useBuilder := exec.Command("docker", "buildx", "use", builderName)
if err := useBuilder.Run(); err != nil {
t.Fatalf("Failed to use builder: %v", err)
}
}

// Create temporary workspace
tmpDir := t.TempDir()

// Initialize git repository for SBOM timestamp normalization
{
gitInit := exec.Command("git", "init")
gitInit.Dir = tmpDir
gitInit.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitInit.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// Configure git user for commits
gitConfigName := exec.Command("git", "config", "user.name", "Test User")
gitConfigName.Dir = tmpDir
gitConfigName.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitConfigName.Run(); err != nil {
t.Fatalf("Failed to configure git user.name: %v", err)
}

gitConfigEmail := exec.Command("git", "config", "user.email", "test@example.com")
gitConfigEmail.Dir = tmpDir
gitConfigEmail.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitConfigEmail.Run(); err != nil {
t.Fatalf("Failed to configure git user.email: %v", err)
}
}

// Create WORKSPACE.yaml with SBOM enabled
workspaceYAML := `defaultTarget: "app:docker"
sbom:
enabled: true
scanVulnerabilities: false`
workspacePath := filepath.Join(tmpDir, "WORKSPACE.yaml")
if err := os.WriteFile(workspacePath, []byte(workspaceYAML), 0644); err != nil {
t.Fatal(err)
}

// Create component directory
appDir := filepath.Join(tmpDir, "app")
if err := os.MkdirAll(appDir, 0755); err != nil {
t.Fatal(err)
}

// Create a simple Dockerfile with some packages for SBOM to scan
dockerfile := `FROM alpine:latest
RUN apk add --no-cache curl wget
LABEL test="sbom-test"
CMD ["echo", "test"]`

dockerfilePath := filepath.Join(appDir, "Dockerfile")
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
t.Fatal(err)
}

// Create BUILD.yaml
buildYAML := fmt.Sprintf(`packages:
- name: docker
type: docker
config:
dockerfile: Dockerfile
exportToCache: %t`, tt.exportToCache)

buildPath := filepath.Join(appDir, "BUILD.yaml")
if err := os.WriteFile(buildPath, []byte(buildYAML), 0644); err != nil {
t.Fatal(err)
}

// Initialize git repository for SBOM timestamp normalization
{
gitInit := exec.Command("git", "init")
gitInit.Dir = tmpDir
gitInit.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitInit.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// Configure git user for commits
gitConfigName := exec.Command("git", "config", "user.name", "Test User")
gitConfigName.Dir = tmpDir
gitConfigName.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitConfigName.Run(); err != nil {
t.Fatalf("Failed to configure git user.name: %v", err)
}

gitConfigEmail := exec.Command("git", "config", "user.email", "test@example.com")
gitConfigEmail.Dir = tmpDir
gitConfigEmail.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitConfigEmail.Run(); err != nil {
t.Fatalf("Failed to configure git user.email: %v", err)
}
}

// Create initial git commit for SBOM timestamp
gitAdd := exec.Command("git", "add", ".")
gitAdd.Dir = tmpDir
gitAdd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
if err := gitAdd.Run(); err != nil {
t.Fatalf("Failed to git add: %v", err)
}

// Use fixed timestamp for deterministic git commit
gitCommit := exec.Command("git", "commit", "-m", "initial")
gitCommit.Dir = tmpDir
gitCommit.Env = append(os.Environ(),
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_CONFIG_SYSTEM=/dev/null",
"GIT_AUTHOR_DATE=2021-01-01T00:00:00Z",
"GIT_COMMITTER_DATE=2021-01-01T00:00:00Z",
)
if err := gitCommit.Run(); err != nil {
t.Fatalf("Failed to git commit: %v", err)
}

// Load workspace
workspace, err := FindWorkspace(tmpDir, Arguments{}, "", "")
if err != nil {
t.Fatal(err)
}

// Verify SBOM is enabled
if !workspace.SBOM.Enabled {
t.Fatal("SBOM should be enabled in workspace")
}

// Create build context
cacheDir := filepath.Join(tmpDir, ".cache")
cache, err := local.NewFilesystemCache(cacheDir)
if err != nil {
t.Fatal(err)
}

buildCtx, err := newBuildContext(buildOptions{
LocalCache: cache,
DockerExportToCache: tt.exportToCache,
DockerExportSet: true,
Reporter: NewConsoleReporter(),
})
if err != nil {
t.Fatal(err)
}

// Get the package
pkg, ok := workspace.Packages["app:docker"]
if !ok {
t.Fatal("package app:docker not found")
}

// Build the package - this should generate SBOM
err = pkg.build(buildCtx)
if err != nil {
t.Fatalf("Build failed: %v", err)
}

t.Logf("✅ Build succeeded with exportToCache=%v", tt.exportToCache)

// Verify SBOM files were created in the cache
cacheLoc, exists := cache.Location(pkg)
if !exists {
t.Fatal("Package not found in cache")
}

// Extract and verify SBOM files from cache
sbomFormats := []string{
"sbom.cdx.json", // CycloneDX
"sbom.spdx.json", // SPDX
"sbom.json", // Syft (native format)
}

foundSBOMs := make(map[string]bool)

// Open the cache tar.gz
f, err := os.Open(cacheLoc)
if err != nil {
t.Fatalf("Failed to open cache file: %v", err)
}
defer f.Close()

gzin, err := gzip.NewReader(f)
if err != nil {
t.Fatalf("Failed to create gzip reader: %v", err)
}
defer gzin.Close()

tarin := tar.NewReader(gzin)
for {
hdr, err := tarin.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("Failed to read tar: %v", err)
}

filename := filepath.Base(hdr.Name)
for _, sbomFile := range sbomFormats {
if filename == sbomFile {
foundSBOMs[sbomFile] = true
t.Logf("✅ Found SBOM file: %s (size: %d bytes)", sbomFile, hdr.Size)

// Read and validate SBOM content
sbomContent := make([]byte, hdr.Size)
if _, err := io.ReadFull(tarin, sbomContent); err != nil {
t.Fatalf("Failed to read SBOM content: %v", err)
}

// Validate it's valid JSON
var sbomData map[string]interface{}
if err := json.Unmarshal(sbomContent, &sbomData); err != nil {
t.Fatalf("SBOM file %s is not valid JSON: %v", sbomFile, err)
}

// Check for expected content based on format
if strings.Contains(sbomFile, "cdx") {
if _, ok := sbomData["bomFormat"]; !ok {
t.Errorf("CycloneDX SBOM missing bomFormat field")
}
} else if strings.Contains(sbomFile, "spdx") {
if _, ok := sbomData["spdxVersion"]; !ok {
t.Errorf("SPDX SBOM missing spdxVersion field")
}
}

t.Logf("✅ SBOM file %s is valid JSON with expected structure", sbomFile)
}
}
}

// Verify all SBOM formats were generated
for _, sbomFile := range sbomFormats {
if !foundSBOMs[sbomFile] {
t.Errorf("❌ SBOM file %s not found in cache", sbomFile)
}
}

if len(foundSBOMs) == len(sbomFormats) {
t.Logf("✅ All %d SBOM formats generated successfully", len(sbomFormats))
}

t.Logf("✅ SBOM generation works correctly with exportToCache=%v", tt.exportToCache)
})
}
}
Loading
Loading