@@ -1705,3 +1705,256 @@ CMD ["echo", "test"]`
17051705 })
17061706 }
17071707}
1708+
1709+
1710+ // TestDockerPackage_SBOM_EnvVar_Integration verifies SBOM generation respects
1711+ // LEEWAY_DOCKER_EXPORT_TO_CACHE environment variable when package config doesn't
1712+ // explicitly set exportToCache.
1713+ //
1714+ // This test validates that when LEEWAY_DOCKER_EXPORT_TO_CACHE=true is set (e.g., for SLSA)
1715+ // but package config doesn't have exportToCache set, the SBOM generation correctly uses the OCI layout.
1716+ func TestDockerPackage_SBOM_EnvVar_Integration (t * testing.T ) {
1717+ if testing .Short () {
1718+ t .Skip ("Skipping integration test in short mode" )
1719+ }
1720+
1721+ // Ensure Docker is available
1722+ if err := exec .Command ("docker" , "version" ).Run (); err != nil {
1723+ t .Skip ("Docker not available, skipping integration test" )
1724+ }
1725+
1726+ // Create docker-container builder for OCI export
1727+ builderName := "leeway-sbom-envvar-test-builder"
1728+ createBuilder := exec .Command ("docker" , "buildx" , "create" , "--name" , builderName , "--driver" , "docker-container" , "--bootstrap" )
1729+ if err := createBuilder .Run (); err != nil {
1730+ t .Logf ("Builder creation failed (might already exist): %v" , err )
1731+ }
1732+ defer func () {
1733+ removeBuilder := exec .Command ("docker" , "buildx" , "rm" , builderName )
1734+ _ = removeBuilder .Run ()
1735+ }()
1736+
1737+ useBuilder := exec .Command ("docker" , "buildx" , "use" , builderName )
1738+ if err := useBuilder .Run (); err != nil {
1739+ t .Fatalf ("Failed to use builder: %v" , err )
1740+ }
1741+
1742+ // Create temporary workspace
1743+ tmpDir := t .TempDir ()
1744+
1745+ // Initialize git repository
1746+ {
1747+ gitInit := exec .Command ("git" , "init" )
1748+ gitInit .Dir = tmpDir
1749+ gitInit .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
1750+ if err := gitInit .Run (); err != nil {
1751+ t .Fatalf ("Failed to initialize git repository: %v" , err )
1752+ }
1753+
1754+ gitConfigName := exec .Command ("git" , "config" , "user.name" , "Test User" )
1755+ gitConfigName .Dir = tmpDir
1756+ gitConfigName .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
1757+ if err := gitConfigName .Run (); err != nil {
1758+ t .Fatalf ("Failed to configure git user.name: %v" , err )
1759+ }
1760+
1761+ gitConfigEmail := exec .Command ("git" , "config" , "user.email" , "test@example.com" )
1762+ gitConfigEmail .Dir = tmpDir
1763+ gitConfigEmail .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
1764+ if err := gitConfigEmail .Run (); err != nil {
1765+ t .Fatalf ("Failed to configure git user.email: %v" , err )
1766+ }
1767+ }
1768+
1769+ // Create WORKSPACE.yaml with SBOM enabled
1770+ workspaceYAML := `defaultTarget: "app:docker"
1771+ sbom:
1772+ enabled: true
1773+ scanVulnerabilities: false`
1774+ workspacePath := filepath .Join (tmpDir , "WORKSPACE.yaml" )
1775+ if err := os .WriteFile (workspacePath , []byte (workspaceYAML ), 0644 ); err != nil {
1776+ t .Fatal (err )
1777+ }
1778+
1779+ // Create component directory
1780+ appDir := filepath .Join (tmpDir , "app" )
1781+ if err := os .MkdirAll (appDir , 0755 ); err != nil {
1782+ t .Fatal (err )
1783+ }
1784+
1785+ // Create a simple Dockerfile
1786+ dockerfile := `FROM alpine:latest
1787+ RUN apk add --no-cache curl wget
1788+ LABEL test="sbom-envvar-test"
1789+ CMD ["echo", "test"]`
1790+
1791+ dockerfilePath := filepath .Join (appDir , "Dockerfile" )
1792+ if err := os .WriteFile (dockerfilePath , []byte (dockerfile ), 0644 ); err != nil {
1793+ t .Fatal (err )
1794+ }
1795+
1796+ // Create BUILD.yaml WITHOUT exportToCache set (this is the key difference)
1797+ buildYAML := `packages:
1798+ - name: docker
1799+ type: docker
1800+ config:
1801+ dockerfile: Dockerfile`
1802+
1803+ buildPath := filepath .Join (appDir , "BUILD.yaml" )
1804+ if err := os .WriteFile (buildPath , []byte (buildYAML ), 0644 ); err != nil {
1805+ t .Fatal (err )
1806+ }
1807+
1808+ // Create initial git commit
1809+ gitAdd := exec .Command ("git" , "add" , "." )
1810+ gitAdd .Dir = tmpDir
1811+ gitAdd .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
1812+ if err := gitAdd .Run (); err != nil {
1813+ t .Fatalf ("Failed to git add: %v" , err )
1814+ }
1815+
1816+ gitCommit := exec .Command ("git" , "commit" , "-m" , "initial" )
1817+ gitCommit .Dir = tmpDir
1818+ gitCommit .Env = append (os .Environ (),
1819+ "GIT_CONFIG_GLOBAL=/dev/null" ,
1820+ "GIT_CONFIG_SYSTEM=/dev/null" ,
1821+ "GIT_AUTHOR_DATE=2021-01-01T00:00:00Z" ,
1822+ "GIT_COMMITTER_DATE=2021-01-01T00:00:00Z" ,
1823+ )
1824+ if err := gitCommit .Run (); err != nil {
1825+ t .Fatalf ("Failed to git commit: %v" , err )
1826+ }
1827+
1828+ // Set LEEWAY_DOCKER_EXPORT_TO_CACHE environment variable
1829+ // This simulates SLSA being enabled via workflow
1830+ t .Setenv (EnvvarDockerExportToCache , "true" )
1831+
1832+ // Load workspace
1833+ workspace , err := FindWorkspace (tmpDir , Arguments {}, "" , "" )
1834+ if err != nil {
1835+ t .Fatal (err )
1836+ }
1837+
1838+ // Verify SBOM is enabled
1839+ if ! workspace .SBOM .Enabled {
1840+ t .Fatal ("SBOM should be enabled in workspace" )
1841+ }
1842+
1843+ // Create build context with exportToCache NOT explicitly set
1844+ // (relying on environment variable)
1845+ cacheDir := filepath .Join (tmpDir , ".cache" )
1846+ cache , err := local .NewFilesystemCache (cacheDir )
1847+ if err != nil {
1848+ t .Fatal (err )
1849+ }
1850+
1851+ buildCtx , err := newBuildContext (buildOptions {
1852+ LocalCache : cache ,
1853+ // NOTE: DockerExportToCache and DockerExportSet are NOT set
1854+ // This forces the code to rely on the environment variable
1855+ Reporter : NewConsoleReporter (),
1856+ })
1857+ if err != nil {
1858+ t .Fatal (err )
1859+ }
1860+
1861+ // Get the package
1862+ pkg , ok := workspace .Packages ["app:docker" ]
1863+ if ! ok {
1864+ t .Fatal ("package app:docker not found" )
1865+ }
1866+
1867+ // Verify package config does NOT have exportToCache set
1868+ dockerCfg , ok := pkg .Config .(DockerPkgConfig )
1869+ if ! ok {
1870+ t .Fatal ("package should have Docker config" )
1871+ }
1872+ if dockerCfg .ExportToCache != nil {
1873+ t .Fatalf ("package config should NOT have exportToCache set, but it is: %v" , * dockerCfg .ExportToCache )
1874+ }
1875+
1876+ // Build the package
1877+ // This should generate SBOM from OCI layout because LEEWAY_DOCKER_EXPORT_TO_CACHE=true
1878+ err = pkg .build (buildCtx )
1879+ if err != nil {
1880+ t .Fatalf ("Build failed: %v" , err )
1881+ }
1882+
1883+ t .Logf ("✅ Build succeeded with LEEWAY_DOCKER_EXPORT_TO_CACHE=true (no package config)" )
1884+
1885+ // Verify SBOM files were created in the cache
1886+ cacheLoc , exists := cache .Location (pkg )
1887+ if ! exists {
1888+ t .Fatal ("Package not found in cache" )
1889+ }
1890+
1891+ // Note: We don't check for image.tar existence because it may be cleaned up after build
1892+ // The log output "Generating SBOM from OCI layout" confirms the correct path was used
1893+
1894+ // Extract and verify SBOM files from cache
1895+ sbomFormats := []string {
1896+ "sbom.cdx.json" , // CycloneDX
1897+ "sbom.spdx.json" , // SPDX
1898+ "sbom.json" , // Syft (native format)
1899+ }
1900+
1901+ foundSBOMs := make (map [string ]bool )
1902+
1903+ // Open the cache tar.gz
1904+ f , err := os .Open (cacheLoc )
1905+ if err != nil {
1906+ t .Fatalf ("Failed to open cache file: %v" , err )
1907+ }
1908+ defer f .Close ()
1909+
1910+ gzin , err := gzip .NewReader (f )
1911+ if err != nil {
1912+ t .Fatalf ("Failed to create gzip reader: %v" , err )
1913+ }
1914+ defer gzin .Close ()
1915+
1916+ tarin := tar .NewReader (gzin )
1917+ for {
1918+ hdr , err := tarin .Next ()
1919+ if errors .Is (err , io .EOF ) {
1920+ break
1921+ }
1922+ if err != nil {
1923+ t .Fatalf ("Failed to read tar: %v" , err )
1924+ }
1925+
1926+ filename := filepath .Base (hdr .Name )
1927+ for _ , sbomFile := range sbomFormats {
1928+ if filename == sbomFile {
1929+ foundSBOMs [sbomFile ] = true
1930+ t .Logf ("✅ Found SBOM file: %s (size: %d bytes)" , sbomFile , hdr .Size )
1931+
1932+ // Read and validate SBOM content
1933+ sbomContent := make ([]byte , hdr .Size )
1934+ if _ , err := io .ReadFull (tarin , sbomContent ); err != nil {
1935+ t .Fatalf ("Failed to read SBOM content: %v" , err )
1936+ }
1937+
1938+ // Validate it's valid JSON
1939+ var sbomData map [string ]interface {}
1940+ if err := json .Unmarshal (sbomContent , & sbomData ); err != nil {
1941+ t .Fatalf ("SBOM file %s is not valid JSON: %v" , sbomFile , err )
1942+ }
1943+
1944+ t .Logf ("✅ SBOM file %s is valid JSON" , sbomFile )
1945+ }
1946+ }
1947+ }
1948+
1949+ // Verify all SBOM formats were generated
1950+ for _ , sbomFile := range sbomFormats {
1951+ if ! foundSBOMs [sbomFile ] {
1952+ t .Errorf ("❌ SBOM file %s not found in cache" , sbomFile )
1953+ }
1954+ }
1955+
1956+ if len (foundSBOMs ) == len (sbomFormats ) {
1957+ t .Logf ("✅ All %d SBOM formats generated successfully" , len (sbomFormats ))
1958+ t .Logf ("✅ SBOM generation correctly respects LEEWAY_DOCKER_EXPORT_TO_CACHE environment variable" )
1959+ }
1960+ }
0 commit comments