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

     1  package task
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/scylladb/go-set/strset"
     9  
    10  	"github.com/anchore/syft/internal/log"
    11  	"github.com/anchore/syft/syft/cataloging"
    12  	"github.com/anchore/syft/syft/cataloging/filecataloging"
    13  )
    14  
    15  // Selection represents the users request for a subset of tasks to run and the resulting set of task names that were
    16  // selected. Additionally, all tokens that were matched on to reach the returned conclusion are also provided.
    17  type Selection struct {
    18  	Request      cataloging.SelectionRequest
    19  	Result       *strset.Set
    20  	TokensByTask map[string]TokenSelection
    21  }
    22  
    23  // TokenSelection represents the tokens that were matched on to either include or exclude a given task (based on expression evaluation).
    24  type TokenSelection struct {
    25  	SelectedOn   *strset.Set
    26  	DeselectedOn *strset.Set
    27  }
    28  
    29  func newTokenSelection(selected, deselected []string) TokenSelection {
    30  	return TokenSelection{
    31  		SelectedOn:   strset.New(selected...),
    32  		DeselectedOn: strset.New(deselected...),
    33  	}
    34  }
    35  
    36  func (ts *TokenSelection) merge(other ...TokenSelection) {
    37  	for _, o := range other {
    38  		if ts.SelectedOn != nil {
    39  			ts.SelectedOn.Add(o.SelectedOn.List()...)
    40  		}
    41  		if ts.DeselectedOn != nil {
    42  			ts.DeselectedOn.Add(o.DeselectedOn.List()...)
    43  		}
    44  	}
    45  }
    46  
    47  func newSelection() Selection {
    48  	return Selection{
    49  		Result:       strset.New(),
    50  		TokensByTask: make(map[string]TokenSelection),
    51  	}
    52  }
    53  
    54  // Select parses the given expressions as two sets: expressions that represent a "set" operation, and expressions that
    55  // represent all other operations. The parsed expressions are then evaluated against the given tasks to return
    56  // a subset (or the same) set of tasks.
    57  func Select(allTasks []Task, selectionRequest cataloging.SelectionRequest) ([]Task, Selection, error) {
    58  	ensureDefaultSelectionHasFiles(&selectionRequest, allTasks)
    59  
    60  	return _select(allTasks, selectionRequest)
    61  }
    62  
    63  func _select(allTasks []Task, selectionRequest cataloging.SelectionRequest) ([]Task, Selection, error) {
    64  	if selectionRequest.IsEmpty() {
    65  		selection := newSelection()
    66  		selection.Request = selectionRequest
    67  		return nil, selection, nil
    68  	}
    69  	nodes := newExpressionsFromSelectionRequest(newExpressionContext(allTasks), selectionRequest)
    70  
    71  	finalTasks, selection := selectByExpressions(allTasks, nodes)
    72  
    73  	selection.Request = selectionRequest
    74  
    75  	return finalTasks, selection, nodes.Validate()
    76  }
    77  
    78  // ensureDefaultSelectionHasFiles ensures that the default selection request has the "file" tag, as this is a required
    79  // for backwards compatibility (when catalogers were only for packages and not for separate groups of tasks).
    80  func ensureDefaultSelectionHasFiles(selectionRequest *cataloging.SelectionRequest, allTasks ...[]Task) {
    81  	for _, ts := range allTasks {
    82  		_, leftOver := tagsOrNamesThatTaskGroupRespondsTo(ts, strset.New(filecataloging.FileTag))
    83  		if leftOver.Has(filecataloging.FileTag) {
    84  			// the given set of tasks do not respond to file, so don't include it in the default selection
    85  			continue
    86  		}
    87  
    88  		defaultNamesOrTags := strset.New(selectionRequest.DefaultNamesOrTags...)
    89  		removals := strset.New(selectionRequest.RemoveNamesOrTags...)
    90  		missingFileIshTag := !defaultNamesOrTags.Has(filecataloging.FileTag) && !defaultNamesOrTags.Has("all") && !defaultNamesOrTags.Has("default")
    91  		if missingFileIshTag && !removals.Has(filecataloging.FileTag) {
    92  			log.Warnf("adding '%s' tag to the default cataloger selection, to override add '-%s' to the cataloger selection request", filecataloging.FileTag, filecataloging.FileTag)
    93  			selectionRequest.DefaultNamesOrTags = append(selectionRequest.DefaultNamesOrTags, filecataloging.FileTag)
    94  		}
    95  	}
    96  }
    97  
    98  // SelectInGroups is a convenience function that allows for selecting tasks from multiple groups of tasks. The original
    99  // request is split into sub-requests, where only tokens that are relevant to the given group of tasks are considered.
   100  // If tokens are passed that are not relevant to any group of tasks, an error is returned.
   101  func SelectInGroups(taskGroups [][]Task, selectionRequest cataloging.SelectionRequest) ([][]Task, Selection, error) {
   102  	ensureDefaultSelectionHasFiles(&selectionRequest, taskGroups...)
   103  
   104  	reqs, errs := splitCatalogerSelectionRequest(selectionRequest, taskGroups)
   105  	if errs != nil {
   106  		return nil, Selection{
   107  			Request: selectionRequest,
   108  		}, errs
   109  	}
   110  
   111  	var finalTasks [][]Task
   112  	var selections []Selection
   113  	for idx, req := range reqs {
   114  		tskGroup := taskGroups[idx]
   115  		subFinalTasks, subSelection, err := _select(tskGroup, req)
   116  		if err != nil {
   117  			return nil, Selection{
   118  				Request: selectionRequest,
   119  			}, err
   120  		}
   121  		finalTasks = append(finalTasks, subFinalTasks)
   122  		selections = append(selections, subSelection)
   123  	}
   124  
   125  	return finalTasks, mergeSelections(selections, selectionRequest), nil
   126  }
   127  
   128  func mergeSelections(selections []Selection, ogRequest cataloging.SelectionRequest) Selection {
   129  	finalSelection := newSelection()
   130  	for _, s := range selections {
   131  		finalSelection.Result.Add(s.Result.List()...)
   132  		for name, tokenSelection := range s.TokensByTask {
   133  			if existing, exists := finalSelection.TokensByTask[name]; exists {
   134  				existing.merge(tokenSelection)
   135  				finalSelection.TokensByTask[name] = existing
   136  			} else {
   137  				finalSelection.TokensByTask[name] = tokenSelection
   138  			}
   139  		}
   140  	}
   141  	finalSelection.Request = ogRequest
   142  	return finalSelection
   143  }
   144  
   145  func splitCatalogerSelectionRequest(req cataloging.SelectionRequest, selectablePkgTaskGroups [][]Task) ([]cataloging.SelectionRequest, error) {
   146  	requestTagsOrNames := allRequestReferences(req)
   147  	leftoverTags := strset.New()
   148  	usedTagsAndNames := strset.New()
   149  	var usedTagGroups []*strset.Set
   150  	for _, taskGroup := range selectablePkgTaskGroups {
   151  		selectedTagOrNames, remainingTagsOrNames := tagsOrNamesThatTaskGroupRespondsTo(taskGroup, requestTagsOrNames)
   152  		leftoverTags = strset.Union(leftoverTags, remainingTagsOrNames)
   153  		usedTagGroups = append(usedTagGroups, selectedTagOrNames)
   154  		usedTagsAndNames.Add(selectedTagOrNames.List()...)
   155  	}
   156  
   157  	leftoverTags = strset.Difference(leftoverTags, usedTagsAndNames)
   158  	leftoverTags.Remove("all")
   159  
   160  	if leftoverTags.Size() > 0 {
   161  		l := leftoverTags.List()
   162  		sort.Strings(l)
   163  		return nil, fmt.Errorf("no cataloger tasks respond to the following selections: %v", strings.Join(l, ", "))
   164  	}
   165  
   166  	var newSelections []cataloging.SelectionRequest
   167  	for _, tags := range usedTagGroups {
   168  		newSelections = append(newSelections, newSelectionWithTags(req, tags))
   169  	}
   170  
   171  	return newSelections, nil
   172  }
   173  
   174  func newSelectionWithTags(req cataloging.SelectionRequest, tags *strset.Set) cataloging.SelectionRequest {
   175  	return cataloging.SelectionRequest{
   176  		DefaultNamesOrTags: filterTags(req.DefaultNamesOrTags, tags),
   177  		SubSelectTags:      filterTags(req.SubSelectTags, tags),
   178  		AddNames:           filterTags(req.AddNames, tags),
   179  		RemoveNamesOrTags:  filterTags(req.RemoveNamesOrTags, tags),
   180  	}
   181  }
   182  
   183  func filterTags(reqTags []string, filterTags *strset.Set) []string {
   184  	var filtered []string
   185  	for _, tag := range reqTags {
   186  		if filterTags.Has(tag) {
   187  			filtered = append(filtered, tag)
   188  		}
   189  	}
   190  	return filtered
   191  }
   192  
   193  func tagsOrNamesThatTaskGroupRespondsTo(tasks []Task, requestTagsOrNames *strset.Set) (*strset.Set, *strset.Set) {
   194  	positiveRefs := strset.New()
   195  	for _, t := range tasks {
   196  		if sel, ok := t.(Selector); ok {
   197  			positiveRefs.Add("all") // everything responds to "all"
   198  			positiveRefs.Add(strset.Intersection(requestTagsOrNames, strset.New(sel.Selectors()...)).List()...)
   199  		}
   200  		positiveRefs.Add(t.Name())
   201  	}
   202  	return positiveRefs, strset.Difference(requestTagsOrNames, positiveRefs)
   203  }
   204  
   205  func allRequestReferences(s cataloging.SelectionRequest) *strset.Set {
   206  	st := strset.New()
   207  	st.Add(s.DefaultNamesOrTags...)
   208  	st.Add(s.SubSelectTags...)
   209  	st.Add(s.AddNames...)
   210  	st.Add(s.RemoveNamesOrTags...)
   211  	return st
   212  }
   213  
   214  // selectByExpressions the set of tasks to run based on the given expression(s).
   215  func selectByExpressions(ts tasks, nodes Expressions) (tasks, Selection) {
   216  	if len(nodes) == 0 {
   217  		return ts, newSelection()
   218  	}
   219  
   220  	finalSet := newSet()
   221  	selectionSet := newSet()
   222  	addSet := newSet()
   223  	removeSet := newSet()
   224  
   225  	allSelections := make(map[string]TokenSelection)
   226  
   227  	nodes = nodes.Clone()
   228  	sort.Sort(nodes)
   229  
   230  	for i, node := range nodes {
   231  		if len(node.Errors) > 0 {
   232  			continue
   233  		}
   234  		selectedTasks, selections := evaluateExpression(ts, node)
   235  
   236  		for name, ss := range selections {
   237  			if selection, exists := allSelections[name]; exists {
   238  				ss.merge(selection)
   239  			}
   240  			allSelections[name] = ss
   241  		}
   242  
   243  		if len(selectedTasks) == 0 {
   244  			log.WithFields("selection", fmt.Sprintf("%q", node.String())).Warn("no cataloger tasks selected found for given selection (this might be a misconfiguration)")
   245  		}
   246  
   247  		switch node.Operation {
   248  		case SetOperation:
   249  			finalSet.Add(selectedTasks...)
   250  		case AddOperation, "":
   251  			addSet.Add(selectedTasks...)
   252  		case RemoveOperation:
   253  			removeSet.Add(selectedTasks...)
   254  		case SubSelectOperation:
   255  			selectionSet.Add(selectedTasks...)
   256  		default:
   257  			nodes[i].Errors = append(nodes[i].Errors, ErrInvalidOperator)
   258  		}
   259  	}
   260  
   261  	if len(selectionSet.tasks) > 0 {
   262  		finalSet.Intersect(selectionSet.Tasks()...)
   263  	}
   264  	finalSet.Remove(removeSet.Tasks()...)
   265  	finalSet.Add(addSet.Tasks()...)
   266  
   267  	finalTasks := finalSet.Tasks()
   268  
   269  	return finalTasks, Selection{
   270  		Result:       strset.New(finalTasks.Names()...),
   271  		TokensByTask: allSelections,
   272  	}
   273  }
   274  
   275  // evaluateExpression returns the set of tasks that match the given expression (as well as all tokens that were matched
   276  // on to reach the returned conclusion).
   277  func evaluateExpression(ts tasks, node Expression) ([]Task, map[string]TokenSelection) {
   278  	selection := make(map[string]TokenSelection)
   279  	var finalTasks []Task
   280  
   281  	for _, t := range ts {
   282  		if !isSelected(t, node.Operand) {
   283  			continue
   284  		}
   285  
   286  		s := newTokenSelection(nil, nil)
   287  
   288  		switch node.Operation {
   289  		case SetOperation, SubSelectOperation, AddOperation:
   290  			s.SelectedOn.Add(node.Operand)
   291  		case RemoveOperation:
   292  			s.DeselectedOn.Add(node.Operand)
   293  		}
   294  
   295  		finalTasks = append(finalTasks, t)
   296  
   297  		if og, exists := selection[t.Name()]; exists {
   298  			s.merge(og)
   299  		}
   300  
   301  		selection[t.Name()] = s
   302  	}
   303  	return finalTasks, selection
   304  }
   305  
   306  // isSelected returns true if the given task matches the given token. If the token is "all" then the task is always selected.
   307  func isSelected(td Task, token string) bool {
   308  	if token == "all" {
   309  		return true
   310  	}
   311  
   312  	if ts, ok := td.(Selector); ok {
   313  		// use the selector to verify all tags
   314  		if ts.HasAllSelectors(token) {
   315  			return true
   316  		}
   317  	}
   318  
   319  	// only do exact name matching
   320  	if td.Name() == token {
   321  		return true
   322  	}
   323  
   324  	return false
   325  }