Skip to content

Commit

Permalink
add cataloger list command
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
wagoodman committed Jan 12, 2024
1 parent b0ab75f commit a84d32d
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 7 deletions.
1 change: 1 addition & 0 deletions cmd/syft/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
rootCmd.AddCommand(
scanCmd,
commands.Packages(app, scanCmd), // this is currently an alias for the scan command
commands.Cataloger(app),
commands.Attest(app),
commands.Convert(app),
clio.VersionCommand(id),
Expand Down
2 changes: 1 addition & 1 deletion cmd/syft/cli/commands/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func attestCommand(sbomFilepath string, opts *attestOptions, userInput string) (
}

func predicateType(outputName string) string {
// Select Cosign predicate type based on defined output type
// select the Cosign predicate type based on defined output type
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
switch strings.ToLower(outputName) {
case "cyclonedx-json":
Expand Down
20 changes: 20 additions & 0 deletions cmd/syft/cli/commands/cataloger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package commands

import (
"github.com/spf13/cobra"

"github.com/anchore/clio"
)

func Cataloger(app clio.Application) *cobra.Command {
cmd := &cobra.Command{
Use: "cataloger",
Short: "Show available catalogers and configuration",
}

cmd.AddCommand(
CatalogerList(app),
)

return cmd
}
240 changes: 240 additions & 0 deletions cmd/syft/cli/commands/cataloger_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package commands

import (
"encoding/json"
"fmt"
"sort"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/scylladb/go-set/strset"
"github.com/spf13/cobra"

"github.com/anchore/clio"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
)

type catalogerListOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"`
}

func (o *catalogerListOptions) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger list (available: table, json)")

flags.StringArrayVarP(&o.DefaultCatalogers, "override-default-catalogers", "", "override the default catalogers with an expression")

flags.StringArrayVarP(&o.SelectCatalogers, "select-catalogers", "", "select catalogers with an expression")

flags.BoolVarP(&o.ShowHidden, "show-hidden", "s", "show catalogers that have been de-selected")
}

func CatalogerList(app clio.Application) *cobra.Command {
opts := &catalogerListOptions{
DefaultCatalogers: []string{"all"},
}

return app.SetupCommand(&cobra.Command{
Use: "list [OPTIONS]",
Short: "List available catalogers",
RunE: func(cmd *cobra.Command, args []string) error {
return runCatalogerList(opts)
},
}, opts)
}

func runCatalogerList(opts *catalogerListOptions) error {
factories := task.DefaultPackageTaskFactories()
allTasks, err := factories.Tasks(task.DefaultCatalogingFactoryConfig())
if err != nil {
return fmt.Errorf("unable to create cataloger tasks: %w", err)
}

selectedTasks, selectionEvidence, err := task.Select(allTasks,
pkgcataloging.NewSelectionRequest().
WithDefaults(opts.DefaultCatalogers...).
WithExpression(opts.SelectCatalogers...),
)
if err != nil {
return fmt.Errorf("unable to select catalogers: %w", err)
}

var report string

switch opts.Output {
case "json":
report, err = renderCatalogerListJSON(selectedTasks, selectionEvidence, opts.SelectCatalogers)
case "table", "":
if opts.ShowHidden {
report = renderCatalogerListTable(allTasks, selectionEvidence, opts.SelectCatalogers)
} else {
report = renderCatalogerListTable(selectedTasks, selectionEvidence, opts.SelectCatalogers)
}
}

if err != nil {
return fmt.Errorf("unable to render cataloger list: %w", err)
}

bus.Report(report)

return nil
}

func renderCatalogerListJSON(tasks []task.Task, selection task.Selection, expressions []string) (string, error) {
type node struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}

names, tagsByName := extractTaskInfo(tasks)

nodesByName := make(map[string]node)

for name := range tagsByName {
tagsSelected := selection.TokensByTask[name].SelectedOn.List()
sort.Strings(tagsSelected)

if tagsSelected == nil {
// ensure collections are not null
tagsSelected = []string{}
}

nodesByName[name] = node{
Name: name,
Tags: tagsSelected,
}
}

type document struct {
Expressions []string `json:"expressions"`
Catalogers []node `json:"catalogers"`
}

if expressions == nil {
// ensure collections are not null
expressions = []string{}
}

doc := document{
Expressions: expressions,
}

for _, name := range names {
doc.Catalogers = append(doc.Catalogers, nodesByName[name])
}

by, err := json.Marshal(doc)

return string(by), err
}

func renderCatalogerListTable(tasks []task.Task, selection task.Selection, expressions []string) string {
t := table.NewWriter()
t.SetStyle(table.StyleLight)
t.AppendHeader(table.Row{"Cataloger", "Tags"})

names, tagsByName := extractTaskInfo(tasks)

rowsByName := make(map[string]table.Row)

for name, tags := range tagsByName {
rowsByName[name] = formatRow(name, tags, selection)
}

for _, name := range names {
t.AppendRow(rowsByName[name])
}

report := t.Render()

if len(expressions) > 0 {
header := "Selected by expressions:\n"
for _, expr := range expressions {
header += fmt.Sprintf(" - %q\n", expr)
}
report = header + report
}

return report
}

func formatRow(name string, tags []string, selection task.Selection) table.Row {
isIncluded := selection.Result.Has(name)
var selections *task.TokenSelection
if s, exists := selection.TokensByTask[name]; exists {
selections = &s
}

var formattedTags []string
for _, tag := range tags {
formattedTags = append(formattedTags, formatToken(tag, selections, isIncluded))
}

var tagStr string
if isIncluded {
tagStr = strings.Join(formattedTags, ", ")
} else {
tagStr = strings.Join(formattedTags, grey.Render(", "))
}

// TODO: selection should keep warnings (non-selections) in struct

return table.Row{
formatToken(name, selections, isIncluded),
tagStr,
}
}

var (
green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // hi green
grey = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey
red = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // high red
)

func formatToken(token string, selection *task.TokenSelection, included bool) string {
if included && selection != nil {
// format all tokens in selection in green
if selection.SelectedOn.Has(token) {
return green.Render(token)
}

return token
}

// format all tokens in selection in red, all others in grey
if selection != nil && selection.DeselectedOn.Has(token) {
return red.Render(token)
}

return grey.Render(token)
}

func extractTaskInfo(tasks []task.Task) ([]string, map[string][]string) {
tagsByName := make(map[string][]string)
var names []string

for _, tsk := range tasks {
var tags []string
name := tsk.Name()

if s, ok := tsk.(task.Selector); ok {
set := strset.New(s.Selectors()...)
set.Remove(name)
tags = set.List()
sort.Strings(tags)
}

tagsByName[name] = tags
names = append(names, name)
}

sort.Strings(names)

return names, tagsByName
}
9 changes: 3 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ require (
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
// we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/charmbracelet/bubbles v0.17.1
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/dave/jennifer v1.7.0
Expand All @@ -42,6 +44,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.7.0
github.com/jedib0t/go-pretty/v6 v6.5.2
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b
Expand Down Expand Up @@ -76,12 +79,6 @@ require (
modernc.org/sqlite v1.28.0
)

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/charmbracelet/bubbles v0.17.1
github.com/jedib0t/go-pretty/v6 v6.5.2
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
Expand Down

0 comments on commit a84d32d

Please sign in to comment.