Skip to content

Commit

Permalink
show selection expression evidence
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 Dec 1, 2023
1 parent 6835670 commit 6af5062
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 45 deletions.
2 changes: 1 addition & 1 deletion cmd/syft/cli/commands/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,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
74 changes: 66 additions & 8 deletions cmd/syft/cli/commands/cataloger_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sort"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/scylladb/go-set/strset"
"github.com/spf13/cobra"
Expand All @@ -20,12 +21,15 @@ import (
type catalogerListOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"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.Catalogers, "select", "s", "select catalogers with an expression")
flags.StringArrayVarP(&o.Catalogers, "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 {
Expand All @@ -42,13 +46,18 @@ func CatalogerList(app clio.Application) *cobra.Command {

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

var (
selectedTasks = allTasks
selectionEvidence *task.Selection
)

if len(opts.Catalogers) > 0 {
tasks, _, err = task.Select(tasks, "", opts.Catalogers...)
selectedTasks, selectionEvidence, err = task.Select(allTasks, "", opts.Catalogers...)
if err != nil {
return fmt.Errorf("unable to select catalogers: %w", err)
}
Expand All @@ -58,9 +67,13 @@ func runCatalogerList(opts *catalogerListOptions) error {

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

if err != nil {
Expand Down Expand Up @@ -116,7 +129,7 @@ func renderCatalogerListJSON(tasks []task.Task, expressions []string) (string, e
return string(by), err
}

func renderCatalogerListTable(tasks []task.Task, expressions []string) string {
func renderCatalogerListTable(tasks []task.Task, selection *task.Selection, expressions []string) string {
t := table.NewWriter()
t.SetStyle(table.StyleLight)
t.AppendHeader(table.Row{"Cataloger", "Tags"})
Expand All @@ -126,8 +139,7 @@ func renderCatalogerListTable(tasks []task.Task, expressions []string) string {
rowsByName := make(map[string]table.Row)

for name, tags := range tagsByName {
tagsStr := strings.Join(tags, ", ")
rowsByName[name] = table.Row{name, tagsStr}
rowsByName[name] = formatRow(name, tags, selection)
}

for _, name := range names {
Expand All @@ -147,6 +159,52 @@ func renderCatalogerListTable(tasks []task.Task, expressions []string) string {
return report
}

func formatRow(name string, tags []string, selection *task.Selection) table.Row {
if selection == nil {
return table.Row{name, strings.Join(tags, ", ")}
}

isIncluded := selection.Selected.Has(name)
var selections *task.TokenSelection
if s, exists := selection.TokenSelectionsByTask[name]; exists {
selections = &s
}

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

return table.Row{
formatToken(name, selections, isIncluded),
strings.Join(formattedTags, ", "),
}
}

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
Expand Down
36 changes: 33 additions & 3 deletions internal/task/selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,49 @@ import (
"fmt"
"regexp"
"strings"

"github.com/scylladb/go-set/strset"
)

var expressionNodePattern = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-+]*&?)+$`)

type Selection struct {
Selected *strset.Set
TokenSelectionsByTask map[string]TokenSelection
Expressions []string
}

type TokenSelection struct {
SelectedOn *strset.Set
DeselectedOn *strset.Set
}
type expressionNodes []expressionNode

type expressionNode struct {
Prefix string
Requirements []string
}

func Select(allTasks []Task, basis string, expressions ...string) ([]Task, []string, error) {
func (ts *TokenSelection) merge(other ...TokenSelection) {
for _, o := range other {
if ts.SelectedOn != nil {
ts.SelectedOn.Add(o.SelectedOn.List()...)
}
if ts.DeselectedOn != nil {
ts.DeselectedOn.Add(o.DeselectedOn.List()...)
}
}
}

func Select(allTasks []Task, basis string, expressions ...string) ([]Task, *Selection, error) {
nodes, err := parseExpressionsWithBasis(basis, expressions...)
if err != nil {
return nil, nil, err
}

var allSelections map[string]TokenSelection
if len(nodes) > 0 {
allTasks, err = tasks(allTasks).Select(nodes...)
allTasks, allSelections, err = tasks(allTasks).selectFromExpression(nodes...)
if err != nil {
return nil, nil, fmt.Errorf("unable to select package cataloger tasks: %w", err)
}
Expand All @@ -32,7 +56,13 @@ func Select(allTasks []Task, basis string, expressions ...string) ([]Task, []str
}
}

return allTasks, expressionNodes(nodes).Strings(), nil
evidence := &Selection{
Selected: strset.New(tasks(allTasks).Names()...),
TokenSelectionsByTask: allSelections,
Expressions: expressionNodes(nodes).Strings(),
}

return allTasks, evidence, nil
}

func parseExpressionsWithBasis(basis string, expressions ...string) ([]expressionNode, error) {
Expand Down
135 changes: 135 additions & 0 deletions internal/task/selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -551,3 +552,137 @@ func Test_parseExpressions(t *testing.T) {
})
}
}

func TestSelect(t *testing.T) {

empty := func() TokenSelection {
return TokenSelection{
SelectedOn: strset.New(),
DeselectedOn: strset.New(),
}
}

removal := func(other TokenSelection, tokens ...string) TokenSelection {
ts := TokenSelection{
SelectedOn: strset.New(),
DeselectedOn: strset.New(tokens...),
}
ts.merge(other)
return ts
}

addition := func(tokens ...string) TokenSelection {
return TokenSelection{
SelectedOn: strset.New(tokens...),
DeselectedOn: strset.New(),
}
}

tests := []struct {
name string
basis string
expressions []string
wantTasks []string
wantSelection *Selection
wantErr require.ErrorAssertionFunc
}{
{
name: "multiple overlapping subtractions",
basis: "image",
expressions: []string{
"-declared,cpp,language",
},
wantTasks: []string{
"dpkg-db-cataloger",
"portage-cataloger",
"rpm-db-cataloger",
"alpm-db-cataloger",
"apk-db-cataloger",
},
wantSelection: &Selection{
Selected: strset.New("dpkg-db-cataloger", "portage-cataloger", "rpm-db-cataloger", "alpm-db-cataloger", "apk-db-cataloger"),
Expressions: []string{
"image",
"-declared",
"-cpp",
"-language",
},
TokenSelectionsByTask: map[string]TokenSelection{
// added
"alpm-db-cataloger": addition("image"),
"apk-db-cataloger": addition("image"),
"dpkg-db-cataloger": addition("image"),
"portage-cataloger": addition("image"),
"rpm-db-cataloger": addition("image"),

// not added
"binary-cataloger": removal(addition("image"), "declared"),
"conan-cataloger": removal(empty(), "declared", "cpp", "language"),
"conan-info-cataloger": removal(addition("image"), "cpp", "language"),
"dart-pubspec-lock-cataloger": removal(empty(), "declared", "language"),
"dotnet-deps-cataloger": removal(empty(), "declared", "language"),
"dotnet-portable-executable-cataloger": removal(addition("image"), "language"),
"elixir-mix-lock-cataloger": removal(empty(), "declared", "language"),
"erlang-rebar-lock-cataloger": removal(empty(), "declared", "language"),
"github-action-workflow-usage-cataloger": removal(empty(), "declared"),
"github-actions-usage-cataloger": removal(empty(), "declared"),
"go-module-binary-cataloger": removal(addition("image"), "language"),
"graalvm-native-image-cataloger": removal(addition("image"), "language"),
"java-archive-cataloger": removal(addition("image"), "language"),
"javascript-lock-cataloger": removal(empty(), "declared", "language"),
"javascript-package-cataloger": removal(addition("image"), "language"),
"php-composer-installed-cataloger": removal(addition("image"), "language"),
"python-installed-package-cataloger": removal(addition("image"), "language"),
"rpm-archive-cataloger": removal(empty(), "declared"),
"ruby-installed-gemspec-cataloger": removal(addition("image"), "language"),
"rust-cargo-lock-cataloger": removal(addition("image"), "language"),
"sbom-cataloger": removal(addition("image"), "declared"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}

gotTasks, gotSelection, err := Select(createPackageTaskDescriptors(), tt.basis, tt.expressions...)
tt.wantErr(t, err)
if err != nil {
return
}

gotTaskNames := strset.New()
for _, g := range gotTasks {
gotTaskNames.Add(g.Name())
}

require.ElementsMatch(t, tt.wantTasks, gotTaskNames.List(), "task selection not matched")

if tt.wantSelection == nil && gotSelection != nil {
t.Fatal("unexpected selection")
}

if tt.wantSelection != nil && gotSelection == nil {
t.Fatal("missing selection")
}

if tt.wantSelection == nil && gotSelection == nil {
return
}

require.ElementsMatch(t, tt.wantSelection.Selected.List(), gotSelection.Selected.List(), "task selection names not matched")

require.Equal(t, tt.wantSelection.Expressions, gotSelection.Expressions, "task selection expressions not matched")

setComparer := cmp.Comparer(func(a, b *strset.Set) bool {
return a.IsEqual(b)
})

if d := cmp.Diff(tt.wantSelection.TokenSelectionsByTask, gotSelection.TokenSelectionsByTask, setComparer); d != "" {
t.Errorf("task selection token selections mismatch (-want +got):\n%s", d)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/task/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ func newSet(tasks ...Task) *set {
return s
}

func (ts *set) ContainsName(name string) bool {
_, exists := ts.tasks[name]
return exists
}

func (ts *set) Add(tasks ...Task) {
for _, t := range tasks {
taskName := t.Name()
Expand Down
Loading

0 comments on commit 6af5062

Please sign in to comment.