github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/cmd/syft/internal/commands/attest_test.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os/exec"
     9  	"regexp"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/scylladb/go-set/strset"
    15  	"github.com/spf13/cobra"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/anchore/clio"
    20  	"github.com/anchore/clio/cliotestutils"
    21  	"github.com/anchore/syft/cmd/syft/internal"
    22  	"github.com/anchore/syft/cmd/syft/internal/options"
    23  	"github.com/anchore/syft/syft/sbom"
    24  	"github.com/anchore/syft/syft/source"
    25  )
    26  
    27  func Test_writeSBOMToFormattedFile(t *testing.T) {
    28  	type args struct {
    29  		s    *sbom.SBOM
    30  		opts *attestOptions
    31  	}
    32  	tests := []struct {
    33  		name         string
    34  		args         args
    35  		wantSbomFile string
    36  		wantErr      bool
    37  	}{
    38  		{
    39  			name: "go case",
    40  			args: args{
    41  				opts: &attestOptions{
    42  					Output: func() options.Output {
    43  						def := defaultAttestOutputOptions()
    44  						def.Outputs = []string{"syft-json"}
    45  						return def
    46  					}(),
    47  				},
    48  				s: &sbom.SBOM{
    49  					Artifacts:     sbom.Artifacts{},
    50  					Relationships: nil,
    51  					Source: source.Description{
    52  						ID:      "source-id",
    53  						Name:    "source-name",
    54  						Version: "source-version",
    55  					},
    56  					Descriptor: sbom.Descriptor{
    57  						Name:    "syft-test",
    58  						Version: "non-version",
    59  					},
    60  				},
    61  			},
    62  			wantSbomFile: `{
    63   "artifacts": [],
    64   "artifactRelationships": [],
    65   "source": {
    66    "id": "source-id",
    67    "name": "source-name",
    68    "version": "source-version",
    69    "type": "",
    70    "metadata": null
    71   },
    72   "distro": {},
    73   "descriptor": {
    74    "name": "syft-test",
    75    "version": "non-version"
    76   },
    77   "schema": {}
    78  }`,
    79  			wantErr: false,
    80  		},
    81  	}
    82  	for _, tt := range tests {
    83  		t.Run(tt.name, func(t *testing.T) {
    84  			sbomFile := &bytes.Buffer{}
    85  
    86  			err := writeSBOMToFormattedFile(tt.args.s, sbomFile, tt.args.opts)
    87  			if (err != nil) != tt.wantErr {
    88  				t.Errorf("writeSBOMToFormattedFile() error = %v, wantErr %v", err, tt.wantErr)
    89  				return
    90  			}
    91  
    92  			// redact the schema block
    93  			re := regexp.MustCompile(`(?s)"schema":\W*\{.*?},?`)
    94  			subject := re.ReplaceAllString(sbomFile.String(), `"schema":{}`)
    95  
    96  			assert.JSONEq(t, tt.wantSbomFile, subject)
    97  		})
    98  	}
    99  }
   100  
   101  func Test_attestCommand(t *testing.T) {
   102  	cmdPrefix := cosignBinName
   103  	lp, err := exec.LookPath(cosignBinName)
   104  	if err == nil {
   105  		cmdPrefix = lp
   106  	}
   107  
   108  	fullCmd := func(args string) string {
   109  		return fmt.Sprintf("%s %s", cmdPrefix, args)
   110  	}
   111  
   112  	type args struct {
   113  		sbomFilepath string
   114  		opts         attestOptions
   115  		userInput    string
   116  	}
   117  	tests := []struct {
   118  		name        string
   119  		args        args
   120  		wantCmd     string
   121  		wantEnvVars map[string]string
   122  		notEnvVars  []string
   123  		wantErr     require.ErrorAssertionFunc
   124  	}{
   125  		{
   126  			name: "with key and password",
   127  			args: args{
   128  				userInput:    "myimage",
   129  				sbomFilepath: "/tmp/sbom-filepath.json",
   130  				opts: func() attestOptions {
   131  					def := defaultAttestOptions()
   132  					def.Outputs = []string{"syft-json"}
   133  					def.Attest.Key = "key"
   134  					def.Attest.Password = "password"
   135  					return def
   136  				}(),
   137  			},
   138  			wantCmd: fullCmd("attest myimage --predicate /tmp/sbom-filepath.json --type custom -y --key key"),
   139  			wantEnvVars: map[string]string{
   140  				"COSIGN_PASSWORD": "password",
   141  			},
   142  			notEnvVars: []string{
   143  				"COSIGN_EXPERIMENTAL", // only for keyless
   144  			},
   145  		},
   146  		{
   147  			name: "keyless",
   148  			args: args{
   149  				userInput:    "myimage",
   150  				sbomFilepath: "/tmp/sbom-filepath.json",
   151  				opts: func() attestOptions {
   152  					def := defaultAttestOptions()
   153  					def.Outputs = []string{"syft-json"}
   154  					return def
   155  				}(),
   156  			},
   157  			wantCmd: fullCmd("attest myimage --predicate /tmp/sbom-filepath.json --type custom -y"),
   158  			wantEnvVars: map[string]string{
   159  				"COSIGN_EXPERIMENTAL": "1",
   160  			},
   161  			notEnvVars: []string{
   162  				"COSIGN_PASSWORD",
   163  			},
   164  		},
   165  	}
   166  	for _, tt := range tests {
   167  		t.Run(tt.name, func(t *testing.T) {
   168  			if tt.wantErr == nil {
   169  				tt.wantErr = require.NoError
   170  			}
   171  
   172  			got, err := attestCommand(tt.args.sbomFilepath, &tt.args.opts, tt.args.userInput)
   173  			tt.wantErr(t, err)
   174  			if err != nil {
   175  				return
   176  			}
   177  
   178  			require.NotNil(t, got)
   179  			assert.Equal(t, tt.wantCmd, got.String())
   180  
   181  			gotEnv := strset.New(got.Env...)
   182  
   183  			for k, v := range tt.wantEnvVars {
   184  				assert.True(t, gotEnv.Has(fmt.Sprintf("%s=%s", k, v)))
   185  			}
   186  
   187  			for _, k := range tt.notEnvVars {
   188  				for _, env := range got.Env {
   189  					fields := strings.Split(env, "=")
   190  					if fields[0] == k {
   191  						t.Errorf("attestCommand() unexpected environment variable %s", k)
   192  					}
   193  				}
   194  			}
   195  		})
   196  	}
   197  }
   198  
   199  func Test_predicateType(t *testing.T) {
   200  	tests := []struct {
   201  		name string
   202  		want string
   203  	}{
   204  		{
   205  			name: "cyclonedx-json",
   206  			want: "cyclonedx",
   207  		},
   208  		{
   209  			name: "spdx-tag-value",
   210  			want: "spdx",
   211  		},
   212  		{
   213  			name: "spdx-tv",
   214  			want: "spdx",
   215  		},
   216  		{
   217  			name: "spdx-json",
   218  			want: "spdxjson",
   219  		},
   220  		{
   221  			name: "json",
   222  			want: "spdxjson",
   223  		},
   224  		{
   225  			name: "syft-json",
   226  			want: "custom",
   227  		},
   228  	}
   229  	for _, tt := range tests {
   230  		t.Run(tt.name, func(t *testing.T) {
   231  			assert.Equalf(t, tt.want, predicateType(tt.name), "predicateType(%v)", tt.name)
   232  		})
   233  	}
   234  }
   235  
   236  func Test_buildSBOMForAttestation(t *testing.T) {
   237  	// note: this test is only meant to test that the filter function is wired
   238  	// and not the correctness of the function in depth
   239  	type args struct {
   240  		id        clio.Identification
   241  		opts      *options.Catalog
   242  		userInput string
   243  	}
   244  	tests := []struct {
   245  		name    string
   246  		args    args
   247  		want    *sbom.SBOM
   248  		wantErr require.ErrorAssertionFunc
   249  	}{
   250  		{
   251  			name: "do not allow directory scans",
   252  			args: args{
   253  				opts: func() *options.Catalog {
   254  					def := defaultAttestOptions()
   255  					return &def.Catalog
   256  				}(),
   257  				userInput: "dir:/tmp/something",
   258  			},
   259  			wantErr: require.Error,
   260  		},
   261  	}
   262  	for _, tt := range tests {
   263  		t.Run(tt.name, func(t *testing.T) {
   264  			if tt.wantErr == nil {
   265  				tt.wantErr = require.NoError
   266  			}
   267  			_, err := generateSBOMForAttestation(context.Background(), tt.args.id, tt.args.opts, tt.args.userInput)
   268  			tt.wantErr(t, err)
   269  			if err != nil {
   270  				return
   271  			}
   272  		})
   273  	}
   274  }
   275  
   276  func Test_attestCLIWiring(t *testing.T) {
   277  	id := clio.Identification{
   278  		Name:    "syft",
   279  		Version: "testing",
   280  	}
   281  	cfg := internal.AppClioSetupConfig(id, io.Discard)
   282  	tests := []struct {
   283  		name          string
   284  		assertionFunc func(*testing.T, *cobra.Command, []string, ...any)
   285  		wantOpts      attestOptions
   286  		args          []string
   287  		env           map[string]string
   288  	}{
   289  		{
   290  			name:          "key flag is accepted",
   291  			args:          []string{"some-image:some-tag", "--key", "some-cosign-key.key"},
   292  			assertionFunc: hasAttestOpts(options.Attest{Key: "some-cosign-key.key"}),
   293  		},
   294  		{
   295  			name: "key password is read from env",
   296  			args: []string{"some-image:some-tag", "--key", "cosign.key"},
   297  			env: map[string]string{
   298  				"SYFT_ATTEST_PASSWORD": "some-password",
   299  			},
   300  			assertionFunc: hasAttestOpts(options.Attest{
   301  				Key:      "cosign.key",
   302  				Password: "some-password",
   303  			}),
   304  		},
   305  	}
   306  	for _, tt := range tests {
   307  		t.Run(tt.name, func(t *testing.T) {
   308  			if tt.env != nil {
   309  				for k, v := range tt.env {
   310  					t.Setenv(k, v)
   311  				}
   312  			}
   313  			app := cliotestutils.NewApplication(t, cfg, tt.assertionFunc)
   314  			cmd := Attest(app)
   315  			cmd.SetArgs(tt.args)
   316  			err := cmd.Execute()
   317  			assert.NoError(t, err)
   318  		})
   319  	}
   320  }
   321  
   322  func hasAttestOpts(wantOpts options.Attest) cliotestutils.AssertionFunc {
   323  	return func(t *testing.T, _ *cobra.Command, _ []string, cfgs ...any) {
   324  		assert.Equal(t, len(cfgs), 1)
   325  		attestOpts, ok := cfgs[0].(*attestOptions)
   326  		require.True(t, ok)
   327  		if d := cmp.Diff(wantOpts, attestOpts.Attest); d != "" {
   328  			t.Errorf("mismatched attest options (-want +got):\n%s", d)
   329  		}
   330  	}
   331  }