Skip to content

Commit 0c347c7

Browse files
leodidoona-agent
andcommitted
test: add integration tests for SBOM with OCI layout
Add comprehensive integration test that verifies SBOM generation works correctly for both Docker daemon and OCI layout export paths. The test validates: - Build succeeds without errors for both exportToCache modes - All 3 SBOM formats are generated (CycloneDX, SPDX, Syft) - SBOM files are valid JSON with expected structure - Format-specific fields are present (bomFormat, spdxVersion) Includes git repository initialization with fixed timestamps for deterministic test results, following existing test patterns. Co-authored-by: Ona <[email protected]>
1 parent 5ff22f7 commit 0c347c7

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

pkg/leeway/build_integration_test.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,24 @@ CMD ["echo", "test"]`
195195
t.Fatal(err)
196196
}
197197

198+
// Create initial git commit for SBOM timestamp
199+
gitAdd := exec.Command("git", "add", ".")
200+
gitAdd.Dir = tmpDir
201+
if err := gitAdd.Run(); err != nil {
202+
t.Fatalf("Failed to git add: %v", err)
203+
}
204+
205+
// Use fixed timestamp for deterministic git commit
206+
gitCommit := exec.Command("git", "commit", "-m", "initial")
207+
gitCommit.Dir = tmpDir
208+
gitCommit.Env = append(os.Environ(),
209+
"GIT_AUTHOR_DATE=2021-01-01T00:00:00Z",
210+
"GIT_COMMITTER_DATE=2021-01-01T00:00:00Z",
211+
)
212+
if err := gitCommit.Run(); err != nil {
213+
t.Fatalf("Failed to git commit: %v", err)
214+
}
215+
198216
// Load workspace
199217
workspace, err := FindWorkspace(tmpDir, Arguments{}, "", "")
200218
if err != nil {
@@ -1196,3 +1214,271 @@ RUN echo "test content" > /test.txt
11961214
})
11971215
}
11981216
}
1217+
1218+
1219+
// TestDockerPackage_SBOM_OCI_Integration verifies SBOM generation works with OCI layout export.
1220+
// Tests two scenarios:
1221+
// 1. SBOM with Docker daemon (exportToCache=false) - traditional path
1222+
// 2. SBOM with OCI layout (exportToCache=true) - should scan oci-archive:image.tar
1223+
//
1224+
// This test validates the fix for the issue where SBOM generation fails with OCI layout
1225+
// because it tries to inspect the Docker daemon instead of scanning the OCI archive.
1226+
func TestDockerPackage_SBOM_OCI_Integration(t *testing.T) {
1227+
if testing.Short() {
1228+
t.Skip("Skipping integration test in short mode")
1229+
}
1230+
1231+
// Ensure Docker is available
1232+
if err := exec.Command("docker", "version").Run(); err != nil {
1233+
t.Skip("Docker not available, skipping integration test")
1234+
}
1235+
1236+
tests := []struct {
1237+
name string
1238+
exportToCache bool
1239+
description string
1240+
}{
1241+
{
1242+
name: "sbom_with_docker_daemon",
1243+
exportToCache: false,
1244+
description: "SBOM generation from Docker daemon (traditional path)",
1245+
},
1246+
{
1247+
name: "sbom_with_oci_layout",
1248+
exportToCache: true,
1249+
description: "SBOM generation from OCI layout (oci-archive:image.tar)",
1250+
},
1251+
}
1252+
1253+
for _, tt := range tests {
1254+
t.Run(tt.name, func(t *testing.T) {
1255+
t.Logf("Testing: %s", tt.description)
1256+
1257+
// Create docker-container builder for OCI export if needed
1258+
if tt.exportToCache {
1259+
builderName := "leeway-sbom-test-builder"
1260+
createBuilder := exec.Command("docker", "buildx", "create", "--name", builderName, "--driver", "docker-container", "--bootstrap")
1261+
if err := createBuilder.Run(); err != nil {
1262+
t.Logf("Builder creation failed (might already exist): %v", err)
1263+
}
1264+
defer func() {
1265+
removeBuilder := exec.Command("docker", "buildx", "rm", builderName)
1266+
_ = removeBuilder.Run()
1267+
}()
1268+
1269+
useBuilder := exec.Command("docker", "buildx", "use", builderName)
1270+
if err := useBuilder.Run(); err != nil {
1271+
t.Fatalf("Failed to use builder: %v", err)
1272+
}
1273+
}
1274+
1275+
// Create temporary workspace
1276+
tmpDir := t.TempDir()
1277+
1278+
// Initialize git repository for SBOM timestamp normalization
1279+
gitInit := exec.Command("git", "init")
1280+
gitInit.Dir = tmpDir
1281+
if err := gitInit.Run(); err != nil {
1282+
t.Fatalf("Failed to initialize git repository: %v", err)
1283+
}
1284+
1285+
// Configure git user for commits
1286+
gitConfigName := exec.Command("git", "config", "user.name", "Test User")
1287+
gitConfigName.Dir = tmpDir
1288+
if err := gitConfigName.Run(); err != nil {
1289+
t.Fatalf("Failed to configure git user.name: %v", err)
1290+
}
1291+
1292+
gitConfigEmail := exec.Command("git", "config", "user.email", "[email protected]")
1293+
gitConfigEmail.Dir = tmpDir
1294+
if err := gitConfigEmail.Run(); err != nil {
1295+
t.Fatalf("Failed to configure git user.email: %v", err)
1296+
}
1297+
1298+
// Create WORKSPACE.yaml with SBOM enabled
1299+
workspaceYAML := `defaultTarget: "app:docker"
1300+
sbom:
1301+
enabled: true
1302+
scanVulnerabilities: false`
1303+
workspacePath := filepath.Join(tmpDir, "WORKSPACE.yaml")
1304+
if err := os.WriteFile(workspacePath, []byte(workspaceYAML), 0644); err != nil {
1305+
t.Fatal(err)
1306+
}
1307+
1308+
// Create component directory
1309+
appDir := filepath.Join(tmpDir, "app")
1310+
if err := os.MkdirAll(appDir, 0755); err != nil {
1311+
t.Fatal(err)
1312+
}
1313+
1314+
// Create a simple Dockerfile with some packages for SBOM to scan
1315+
dockerfile := `FROM alpine:latest
1316+
RUN apk add --no-cache curl wget
1317+
LABEL test="sbom-test"
1318+
CMD ["echo", "test"]`
1319+
1320+
dockerfilePath := filepath.Join(appDir, "Dockerfile")
1321+
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
1322+
t.Fatal(err)
1323+
}
1324+
1325+
// Create BUILD.yaml
1326+
buildYAML := fmt.Sprintf(`packages:
1327+
- name: docker
1328+
type: docker
1329+
config:
1330+
dockerfile: Dockerfile
1331+
exportToCache: %t`, tt.exportToCache)
1332+
1333+
buildPath := filepath.Join(appDir, "BUILD.yaml")
1334+
if err := os.WriteFile(buildPath, []byte(buildYAML), 0644); err != nil {
1335+
t.Fatal(err)
1336+
}
1337+
1338+
// Create initial git commit for SBOM timestamp
1339+
gitAdd := exec.Command("git", "add", ".")
1340+
gitAdd.Dir = tmpDir
1341+
if err := gitAdd.Run(); err != nil {
1342+
t.Fatalf("Failed to git add: %v", err)
1343+
}
1344+
1345+
// Use fixed timestamp for deterministic git commit
1346+
gitCommit := exec.Command("git", "commit", "-m", "initial")
1347+
gitCommit.Dir = tmpDir
1348+
gitCommit.Env = append(os.Environ(),
1349+
"GIT_AUTHOR_DATE=2021-01-01T00:00:00Z",
1350+
"GIT_COMMITTER_DATE=2021-01-01T00:00:00Z",
1351+
)
1352+
if err := gitCommit.Run(); err != nil {
1353+
t.Fatalf("Failed to git commit: %v", err)
1354+
}
1355+
1356+
// Load workspace
1357+
workspace, err := FindWorkspace(tmpDir, Arguments{}, "", "")
1358+
if err != nil {
1359+
t.Fatal(err)
1360+
}
1361+
1362+
// Verify SBOM is enabled
1363+
if !workspace.SBOM.Enabled {
1364+
t.Fatal("SBOM should be enabled in workspace")
1365+
}
1366+
1367+
// Create build context
1368+
cacheDir := filepath.Join(tmpDir, ".cache")
1369+
cache, err := local.NewFilesystemCache(cacheDir)
1370+
if err != nil {
1371+
t.Fatal(err)
1372+
}
1373+
1374+
buildCtx, err := newBuildContext(buildOptions{
1375+
LocalCache: cache,
1376+
DockerExportToCache: tt.exportToCache,
1377+
DockerExportSet: true,
1378+
Reporter: NewConsoleReporter(),
1379+
})
1380+
if err != nil {
1381+
t.Fatal(err)
1382+
}
1383+
1384+
// Get the package
1385+
pkg, ok := workspace.Packages["app:docker"]
1386+
if !ok {
1387+
t.Fatal("package app:docker not found")
1388+
}
1389+
1390+
// Build the package - this should generate SBOM
1391+
err = pkg.build(buildCtx)
1392+
if err != nil {
1393+
t.Fatalf("Build failed: %v", err)
1394+
}
1395+
1396+
t.Logf("✅ Build succeeded with exportToCache=%v", tt.exportToCache)
1397+
1398+
// Verify SBOM files were created in the cache
1399+
cacheLoc, exists := cache.Location(pkg)
1400+
if !exists {
1401+
t.Fatal("Package not found in cache")
1402+
}
1403+
1404+
// Extract and verify SBOM files from cache
1405+
sbomFormats := []string{
1406+
"sbom.cdx.json", // CycloneDX
1407+
"sbom.spdx.json", // SPDX
1408+
"sbom.json", // Syft (native format)
1409+
}
1410+
1411+
foundSBOMs := make(map[string]bool)
1412+
1413+
// Open the cache tar.gz
1414+
f, err := os.Open(cacheLoc)
1415+
if err != nil {
1416+
t.Fatalf("Failed to open cache file: %v", err)
1417+
}
1418+
defer f.Close()
1419+
1420+
gzin, err := gzip.NewReader(f)
1421+
if err != nil {
1422+
t.Fatalf("Failed to create gzip reader: %v", err)
1423+
}
1424+
defer gzin.Close()
1425+
1426+
tarin := tar.NewReader(gzin)
1427+
for {
1428+
hdr, err := tarin.Next()
1429+
if errors.Is(err, io.EOF) {
1430+
break
1431+
}
1432+
if err != nil {
1433+
t.Fatalf("Failed to read tar: %v", err)
1434+
}
1435+
1436+
filename := filepath.Base(hdr.Name)
1437+
for _, sbomFile := range sbomFormats {
1438+
if filename == sbomFile {
1439+
foundSBOMs[sbomFile] = true
1440+
t.Logf("✅ Found SBOM file: %s (size: %d bytes)", sbomFile, hdr.Size)
1441+
1442+
// Read and validate SBOM content
1443+
sbomContent := make([]byte, hdr.Size)
1444+
if _, err := io.ReadFull(tarin, sbomContent); err != nil {
1445+
t.Fatalf("Failed to read SBOM content: %v", err)
1446+
}
1447+
1448+
// Validate it's valid JSON
1449+
var sbomData map[string]interface{}
1450+
if err := json.Unmarshal(sbomContent, &sbomData); err != nil {
1451+
t.Fatalf("SBOM file %s is not valid JSON: %v", sbomFile, err)
1452+
}
1453+
1454+
// Check for expected content based on format
1455+
if strings.Contains(sbomFile, "cdx") {
1456+
if _, ok := sbomData["bomFormat"]; !ok {
1457+
t.Errorf("CycloneDX SBOM missing bomFormat field")
1458+
}
1459+
} else if strings.Contains(sbomFile, "spdx") {
1460+
if _, ok := sbomData["spdxVersion"]; !ok {
1461+
t.Errorf("SPDX SBOM missing spdxVersion field")
1462+
}
1463+
}
1464+
1465+
t.Logf("✅ SBOM file %s is valid JSON with expected structure", sbomFile)
1466+
}
1467+
}
1468+
}
1469+
1470+
// Verify all SBOM formats were generated
1471+
for _, sbomFile := range sbomFormats {
1472+
if !foundSBOMs[sbomFile] {
1473+
t.Errorf("❌ SBOM file %s not found in cache", sbomFile)
1474+
}
1475+
}
1476+
1477+
if len(foundSBOMs) == len(sbomFormats) {
1478+
t.Logf("✅ All %d SBOM formats generated successfully", len(sbomFormats))
1479+
}
1480+
1481+
t.Logf("✅ SBOM generation works correctly with exportToCache=%v", tt.exportToCache)
1482+
})
1483+
}
1484+
}

0 commit comments

Comments
 (0)