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