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  }