github.com/anchore/syft@v1.38.2/internal/task/selection_test.go (about)

     1  package task
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/scylladb/go-set/strset"
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/anchore/syft/internal/sbomsync"
    13  	"github.com/anchore/syft/syft/cataloging"
    14  	"github.com/anchore/syft/syft/file"
    15  )
    16  
    17  func dummyTask(name string, tags ...string) Task {
    18  	return NewTask(name, func(ctx context.Context, resolver file.Resolver, sbom sbomsync.Builder) error {
    19  		panic("not implemented")
    20  	}, tags...)
    21  }
    22  
    23  // note: this test fixture does not need to be kept up to date here, but makes a great test subject
    24  func createDummyPackageTasks() tasks {
    25  	return []Task{
    26  		// OS package installed catalogers
    27  		dummyTask("alpm-db-cataloger", "package", "directory", "installed", "image", "os", "alpm", "archlinux"),
    28  		dummyTask("apk-db-cataloger", "package", "directory", "installed", "image", "os", "apk", "alpine"),
    29  		dummyTask("dpkg-db-cataloger", "package", "directory", "installed", "image", "os", "dpkg", "debian"),
    30  		dummyTask("portage-cataloger", "package", "directory", "installed", "image", "os", "portage", "gentoo"),
    31  		dummyTask("rpm-db-cataloger", "package", "directory", "installed", "image", "os", "rpm", "redhat"),
    32  
    33  		// OS package declared catalogers
    34  		dummyTask("rpm-archive-cataloger", "package", "declared", "directory", "os", "rpm", "redhat"),
    35  
    36  		// language-specific package installed catalogers
    37  		dummyTask("conan-info-cataloger", "package", "installed", "image", "language", "cpp", "conan"),
    38  		dummyTask("javascript-package-cataloger", "package", "installed", "image", "language", "javascript", "node"),
    39  		dummyTask("php-composer-installed-cataloger", "package", "installed", "image", "language", "php", "composer"),
    40  		dummyTask("ruby-installed-gemspec-cataloger", "package", "installed", "image", "language", "ruby", "gem", "gemspec"),
    41  		dummyTask("rust-cargo-lock-cataloger", "package", "installed", "image", "language", "rust", "binary"),
    42  
    43  		// language-specific package declared catalogers
    44  		dummyTask("conan-cataloger", "package", "declared", "directory", "language", "cpp", "conan"),
    45  		dummyTask("dart-pubspec-lock-cataloger", "package", "declared", "directory", "language", "dart"),
    46  		dummyTask("dotnet-deps-cataloger", "package", "declared", "directory", "language", "dotnet", "c#"),
    47  		dummyTask("elixir-mix-lock-cataloger", "package", "declared", "directory", "language", "elixir"),
    48  		dummyTask("erlang-rebar-lock-cataloger", "package", "declared", "directory", "language", "erlang"),
    49  		dummyTask("javascript-lock-cataloger", "package", "declared", "directory", "language", "javascript", "node", "npm"),
    50  
    51  		// language-specific package for both image and directory scans (but not necessarily declared)
    52  		dummyTask("dotnet-portable-executable-cataloger", "package", "directory", "installed", "image", "language", "dotnet", "c#"),
    53  		dummyTask("python-installed-package-cataloger", "package", "directory", "installed", "image", "language", "python"),
    54  		dummyTask("go-module-binary-cataloger", "package", "directory", "installed", "image", "language", "go", "golang", "gomod", "binary"),
    55  		dummyTask("java-archive-cataloger", "package", "directory", "installed", "image", "language", "java", "maven"),
    56  		dummyTask("graalvm-native-image-cataloger", "package", "directory", "installed", "image", "language", "java"),
    57  
    58  		// other package catalogers
    59  		dummyTask("binary-cataloger", "package", "declared", "directory", "image", "binary"),
    60  		dummyTask("github-actions-usage-cataloger", "package", "declared", "directory", "github", "github-actions"),
    61  		dummyTask("github-action-workflow-usage-cataloger", "package", "declared", "directory", "github", "github-actions"),
    62  		dummyTask("sbom-cataloger", "package", "declared", "directory", "image", "sbom"),
    63  	}
    64  }
    65  
    66  func createDummyFileTasks() tasks {
    67  	return []Task{
    68  		dummyTask("file-content-cataloger", "file", "content"),
    69  		dummyTask("file-metadata-cataloger", "file", "metadata"),
    70  		dummyTask("file-digest-cataloger", "file", "digest"),
    71  		dummyTask("file-executable-cataloger", "file", "binary-metadata"),
    72  	}
    73  }
    74  
    75  func TestSelect(t *testing.T) {
    76  
    77  	tests := []struct {
    78  		name        string
    79  		allTasks    []Task
    80  		basis       []string
    81  		expressions []string
    82  		wantNames   []string
    83  		wantTokens  map[string]TokenSelection
    84  		wantRequest cataloging.SelectionRequest
    85  		wantErr     assert.ErrorAssertionFunc
    86  	}{
    87  		{
    88  			name:        "empty input",
    89  			allTasks:    []Task{},
    90  			basis:       []string{},
    91  			expressions: []string{},
    92  			wantNames:   []string{},
    93  			wantTokens:  map[string]TokenSelection{},
    94  			wantRequest: cataloging.SelectionRequest{},
    95  		},
    96  		{
    97  			name:     "use default tasks",
    98  			allTasks: createDummyPackageTasks(),
    99  			basis: []string{
   100  				"image",
   101  			},
   102  			expressions: []string{},
   103  			wantNames: []string{
   104  				"alpm-db-cataloger",
   105  				"apk-db-cataloger",
   106  				"dpkg-db-cataloger",
   107  				"portage-cataloger",
   108  				"rpm-db-cataloger",
   109  				"conan-info-cataloger",
   110  				"javascript-package-cataloger",
   111  				"php-composer-installed-cataloger",
   112  				"ruby-installed-gemspec-cataloger",
   113  				"rust-cargo-lock-cataloger",
   114  				"dotnet-portable-executable-cataloger",
   115  				"python-installed-package-cataloger",
   116  				"go-module-binary-cataloger",
   117  				"java-archive-cataloger",
   118  				"graalvm-native-image-cataloger",
   119  				"binary-cataloger",
   120  				"sbom-cataloger",
   121  			},
   122  			wantTokens: map[string]TokenSelection{
   123  				"alpm-db-cataloger":                    newTokenSelection([]string{"image"}, nil),
   124  				"apk-db-cataloger":                     newTokenSelection([]string{"image"}, nil),
   125  				"dpkg-db-cataloger":                    newTokenSelection([]string{"image"}, nil),
   126  				"portage-cataloger":                    newTokenSelection([]string{"image"}, nil),
   127  				"rpm-db-cataloger":                     newTokenSelection([]string{"image"}, nil),
   128  				"conan-info-cataloger":                 newTokenSelection([]string{"image"}, nil),
   129  				"javascript-package-cataloger":         newTokenSelection([]string{"image"}, nil),
   130  				"php-composer-installed-cataloger":     newTokenSelection([]string{"image"}, nil),
   131  				"ruby-installed-gemspec-cataloger":     newTokenSelection([]string{"image"}, nil),
   132  				"rust-cargo-lock-cataloger":            newTokenSelection([]string{"image"}, nil),
   133  				"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
   134  				"python-installed-package-cataloger":   newTokenSelection([]string{"image"}, nil),
   135  				"go-module-binary-cataloger":           newTokenSelection([]string{"image"}, nil),
   136  				"java-archive-cataloger":               newTokenSelection([]string{"image"}, nil),
   137  				"graalvm-native-image-cataloger":       newTokenSelection([]string{"image"}, nil),
   138  				"binary-cataloger":                     newTokenSelection([]string{"image"}, nil),
   139  				"sbom-cataloger":                       newTokenSelection([]string{"image"}, nil),
   140  			},
   141  			wantRequest: cataloging.SelectionRequest{
   142  				DefaultNamesOrTags: []string{"image"},
   143  			},
   144  		},
   145  		{
   146  			name:     "select, add, and remove tasks",
   147  			allTasks: createDummyPackageTasks(),
   148  			basis: []string{
   149  				"image",
   150  			},
   151  			expressions: []string{
   152  				"+github-actions-usage-cataloger",
   153  				"-dpkg",
   154  				"os",
   155  			},
   156  			wantNames: []string{
   157  				"alpm-db-cataloger",
   158  				"apk-db-cataloger",
   159  				"portage-cataloger",
   160  				"rpm-db-cataloger",
   161  				"github-actions-usage-cataloger",
   162  			},
   163  			wantTokens: map[string]TokenSelection{
   164  				// selected
   165  				"alpm-db-cataloger":              newTokenSelection([]string{"image", "os"}, nil),
   166  				"apk-db-cataloger":               newTokenSelection([]string{"image", "os"}, nil),
   167  				"dpkg-db-cataloger":              newTokenSelection([]string{"image", "os"}, []string{"dpkg"}),
   168  				"portage-cataloger":              newTokenSelection([]string{"image", "os"}, nil),
   169  				"rpm-db-cataloger":               newTokenSelection([]string{"image", "os"}, nil),
   170  				"github-actions-usage-cataloger": newTokenSelection([]string{"github-actions-usage-cataloger"}, nil),
   171  
   172  				// ultimately not selected
   173  				"rpm-archive-cataloger":                newTokenSelection([]string{"os"}, nil),
   174  				"conan-info-cataloger":                 newTokenSelection([]string{"image"}, nil),
   175  				"javascript-package-cataloger":         newTokenSelection([]string{"image"}, nil),
   176  				"php-composer-installed-cataloger":     newTokenSelection([]string{"image"}, nil),
   177  				"ruby-installed-gemspec-cataloger":     newTokenSelection([]string{"image"}, nil),
   178  				"rust-cargo-lock-cataloger":            newTokenSelection([]string{"image"}, nil),
   179  				"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
   180  				"python-installed-package-cataloger":   newTokenSelection([]string{"image"}, nil),
   181  				"go-module-binary-cataloger":           newTokenSelection([]string{"image"}, nil),
   182  				"java-archive-cataloger":               newTokenSelection([]string{"image"}, nil),
   183  				"graalvm-native-image-cataloger":       newTokenSelection([]string{"image"}, nil),
   184  				"binary-cataloger":                     newTokenSelection([]string{"image"}, nil),
   185  				"sbom-cataloger":                       newTokenSelection([]string{"image"}, nil),
   186  			},
   187  			wantRequest: cataloging.SelectionRequest{
   188  				DefaultNamesOrTags: []string{"image"},
   189  				SubSelectTags:      []string{"os"},
   190  				RemoveNamesOrTags:  []string{"dpkg"},
   191  				AddNames:           []string{"github-actions-usage-cataloger"},
   192  			},
   193  		},
   194  		{
   195  			name:     "allow for partial selections",
   196  			allTasks: createDummyPackageTasks(),
   197  			basis: []string{
   198  				"image",
   199  			},
   200  			expressions: []string{
   201  				// valid...
   202  				"+github-actions-usage-cataloger",
   203  				"-dpkg",
   204  				"os",
   205  				// invalid...
   206  				"+python",
   207  				"rust-cargo-lock-cataloger",
   208  			},
   209  			wantNames: []string{
   210  				"alpm-db-cataloger",
   211  				"apk-db-cataloger",
   212  				"portage-cataloger",
   213  				"rpm-db-cataloger",
   214  				"github-actions-usage-cataloger",
   215  			},
   216  			wantTokens: map[string]TokenSelection{
   217  				// selected
   218  				"alpm-db-cataloger":              newTokenSelection([]string{"image", "os"}, nil),
   219  				"apk-db-cataloger":               newTokenSelection([]string{"image", "os"}, nil),
   220  				"dpkg-db-cataloger":              newTokenSelection([]string{"image", "os"}, []string{"dpkg"}),
   221  				"portage-cataloger":              newTokenSelection([]string{"image", "os"}, nil),
   222  				"rpm-db-cataloger":               newTokenSelection([]string{"image", "os"}, nil),
   223  				"github-actions-usage-cataloger": newTokenSelection([]string{"github-actions-usage-cataloger"}, nil),
   224  
   225  				// ultimately not selected
   226  				"rpm-archive-cataloger":                newTokenSelection([]string{"os"}, nil),
   227  				"conan-info-cataloger":                 newTokenSelection([]string{"image"}, nil),
   228  				"javascript-package-cataloger":         newTokenSelection([]string{"image"}, nil),
   229  				"php-composer-installed-cataloger":     newTokenSelection([]string{"image"}, nil),
   230  				"ruby-installed-gemspec-cataloger":     newTokenSelection([]string{"image"}, nil),
   231  				"rust-cargo-lock-cataloger":            newTokenSelection([]string{"image"}, nil),
   232  				"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
   233  				"python-installed-package-cataloger":   newTokenSelection([]string{"image"}, nil), // note: there is no python token used for selection
   234  				"go-module-binary-cataloger":           newTokenSelection([]string{"image"}, nil),
   235  				"java-archive-cataloger":               newTokenSelection([]string{"image"}, nil),
   236  				"graalvm-native-image-cataloger":       newTokenSelection([]string{"image"}, nil),
   237  				"binary-cataloger":                     newTokenSelection([]string{"image"}, nil),
   238  				"sbom-cataloger":                       newTokenSelection([]string{"image"}, nil),
   239  			},
   240  			wantRequest: cataloging.SelectionRequest{
   241  				DefaultNamesOrTags: []string{"image"},
   242  				SubSelectTags:      []string{"os", "rust-cargo-lock-cataloger"},
   243  				RemoveNamesOrTags:  []string{"dpkg"},
   244  				AddNames:           []string{"github-actions-usage-cataloger", "python"},
   245  			},
   246  			wantErr: assert.Error, // !important!
   247  		},
   248  		{
   249  			name:     "select all tasks",
   250  			allTasks: createDummyPackageTasks(),
   251  			basis: []string{
   252  				"all",
   253  			},
   254  			expressions: []string{},
   255  			wantNames: []string{
   256  				"alpm-db-cataloger",
   257  				"apk-db-cataloger",
   258  				"dpkg-db-cataloger",
   259  				"portage-cataloger",
   260  				"rpm-db-cataloger",
   261  				"rpm-archive-cataloger",
   262  				"conan-info-cataloger",
   263  				"javascript-package-cataloger",
   264  				"php-composer-installed-cataloger",
   265  				"ruby-installed-gemspec-cataloger",
   266  				"rust-cargo-lock-cataloger",
   267  				"conan-cataloger",
   268  				"dart-pubspec-lock-cataloger",
   269  				"dotnet-deps-cataloger",
   270  				"elixir-mix-lock-cataloger",
   271  				"erlang-rebar-lock-cataloger",
   272  				"javascript-lock-cataloger",
   273  				"dotnet-portable-executable-cataloger",
   274  				"python-installed-package-cataloger",
   275  				"go-module-binary-cataloger",
   276  				"java-archive-cataloger",
   277  				"graalvm-native-image-cataloger",
   278  				"binary-cataloger",
   279  				"github-actions-usage-cataloger",
   280  				"github-action-workflow-usage-cataloger",
   281  				"sbom-cataloger",
   282  			},
   283  			wantTokens: map[string]TokenSelection{
   284  				"alpm-db-cataloger":                      newTokenSelection([]string{"all"}, nil),
   285  				"apk-db-cataloger":                       newTokenSelection([]string{"all"}, nil),
   286  				"dpkg-db-cataloger":                      newTokenSelection([]string{"all"}, nil),
   287  				"portage-cataloger":                      newTokenSelection([]string{"all"}, nil),
   288  				"rpm-db-cataloger":                       newTokenSelection([]string{"all"}, nil),
   289  				"rpm-archive-cataloger":                  newTokenSelection([]string{"all"}, nil),
   290  				"conan-info-cataloger":                   newTokenSelection([]string{"all"}, nil),
   291  				"javascript-package-cataloger":           newTokenSelection([]string{"all"}, nil),
   292  				"php-composer-installed-cataloger":       newTokenSelection([]string{"all"}, nil),
   293  				"ruby-installed-gemspec-cataloger":       newTokenSelection([]string{"all"}, nil),
   294  				"rust-cargo-lock-cataloger":              newTokenSelection([]string{"all"}, nil),
   295  				"conan-cataloger":                        newTokenSelection([]string{"all"}, nil),
   296  				"dart-pubspec-lock-cataloger":            newTokenSelection([]string{"all"}, nil),
   297  				"dotnet-deps-cataloger":                  newTokenSelection([]string{"all"}, nil),
   298  				"elixir-mix-lock-cataloger":              newTokenSelection([]string{"all"}, nil),
   299  				"erlang-rebar-lock-cataloger":            newTokenSelection([]string{"all"}, nil),
   300  				"javascript-lock-cataloger":              newTokenSelection([]string{"all"}, nil),
   301  				"dotnet-portable-executable-cataloger":   newTokenSelection([]string{"all"}, nil),
   302  				"python-installed-package-cataloger":     newTokenSelection([]string{"all"}, nil),
   303  				"go-module-binary-cataloger":             newTokenSelection([]string{"all"}, nil),
   304  				"java-archive-cataloger":                 newTokenSelection([]string{"all"}, nil),
   305  				"graalvm-native-image-cataloger":         newTokenSelection([]string{"all"}, nil),
   306  				"binary-cataloger":                       newTokenSelection([]string{"all"}, nil),
   307  				"github-actions-usage-cataloger":         newTokenSelection([]string{"all"}, nil),
   308  				"github-action-workflow-usage-cataloger": newTokenSelection([]string{"all"}, nil),
   309  				"sbom-cataloger":                         newTokenSelection([]string{"all"}, nil),
   310  			},
   311  			wantRequest: cataloging.SelectionRequest{
   312  				DefaultNamesOrTags: []string{"all"},
   313  			},
   314  		},
   315  		{
   316  			name:     "set default with multiple tags",
   317  			allTasks: createDummyPackageTasks(),
   318  			basis: []string{
   319  				"gemspec",
   320  				"python",
   321  			},
   322  			expressions: []string{},
   323  			wantNames: []string{
   324  				"ruby-installed-gemspec-cataloger",
   325  				"python-installed-package-cataloger",
   326  			},
   327  			wantTokens: map[string]TokenSelection{
   328  				"ruby-installed-gemspec-cataloger":   newTokenSelection([]string{"gemspec"}, nil),
   329  				"python-installed-package-cataloger": newTokenSelection([]string{"python"}, nil),
   330  			},
   331  			wantRequest: cataloging.SelectionRequest{
   332  				DefaultNamesOrTags: []string{"gemspec", "python"},
   333  			},
   334  		},
   335  		{
   336  			name:        "automatically add file to default tags",
   337  			allTasks:    createDummyFileTasks(),
   338  			basis:       []string{},
   339  			expressions: []string{},
   340  			wantNames: []string{
   341  				"file-content-cataloger",
   342  				"file-metadata-cataloger",
   343  				"file-digest-cataloger",
   344  				"file-executable-cataloger",
   345  			},
   346  			wantTokens: map[string]TokenSelection{
   347  				"file-content-cataloger":    newTokenSelection([]string{"file"}, nil),
   348  				"file-metadata-cataloger":   newTokenSelection([]string{"file"}, nil),
   349  				"file-digest-cataloger":     newTokenSelection([]string{"file"}, nil),
   350  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   351  			},
   352  			wantRequest: cataloging.SelectionRequest{
   353  				DefaultNamesOrTags: []string{"file"},
   354  			},
   355  		},
   356  	}
   357  	for _, tt := range tests {
   358  		t.Run(tt.name, func(t *testing.T) {
   359  			if tt.wantErr == nil {
   360  				tt.wantErr = assert.NoError
   361  			}
   362  
   363  			req := cataloging.NewSelectionRequest().WithDefaults(tt.basis...).WithExpression(tt.expressions...)
   364  
   365  			got, gotEvidence, err := Select(tt.allTasks, req)
   366  			tt.wantErr(t, err)
   367  			if err != nil {
   368  				// dev note: this is useful for debugging when needed...
   369  				//for _, e := range gotEvidence.Request.Expressions {
   370  				//	t.Logf("expression (errors %q): %#v", e.Errors, e)
   371  				//}
   372  
   373  				// note: we DON'T bail early in validations... this is because we should always return the full set of
   374  				// of selected tasks and surrounding evidence.
   375  			}
   376  
   377  			gotNames := make([]string, 0)
   378  			for _, g := range got {
   379  				gotNames = append(gotNames, g.Name())
   380  			}
   381  
   382  			assert.Equal(t, tt.wantNames, gotNames)
   383  
   384  			// names in selection should match all tasks returned
   385  			require.Len(t, tt.wantNames, gotEvidence.Result.Size(), "selected tasks should match all tasks returned (but does not)")
   386  			assert.ElementsMatch(t, tt.wantNames, gotEvidence.Result.List(), "selected tasks should match all tasks returned (but does not)")
   387  
   388  			setCompare := cmp.Comparer(func(x, y *strset.Set) bool {
   389  				return x.IsEqual(y)
   390  			})
   391  
   392  			if d := cmp.Diff(tt.wantTokens, gotEvidence.TokensByTask, setCompare); d != "" {
   393  				t.Errorf("unexpected tokens by task (-want +got):\n%s", d)
   394  			}
   395  			assert.Equal(t, tt.wantRequest, gotEvidence.Request)
   396  
   397  		})
   398  	}
   399  }
   400  
   401  func TestSelectInGroups(t *testing.T) {
   402  	tests := []struct {
   403  		name         string
   404  		taskGroups   [][]Task
   405  		selectionReq cataloging.SelectionRequest
   406  		wantGroups   [][]string
   407  		wantTokens   map[string]TokenSelection
   408  		wantRequest  cataloging.SelectionRequest
   409  		wantErr      assert.ErrorAssertionFunc
   410  	}{
   411  		{
   412  			name: "select only within the file tasks (leave package tasks alone)",
   413  			taskGroups: [][]Task{
   414  				createDummyPackageTasks(),
   415  				createDummyFileTasks(),
   416  			},
   417  			selectionReq: cataloging.NewSelectionRequest().
   418  				WithDefaults("image"). // note: file missing
   419  				WithSubSelections("content", "digest"),
   420  			wantGroups: [][]string{
   421  				{
   422  					// this is the original, untouched package task list
   423  					"alpm-db-cataloger",
   424  					"apk-db-cataloger",
   425  					"dpkg-db-cataloger",
   426  					"portage-cataloger",
   427  					"rpm-db-cataloger",
   428  					"conan-info-cataloger",
   429  					"javascript-package-cataloger",
   430  					"php-composer-installed-cataloger",
   431  					"ruby-installed-gemspec-cataloger",
   432  					"rust-cargo-lock-cataloger",
   433  					"dotnet-portable-executable-cataloger",
   434  					"python-installed-package-cataloger",
   435  					"go-module-binary-cataloger",
   436  					"java-archive-cataloger",
   437  					"graalvm-native-image-cataloger",
   438  					"binary-cataloger",
   439  					"sbom-cataloger",
   440  				},
   441  				{
   442  					// this has been filtered based on the request
   443  					"file-content-cataloger",
   444  					"file-digest-cataloger",
   445  				},
   446  			},
   447  			wantTokens: map[string]TokenSelection{
   448  				// packages
   449  				"alpm-db-cataloger":                    newTokenSelection([]string{"image"}, nil),
   450  				"apk-db-cataloger":                     newTokenSelection([]string{"image"}, nil),
   451  				"binary-cataloger":                     newTokenSelection([]string{"image"}, nil),
   452  				"conan-info-cataloger":                 newTokenSelection([]string{"image"}, nil),
   453  				"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
   454  				"dpkg-db-cataloger":                    newTokenSelection([]string{"image"}, nil),
   455  				"go-module-binary-cataloger":           newTokenSelection([]string{"image"}, nil),
   456  				"graalvm-native-image-cataloger":       newTokenSelection([]string{"image"}, nil),
   457  				"java-archive-cataloger":               newTokenSelection([]string{"image"}, nil),
   458  				"javascript-package-cataloger":         newTokenSelection([]string{"image"}, nil),
   459  				"php-composer-installed-cataloger":     newTokenSelection([]string{"image"}, nil),
   460  				"portage-cataloger":                    newTokenSelection([]string{"image"}, nil),
   461  				"python-installed-package-cataloger":   newTokenSelection([]string{"image"}, nil),
   462  				"rpm-db-cataloger":                     newTokenSelection([]string{"image"}, nil),
   463  				"ruby-installed-gemspec-cataloger":     newTokenSelection([]string{"image"}, nil),
   464  				"rust-cargo-lock-cataloger":            newTokenSelection([]string{"image"}, nil),
   465  				"sbom-cataloger":                       newTokenSelection([]string{"image"}, nil),
   466  				// files
   467  				"file-content-cataloger":    newTokenSelection([]string{"content", "file"}, nil),
   468  				"file-digest-cataloger":     newTokenSelection([]string{"digest", "file"}, nil),
   469  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   470  				"file-metadata-cataloger":   newTokenSelection([]string{"file"}, nil),
   471  			},
   472  			wantRequest: cataloging.SelectionRequest{
   473  				DefaultNamesOrTags: []string{"image", "file"}, // note: file automatically added
   474  				SubSelectTags:      []string{"content", "digest"},
   475  			},
   476  			wantErr: assert.NoError,
   477  		},
   478  		{
   479  			name: "select package tasks (leave file tasks alone)",
   480  			taskGroups: [][]Task{
   481  				createDummyPackageTasks(),
   482  				createDummyFileTasks(),
   483  			},
   484  			selectionReq: cataloging.NewSelectionRequest().WithDefaults("image").WithSubSelections("os"),
   485  			wantGroups: [][]string{
   486  				{
   487  					// filtered based on the request
   488  					"alpm-db-cataloger",
   489  					"apk-db-cataloger",
   490  					"dpkg-db-cataloger",
   491  					"portage-cataloger",
   492  					"rpm-db-cataloger",
   493  				},
   494  				{
   495  					// this is the original, untouched file task list
   496  					"file-content-cataloger",
   497  					"file-metadata-cataloger",
   498  					"file-digest-cataloger",
   499  					"file-executable-cataloger",
   500  				},
   501  			},
   502  			wantTokens: map[string]TokenSelection{
   503  				// packages - os
   504  				"alpm-db-cataloger":     newTokenSelection([]string{"os", "image"}, nil),
   505  				"apk-db-cataloger":      newTokenSelection([]string{"os", "image"}, nil),
   506  				"rpm-archive-cataloger": newTokenSelection([]string{"os"}, nil),
   507  				"rpm-db-cataloger":      newTokenSelection([]string{"os", "image"}, nil),
   508  				"portage-cataloger":     newTokenSelection([]string{"os", "image"}, nil),
   509  				"dpkg-db-cataloger":     newTokenSelection([]string{"os", "image"}, nil),
   510  				// packages - remaining
   511  				"binary-cataloger":                     newTokenSelection([]string{"image"}, nil),
   512  				"conan-info-cataloger":                 newTokenSelection([]string{"image"}, nil),
   513  				"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
   514  				"go-module-binary-cataloger":           newTokenSelection([]string{"image"}, nil),
   515  				"graalvm-native-image-cataloger":       newTokenSelection([]string{"image"}, nil),
   516  				"java-archive-cataloger":               newTokenSelection([]string{"image"}, nil),
   517  				"javascript-package-cataloger":         newTokenSelection([]string{"image"}, nil),
   518  				"php-composer-installed-cataloger":     newTokenSelection([]string{"image"}, nil),
   519  				"python-installed-package-cataloger":   newTokenSelection([]string{"image"}, nil),
   520  				"ruby-installed-gemspec-cataloger":     newTokenSelection([]string{"image"}, nil),
   521  				"rust-cargo-lock-cataloger":            newTokenSelection([]string{"image"}, nil),
   522  				"sbom-cataloger":                       newTokenSelection([]string{"image"}, nil),
   523  				// files
   524  				"file-content-cataloger":    newTokenSelection([]string{"file"}, nil),
   525  				"file-digest-cataloger":     newTokenSelection([]string{"file"}, nil),
   526  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   527  				"file-metadata-cataloger":   newTokenSelection([]string{"file"}, nil),
   528  			},
   529  			wantRequest: cataloging.SelectionRequest{
   530  				DefaultNamesOrTags: []string{"image", "file"},
   531  				SubSelectTags:      []string{"os"},
   532  			},
   533  			wantErr: assert.NoError,
   534  		},
   535  		{
   536  			name: "select only file tasks (default)",
   537  			taskGroups: [][]Task{
   538  				createDummyPackageTasks(),
   539  				createDummyFileTasks(),
   540  			},
   541  			selectionReq: cataloging.NewSelectionRequest().WithDefaults("file"),
   542  			wantGroups: [][]string{
   543  				// filtered based on the request
   544  				nil,
   545  				{
   546  					// this is the original, untouched file task list
   547  					"file-content-cataloger",
   548  					"file-metadata-cataloger",
   549  					"file-digest-cataloger",
   550  					"file-executable-cataloger",
   551  				},
   552  			},
   553  			wantTokens: map[string]TokenSelection{
   554  				// files
   555  				"file-content-cataloger":    newTokenSelection([]string{"file"}, nil),
   556  				"file-digest-cataloger":     newTokenSelection([]string{"file"}, nil),
   557  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   558  				"file-metadata-cataloger":   newTokenSelection([]string{"file"}, nil),
   559  			},
   560  			wantRequest: cataloging.SelectionRequest{
   561  				DefaultNamesOrTags: []string{"file"},
   562  			},
   563  			wantErr: assert.NoError,
   564  		},
   565  		{
   566  			name: "select only file tasks (via removal of package)",
   567  			taskGroups: [][]Task{
   568  				createDummyPackageTasks(),
   569  				createDummyFileTasks(),
   570  			},
   571  			selectionReq: cataloging.NewSelectionRequest().WithDefaults("file", "image").WithRemovals("package"),
   572  			wantGroups: [][]string{
   573  				// filtered based on the request
   574  				nil,
   575  				{
   576  					// this is the original, untouched file task list
   577  					"file-content-cataloger",
   578  					"file-metadata-cataloger",
   579  					"file-digest-cataloger",
   580  					"file-executable-cataloger",
   581  				},
   582  			},
   583  			wantTokens: map[string]TokenSelection{
   584  				// packages
   585  				"alpm-db-cataloger":                      newTokenSelection([]string{"image"}, []string{"package"}),
   586  				"apk-db-cataloger":                       newTokenSelection([]string{"image"}, []string{"package"}),
   587  				"binary-cataloger":                       newTokenSelection([]string{"image"}, []string{"package"}),
   588  				"conan-info-cataloger":                   newTokenSelection([]string{"image"}, []string{"package"}),
   589  				"dotnet-portable-executable-cataloger":   newTokenSelection([]string{"image"}, []string{"package"}),
   590  				"dpkg-db-cataloger":                      newTokenSelection([]string{"image"}, []string{"package"}),
   591  				"go-module-binary-cataloger":             newTokenSelection([]string{"image"}, []string{"package"}),
   592  				"graalvm-native-image-cataloger":         newTokenSelection([]string{"image"}, []string{"package"}),
   593  				"java-archive-cataloger":                 newTokenSelection([]string{"image"}, []string{"package"}),
   594  				"javascript-package-cataloger":           newTokenSelection([]string{"image"}, []string{"package"}),
   595  				"php-composer-installed-cataloger":       newTokenSelection([]string{"image"}, []string{"package"}),
   596  				"portage-cataloger":                      newTokenSelection([]string{"image"}, []string{"package"}),
   597  				"python-installed-package-cataloger":     newTokenSelection([]string{"image"}, []string{"package"}),
   598  				"rpm-db-cataloger":                       newTokenSelection([]string{"image"}, []string{"package"}),
   599  				"ruby-installed-gemspec-cataloger":       newTokenSelection([]string{"image"}, []string{"package"}),
   600  				"rust-cargo-lock-cataloger":              newTokenSelection([]string{"image"}, []string{"package"}),
   601  				"sbom-cataloger":                         newTokenSelection([]string{"image"}, []string{"package"}),
   602  				"rpm-archive-cataloger":                  newTokenSelection(nil, []string{"package"}),
   603  				"conan-cataloger":                        newTokenSelection(nil, []string{"package"}),
   604  				"dart-pubspec-lock-cataloger":            newTokenSelection(nil, []string{"package"}),
   605  				"dotnet-deps-cataloger":                  newTokenSelection(nil, []string{"package"}),
   606  				"elixir-mix-lock-cataloger":              newTokenSelection(nil, []string{"package"}),
   607  				"erlang-rebar-lock-cataloger":            newTokenSelection(nil, []string{"package"}),
   608  				"javascript-lock-cataloger":              newTokenSelection(nil, []string{"package"}),
   609  				"github-actions-usage-cataloger":         newTokenSelection(nil, []string{"package"}),
   610  				"github-action-workflow-usage-cataloger": newTokenSelection(nil, []string{"package"}),
   611  				// files
   612  				"file-content-cataloger":    newTokenSelection([]string{"file"}, nil),
   613  				"file-digest-cataloger":     newTokenSelection([]string{"file"}, nil),
   614  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   615  				"file-metadata-cataloger":   newTokenSelection([]string{"file"}, nil),
   616  			},
   617  			wantRequest: cataloging.SelectionRequest{
   618  				DefaultNamesOrTags: []string{"file", "image"},
   619  				RemoveNamesOrTags:  []string{"package"},
   620  			},
   621  			wantErr: assert.NoError,
   622  		},
   623  		{
   624  			name: "select file and package tasks",
   625  			taskGroups: [][]Task{
   626  				createDummyPackageTasks(),
   627  				createDummyFileTasks(),
   628  			},
   629  			selectionReq: cataloging.NewSelectionRequest().
   630  				WithDefaults("image").
   631  				WithSubSelections("os", "content", "digest"),
   632  			wantGroups: [][]string{
   633  				{
   634  					// filtered based on the request
   635  					"alpm-db-cataloger",
   636  					"apk-db-cataloger",
   637  					"dpkg-db-cataloger",
   638  					"portage-cataloger",
   639  					"rpm-db-cataloger",
   640  				},
   641  				{
   642  					// filtered based on the request
   643  					"file-content-cataloger",
   644  					"file-digest-cataloger",
   645  				},
   646  			},
   647  			wantTokens: map[string]TokenSelection{
   648  				// packages - os
   649  				"alpm-db-cataloger":     newTokenSelection([]string{"os", "image"}, nil),
   650  				"apk-db-cataloger":      newTokenSelection([]string{"os", "image"}, nil),
   651  				"rpm-archive-cataloger": newTokenSelection([]string{"os"}, nil),
   652  				"rpm-db-cataloger":      newTokenSelection([]string{"os", "image"}, nil),
   653  				"portage-cataloger":     newTokenSelection([]string{"os", "image"}, nil),
   654  				"dpkg-db-cataloger":     newTokenSelection([]string{"os", "image"}, nil),
   655  				// packages - remaining
   656  				"binary-cataloger":                     newTokenSelection([]string{"image"}, nil),
   657  				"conan-info-cataloger":                 newTokenSelection([]string{"image"}, nil),
   658  				"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
   659  				"go-module-binary-cataloger":           newTokenSelection([]string{"image"}, nil),
   660  				"graalvm-native-image-cataloger":       newTokenSelection([]string{"image"}, nil),
   661  				"java-archive-cataloger":               newTokenSelection([]string{"image"}, nil),
   662  				"javascript-package-cataloger":         newTokenSelection([]string{"image"}, nil),
   663  				"php-composer-installed-cataloger":     newTokenSelection([]string{"image"}, nil),
   664  				"python-installed-package-cataloger":   newTokenSelection([]string{"image"}, nil),
   665  				"ruby-installed-gemspec-cataloger":     newTokenSelection([]string{"image"}, nil),
   666  				"rust-cargo-lock-cataloger":            newTokenSelection([]string{"image"}, nil),
   667  				"sbom-cataloger":                       newTokenSelection([]string{"image"}, nil),
   668  				// files
   669  				"file-content-cataloger":    newTokenSelection([]string{"file", "content"}, nil), // note extra tags
   670  				"file-digest-cataloger":     newTokenSelection([]string{"file", "digest"}, nil),  // note extra tags
   671  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   672  				"file-metadata-cataloger":   newTokenSelection([]string{"file"}, nil),
   673  			},
   674  			wantRequest: cataloging.SelectionRequest{
   675  				DefaultNamesOrTags: []string{"image", "file"},
   676  				SubSelectTags:      []string{"os", "content", "digest"},
   677  			},
   678  			wantErr: assert.NoError,
   679  		},
   680  		{
   681  			name: "complex selection with multiple operators across groups",
   682  			taskGroups: [][]Task{
   683  				createDummyPackageTasks(),
   684  				createDummyFileTasks(),
   685  			},
   686  			selectionReq: cataloging.NewSelectionRequest().
   687  				WithDefaults("os"). // note: no file tag present
   688  				WithExpression("+github-actions-usage-cataloger", "-dpkg", "-digest", "content", "+file-metadata-cataloger", "-declared"),
   689  			wantGroups: [][]string{
   690  				{
   691  					"alpm-db-cataloger",
   692  					"apk-db-cataloger",
   693  					"portage-cataloger",
   694  					"rpm-db-cataloger",
   695  					"github-actions-usage-cataloger",
   696  				},
   697  				{
   698  					"file-content-cataloger",
   699  					"file-metadata-cataloger",
   700  				},
   701  			},
   702  			wantTokens: map[string]TokenSelection{
   703  				// selected package tasks
   704  				"alpm-db-cataloger":              newTokenSelection([]string{"os"}, nil),
   705  				"apk-db-cataloger":               newTokenSelection([]string{"os"}, nil),
   706  				"dpkg-db-cataloger":              newTokenSelection([]string{"os"}, []string{"dpkg"}),
   707  				"portage-cataloger":              newTokenSelection([]string{"os"}, nil),
   708  				"rpm-archive-cataloger":          newTokenSelection([]string{"os"}, []string{"declared"}),
   709  				"rpm-db-cataloger":               newTokenSelection([]string{"os"}, nil),
   710  				"github-actions-usage-cataloger": newTokenSelection([]string{"github-actions-usage-cataloger"}, []string{"declared"}),
   711  
   712  				// selected file tasks
   713  				"file-content-cataloger":  newTokenSelection([]string{"content", "file"}, nil),
   714  				"file-metadata-cataloger": newTokenSelection([]string{"file-metadata-cataloger", "file"}, nil),
   715  
   716  				// removed package tasks
   717  				"binary-cataloger":                       newTokenSelection(nil, []string{"declared"}),
   718  				"conan-cataloger":                        newTokenSelection(nil, []string{"declared"}),
   719  				"dart-pubspec-lock-cataloger":            newTokenSelection(nil, []string{"declared"}),
   720  				"dotnet-deps-cataloger":                  newTokenSelection(nil, []string{"declared"}),
   721  				"elixir-mix-lock-cataloger":              newTokenSelection(nil, []string{"declared"}),
   722  				"erlang-rebar-lock-cataloger":            newTokenSelection(nil, []string{"declared"}),
   723  				"github-action-workflow-usage-cataloger": newTokenSelection(nil, []string{"declared"}),
   724  				"javascript-lock-cataloger":              newTokenSelection(nil, []string{"declared"}),
   725  				"sbom-cataloger":                         newTokenSelection(nil, []string{"declared"}),
   726  
   727  				// removed file tasks
   728  				"file-executable-cataloger": newTokenSelection([]string{"file"}, nil),
   729  				"file-digest-cataloger":     newTokenSelection([]string{"file"}, []string{"digest"}),
   730  			},
   731  			wantRequest: cataloging.SelectionRequest{
   732  				DefaultNamesOrTags: []string{"os", "file"}, // note: file added automatically
   733  				SubSelectTags:      []string{"content"},
   734  				RemoveNamesOrTags:  []string{"dpkg", "digest", "declared"},
   735  				AddNames:           []string{"github-actions-usage-cataloger", "file-metadata-cataloger"},
   736  			},
   737  			wantErr: assert.NoError,
   738  		},
   739  		{
   740  			name: "invalid tag",
   741  			taskGroups: [][]Task{
   742  				createDummyPackageTasks(),
   743  				createDummyFileTasks(),
   744  			},
   745  			selectionReq: cataloging.NewSelectionRequest().WithDefaults("invalid"),
   746  			wantGroups:   nil,
   747  			wantTokens:   nil,
   748  			wantRequest: cataloging.SelectionRequest{
   749  				DefaultNamesOrTags: []string{"invalid", "file"},
   750  			},
   751  			wantErr: assert.Error,
   752  		},
   753  	}
   754  
   755  	for _, tt := range tests {
   756  		t.Run(tt.name, func(t *testing.T) {
   757  			if tt.wantErr == nil {
   758  				tt.wantErr = assert.NoError
   759  			}
   760  
   761  			gotGroups, gotSelection, err := SelectInGroups(tt.taskGroups, tt.selectionReq)
   762  			tt.wantErr(t, err)
   763  			if err != nil {
   764  				// dev note: this is useful for debugging when needed...
   765  				//for _, e := range gotEvidence.Request.Expressions {
   766  				//	t.Logf("expression (errors %q): %#v", e.Errors, e)
   767  				//}
   768  
   769  				// note: we DON'T bail early in validations... this is because we should always return the full set of
   770  				// of selected tasks and surrounding evidence.
   771  			}
   772  
   773  			var gotGroupNames [][]string
   774  			for _, group := range gotGroups {
   775  				var names []string
   776  				for _, task := range group {
   777  					names = append(names, task.Name())
   778  				}
   779  				gotGroupNames = append(gotGroupNames, names)
   780  			}
   781  
   782  			assert.Equal(t, tt.wantGroups, gotGroupNames)
   783  			assert.Equal(t, tt.wantTokens, gotSelection.TokensByTask)
   784  			assert.Equal(t, tt.wantRequest, gotSelection.Request)
   785  		})
   786  	}
   787  }