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 }