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  }