github.com/anchore/syft@v1.38.2/syft/create_sbom_config_test.go (about) 1 package syft 2 3 import ( 4 "context" 5 "sort" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/google/go-cmp/cmp/cmpopts" 10 "github.com/scylladb/go-set/strset" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 14 "github.com/anchore/syft/internal/task" 15 "github.com/anchore/syft/syft/artifact" 16 "github.com/anchore/syft/syft/cataloging" 17 "github.com/anchore/syft/syft/cataloging/filecataloging" 18 "github.com/anchore/syft/syft/cataloging/pkgcataloging" 19 "github.com/anchore/syft/syft/file" 20 "github.com/anchore/syft/syft/pkg" 21 "github.com/anchore/syft/syft/source" 22 ) 23 24 var _ pkg.Cataloger = (*dummyCataloger)(nil) 25 26 type dummyCataloger struct { 27 name string 28 } 29 30 func newDummyCataloger(name string) pkg.Cataloger { 31 return dummyCataloger{name: name} 32 } 33 34 func (d dummyCataloger) Name() string { 35 return d.name 36 } 37 38 func (d dummyCataloger) Catalog(_ context.Context, _ file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { 39 return nil, nil, nil 40 } 41 42 func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) { 43 pkgIntersect := func(intersect ...string) []string { 44 var sets []*strset.Set 45 for _, s := range intersect { 46 sets = append(sets, strset.New(pkgCatalogerNamesWithTagOrName(t, s)...)) 47 } 48 49 intersectSet := strset.Intersection(sets...) 50 51 slice := intersectSet.List() 52 53 sort.Strings(slice) 54 55 return slice 56 } 57 58 addTo := func(slice []string, add ...string) []string { 59 slice = append(slice, add...) 60 sort.Strings(slice) 61 return slice 62 } 63 64 imgSrc := source.Description{ 65 Metadata: source.ImageMetadata{}, 66 } 67 68 dirSrc := source.Description{ 69 Metadata: source.DirectoryMetadata{}, 70 } 71 72 fileSrc := source.Description{ 73 Metadata: source.FileMetadata{}, 74 } 75 76 tests := []struct { 77 name string 78 src source.Description 79 cfg *CreateSBOMConfig 80 wantTaskNames [][]string 81 wantManifest *catalogerManifest 82 wantErr require.ErrorAssertionFunc 83 }{ 84 { 85 name: "default catalogers for image source", 86 src: imgSrc, 87 cfg: DefaultCreateSBOMConfig(), 88 wantTaskNames: [][]string{ 89 environmentCatalogerNames(), 90 pkgCatalogerNamesWithTagOrName(t, "image"), 91 fileCatalogerNames(), 92 relationshipCatalogerNames(), 93 unknownsTaskNames(), 94 osFeatureDetectionTaskNames(), 95 }, 96 wantManifest: &catalogerManifest{ 97 Requested: cataloging.SelectionRequest{ 98 DefaultNamesOrTags: []string{"image", "file"}, 99 }, 100 Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames()), 101 }, 102 wantErr: require.NoError, 103 }, 104 { 105 name: "default catalogers for directory source", 106 src: dirSrc, 107 cfg: DefaultCreateSBOMConfig(), 108 wantTaskNames: [][]string{ 109 environmentCatalogerNames(), 110 pkgCatalogerNamesWithTagOrName(t, "directory"), 111 fileCatalogerNames(), 112 relationshipCatalogerNames(), 113 unknownsTaskNames(), 114 osFeatureDetectionTaskNames(), 115 }, 116 wantManifest: &catalogerManifest{ 117 Requested: cataloging.SelectionRequest{ 118 DefaultNamesOrTags: []string{"directory", "file"}, 119 }, 120 Used: flatten(pkgCatalogerNamesWithTagOrName(t, "directory"), fileCatalogerNames()), 121 }, 122 wantErr: require.NoError, 123 }, 124 { 125 // note, the file source acts like a directory scan 126 name: "default catalogers for file source", 127 src: fileSrc, 128 cfg: DefaultCreateSBOMConfig(), 129 wantTaskNames: [][]string{ 130 environmentCatalogerNames(), 131 pkgCatalogerNamesWithTagOrName(t, "directory"), 132 fileCatalogerNames(), 133 relationshipCatalogerNames(), 134 unknownsTaskNames(), 135 osFeatureDetectionTaskNames(), 136 }, 137 wantManifest: &catalogerManifest{ 138 Requested: cataloging.SelectionRequest{ 139 DefaultNamesOrTags: []string{"directory", "file"}, 140 }, 141 Used: flatten(pkgCatalogerNamesWithTagOrName(t, "directory"), fileCatalogerNames()), 142 }, 143 wantErr: require.NoError, 144 }, 145 { 146 name: "no file digest cataloger", 147 src: imgSrc, 148 cfg: DefaultCreateSBOMConfig().WithCatalogerSelection(cataloging.NewSelectionRequest().WithRemovals("digest")), 149 wantTaskNames: [][]string{ 150 environmentCatalogerNames(), 151 pkgCatalogerNamesWithTagOrName(t, "image"), 152 fileCatalogerNames("file-metadata", "content", "binary-metadata"), 153 relationshipCatalogerNames(), 154 unknownsTaskNames(), 155 osFeatureDetectionTaskNames(), 156 }, 157 wantManifest: &catalogerManifest{ 158 Requested: cataloging.SelectionRequest{ 159 DefaultNamesOrTags: []string{"image", "file"}, 160 RemoveNamesOrTags: []string{"digest"}, 161 }, 162 Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames("file-metadata", "content", "binary-metadata")), 163 }, 164 wantErr: require.NoError, 165 }, 166 { 167 name: "select no file catalogers", 168 src: imgSrc, 169 cfg: DefaultCreateSBOMConfig().WithCatalogerSelection(cataloging.NewSelectionRequest().WithRemovals("file")), 170 wantTaskNames: [][]string{ 171 environmentCatalogerNames(), 172 pkgCatalogerNamesWithTagOrName(t, "image"), 173 nil, // note: there is a file cataloging group, with no items in it 174 relationshipCatalogerNames(), 175 unknownsTaskNames(), 176 osFeatureDetectionTaskNames(), 177 }, 178 wantManifest: &catalogerManifest{ 179 Requested: cataloging.SelectionRequest{ 180 DefaultNamesOrTags: []string{"image", "file"}, 181 RemoveNamesOrTags: []string{"file"}, 182 }, 183 Used: pkgCatalogerNamesWithTagOrName(t, "image"), 184 }, 185 wantErr: require.NoError, 186 }, 187 { 188 name: "select all file catalogers", 189 src: imgSrc, 190 cfg: DefaultCreateSBOMConfig().WithFilesConfig(filecataloging.DefaultConfig().WithSelection(file.AllFilesSelection)), 191 wantTaskNames: [][]string{ 192 environmentCatalogerNames(), 193 // note: there is a single group of catalogers for pkgs and files 194 append( 195 pkgCatalogerNamesWithTagOrName(t, "image"), 196 fileCatalogerNames()..., 197 ), 198 relationshipCatalogerNames(), 199 unknownsTaskNames(), 200 osFeatureDetectionTaskNames(), 201 }, 202 wantManifest: &catalogerManifest{ 203 Requested: cataloging.SelectionRequest{ 204 DefaultNamesOrTags: []string{"image", "file"}, 205 }, 206 Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames()), 207 }, 208 wantErr: require.NoError, 209 }, 210 { 211 name: "user-provided persistent cataloger is always run (image)", 212 src: imgSrc, 213 cfg: DefaultCreateSBOMConfig().WithCatalogers( 214 pkgcataloging.NewAlwaysEnabledCatalogerReference(newDummyCataloger("persistent")), 215 ), 216 wantTaskNames: [][]string{ 217 environmentCatalogerNames(), 218 addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "persistent"), 219 fileCatalogerNames(), 220 relationshipCatalogerNames(), 221 unknownsTaskNames(), 222 osFeatureDetectionTaskNames(), 223 }, 224 wantManifest: &catalogerManifest{ 225 Requested: cataloging.SelectionRequest{ 226 DefaultNamesOrTags: []string{"image", "file"}, 227 }, 228 Used: flatten(addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "persistent"), fileCatalogerNames()), 229 }, 230 wantErr: require.NoError, 231 }, 232 { 233 name: "user-provided persistent cataloger is always run (directory)", 234 src: dirSrc, 235 cfg: DefaultCreateSBOMConfig().WithCatalogers( 236 pkgcataloging.NewAlwaysEnabledCatalogerReference(newDummyCataloger("persistent")), 237 ), 238 wantTaskNames: [][]string{ 239 environmentCatalogerNames(), 240 addTo(pkgCatalogerNamesWithTagOrName(t, "directory"), "persistent"), 241 fileCatalogerNames(), 242 relationshipCatalogerNames(), 243 unknownsTaskNames(), 244 osFeatureDetectionTaskNames(), 245 }, 246 wantManifest: &catalogerManifest{ 247 Requested: cataloging.SelectionRequest{ 248 DefaultNamesOrTags: []string{"directory", "file"}, 249 }, 250 Used: flatten(addTo(pkgCatalogerNamesWithTagOrName(t, "directory"), "persistent"), fileCatalogerNames()), 251 }, 252 wantErr: require.NoError, 253 }, 254 { 255 name: "user-provided persistent cataloger is always run (user selection does not affect this)", 256 src: imgSrc, 257 cfg: DefaultCreateSBOMConfig().WithCatalogers( 258 pkgcataloging.NewAlwaysEnabledCatalogerReference(newDummyCataloger("persistent")), 259 ).WithCatalogerSelection(cataloging.NewSelectionRequest().WithSubSelections("javascript")), 260 wantTaskNames: [][]string{ 261 environmentCatalogerNames(), 262 addTo(pkgIntersect("image", "javascript"), "persistent"), 263 fileCatalogerNames(), 264 relationshipCatalogerNames(), 265 unknownsTaskNames(), 266 osFeatureDetectionTaskNames(), 267 }, 268 wantManifest: &catalogerManifest{ 269 Requested: cataloging.SelectionRequest{ 270 DefaultNamesOrTags: []string{"image", "file"}, 271 SubSelectTags: []string{"javascript"}, 272 }, 273 Used: flatten(addTo(pkgIntersect("image", "javascript"), "persistent"), fileCatalogerNames()), 274 }, 275 wantErr: require.NoError, 276 }, 277 { 278 name: "user-provided cataloger runs when selected", 279 src: imgSrc, 280 cfg: DefaultCreateSBOMConfig().WithCatalogers( 281 pkgcataloging.NewCatalogerReference(newDummyCataloger("user-provided"), []string{"image"}), 282 ), 283 wantTaskNames: [][]string{ 284 environmentCatalogerNames(), 285 addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "user-provided"), 286 fileCatalogerNames(), 287 relationshipCatalogerNames(), 288 unknownsTaskNames(), 289 osFeatureDetectionTaskNames(), 290 }, 291 wantManifest: &catalogerManifest{ 292 Requested: cataloging.SelectionRequest{ 293 DefaultNamesOrTags: []string{"image", "file"}, 294 }, 295 Used: flatten(addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "user-provided"), fileCatalogerNames()), 296 }, 297 wantErr: require.NoError, 298 }, 299 { 300 name: "user-provided cataloger NOT run when NOT selected", 301 src: imgSrc, 302 cfg: DefaultCreateSBOMConfig().WithCatalogers( 303 pkgcataloging.NewCatalogerReference(newDummyCataloger("user-provided"), []string{"bogus-selector-will-never-be-used"}), 304 ), 305 wantTaskNames: [][]string{ 306 environmentCatalogerNames(), 307 pkgCatalogerNamesWithTagOrName(t, "image"), 308 fileCatalogerNames(), 309 relationshipCatalogerNames(), 310 unknownsTaskNames(), 311 osFeatureDetectionTaskNames(), 312 }, 313 wantManifest: &catalogerManifest{ 314 Requested: cataloging.SelectionRequest{ 315 DefaultNamesOrTags: []string{"image", "file"}, 316 }, 317 Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames()), 318 }, 319 wantErr: require.NoError, 320 }, 321 } 322 for _, tt := range tests { 323 t.Run(tt.name, func(t *testing.T) { 324 if tt.wantErr == nil { 325 tt.wantErr = require.NoError 326 } 327 328 // sanity check 329 require.NotEmpty(t, tt.wantTaskNames) 330 331 // test the subject 332 gotTasks, gotManifest, err := tt.cfg.makeTaskGroups(tt.src) 333 tt.wantErr(t, err) 334 if err != nil { 335 return 336 } 337 338 gotNames := taskGroupNames(gotTasks) 339 340 if d := cmp.Diff( 341 tt.wantTaskNames, 342 gotNames, 343 // order within a group does not matter 344 cmpopts.SortSlices(func(a, b string) bool { 345 return a < b 346 }), 347 ); d != "" { 348 t.Errorf("mismatched task group names (-want +got):\n%s", d) 349 } 350 351 if d := cmp.Diff(tt.wantManifest, gotManifest); d != "" { 352 t.Errorf("mismatched cataloger manifest (-want +got):\n%s", d) 353 } 354 }) 355 } 356 } 357 358 func pkgCatalogerNamesWithTagOrName(t *testing.T, token string) []string { 359 var names []string 360 cfg := task.DefaultCatalogingFactoryConfig() 361 for _, factory := range task.DefaultPackageTaskFactories() { 362 cat := factory(cfg) 363 364 name := cat.Name() 365 366 if selector, ok := cat.(task.Selector); ok { 367 if selector.HasAllSelectors(token) { 368 names = append(names, name) 369 continue 370 } 371 } 372 if name == token { 373 names = append(names, name) 374 } 375 } 376 377 // these thresholds are arbitrary but should be large enough to catch any major changes 378 switch token { 379 case "image": 380 require.Greater(t, len(names), 18, "minimum cataloger sanity check failed token") 381 case "directory": 382 require.Greater(t, len(names), 25, "minimum cataloger sanity check failed token") 383 default: 384 require.Greater(t, len(names), 0, "minimum cataloger sanity check failed token") 385 } 386 387 sort.Strings(names) 388 return names 389 } 390 391 func fileCatalogerNames(tokens ...string) []string { 392 var names []string 393 cfg := task.DefaultCatalogingFactoryConfig() 394 topLoop: 395 for _, factory := range task.DefaultFileTaskFactories() { 396 cat := factory(cfg) 397 398 if cat == nil { 399 continue 400 } 401 402 name := cat.Name() 403 404 if len(tokens) == 0 { 405 names = append(names, name) 406 continue 407 } 408 409 for _, token := range tokens { 410 if selector, ok := cat.(task.Selector); ok { 411 if selector.HasAllSelectors(token) { 412 names = append(names, name) 413 continue topLoop 414 } 415 416 } 417 if name == token { 418 names = append(names, name) 419 } 420 } 421 } 422 423 sort.Strings(names) 424 return names 425 } 426 427 func flatten(lists ...[]string) []string { 428 var final []string 429 for _, lst := range lists { 430 final = append(final, lst...) 431 } 432 sort.Strings(final) 433 return final 434 } 435 436 func relationshipCatalogerNames() []string { 437 return []string{"relationships-cataloger"} 438 } 439 440 func unknownsTaskNames() []string { 441 return []string{"unknowns-labeler"} 442 } 443 444 func osFeatureDetectionTaskNames() []string { 445 return []string{"os-feature-detection"} 446 } 447 448 func environmentCatalogerNames() []string { 449 return []string{"environment-cataloger"} 450 } 451 452 func taskGroupNames(groups [][]task.Task) [][]string { 453 var names [][]string 454 for _, group := range groups { 455 var groupNames []string 456 for _, tsk := range group { 457 groupNames = append(groupNames, tsk.Name()) 458 } 459 names = append(names, groupNames) 460 } 461 return names 462 } 463 464 func Test_replaceDefaultTagReferences(t *testing.T) { 465 466 tests := []struct { 467 name string 468 lst []string 469 want []string 470 }{ 471 { 472 name: "no default tag", 473 lst: []string{"foo", "bar"}, 474 want: []string{"foo", "bar"}, 475 }, 476 { 477 name: "replace default tag", 478 lst: []string{"foo", "default", "bar"}, 479 want: []string{"foo", "replacement", "bar"}, 480 }, 481 } 482 for _, tt := range tests { 483 t.Run(tt.name, func(t *testing.T) { 484 assert.Equal(t, tt.want, replaceDefaultTagReferences([]string{"replacement"}, tt.lst)) 485 }) 486 } 487 } 488 489 func Test_findDefaultTag(t *testing.T) { 490 491 tests := []struct { 492 name string 493 src source.Description 494 want []string 495 wantErr require.ErrorAssertionFunc 496 }{ 497 { 498 name: "image", 499 src: source.Description{ 500 Metadata: source.ImageMetadata{}, 501 }, 502 want: []string{pkgcataloging.ImageTag, filecataloging.FileTag}, 503 }, 504 { 505 name: "directory", 506 src: source.Description{ 507 Metadata: source.DirectoryMetadata{}, 508 }, 509 want: []string{pkgcataloging.DirectoryTag, filecataloging.FileTag}, 510 }, 511 { 512 name: "file", 513 src: source.Description{ 514 Metadata: source.FileMetadata{}, 515 }, 516 want: []string{pkgcataloging.DirectoryTag, filecataloging.FileTag}, // not a mistake... 517 }, 518 { 519 name: "unknown", 520 src: source.Description{ 521 Metadata: struct{}{}, 522 }, 523 wantErr: require.Error, 524 }, 525 } 526 for _, tt := range tests { 527 t.Run(tt.name, func(t *testing.T) { 528 if tt.wantErr == nil { 529 tt.wantErr = require.NoError 530 } 531 got, err := findDefaultTags(tt.src) 532 tt.wantErr(t, err) 533 if err != nil { 534 return 535 } 536 assert.Equal(t, tt.want, got) 537 }) 538 } 539 } 540 541 func TestCreateSBOMConfig_validate(t *testing.T) { 542 tests := []struct { 543 name string 544 cfg *CreateSBOMConfig 545 wantErr assert.ErrorAssertionFunc 546 }{ 547 { 548 name: "incompatible ExcludeBinaryPackagesWithFileOwnershipOverlap selection", 549 cfg: DefaultCreateSBOMConfig(). 550 WithRelationshipsConfig( 551 cataloging.DefaultRelationshipsConfig(). 552 WithExcludeBinaryPackagesWithFileOwnershipOverlap(true). 553 WithPackageFileOwnershipOverlap(false), 554 ), 555 wantErr: assert.Error, 556 }, 557 } 558 for _, tt := range tests { 559 t.Run(tt.name, func(t *testing.T) { 560 if tt.wantErr == nil { 561 tt.wantErr = assert.NoError 562 } 563 tt.wantErr(t, tt.cfg.validate()) 564 }) 565 } 566 }