@@ -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