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  }