github.com/anchore/syft@v1.38.2/cmd/syft/internal/commands/cataloger_list.go (about) 1 package commands 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "sort" 8 "strings" 9 10 "github.com/charmbracelet/lipgloss" 11 "github.com/jedib0t/go-pretty/v6/table" 12 "github.com/scylladb/go-set/strset" 13 "github.com/spf13/cobra" 14 15 "github.com/anchore/clio" 16 "github.com/anchore/syft/cmd/syft/internal/options" 17 "github.com/anchore/syft/internal/bus" 18 "github.com/anchore/syft/internal/task" 19 "github.com/anchore/syft/syft/cataloging" 20 ) 21 22 var ( 23 activelyAddedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // hi green 24 deselectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey 25 activelyRemovedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // high red 26 defaultStyle = lipgloss.NewStyle().Underline(true) 27 deselectedDefaultStyle = lipgloss.NewStyle().Inherit(deselectedStyle).Underline(true) 28 ) 29 30 type catalogerListOptions struct { 31 Output string `yaml:"output" json:"output" mapstructure:"output"` 32 DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"` 33 SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"` 34 ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"` 35 } 36 37 func (o *catalogerListOptions) AddFlags(flags clio.FlagSet) { 38 flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger list (available: table, json)") 39 40 flags.StringArrayVarP(&o.DefaultCatalogers, "override-default-catalogers", "", "override the default catalogers with an expression") 41 42 flags.StringArrayVarP(&o.SelectCatalogers, "select-catalogers", "", "select catalogers with an expression") 43 44 flags.BoolVarP(&o.ShowHidden, "show-hidden", "s", "show catalogers that have been de-selected") 45 } 46 47 func defaultCatalogerListOptions() *catalogerListOptions { 48 return &catalogerListOptions{ 49 DefaultCatalogers: []string{"all"}, 50 } 51 } 52 53 func CatalogerList(app clio.Application) *cobra.Command { 54 opts := defaultCatalogerListOptions() 55 56 return app.SetupCommand(&cobra.Command{ 57 Use: "list [OPTIONS]", 58 Short: "List available catalogers", 59 PreRunE: disableUI(app, os.Stdout), 60 RunE: func(_ *cobra.Command, _ []string) error { 61 return runCatalogerList(opts) 62 }, 63 }, opts) 64 } 65 66 func runCatalogerList(opts *catalogerListOptions) error { 67 pkgTaskFactories := task.DefaultPackageTaskFactories() 68 fileTaskFactories := task.DefaultFileTaskFactories() 69 allPkgTasks, err := pkgTaskFactories.Tasks(task.DefaultCatalogingFactoryConfig()) 70 if err != nil { 71 return fmt.Errorf("unable to create pkg cataloger tasks: %w", err) 72 } 73 74 allFileTasks, err := fileTaskFactories.Tasks(task.DefaultCatalogingFactoryConfig()) 75 if err != nil { 76 return fmt.Errorf("unable to create file cataloger tasks: %w", err) 77 } 78 79 report, err := catalogerListReport(opts, [][]task.Task{allPkgTasks, allFileTasks}) 80 if err != nil { 81 return fmt.Errorf("unable to generate cataloger list report: %w", err) 82 } 83 84 bus.Report(report) 85 86 return nil 87 } 88 89 func catalogerListReport(opts *catalogerListOptions, allTaskGroups [][]task.Task) (string, error) { 90 defaultCatalogers := options.FlattenAndSort(opts.DefaultCatalogers) 91 selectCatalogers := options.FlattenAndSort(opts.SelectCatalogers) 92 selectedTaskGroups, selectionEvidence, err := task.SelectInGroups( 93 allTaskGroups, 94 cataloging.NewSelectionRequest(). 95 WithDefaults(defaultCatalogers...). 96 WithExpression(selectCatalogers...), 97 ) 98 if err != nil { 99 return "", fmt.Errorf("unable to select catalogers: %w", err) 100 } 101 var report string 102 103 switch opts.Output { 104 case "json": 105 report, err = renderCatalogerListJSON(flattenTaskGroups(selectedTaskGroups), selectionEvidence, defaultCatalogers, selectCatalogers) 106 case "table", "": 107 if opts.ShowHidden { 108 report = renderCatalogerListTables(allTaskGroups, selectionEvidence) 109 } else { 110 report = renderCatalogerListTables(selectedTaskGroups, selectionEvidence) 111 } 112 } 113 114 if err != nil { 115 return "", fmt.Errorf("unable to render cataloger list: %w", err) 116 } 117 118 return report, nil 119 } 120 121 func flattenTaskGroups(taskGroups [][]task.Task) []task.Task { 122 var allTasks []task.Task 123 for _, tasks := range taskGroups { 124 allTasks = append(allTasks, tasks...) 125 } 126 return allTasks 127 } 128 129 func renderCatalogerListJSON(tasks []task.Task, selection task.Selection, defaultSelections, selections []string) (string, error) { 130 type node struct { 131 Name string `json:"name"` 132 Tags []string `json:"tags"` 133 } 134 135 names, tagsByName := extractTaskInfo(tasks) 136 137 nodesByName := make(map[string]node) 138 139 for name := range tagsByName { 140 tokensByTask, ok := selection.TokensByTask[name] 141 142 var tagsSelected []string 143 if ok { 144 tagsSelected = tokensByTask.SelectedOn.List() 145 } 146 147 if len(tagsSelected) == 1 && tagsSelected[0] == "all" { 148 tagsSelected = tagsByName[name] 149 } 150 151 sort.Strings(tagsSelected) 152 153 if tagsSelected == nil { 154 // ensure collections are not null 155 tagsSelected = []string{} 156 } 157 158 nodesByName[name] = node{ 159 Name: name, 160 Tags: tagsSelected, 161 } 162 } 163 164 type document struct { 165 DefaultSelection []string `json:"default"` 166 Selection []string `json:"selection"` 167 Catalogers []node `json:"catalogers"` 168 } 169 170 if selections == nil { 171 // ensure collections are not null 172 selections = []string{} 173 } 174 175 doc := document{ 176 DefaultSelection: defaultSelections, 177 Selection: selections, 178 } 179 180 for _, name := range names { 181 doc.Catalogers = append(doc.Catalogers, nodesByName[name]) 182 } 183 184 by, err := json.Marshal(doc) 185 186 return string(by), err 187 } 188 189 func renderCatalogerListTables(taskGroups [][]task.Task, selection task.Selection) string { 190 pkgCatalogerTable := renderCatalogerListTable(taskGroups[0], selection, "Package Cataloger") 191 fileCatalogerTable := renderCatalogerListTable(taskGroups[1], selection, "File Cataloger") 192 193 report := fileCatalogerTable + "\n" + pkgCatalogerTable + "\n" 194 195 hasAdditions := len(selection.Request.AddNames) > 0 196 hasDefaults := len(selection.Request.DefaultNamesOrTags) > 0 197 hasRemovals := len(selection.Request.RemoveNamesOrTags) > 0 198 hasSubSelections := len(selection.Request.SubSelectTags) > 0 199 expressions := len(selection.Request.SubSelectTags) + len(selection.Request.AddNames) + len(selection.Request.RemoveNamesOrTags) 200 201 var header string 202 203 header += fmt.Sprintf("Default selections: %d\n", len(selection.Request.DefaultNamesOrTags)) 204 if hasDefaults { 205 for _, expr := range selection.Request.DefaultNamesOrTags { 206 header += fmt.Sprintf(" • '%s'\n", expr) 207 } 208 } 209 210 header += fmt.Sprintf("Selection expressions: %d\n", expressions) 211 212 if hasSubSelections { 213 for _, n := range selection.Request.SubSelectTags { 214 header += fmt.Sprintf(" • '%s' (intersect)\n", n) 215 } 216 } 217 if hasRemovals { 218 for _, n := range selection.Request.RemoveNamesOrTags { 219 header += fmt.Sprintf(" • '-%s' (remove)\n", n) 220 } 221 } 222 if hasAdditions { 223 for _, n := range selection.Request.AddNames { 224 header += fmt.Sprintf(" • '+%s' (add)\n", n) 225 } 226 } 227 228 return header + report 229 } 230 231 func renderCatalogerListTable(tasks []task.Task, selection task.Selection, kindTitle string) string { 232 if len(tasks) == 0 { 233 return activelyRemovedStyle.Render(fmt.Sprintf("No %ss selected", strings.ToLower(kindTitle))) 234 } 235 236 t := table.NewWriter() 237 t.SetStyle(table.StyleLight) 238 t.AppendHeader(table.Row{kindTitle, "Tags"}) 239 240 names, tagsByName := extractTaskInfo(tasks) 241 242 rowsByName := make(map[string]table.Row) 243 244 for name, tags := range tagsByName { 245 rowsByName[name] = formatRow(name, tags, selection) 246 } 247 248 for _, name := range names { 249 t.AppendRow(rowsByName[name]) 250 } 251 252 report := t.Render() 253 254 return report 255 } 256 257 func formatRow(name string, tags []string, selection task.Selection) table.Row { 258 isIncluded := selection.Result.Has(name) 259 defaults := strset.New(selection.Request.DefaultNamesOrTags...) 260 var selections *task.TokenSelection 261 if s, exists := selection.TokensByTask[name]; exists { 262 selections = &s 263 } 264 265 var formattedTags []string 266 for _, tag := range tags { 267 formattedTags = append(formattedTags, formatToken(tag, selections, isIncluded, defaults)) 268 } 269 270 var tagStr string 271 if isIncluded { 272 tagStr = strings.Join(formattedTags, ", ") 273 } else { 274 tagStr = strings.Join(formattedTags, deselectedStyle.Render(", ")) 275 } 276 277 // TODO: selection should keep warnings (non-selections) in struct 278 279 return table.Row{ 280 formatToken(name, selections, isIncluded, defaults), 281 tagStr, 282 } 283 } 284 285 func formatToken(token string, selection *task.TokenSelection, included bool, defaults *strset.Set) string { 286 if included && selection != nil { 287 // format all tokens in selection in green 288 if selection.SelectedOn.Has(token) { 289 if defaults.Has(token) { 290 return defaultStyle.Render(token) 291 } 292 return activelyAddedStyle.Render(token) 293 } 294 295 return token 296 } 297 298 // format all tokens in selection in red, all others in grey 299 if selection != nil && selection.DeselectedOn.Has(token) { 300 return activelyRemovedStyle.Render(token) 301 } 302 if defaults.Has(token) { 303 return deselectedDefaultStyle.Render(token) 304 } 305 return deselectedStyle.Render(token) 306 } 307 308 func extractTaskInfo(tasks []task.Task) ([]string, map[string][]string) { 309 tagsByName := make(map[string][]string) 310 var names []string 311 312 for _, tsk := range tasks { 313 var tags []string 314 name := tsk.Name() 315 316 if s, ok := tsk.(task.Selector); ok { 317 set := strset.New(s.Selectors()...) 318 set.Remove(name) 319 tags = set.List() 320 sort.Strings(tags) 321 } 322 323 tagsByName[name] = tags 324 names = append(names, name) 325 } 326 327 sort.Strings(names) 328 329 return names, tagsByName 330 }