github.com/goreleaser/goreleaser@v1.25.1/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/goreleaser/goreleaser/internal/artifact"
    12  	"github.com/goreleaser/goreleaser/internal/skips"
    13  	"github.com/goreleaser/goreleaser/internal/testctx"
    14  	"github.com/goreleaser/goreleaser/internal/testlib"
    15  	"github.com/goreleaser/goreleaser/internal/tmpl"
    16  	"github.com/goreleaser/goreleaser/pkg/config"
    17  	"github.com/goreleaser/goreleaser/pkg/context"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    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", "--output", "spdx-json=$document"}
    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  							"--output",
   311  							"spdx-json=$document0",
   312  							"artifact5.tar.gz",
   313  						},
   314  						Documents: []string{
   315  							"final.sbom",
   316  						},
   317  					},
   318  				},
   319  			}),
   320  			sbomPaths: []string{"final.sbom"},
   321  			sbomNames: []string{"final.sbom"},
   322  		},
   323  		{
   324  			desc: "multiple SBOM configs",
   325  			ctx: testctx.NewWithCfg(config.Project{
   326  				Env: []string{
   327  					"SBOM_SUFFIX=s2-ish",
   328  				},
   329  				SBOMs: []config.SBOM{
   330  					{
   331  						ID:        "s1",
   332  						Artifacts: "binary",
   333  					},
   334  					{
   335  						ID:        "s2",
   336  						Artifacts: "archive",
   337  						Documents: []string{"{{ .ArtifactName }}.{{ .Env.SBOM_SUFFIX }}.sbom"},
   338  					},
   339  				},
   340  			}),
   341  			sbomPaths: []string{
   342  				"artifact1.s2-ish.sbom",
   343  				"artifact2.s2-ish.sbom",
   344  				"artifact3-name_1.2.2_linux_amd64.sbom",
   345  				"artifact4-name_1.2.2_linux_amd64.sbom",
   346  			},
   347  			sbomNames: []string{
   348  				"artifact1.s2-ish.sbom",
   349  				"artifact2.s2-ish.sbom",
   350  				"artifact3-name_1.2.2_linux_amd64.sbom",
   351  				"artifact4-name_1.2.2_linux_amd64.sbom",
   352  			},
   353  		},
   354  		{
   355  			desc: "catalog artifacts with filtered by ID",
   356  			ctx: testctx.NewWithCfg(config.Project{
   357  				SBOMs: []config.SBOM{
   358  					{
   359  						Artifacts: "binary",
   360  						IDs:       []string{"foo"},
   361  					},
   362  				},
   363  			}),
   364  			sbomPaths: []string{
   365  				"artifact3-name_1.2.2_linux_amd64.sbom",
   366  			},
   367  			sbomNames: []string{
   368  				"artifact3-name_1.2.2_linux_amd64.sbom",
   369  			},
   370  		},
   371  		{
   372  			desc: "catalog binary artifacts with env in arguments",
   373  			ctx: testctx.NewWithCfg(config.Project{
   374  				SBOMs: []config.SBOM{
   375  					{
   376  						Artifacts: "binary",
   377  						Args: []string{
   378  							"--output",
   379  							"spdx-json=$document",
   380  							"$artifact",
   381  						},
   382  						Documents: []string{
   383  							"{{ .ArtifactName }}.{{ .Env.TEST_USER }}.sbom",
   384  						},
   385  					},
   386  				},
   387  				Env: []string{
   388  					"TEST_USER=test-user-name",
   389  				},
   390  			}),
   391  			sbomPaths: []string{
   392  				"artifact3-name.test-user-name.sbom",
   393  				"artifact4.test-user-name.sbom",
   394  			},
   395  			sbomNames: []string{
   396  				"artifact3-name.test-user-name.sbom",
   397  				"artifact4.test-user-name.sbom",
   398  			},
   399  		},
   400  		{
   401  			desc: "cataloging 'any' artifacts fails",
   402  			ctx: testctx.NewWithCfg(config.Project{
   403  				SBOMs: []config.SBOM{
   404  					{
   405  						Artifacts: "any",
   406  						Cmd:       "false",
   407  					},
   408  				},
   409  			}),
   410  			expectedErrMsg: "cataloging artifacts: false failed: exit status 1: ",
   411  		},
   412  		{
   413  			desc: "catalog wrong command",
   414  			ctx: testctx.NewWithCfg(config.Project{
   415  				SBOMs: []config.SBOM{
   416  					{Args: []string{"$artifact", "--file", "$sbom", "--output", "spdx-json"}},
   417  				},
   418  			}),
   419  			expectedErrMsg: "cataloging artifacts: command did not write any files, check your configuration",
   420  		},
   421  		{
   422  			desc: "no matches",
   423  			ctx: testctx.NewWithCfg(config.Project{
   424  				SBOMs: []config.SBOM{
   425  					{IDs: []string{"nopenopenope"}},
   426  				},
   427  			}),
   428  		},
   429  	}
   430  
   431  	for _, test := range tests {
   432  		t.Run(test.desc, func(t *testing.T) {
   433  			testSBOMCataloging(
   434  				t,
   435  				test.ctx,
   436  				test.sbomPaths,
   437  				test.sbomNames,
   438  				test.expectedErrAs,
   439  				test.expectedErrMsg,
   440  			)
   441  		})
   442  	}
   443  }
   444  
   445  func testSBOMCataloging(
   446  	tb testing.TB,
   447  	ctx *context.Context,
   448  	sbomPaths, sbomNames []string,
   449  	expectedErrAs any,
   450  	expectedErrMsg string,
   451  ) {
   452  	tb.Helper()
   453  	testlib.CheckPath(tb, "syft")
   454  	tmpdir := tb.TempDir()
   455  
   456  	ctx.Config.Dist = tmpdir
   457  	ctx.Version = "1.2.2"
   458  
   459  	// create some fake artifacts
   460  	artifacts := []string{"artifact1", "artifact2", "artifact3", "package1.deb"}
   461  	require.NoError(tb, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm))
   462  	for _, f := range artifacts {
   463  		file := filepath.Join(tmpdir, f)
   464  		require.NoError(tb, os.WriteFile(file, []byte("foo"), 0o644))
   465  	}
   466  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0o644))
   467  	artifacts = append(artifacts, "linux_amd64/artifact4")
   468  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0o644))
   469  	artifacts = append(artifacts, "artifact5.tar.gz")
   470  	ctx.Artifacts.Add(&artifact.Artifact{
   471  		Name: "artifact1",
   472  		Path: filepath.Join(tmpdir, "artifact1"),
   473  		Type: artifact.UploadableArchive,
   474  		Extra: map[string]interface{}{
   475  			artifact.ExtraID: "foo",
   476  		},
   477  	})
   478  	ctx.Artifacts.Add(&artifact.Artifact{
   479  		Name: "artifact2",
   480  		Path: filepath.Join(tmpdir, "artifact2"),
   481  		Type: artifact.UploadableArchive,
   482  		Extra: map[string]interface{}{
   483  			artifact.ExtraID: "foo3",
   484  		},
   485  	})
   486  	ctx.Artifacts.Add(&artifact.Artifact{
   487  		Name:   "artifact3-name",
   488  		Path:   filepath.Join(tmpdir, "artifact3"),
   489  		Goos:   "linux",
   490  		Goarch: "amd64",
   491  		Type:   artifact.UploadableBinary,
   492  		Extra: map[string]interface{}{
   493  			artifact.ExtraID:     "foo",
   494  			artifact.ExtraBinary: "artifact3-name",
   495  		},
   496  	})
   497  	ctx.Artifacts.Add(&artifact.Artifact{
   498  		Name:   "artifact4",
   499  		Path:   filepath.Join(tmpdir, "linux_amd64", "artifact4"),
   500  		Goos:   "linux",
   501  		Goarch: "amd64",
   502  		Type:   artifact.Binary,
   503  		Extra: map[string]interface{}{
   504  			artifact.ExtraID:     "foo3",
   505  			artifact.ExtraBinary: "artifact4-name",
   506  		},
   507  	})
   508  	ctx.Artifacts.Add(&artifact.Artifact{
   509  		Name: "artifact5.tar.gz",
   510  		Path: filepath.Join(tmpdir, "artifact5.tar.gz"),
   511  		Type: artifact.UploadableSourceArchive,
   512  	})
   513  	ctx.Artifacts.Add(&artifact.Artifact{
   514  		Name: "package1.deb",
   515  		Path: filepath.Join(tmpdir, "package1.deb"),
   516  		Type: artifact.LinuxPackage,
   517  		Extra: map[string]interface{}{
   518  			artifact.ExtraID: "foo",
   519  		},
   520  	})
   521  
   522  	// configure the pipeline
   523  	require.NoError(tb, Pipe{}.Default(ctx))
   524  
   525  	// run the pipeline
   526  	if expectedErrMsg != "" {
   527  		err := Pipe{}.Run(ctx)
   528  		require.ErrorContains(tb, err, expectedErrMsg)
   529  		return
   530  	}
   531  	if expectedErrAs != nil {
   532  		require.ErrorAs(tb, Pipe{}.Run(ctx), expectedErrAs)
   533  		return
   534  	}
   535  
   536  	require.NoError(tb, Pipe{}.Run(ctx))
   537  
   538  	// ensure all artifacts have an ID
   539  	for _, arti := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() {
   540  		require.NotEmptyf(tb, arti.ID(), ".Extra.ID on %s", arti.Path)
   541  	}
   542  
   543  	// verify that only the artifacts and the sboms are in the dist dir
   544  	gotFiles := []string{}
   545  
   546  	require.NoError(tb, filepath.Walk(tmpdir,
   547  		func(path string, info os.FileInfo, err error) error {
   548  			if err != nil {
   549  				return err
   550  			}
   551  			if info.IsDir() {
   552  				return nil
   553  			}
   554  			relPath, err := filepath.Rel(tmpdir, path)
   555  			if err != nil {
   556  				return err
   557  			}
   558  			gotFiles = append(gotFiles, relPath)
   559  			return nil
   560  		}),
   561  	)
   562  
   563  	wantFiles := append(artifacts, sbomPaths...)
   564  	sort.Strings(wantFiles)
   565  	require.ElementsMatch(tb, wantFiles, gotFiles, "SBOM paths differ")
   566  
   567  	var sbomArtifacts []string
   568  	for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() {
   569  		sbomArtifacts = append(sbomArtifacts, sig.Name)
   570  	}
   571  
   572  	require.ElementsMatch(tb, sbomArtifacts, sbomNames, "SBOM names differ")
   573  }
   574  
   575  func Test_subprocessDistPath(t *testing.T) {
   576  	cwd, err := os.Getwd()
   577  	require.NoError(t, err)
   578  
   579  	tests := []struct {
   580  		name              string
   581  		distDir           string
   582  		pathRelativeToCwd string
   583  		expects           string
   584  	}{
   585  		{
   586  			name:              "relative dist with anchor",
   587  			distDir:           "./dist",
   588  			pathRelativeToCwd: "dist/my.sbom",
   589  			expects:           "my.sbom",
   590  		},
   591  		{
   592  			name:              "relative dist without anchor",
   593  			distDir:           "dist",
   594  			pathRelativeToCwd: "dist/my.sbom",
   595  			expects:           "my.sbom",
   596  		},
   597  		{
   598  			name:              "relative dist with nested resource",
   599  			distDir:           "dist",
   600  			pathRelativeToCwd: "dist/something/my.sbom",
   601  			expects:           "something/my.sbom",
   602  		},
   603  		{
   604  			name:              "absolute dist with nested resource",
   605  			distDir:           filepath.Join(cwd, "dist/"),
   606  			pathRelativeToCwd: "dist/something/my.sbom",
   607  			expects:           "something/my.sbom",
   608  		},
   609  	}
   610  	for _, test := range tests {
   611  		t.Run(test.name, func(t *testing.T) {
   612  			actual, err := subprocessDistPath(test.distDir, test.pathRelativeToCwd)
   613  			require.NoError(t, err)
   614  			assert.Equal(t, test.expects, actual)
   615  		})
   616  	}
   617  }
   618  
   619  func Test_templateNames(t *testing.T) {
   620  	art := artifact.Artifact{
   621  		Name:   "name-it",
   622  		Path:   "to/a/place",
   623  		Goos:   "darwin",
   624  		Goarch: "amd64",
   625  		Type:   artifact.Binary,
   626  		Extra: map[string]interface{}{
   627  			artifact.ExtraID: "id-it",
   628  			"Binary":         "binary-name",
   629  		},
   630  	}
   631  
   632  	wd, err := os.Getwd()
   633  	require.NoError(t, err)
   634  
   635  	tests := []struct {
   636  		name           string
   637  		dist           string
   638  		version        string
   639  		cfg            config.SBOM
   640  		artifact       artifact.Artifact
   641  		expectedValues map[string]string
   642  		expectedPaths  []string
   643  	}{
   644  		{
   645  			name:     "default configuration",
   646  			artifact: art,
   647  			cfg:      config.SBOM{},
   648  			dist:     "/somewhere/to/dist",
   649  			expectedPaths: []string{
   650  				"/somewhere/to/dist/name-it.sbom",
   651  			},
   652  			expectedValues: map[string]string{
   653  				"artifact":   "to/a/place",
   654  				"artifactID": "id-it",
   655  				"document":   "/somewhere/to/dist/name-it.sbom",
   656  				"document0":  "/somewhere/to/dist/name-it.sbom",
   657  			},
   658  		},
   659  		{
   660  			name:     "default configuration + relative dist",
   661  			artifact: art,
   662  			cfg:      config.SBOM{},
   663  			dist:     "somewhere/to/dist",
   664  			expectedPaths: []string{
   665  				filepath.Join(wd, "somewhere/to/dist/name-it.sbom"),
   666  			},
   667  			expectedValues: map[string]string{
   668  				"artifact":   "to/a/place", // note: this is always relative to ${dist}
   669  				"artifactID": "id-it",
   670  				"document":   filepath.Join(wd, "somewhere/to/dist/name-it.sbom"),
   671  				"document0":  filepath.Join(wd, "somewhere/to/dist/name-it.sbom"),
   672  			},
   673  		},
   674  		{
   675  			name: "custom document using $artifact",
   676  			// note: this configuration is probably a misconfiguration since it is placing SBOMs within each bin
   677  			// directory, however, it will behave as correctly as possible.
   678  			artifact: art,
   679  			cfg: config.SBOM{
   680  				Documents: []string{
   681  					// note: the artifact name is probably an incorrect value here since it can't express all attributes
   682  					// of the binary (os, arch, etc), so builds with multiple architectures will create SBOMs with the
   683  					// same name.
   684  					"${artifact}.cdx.sbom",
   685  				},
   686  			},
   687  			dist: "somewhere/to/dist",
   688  			expectedPaths: []string{
   689  				filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"),
   690  			},
   691  			expectedValues: map[string]string{
   692  				"artifact":   "to/a/place",
   693  				"artifactID": "id-it",
   694  				"document":   filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"),
   695  				"document0":  filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"),
   696  			},
   697  		},
   698  		{
   699  			name:     "custom document using build vars",
   700  			artifact: art,
   701  			cfg: config.SBOM{
   702  				Documents: []string{
   703  					"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom",
   704  				},
   705  			},
   706  			version: "1.0.0",
   707  			dist:    "somewhere/to/dist",
   708  			expectedPaths: []string{
   709  				filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   710  			},
   711  			expectedValues: map[string]string{
   712  				"artifact":   "to/a/place",
   713  				"artifactID": "id-it",
   714  				"document":   filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   715  				"document0":  filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   716  			},
   717  		},
   718  		{
   719  			name:     "env vars with go templated options",
   720  			artifact: art,
   721  			cfg: config.SBOM{
   722  				Documents: []string{
   723  					"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom",
   724  				},
   725  				Env: []string{
   726  					"with-env-var=value",
   727  					"custom-os={{ .Os }}-unique",
   728  					"custom-arch={{ .Arch }}-unique",
   729  				},
   730  			},
   731  			version: "1.0.0",
   732  			dist:    "somewhere/to/dist",
   733  			expectedPaths: []string{
   734  				filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   735  			},
   736  			expectedValues: map[string]string{
   737  				"artifact":     "to/a/place",
   738  				"artifactID":   "id-it",
   739  				"with-env-var": "value",
   740  				"custom-os":    "darwin-unique",
   741  				"custom-arch":  "amd64-unique",
   742  				"document":     filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   743  				"document0":    filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"),
   744  			},
   745  		},
   746  	}
   747  	for _, tt := range tests {
   748  		t.Run(tt.name, func(t *testing.T) {
   749  			ctx := testctx.NewWithCfg(config.Project{
   750  				Dist: tt.dist,
   751  			}, testctx.WithVersion(tt.version))
   752  
   753  			cfg := tt.cfg
   754  			require.NoError(t, setConfigDefaults(&cfg))
   755  
   756  			var inputArgs []string
   757  			var expectedArgs []string
   758  			for key, value := range tt.expectedValues {
   759  				inputArgs = append(inputArgs, fmt.Sprintf("${%s}", key))
   760  				expectedArgs = append(expectedArgs, value)
   761  			}
   762  			cfg.Args = inputArgs
   763  
   764  			actualArgs, actualEnvs, actualPaths, err := applyTemplate(ctx, cfg, &tt.artifact)
   765  			require.NoError(t, err)
   766  
   767  			assert.Equal(t, tt.expectedPaths, actualPaths, "paths differ")
   768  
   769  			assert.Equal(t, expectedArgs, actualArgs, "arguments differ")
   770  
   771  			actualEnv := make(map[string]string)
   772  			for _, str := range actualEnvs {
   773  				k, v, ok := strings.Cut(str, "=")
   774  				require.True(t, ok)
   775  				actualEnv[k] = v
   776  			}
   777  
   778  			for k, v := range tt.expectedValues {
   779  				assert.Equal(t, v, actualEnv[k])
   780  			}
   781  		})
   782  	}
   783  }
   784  
   785  func TestDependencies(t *testing.T) {
   786  	ctx := testctx.NewWithCfg(config.Project{
   787  		SBOMs: []config.SBOM{
   788  			{Cmd: "syft"},
   789  			{Cmd: "foobar"},
   790  		},
   791  	})
   792  	require.Equal(t, []string{"syft", "foobar"}, Pipe{}.Dependencies(ctx))
   793  }