Skip to content

Commit

Permalink
feat(outputs): allow to set multiple outputs (anchore#648)
Browse files Browse the repository at this point in the history
  • Loading branch information
olivierboudet committed Jun 10, 2023
1 parent e7fa9d6 commit a5cf551
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 125 deletions.
30 changes: 15 additions & 15 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import (
"strings"
"sync"

"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"

"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/db"
grypeDb "github.com/anchore/grype/grype/db/v5"
Expand Down Expand Up @@ -44,6 +38,11 @@ import (
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
)

var persistentOpts = config.CliOnlyOptions{}
Expand Down Expand Up @@ -130,14 +129,14 @@ func setRootFlags(flags *pflag.FlagSet) {
fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes),
)

flags.StringP(
"output", "o", "",
flags.StringArrayP(
"output", "o", nil,
fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", presenter.AvailableFormats, presenter.DeprecatedFormats),
)

flags.StringP(
"file", "", "",
"file to write the report output to (default is STDOUT)",
"file to write the default report output to (default is STDOUT)",
)

flags.StringP(
Expand Down Expand Up @@ -299,7 +298,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
go func() {
defer close(errs)

presenterConfig, err := presenter.ValidatedConfig(appConfig.Output, appConfig.OutputTemplateFile, appConfig.ShowSuppressed)
presenterConfig, err := presenter.ValidatedConfig(appConfig.Outputs, appConfig.File, appConfig.OutputTemplateFile, appConfig.ShowSuppressed)
if err != nil {
errs <- err
return
Expand Down Expand Up @@ -389,11 +388,12 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
AppConfig: appConfig,
DBStatus: status,
}

bus.Publish(partybus.Event{
Type: event.VulnerabilityScanningFinished,
Value: presenter.GetPresenter(presenterConfig, pb),
})
for _, presenter := range presenter.GetPresenters(presenterConfig, pb) {
bus.Publish(partybus.Event{
Type: event.VulnerabilityScanningFinished,
Value: presenter,
})
}
}()
return errs
}
Expand Down
69 changes: 69 additions & 0 deletions cmd/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cmd

import (
"fmt"
"strings"

"github.com/hashicorp/go-multierror"

"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
)

// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath)
if err != nil {
return nil, err
}

writer, err := sbom.NewWriter(outputOptions...)
if err != nil {
return nil, err
}

return writer, nil
}

// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) {
// always should have one option -- we generally get the default of "table", but just make sure
if len(outputs) == 0 {
outputs = append(outputs, table.ID.String())
}

for _, name := range outputs {
name = strings.TrimSpace(name)

// split to at most two parts for <format>=<file>
parts := strings.SplitN(name, "=", 2)

// the format name is the first part
name = parts[0]

// default to the --file or empty string if not specified
file := defaultFile

// If a file is specified as part of the output formatName, use that
if len(parts) > 1 {
file = parts[1]
}

format := formats.ByName(name)
if format == nil {
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formats.AllIDs()))
continue
}

if tmpl, ok := format.(template.OutputFormat); ok {
tmpl.SetTemplatePath(templateFilePath)
format = tmpl
}

out = append(out, sbom.NewWriterOption(format, file))
}
return out, errs
}
89 changes: 59 additions & 30 deletions grype/presenter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,91 @@ import (
"errors"
"fmt"
"os"
"strings"
"text/template"

presenterTemplate "github.com/anchore/grype/grype/presenter/template"
)

// Config is the presenter domain's configuration data structure.
type Config struct {
format format
formats []format
templateFilePath string
showSuppressed bool
}

