github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/cmd/syft/internal/commands/cataloger_list.go (about) 1 package commands 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/charmbracelet/lipgloss" 10 "github.com/jedib0t/go-pretty/v6/table" 11 "github.com/scylladb/go-set/strset" 12 "github.com/spf13/cobra" 13 14 "github.com/anchore/clio" 15 "github.com/anchore/syft/internal/bus" 16 "github.com/anchore/syft/internal/task" 17 "github.com/anchore/syft/syft/cataloging/pkgcataloging" 18 ) 19 20 type catalogerListOptions struct { 21 Output string `yaml:"output" json:"output" mapstructure:"output"` 22 DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"` 23 SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"` 24 ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"` 25 } 26 27 func (o *catalogerListOptions) AddFlags(flags clio.FlagSet) { 28 flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger list (available: table, json)") 29 30 flags.StringArrayVarP(&o.DefaultCatalogers, "override-default-catalogers", "", "override the default catalogers with an expression") 31 32 flags.StringArrayVarP(&o.SelectCatalogers, "select-catalogers", "", "select catalogers with an expression") 33 34 flags.BoolVarP(&o.ShowHidden, "show-hidden", "s", "show catalogers that have been de-selected") 35 } 36 37 func defaultCatalogerListOptions() *catalogerListOptions { 38 return &catalogerListOptions{ 39 DefaultCatalogers: []string{"all"}, 40 } 41 } 42 43 func CatalogerList(app clio.Application) *cobra.Command { 44 opts := defaultCatalogerListOptions() 45 46 return app.SetupCommand(&cobra.Command{ 47 Use: "list [OPTIONS]", 48 Short: "List available catalogers", 49 RunE: func(_ *cobra.Command, _ []string) error { 50 return runCatalogerList(opts) 51 }, 52 }, opts) 53 } 54 55 func runCatalogerList(opts *catalogerListOptions) error { 56 factories := task.DefaultPackageTaskFactories() 57 allTasks, err := factories.Tasks(task.DefaultCatalogingFactoryConfig()) 58 if err != nil { 59 return fmt.Errorf("unable to create cataloger tasks: %w", err) 60 } 61 62 report, err := catalogerListReport(opts, allTasks) 63 if err != nil { 64 return fmt.Errorf("unable to generate cataloger list report: %w", err) 65 } 66 67 bus.Report(report) 68 69 return nil 70 } 71 72 func catalogerListReport(opts *catalogerListOptions, allTasks []task.Task) (string, error) { 73 selectedTasks, selectionEvidence, err := task.Select(allTasks, 74 pkgcataloging.NewSelectionRequest(). 75 WithDefaults(opts.DefaultCatalogers...). 76 WithExpression(opts.SelectCatalogers...), 77 ) 78 if err != nil { 79 return "", fmt.Errorf("unable to select catalogers: %w", err) 80 } 81 var report string 82 83 switch opts.Output { 84 case "json": 85 report, err = renderCatalogerListJSON(selectedTasks, selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers) 86 case "table", "": 87 if opts.ShowHidden { 88 report = renderCatalogerListTable(allTasks, selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers) 89 } else { 90 report = renderCatalogerListTable(selectedTasks, selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers) 91 } 92 } 93 94 if err != nil { 95 return "", fmt.Errorf("unable to render cataloger list: %w", err) 96 } 97 98 return report, nil 99 } 100 101 func renderCatalogerListJSON(tasks []task.Task, selection task.Selection, defaultSelections, selections []string) (string, error) { 102 type node struct { 103 Name string `json:"name"` 104 Tags []string `json:"tags"` 105 } 106 107 names, tagsByName := extractTaskInfo(tasks) 108 109 nodesByName := make(map[string]node) 110 111 for name := range tagsByName { 112 tagsSelected := selection.TokensByTask[name].SelectedOn.List() 113 114 if len(tagsSelected) == 1 && tagsSelected[0] == "all" { 115 tagsSelected = tagsByName[name] 116 } 117 118 sort.Strings(tagsSelected) 119 120 if tagsSelected == nil { 121 // ensure collections are not null 122 tagsSelected = []string{} 123 } 124 125 nodesByName[name] = node{ 126 Name: name, 127 Tags: tagsSelected, 128 } 129 } 130 131 type document struct { 132 DefaultSelection []string `json:"default"` 133 Selection []string `json:"selection"` 134 Catalogers []node `json:"catalogers"` 135 } 136 137 if selections == nil { 138 // ensure collections are not null 139 selections = []string{} 140 } 141 142 doc := document{ 143 DefaultSelection: defaultSelections, 144 Selection: selections, 145 } 146 147 for _, name := range names { 148 doc.Catalogers = append(doc.Catalogers, nodesByName[name]) 149 } 150 151 by, err := json.Marshal(doc) 152 153 return string(by), err 154 } 155 156 func renderCatalogerListTable(tasks []task.Task, selection task.Selection, defaultSelections, selections []string) string { 157 t := table.NewWriter() 158 t.SetStyle(table.StyleLight) 159 t.AppendHeader(table.Row{"Cataloger", "Tags"}) 160 161 names, tagsByName := extractTaskInfo(tasks) 162 163 rowsByName := make(map[string]table.Row) 164 165 for name, tags := range tagsByName { 166 rowsByName[name] = formatRow(name, tags, selection) 167 } 168 169 for _, name := range names { 170 t.AppendRow(rowsByName[name]) 171 } 172 173 report := t.Render() 174 175 if len(selections) > 0 { 176 header := "Selected by expressions:\n" 177 for _, expr := range selections { 178 header += fmt.Sprintf(" - %q\n", expr) 179 } 180 report = header + report 181 } 182 183 if len(defaultSelections) > 0 { 184 header := "Default selections:\n" 185 for _, expr := range defaultSelections { 186 header += fmt.Sprintf(" - %q\n", expr) 187 } 188 report = header + report 189 } 190 191 return report 192 } 193 194 func formatRow(name string, tags []string, selection task.Selection) table.Row { 195 isIncluded := selection.Result.Has(name) 196 var selections *task.TokenSelection 197 if s, exists := selection.TokensByTask[name]; exists { 198 selections = &s 199 } 200 201 var formattedTags []string 202 for _, tag := range tags { 203 formattedTags = append(formattedTags, formatToken(tag, selections, isIncluded)) 204 } 205 206 var tagStr string 207 if isIncluded { 208 tagStr = strings.Join(formattedTags, ", ") 209 } else { 210 tagStr = strings.Join(formattedTags, grey.Render(", ")) 211 } 212 213 // TODO: selection should keep warnings (non-selections) in struct 214 215 return table.Row{ 216 formatToken(name, selections, isIncluded), 217 tagStr, 218 } 219 } 220 221 var ( 222 green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // hi green 223 grey = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey 224 red = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // high red 225 ) 226 227 func formatToken(token string, selection *task.TokenSelection, included bool) string { 228 if included && selection != nil { 229 // format all tokens in selection in green 230 if selection.SelectedOn.Has(token) { 231 return green.Render(token) 232 } 233 234 return token 235 } 236 237 // format all tokens in selection in red, all others in grey 238 if selection != nil && selection.DeselectedOn.Has(token) { 239 return red.Render(token) 240 } 241 242 return grey.Render(token) 243 } 244 245 func extractTaskInfo(tasks []task.Task) ([]string, map[string][]string) { 246 tagsByName := make(map[string][]string) 247 var names []string 248 249 for _, tsk := range tasks { 250 var tags []string 251 name := tsk.Name() 252 253 if s, ok := tsk.(task.Selector); ok { 254 set := strset.New(s.Selectors()...) 255 set.Remove(name) 256 tags = set.List() 257 sort.Strings(tags) 258 } 259 260 tagsByName[name] = tags 261 names = append(names, name) 262 } 263 264 sort.Strings(names) 265 266 return names, tagsByName 267 }