github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/internal/task/selection.go (about)

     1  package task
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/scylladb/go-set/strset"
     8  
     9  	"github.com/anchore/syft/internal/log"
    10  	"github.com/anchore/syft/syft/cataloging/pkgcataloging"
    11  )
    12  
    13  // Selection represents the users request for a subset of tasks to run and the resulting set of task names that were
    14  // selected. Additionally, all tokens that were matched on to reach the returned conclusion are also provided.
    15  type Selection struct {
    16  	Request      pkgcataloging.SelectionRequest
    17  	Result       *strset.Set
    18  	TokensByTask map[string]TokenSelection
    19  }
    20  
    21  // TokenSelection represents the tokens that were matched on to either include or exclude a given task (based on expression evaluation).
    22  type TokenSelection struct {
    23  	SelectedOn   *strset.Set
    24  	DeselectedOn *strset.Set
    25  }
    26  
    27  func newTokenSelection(selected, deselected []string) TokenSelection {
    28  	return TokenSelection{
    29  		SelectedOn:   strset.New(selected...),
    30  		DeselectedOn: strset.New(deselected...),
    31  	}
    32  }
    33  
    34  func (ts *TokenSelection) merge(other ...TokenSelection) {
    35  	for _, o := range other {
    36  		if ts.SelectedOn != nil {
    37  			ts.SelectedOn.Add(o.SelectedOn.List()...)
    38  		}
    39  		if ts.DeselectedOn != nil {
    40  			ts.DeselectedOn.Add(o.DeselectedOn.List()...)
    41  		}
    42  	}
    43  }
    44  
    45  func newSelection() Selection {
    46  	return Selection{
    47  		Result:       strset.New(),
    48  		TokensByTask: make(map[string]TokenSelection),
    49  	}
    50  }
    51  
    52  // Select parses the given expressions as two sets: expressions that represent a "set" operation, and expressions that
    53  // represent all other operations. The parsed expressions are then evaluated against the given tasks to return
    54  // a subset (or the same) set of tasks.
    55  func Select(allTasks []Task, selectionRequest pkgcataloging.SelectionRequest) ([]Task, Selection, error) {
    56  	nodes := newExpressionsFromSelectionRequest(newExpressionContext(allTasks), selectionRequest)
    57  
    58  	finalTasks, selection := selectByExpressions(allTasks, nodes)
    59  
    60  	selection.Request = selectionRequest
    61  
    62  	return finalTasks, selection, nodes.Validate()
    63  }
    64  
    65  // selectByExpressions the set of tasks to run based on the given expression(s).
    66  func selectByExpressions(ts tasks, nodes Expressions) (tasks, Selection) {
    67  	if len(nodes) == 0 {
    68  		return ts, newSelection()
    69  	}
    70  
    71  	finalSet := newSet()
    72  	selectionSet := newSet()
    73  	addSet := newSet()
    74  	removeSet := newSet()
    75  
    76  	allSelections := make(map[string]TokenSelection)
    77  
    78  	nodes = nodes.Clone()
    79  	sort.Sort(nodes)
    80  
    81  	for i, node := range nodes {
    82  		if len(node.Errors) > 0 {
    83  			continue
    84  		}
    85  		selectedTasks, selections := evaluateExpression(ts, node)
    86  
    87  		for name, ss := range selections {
    88  			if selection, exists := allSelections[name]; exists {
    89  				ss.merge(selection)
    90  			}
    91  			allSelections[name] = ss
    92  		}
    93  
    94  		if len(selectedTasks) == 0 {
    95  			log.WithFields("selection", fmt.Sprintf("%q", node.String())).Warn("no cataloger tasks selected found for given selection (this might be a misconfiguration)")
    96  		}
    97  
    98  		switch node.Operation {
    99  		case SetOperation:
   100  			finalSet.Add(selectedTasks...)
   101  		case AddOperation, "":
   102  			addSet.Add(selectedTasks...)
   103  		case RemoveOperation:
   104  			removeSet.Add(selectedTasks...)
   105  		case SubSelectOperation:
   106  			selectionSet.Add(selectedTasks...)
   107  		default:
   108  			nodes[i].Errors = append(nodes[i].Errors, ErrInvalidOperator)
   109  		}
   110  	}
   111  
   112  	if len(selectionSet.tasks) > 0 {
   113  		finalSet.Intersect(selectionSet.Tasks()...)
   114  	}
   115  	finalSet.Remove(removeSet.Tasks()...)
   116  	finalSet.Add(addSet.Tasks()...)
   117  
   118  	finalTasks := finalSet.Tasks()
   119  
   120  	return finalTasks, Selection{
   121  		Result:       strset.New(finalTasks.Names()...),
   122  		TokensByTask: allSelections,
   123  	}
   124  }
   125  
   126  // evaluateExpression returns the set of tasks that match the given expression (as well as all tokens that were matched
   127  // on to reach the returned conclusion).
   128  func evaluateExpression(ts tasks, node Expression) ([]Task, map[string]TokenSelection) {
   129  	selection := make(map[string]TokenSelection)
   130  	var finalTasks []Task
   131  
   132  	for _, t := range ts {
   133  		if !isSelected(t, node.Operand) {
   134  			continue
   135  		}
   136  
   137  		s := newTokenSelection(nil, nil)
   138  
   139  		switch node.Operation {
   140  		case SetOperation, SubSelectOperation, AddOperation:
   141  			s.SelectedOn.Add(node.Operand)
   142  		case RemoveOperation:
   143  			s.DeselectedOn.Add(node.Operand)
   144  		}
   145  
   146  		finalTasks = append(finalTasks, t)
   147  
   148  		if og, exists := selection[t.Name()]; exists {
   149  			s.merge(og)
   150  		}
   151  
   152  		selection[t.Name()] = s
   153  	}
   154  	return finalTasks, selection
   155  }
   156  
   157  // isSelected returns true if the given task matches the given token. If the token is "all" then the task is always selected.
   158  func isSelected(td Task, token string) bool {
   159  	if token == "all" {
   160  		return true
   161  	}
   162  
   163  	if ts, ok := td.(Selector); ok {
   164  		// use the selector to verify all tags
   165  		if ts.HasAllSelectors(token) {
   166  			return true
   167  		}
   168  	}
   169  
   170  	// only do exact name matching
   171  	if td.Name() == token {
   172  		return true
   173  	}
   174  
   175  	return false
   176  }