// ValidatedConfig returns a new, validated presenter.Config. If a valid Config cannot be created using the given input,
// an error is returned.
func ValidatedConfig(output, outputTemplateFile string, showSuppressed bool) (Config, error) {
format := parse(output)
func ValidatedConfig(outputs []string, defaultFile string, outputTemplateFile string, showSuppressed bool) (Config, error) {
formats, _ := parseOutputs(outputs, defaultFile)
hasTemplateFormat := false

if format == unknownFormat {
return Config{}, fmt.Errorf("unsupported output format %q, supported formats are: %+v", output,
AvailableFormats)
}

if format == templateFormat {
if outputTemplateFile == "" {
return Config{}, fmt.Errorf("must specify path to template file when using %q output format",
templateFormat)
for _, format := range formats {
if format.id == unknownFormat {
return Config{}, fmt.Errorf("unsupported output format %q, supported formats are: %+v", format.id,
AvailableFormats)
}

if _, err := os.Stat(outputTemplateFile); errors.Is(err, os.ErrNotExist) {
// file does not exist
return Config{}, fmt.Errorf("template file %q does not exist",
outputTemplateFile)
}
if format.id == templateFormat {
hasTemplateFormat = true

if _, err := os.ReadFile(outputTemplateFile); err != nil {
return Config{}, fmt.Errorf("unable to read template file: %w", err)
}
if outputTemplateFile == "" {
return Config{}, fmt.Errorf("must specify path to template file when using %q output format",
templateFormat)
}

if _, err := template.New("").Funcs(presenterTemplate.FuncMap).ParseFiles(outputTemplateFile); err != nil {
return Config{}, fmt.Errorf("unable to parse template: %w", err)
}
if _, err := os.Stat(outputTemplateFile); errors.Is(err, os.ErrNotExist) {
// file does not exist
return Config{}, fmt.Errorf("template file %q does not exist",
outputTemplateFile)
}

return Config{
format: format,
templateFilePath: outputTemplateFile,
}, nil
if _, err := os.ReadFile(outputTemplateFile); err != nil {
return Config{}, fmt.Errorf("unable to read template file: %w", err)
}

if _, err := template.New("").Funcs(presenterTemplate.FuncMap).ParseFiles(outputTemplateFile); err != nil {
return Config{}, fmt.Errorf("unable to parse template: %w", err)
}

}
}

if outputTemplateFile != "" {
if outputTemplateFile != "" && !hasTemplateFormat {
return Config{}, fmt.Errorf("specified template file %q, but "+
"%q output format must be selected in order to use a template file",
outputTemplateFile, templateFormat)
}

return Config{
format: format,
showSuppressed: showSuppressed,
formats: formats,
showSuppressed: showSuppressed,
templateFilePath: outputTemplateFile,
}, nil
}

// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
func parseOutputs(outputs []string, defaultFile string) (out []format, errs error) {
for _, name := range outputs {
name = strings.TrimSpace(name)

// split to at most two parts for <format>=<file>
parts := strings.SplitN(name, "=", 2)

// the format name is the first part
name = parts[0]

// default to the --file or empty string if not specified
file := defaultFile

// If a file is specified as part of the output formatName, use that
if len(parts) > 1 {
file = parts[1]
}

format := parse(name)
format.outputFilePath = file
out = append(out, format)
}
return out, errs
}
16 changes: 8 additions & 8 deletions grype/presenter/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,34 @@ import (
func TestValidatedConfig(t *testing.T) {
cases := []struct {
name string
outputValue string
outputValue []string
includeSuppressed bool
outputTemplateFileValue string
expectedConfig Config
assertErrExpectation func(assert.TestingT, error, ...interface{}) bool
}{
{
"valid template config",
"template",
[]string{"template"},
false,
"./template/test-fixtures/test.valid.template",
Config{
format: "template",
formats: []format{{id: templateFormat}},
templateFilePath: "./template/test-fixtures/test.valid.template",
},
assert.NoError,
},
{
"template file with non-template format",
"json",
[]string{"json"},
false,
"./some/path/to/a/custom.template",
Config{},
assert.Error,
},
{
"unknown format",
"some-made-up-format",
[]string{"some-made-up-format"},
false,
"",
Config{},
Expand All @@ -45,11 +45,11 @@ func TestValidatedConfig(t *testing.T) {

{
"table format",
"table",
[]string{"table"},
true,
"",
Config{
format: tableFormat,
formats: []format{{id: tableFormat}},
showSuppressed: true,
},
assert.NoError,
Expand All @@ -58,7 +58,7 @@ func TestValidatedConfig(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actualConfig, actualErr := ValidatedConfig(tc.outputValue, tc.outputTemplateFileValue, tc.includeSuppressed)
actualConfig, actualErr := ValidatedConfig(tc.outputValue, "", tc.outputTemplateFileValue, tc.includeSuppressed)

assert.Equal(t, tc.expectedConfig, actualConfig)
tc.assertErrExpectation(t, actualErr)
Expand Down
Loading

0 comments on commit a5cf551

Please sign in to comment.