github.com/windmeup/goreleaser@v1.21.95/internal/pipe/sbom/sbom_test.go (about)

     1  package sbom
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"sort"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	"github.com/windmeup/goreleaser/internal/artifact"
    14  	"github.com/windmeup/goreleaser/internal/skips"
    15  	"github.com/windmeup/goreleaser/internal/testctx"
    16  	"github.com/windmeup/goreleaser/internal/testlib"
    17  	"github.com/windmeup/goreleaser/internal/tmpl"
    18  	"github.com/windmeup/goreleaser/pkg/config"
    19  	"github.com/windmeup/goreleaser/pkg/context"
    20  )
    21  
    22  func TestDescription(t *testing.T) {
    23  	require.NotEmpty(t, Pipe{}.String())
    24  }
    25  
    26  func TestSBOMCatalogDefault(t *testing.T) {
    27  	defaultArgs := []string{"$artifact", "--file", "$document", "--output", "spdx-json"}
    28  	defaultSboms := []string{
    29  		"{{ .ArtifactName }}.sbom",
    30  	}
    31  	defaultCmd := "syft"
    32  	tests := []struct {
    33  		configs  []config.SBOM
    34  		artifact string
    35  		cmd      string
    36  		sboms    []string
    37  		args     []string
    38  		env      []string
    39  		err      bool
    40  	}{
    41  		{
    42  			configs: []config.SBOM{
    43  				{
    44  					// empty
    45  				},
    46  			},
    47  			artifact: "archive",
    48  			cmd:      defaultCmd,
    49  			sboms:    defaultSboms,
    50  			args:     defaultArgs,
    51  			env: []string{
    52  				"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
    53  			},
    54  		},
    55  		{
    56  			configs: []config.SBOM{
    57  				{
    58  					Artifacts: "package",
    59  				},
    60  			},
    61  			artifact: "package",
    62  			cmd:      defaultCmd,
    63  			sboms:    defaultSboms,
    64  			args:     defaultArgs,
    65  		},
    66  		{
    67  			configs: []config.SBOM{
    68  				{
    69  					Artifacts: "archive",
    70  				},
    71  			},
    72  			artifact: "archive",
    73  			cmd:      defaultCmd,
    74  			sboms:    defaultSboms,
    75  			args:     defaultArgs,
    76  			env: []string{
    77  				"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
    78  			},
    79  		},
    80  		{
    81  			configs: []config.SBOM{
    82  				{
    83  					Artifacts: "archive",
    84  					Env: []string{
    85  						"something=something-else",
    86  					},
    87  				},
    88  			},
    89  			artifact: "archive",
    90  			cmd:      defaultCmd,
    91  			sboms:    defaultSboms,
    92  			args:     defaultArgs,
    93  			env: []string{
    94  				"something=something-else",
    95  			},
    96  		},
    97  		{
    98  			configs: []config.SBOM{
    99  				{
   100  					Artifacts: "any",
   101  				},
   102  			},
   103  			artifact: "any",
   104  			cmd:      defaultCmd,
   105  			sboms:    []string{},
   106  			args:     defaultArgs,
   107  		},
   108  		{
   109  			configs: []config.SBOM{
   110  				{
   111  					Artifacts: "binary",
   112  				},
   113  			},
   114  			artifact: "binary",
   115  			cmd:      defaultCmd,
   116  			sboms:    []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"},
   117  			args:     defaultArgs,
   118  		},
   119  		{
   120  			configs: []config.SBOM{
   121  				{
   122  					Artifacts: "source",
   123  				},
   124  			},
   125  			artifact: "source",
   126  			cmd:      defaultCmd,
   127  			sboms:    defaultSboms,
   128  			args:     defaultArgs,
   129  			env: []string{
   130  				"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
   131  			},
   132  		},
   133  		{
   134  			// multiple documents are not allowed when artifacts != "any"
   135  			configs: []config.SBOM{
   136  				{
   137  					Artifacts: "binary",
   138  					Documents: []string{
   139  						"doc1",
   140  						"doc2",
   141  					},
   142  				},
   143  			},
   144  			err: true,
   145  		},
   146  	}
   147  
   148  	for _, test := range tests {
   149  		t.Run(fmt.Sprintf("artifact=%q", test.configs[0].Artifacts), func(t *testing.T) {
   150  			testlib.CheckPath(t, "syft")
   151  			ctx := testctx.NewWithCfg(config.Project{
   152  				SBOMs: test.configs,
   153  			})
   154  			err := Pipe{}.Default(ctx)
   155  			if test.err {
   156  				require.Error(t, err)
   157  				return
   158  			}
   159  			require.NoError(t, err)
   160  			require.Equal(t, ctx.Config.SBOMs[0].Cmd, test.cmd)
   161  			require.Equal(t, ctx.Config.SBOMs[0].Documents, test.sboms)
   162  			require.Equal(t, ctx.Config.SBOMs[0].Args, test.args)
   163  			require.Equal(t, ctx.Config.SBOMs[0].Env, test.env)
   164  			require.Equal(t, ctx.Config.SBOMs[0].Artifacts, test.artifact)
   165  		})
   166  	}
   167  }
   168  
   169  func TestSBOMCatalogInvalidArtifacts(t *testing.T) {
   170  	ctx := testctx.NewWithCfg(config.Project{
   171  		SBOMs: []config.SBOM{{Artifacts: "foo"}},
   172  	})
   173  	err := Pipe{}.Run(ctx)
   174  	require.EqualError(t, err, "invalid list of artifacts to catalog: foo")
   175  }
   176  
   177  func TestSeveralSBOMsWithTheSameID(t *testing.T) {
   178  	ctx := testctx.NewWithCfg(config.Project{
   179  		SBOMs: []config.SBOM{
   180  			{
   181  				ID: "a",
   182  			},
   183  			{
   184  				ID: "a",
   185  			},
   186  		},
   187  	})
   188  	require.EqualError(t, Pipe{}.Default(ctx), "found 2 sboms with the ID 'a', please fix your config")
   189  }
   190  
   191  func TestSkipCataloging(t *testing.T) {
   192  	t.Run("skip", func(t *testing.T) {
   193  		require.True(t, Pipe{}.Skip(testctx.New()))
   194  	})
   195  
   196  	t.Run("skip SBOM cataloging", func(t *testing.T) {
   197  		ctx := testctx.NewWithCfg(config.Project{
   198  			SBOMs: []config.SBOM{
   199  				{
   200  					Artifacts: "all",
   201  				},
   202  			},
   203  		}, testctx.Skip(skips.SBOM))
   204  		require.True(t, Pipe{}.Skip(ctx))
   205  	})
   206  
   207  	t.Run("dont skip", func(t *testing.T) {
   208  		ctx := testctx.NewWithCfg(config.Project{
   209  			SBOMs: []config.SBOM{
   210  				{
   211  					Artifacts: "all",
   212  				},
   213  			},
   214  		})
   215  		require.False(t, Pipe{}.Skip(ctx))
   216  	})
   217  }
   218  
   219  func TestSBOMCatalogArtifacts(t *testing.T) {
   220  	tests := []struct {
   221  		desc           string
   222  		ctx            *context.Context
   223  		sbomPaths      []string
   224  		sbomNames      []string
   225  		expectedErrAs  any
   226  		expectedErrMsg string
   227  	}{
   228  		{
   229  			desc:           "catalog errors",
   230  			expectedErrMsg: "cataloging artifacts: exit failed",
   231  			ctx: testctx.NewWithCfg(config.Project{
   232  				SBOMs: []config.SBOM{
   233  					{
   234  						Artifacts: "binary",
   235  						Cmd:       "exit",
   236  						Args:      []string{"1"},
   237  					},
   238  				},
   239  			}),
   240  		},
   241  		{
   242  			desc:          "invalid args template",
   243  			expectedErrAs: &tmpl.Error{},
   244  			ctx: testctx.NewWithCfg(config.Project{
   245  				SBOMs: []config.SBOM{
   246  					{
   247  						Artifacts: "binary",
   248  						Cmd:       "exit",
   249  						Args:      []string{"${FOO}-{{ .foo }{{}}{"},
   250  					},
   251  				},
   252  				Env: []string{
   253  					"FOO=BAR",
   254  				},
   255  			}),
   256  		},
   257  		{
   258  			desc: "catalog source archives",
   259  			ctx: testctx.NewWithCfg(config.Project{
   260  				SBOMs: []config.SBOM{
   261  					{Artifacts: "source"},
   262  				},
   263  			}),
   264  			sbomPaths: []string{"artifact5.tar.gz.sbom"},
   265  			sbomNames: []string{"artifact5.tar.gz.sbom"},
   266  		},
   267  		{
   268  			desc: "catalog archives",
   269  			ctx: testctx.NewWithCfg(config.Project{
   270  				SBOMs: []config.SBOM{
   271  					{Artifacts: "archive"},
   272  				},
   273  			}),
   274  			sbomPaths: []string{"artifact1.sbom", "artifact2.sbom"},
   275  			sbomNames: []string{"artifact1.sbom", "artifact2.sbom"},
   276  		},
   277  		{
   278  			desc: "catalog linux packages",
   279  			ctx: testctx.NewWithCfg(config.Project{
   280  				SBOMs: []config.SBOM{
   281  					{Artifacts: "package"},
   282  				},
   283  			}),
   284  			sbomPaths: []string{"package1.deb.sbom"},
   285  			sbomNames: []string{"package1.deb.sbom"},
   286  		},
   287  		{
   288  			desc: "catalog binaries",
   289  			ctx: testctx.NewWithCfg(config.Project{
   290  				SBOMs: []config.SBOM{
   291  					{Artifacts: "binary"},
   292  				},
   293  			}),
   294  			sbomPaths: []string{
   295  				"artifact3-name_1.2.2_linux_amd64.sbom",
   296  				"artifact4-name_1.2.2_linux_amd64.sbom",
   297  			},
   298  			sbomNames: []string{
   299  				"artifact3-name_1.2.2_linux_amd64.sbom",
   300  				"artifact4-name_1.2.2_linux_amd64.sbom",
   301  			},
   302  		},
   303  		{
   304  			desc: "manual cataloging",
   305  			ctx: testctx.NewWithCfg(config.Project{
   306  				SBOMs: []config.SBOM{
   307  					{
   308  						Artifacts: "any",
   309  						Args: []string{
   310  							"--file",
   311  							"$document0",
   312  							"--output",
   313  							"spdx-json",
   314  							"artifact5.tar.gz",
   315  						},
   316  						Documents: []string{
   317  							"final.sbom",
   318  						},
   319  					},
   320  				},
   321  			}),
   322  			sbomPaths: []string{"final.sbom"},
   323  			sbomNames: []string{"final.sbom"},
   324  		},
   325  		{
   326  			desc: "multiple SBOM configs",
   327  			ctx: testctx.NewWithCfg(config.Project{
   328  				Env: []string{
   329  					"SBOM_SUFFIX=s2-ish",
   330  				},
   331  				SBOMs: []config.SBOM{
   332  					{
   333  						ID:        "s1",
   334  						Artifacts: "binary",
   335  					},
   336  					{
   337  						ID:        "s2",
   338  						Artifacts: "archive",
   339  						Documents: []string{"{{ .ArtifactName }}.{{ .Env.SBOM_SUFFIX }}.sbom"},
   340  					},
   341  				},
   342  			}),
   343  			sbomPaths: []string{
   344  				"artifact1.s2-ish.sbom",
   345  				"artifact2.s2-ish.sbom",
   346  				"artifact3-name_1.2.2_linux_amd64.sbom",
   347  				"artifact4-name_1.2.2_linux_amd64.sbom",
   348  			},
   349  			sbomNames: []string{
   350  				"artifact1.s2-ish.sbom",
   351  				"artifact2.s2-ish.sbom",
   352  				"artifact3-name_1.2.2_linux_amd64.sbom",
   353  				"artifact4-name_1.2.2_linux_amd64.sbom",
   354  			},
   355  		},
   356  		{
   357  			desc: "catalog artifacts with filtered by ID",
   358  			ctx: testctx.NewWithCfg(config.Project{
   359  				SBOMs: []config.SBOM{
   360  					{
   361  						Artifacts: "binary",
   362  						IDs:       []string{"foo"},
   363  					},
   364  				},
   365  			}),
   366  			sbomPaths: []string{
   367  				"artifact3-name_1.2.2_linux_amd64.sbom",
   368  			},
   369  			sbomNames: []string{
   370  				"artifact3-name_1.2.2_linux_amd64.sbom",
   371  			},
   372  		},
   373  		{
   374  			desc: "catalog binary artifacts with env in arguments",
   375  			ctx: testctx.NewWithCfg(config.Project{
   376  				SBOMs: []config.SBOM{
   377  					{
   378  						Artifacts: "binary",
   379  						Args: []string{
   380  							"--file",
   381  							"$document",
   382  							"--output",
   383  							"spdx-json",
   384  							"$artifact",
   385  						},
   386  						Documents: []string{
   387  							"{{ .ArtifactName }}.{{ .Env.TEST_USER }}.sbom",
   388  						},
   389  					},
   390  				},
   391  				Env: []string{
   392  					"TEST_USER=test-user-name",
   393  				},
   394  			}),
   395  			sbomPaths: []string{
   396  				"artifact3-name.test-user-name.sbom",
   397  				"artifact4.test-user-name.sbom",
   398  			},
   399  			sbomNames: []string{
   400  				"artifact3-name.test-user-name.sbom",
   401  				"artifact4.test-user-name.sbom",
   402  			},
   403  		},
   404  		{
   405  			desc: "cataloging 'any' artifacts fails",
   406  			ctx: testctx.NewWithCfg(config.Project{
   407  				SBOMs: []config.SBOM{
   408  					{
   409  						Artifacts: "any",
   410  						Cmd:       "false",
   411  					},
   412  				},
   413  			}),
   414  			expectedErrMsg: "cataloging artifacts: false failed: exit status 1: ",
   415  		},
   416  	}
   417  
   418  	for _, test := range tests {
   419  		t.Run(test.desc, func(t *testing.T) {
   420  			testSBOMCataloging(
   421  				t,
   422  				test.ctx,
   423  				test.sbomPaths,
   424  				test.sbomNames,
   425  				test.expectedErrAs,
   426  				test.expectedErrMsg,
   427  			)
   428  		})
   429  	}
   430  }
   431  
   432  func testSBOMCataloging(
   433  	tb testing.TB,
   434  	ctx *context.Context,
   435  	sbomPaths, sbomNames []string,
   436  	expectedErrAs any,
   437  	expectedErrMsg string,
   438  ) {
   439  	tb.Helper()
   440  	testlib.CheckPath(tb, "syft")
   441  	tmpdir := tb.TempDir()
   442  
   443  	ctx.Config.Dist = tmpdir
   444  	ctx.Version = "1.2.2"
   445  
   446  	// create some fake artifacts
   447  	artifacts := []string{"artifact1", "artifact2", "artifact3", "package1.deb"}
   448  	require.NoError(tb, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm))
   449  	for _, f := range artifacts {
   450  		file := filepath.Join(tmpdir, f)
   451  		require.NoError(tb, os.WriteFile(file, []byte("foo"), 0o644))
   452  	}
   453  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0o644))
   454  	artifacts = append(artifacts, "linux_amd64/artifact4")
   455  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0o644))
   456  	artifacts = append(artifacts, "artifact5.tar.gz")
   457  	ctx.Artifacts.Add(&artifact.Artifact{
   458  		Name: "artifact1",
   459  		Path: filepath.Join(tmpdir, "artifact1"),
   460  		Type: artifact.UploadableArchive,
   461  		Extra: map[string]interface{}{
   462  			artifact.ExtraID: "foo",
   463  		},
   464  	})
   465  	ctx.Artifacts.Add(&artifact.Artifact{
   466  		Name: "artifact2",
   467  		Path: filepath.Join(tmpdir, "artifact2"),
   468  		Type: artifact.UploadableArchive,
   469  		Extra: map[string]interface{}{
   470  			artifact.ExtraID: "foo3",
   471  		},
   472  	})
   473  	ctx.Artifacts.Add(&artifact.Artifact{
   474  		Name:   "artifact3-name",
   475  		Path:   filepath.Join(tmpdir, "artifact3"),
   476  		Goos:   "linux",
   477  		Goarch: "amd64",
   478  		Type:   artifact.UploadableBinary,
   479  		Extra: map[string]interface{}{
   480  			artifact.ExtraID:     "foo",
   481  			artifact.ExtraBinary: "artifact3-name",
   482  		},
   483  	})
   484  	ctx.Artifacts.Add(&artifact.Artifact{
   485  		Name:   "artifact4",
   486  		Path:   filepath.Join(tmpdir, "linux_amd64", "artifact4"),
   487  		Goos:   "linux",
   488  		Goarch: "amd64",
   489  		Type:   artifact.Binary,
   490  		Extra: map[string]interface{}{
   491  			artifact.ExtraID:     "foo3",
   492  			artifact.ExtraBinary: "artifact4-name",
   493  		},
   494  	})
   495  	ctx.Artifacts.Add(&artifact.Artifact{
   496  		Name: "artifact5.tar.gz",
   497  		Path: filepath.Join(tmpdir, "artifact5.tar.gz"),
   498  		Type: artifact.UploadableSourceArchive,
   499  	})
   500  	ctx.Artifacts.Add(&artifact.Artifact{
   501  		Name: "package1.deb",
   502  		Path: filepath.Join(tmpdir, "package1.deb"),
   503  		Type: artifact.LinuxPackage,
   504  		Extra: map[string]interface{}{
   505  			artifact.ExtraID: "foo",
   506  		},
   507  	})
   508  
   509  	// configure the pipeline
   510  	require.NoError(tb, Pipe{}.Default(ctx))
   511  
   512  	// run the pipeline
   513  	if expectedErrMsg != "" {
   514  		err := Pipe{}.Run(ctx)
   515  		require.Error(tb, err)
   516  		require.Contains(tb, err.Error(), expectedErrMsg)
   517  		return
   518  	}
   519  	if expectedErrAs != nil {
   520  		require.ErrorAs(tb, Pipe{}.Run(ctx), expectedErrAs)
   521  		return
   522  	}
   523  
   524  	require.NoError(tb, Pipe{}.Run(ctx))
   525  
   526  	// ensure all artifacts have an ID
   527  	for _, arti := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() {
   528  		require.NotEmptyf(tb, arti.ID(), ".Extra.ID on %s", arti.Path)
   529  	}
   530  
   531  	// verify that only the artifacts and the sboms are in the dist dir
   532  	gotFiles := []string{}
   533  
   534  	require.NoError(tb, filepath.Walk(tmpdir,
   535  		func(path string, info os.FileInfo, err error) error {
   536  			if err != nil {
   537  				return err
   538  			}
   539  			if info.IsDir() {
   540  				return nil
   541  			}
   542  			relPath, err := filepath.Rel(tmpdir, path)
   543  			if err != nil {
   544  				return err
   545  			}
   546  			gotFiles = append(gotFiles, relPath)
   547  			return nil
   548  		}),
   549  	)
   550  
   551  	wantFiles := append(artifacts, sbomPaths...)
   552  	sort.Strings(wantFiles)
   553  	require.ElementsMatch(tb, wantFiles, gotFiles, "SBOM paths differ")
   554  
   555  	var sbomArtifacts []string
   556  	for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() {
   557  		sbomArtifacts = append(sbomArtifacts, sig.Name)
   558  	}
   559  
   560  	require.ElementsMatch(tb, sbomArtifacts, sbomNames, "SBOM names differ")
   561  }
   562  
   563  func Test_subprocessDistPath(t *testing.T) {
   564  	cwd, err := os.Getwd()
   565  	require.NoError(t, err)
   566  
   567  	tests := []struct {
   568  		name              string
   569  		distDir           string
   570  		pathRelativeToCwd string
   571  		expects           string
   572  	}{
   573  		{
   574  			name:              "relative dist with anchor",
   575  			distDir:           "./dist",
   576  			pathRelativeToCwd: "dist/my.sbom",
   577  			expects:           "my.sbom",
   578  		},
   579  		{
   580  			name:              "relative dist without anchor",
   581  			distDir:           "dist",
   582  			pathRelativeToCwd: "dist/my.sbom",
   583  			expects:           "my.sbom",
   584  		},
   585  		{
   586  			name:              "relative dist with nested resource",
   587  			distDir:           "dist",
   588  			pathRelativeToCwd: "dist/something/my.sbom",
   589  			expects:           "something/my.sbom",
   590  		},
   591  		{
   592  			name:              "absolute dist with nested resource",
   593  			distDir:           filepath.Join(cwd, "dist/"),
   594  			pathRelativeToCwd: "dist/something/my.sbom",
   595  			expects:           "something/my.sbom",
   596  		},
   597  	}
   598  	for _, test := range tests {
   599  		t.Run(test.name, func(t *testing.T) {
   600  			actual, err := subprocessDistPath(test.distDir, test.pathRelativeToCwd)
   601  			require.NoError(t, err)
   602  			assert.Equal(t, test.expects, actual)
   603  		})
   604  	}
   605  }
   606  
   607  func Test_templateNames(t *testing.T) {
   608  	art := artifact.Artifact{
   609  		Name:   "name-it",
   610  		Path:   "to/a/place",
   611  		Goos:   "darwin",
   612  		Goarch: "amd64",
   613  		Type:   artifact.Binary,
   614  		Extra: map[string]interface{}{
   615  			artifact.ExtraID: "id-it",
   616  			"Binary":         "binary-name",
   617  		},
   618  	}
   619  
   620  	wd, err := os.Getwd()
   621  	require.NoError(t, err)
   622  
   623  	tests := []struct {
   624  		name           string
   625  		dist           string
   626  		version        string
   627  		cfg            config.SBOM
   628  		artifact       artifact.Artifact
   629  		expectedValues map[string]string
   630  		expectedPaths  []string
   631  	}{
   632  		{
   633  			name:     "default configuration",
   634  			artifact: art,
   635  			cfg:      config.SBOM{},
   636  			dist:     "/somewhere/to/dist",
   637  			expectedPaths: []string{
   638  				"/somewhere/to/dist/name-it.sbom",
   639  			},
   640  			expectedValues: map[string]string{
   641  				"artifact":   "to/a/place",
   642  				"artifactID": "id-it",
   643  				"document":   "/somewhere/to/dist/name-it.sbom",
   644  				"document0":  "/somewhere/to/dist/name-it.sbom",
   645  			},
   646  		},
   647  		{
   648  			name:     "default configuration + relative dist",
   649  			artifact: art,
   650  			cfg:      config.SBOM{},
   651  			dist:     "somewhere/to/dist",
   652  			expectedPaths: []string{
   653  				filepath.Join(wd, "somewhere/to/dist/name-it.sbom"),
   654  			},
   655  			expectedValues: map[string]string{
   656  				"artifact":   "to/a/place", // note: this is always relative to ${dist}
   657  				"artifactID": "id-it",
   658  				"document":   filepath.Join(wd, "somewhere/to/dist/name-it.sbom"),
   659  				"document0":  filepath.Join(wd, "somewhere/to/dist/name-it.sbom"),
   660  			},
   661  		},
   662  		{
   663  			name: "custom document using $artifact",
   664  			// note: this configuration is probably a misconfiguration since it is placing SBOMs within each bin
   665  			// directory, however, it will behave as correctly as possible.
   666  			artifact: art,
   667  			cfg: config.SBOM{
   668  				Documents: []string{
   669  					// note: the artifact name is probably an incorrect value here since it can't express all attributes
   670  					// of the binary (os, arch, etc), so builds with multiple architectures will create SBOMs with the
   671  					// same name.
   672  					"${artifact}.cdx.sbom",
   673  				},
   674  			},
   675  			dist: "somewhere/to/dist",
   676  			expectedPaths: []string{
   677  				filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"),
   678  			},
   679  			expectedValues: map[string]string{
   680  				"artifact":   "to/a/place",
   681  				"artifactID": "id-it",
   682  				"document":   filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"),
   683  				"document0":  filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"),
   684  			},
   685  		},
   686  		{
   687  			name:     "custom document using build vars",
   688  			artifact: art,
   689  			cfg: config.SBOM{
   690  				Documents: []string{
   691  					"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom",
   692  				},
   693  			},
   694  			version: "1.0.0",
   695  			dist:    "somewhere/to/dist",
   696  			expectedPaths: []string{
   697  				filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   698  			},
   699  			expectedValues: map[string]string{
   700  				"artifact":   "to/a/place",
   701  				"artifactID": "id-it",
   702  				"document":   filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   703  				"document0":  filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   704  			},
   705  		},
   706  		{
   707  			name:     "env vars with go templated options",
   708  			artifact: art,
   709  			cfg: config.SBOM{
   710  				Documents: []string{
   711  					"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom",
   712  				},
   713  				Env: []string{
   714  					"with-env-var=value",
   715  					"custom-os={{ .Os }}-unique",
   716  					"custom-arch={{ .Arch }}-unique",
   717  				},
   718  			},
   719  			version: "1.0.0",
   720  			dist:    "somewhere/to/dist",
   721  			expectedPaths: []string{
   722  				filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   723  			},
   724  			expectedValues: map[string]string{
   725  				"artifact":     "to/a/place",
   726  				"artifactID":   "id-it",
   727  				"with-env-var": "value",
   728  				"custom-os":    "darwin-unique",
   729  				"custom-arch":  "amd64-unique",
   730  				"document":     filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   731  				"document0":    filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   732  			},
   733  		},
   734  	}
   735  	for _, tt := range tests {
   736  		t.Run(tt.name, func(t *testing.T) {
   737  			ctx := testctx.NewWithCfg(config.Project{
   738  				Dist: tt.dist,
   739  			}, testctx.WithVersion(tt.version))
   740  
   741  			cfg := tt.cfg
   742  			require.NoError(t, setConfigDefaults(&cfg))
   743  
   744  			var inputArgs []string
   745  			var expectedArgs []string
   746  			for key, value := range tt.expectedValues {
   747  				inputArgs = append(inputArgs, fmt.Sprintf("${%s}", key))
   748  				expectedArgs = append(expectedArgs, value)
   749  			}
   750  			cfg.Args = inputArgs
   751  
   752  			actualArgs, actualEnvs, actualPaths, err := applyTemplate(ctx, cfg, &tt.artifact)
   753  			require.NoError(t, err)
   754  
   755  			assert.Equal(t, tt.expectedPaths, actualPaths, "paths differ")
   756  
   757  			assert.Equal(t, expectedArgs, actualArgs, "arguments differ")
   758  
   759  			actualEnv := make(map[string]string)
   760  			for _, str := range actualEnvs {
   761  				k, v, ok := strings.Cut(str, "=")
   762  				require.True(t, ok)
   763  				actualEnv[k] = v
   764  			}
   765  
   766  			for k, v := range tt.expectedValues {
   767  				assert.Equal(t, v, actualEnv[k])
   768  			}
   769  		})
   770  	}
   771  }
   772  
   773  func TestDependencies(t *testing.T) {
   774  	ctx := testctx.NewWithCfg(config.Project{
   775  		SBOMs: []config.SBOM{
   776  			{Cmd: "syft"},
   777  			{Cmd: "foobar"},
   778  		},
   779  	})
   780  	require.Equal(t, []string{"syft", "foobar"}, Pipe{}.Dependencies(ctx))
   781  }