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 }