mirror of
https://github.com/anchore/syft.git
synced 2026-02-14 19:46:42 +01:00
Add support for multiple output files in different formats (#732)
This commit is contained in:
parent
38c4b17847
commit
5e5312c72d
24
README.md
24
README.md
@ -112,7 +112,8 @@ may attempt to expand wildcards, so put those parameters in single quotes, like:
|
|||||||
|
|
||||||
### Output formats
|
### Output formats
|
||||||
|
|
||||||
The output format for Syft is configurable as well:
|
The output format for Syft is configurable as well using the
|
||||||
|
`-o` (or `--output`) option:
|
||||||
|
|
||||||
```
|
```
|
||||||
syft packages <image> -o <format>
|
syft packages <image> -o <format>
|
||||||
@ -127,6 +128,15 @@ Where the `formats` available are:
|
|||||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||||
- `table`: A columnar summary (default).
|
- `table`: A columnar summary (default).
|
||||||
|
|
||||||
|
#### Multiple outputs
|
||||||
|
|
||||||
|
Syft can also output _multiple_ files in differing formats by appending
|
||||||
|
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
syft packages <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
|
||||||
|
```
|
||||||
|
|
||||||
## Private Registry Authentication
|
## Private Registry Authentication
|
||||||
|
|
||||||
### Local Docker Credentials
|
### Local Docker Credentials
|
||||||
@ -221,8 +231,12 @@ Configuration search paths:
|
|||||||
Configuration options (example values are the default):
|
Configuration options (example values are the default):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# the output format of the SBOM report (options: table, text, json)
|
# the output format(s) of the SBOM report (options: table, text, json, spdx, ...)
|
||||||
# same as -o ; SYFT_OUTPUT env var
|
# same as -o, --output, and SYFT_OUTPUT env var
|
||||||
|
# to specify multiple output files in differing formats, use a list:
|
||||||
|
# output:
|
||||||
|
# - "json=<syft-json-output-file>"
|
||||||
|
# - "spdx-json=<spdx-json-output-file>"
|
||||||
output: "table"
|
output: "table"
|
||||||
|
|
||||||
# suppress all output (except for the SBOM report)
|
# suppress all output (except for the SBOM report)
|
||||||
@ -238,8 +252,8 @@ check-for-app-update: true
|
|||||||
|
|
||||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||||
# exclude:
|
# exclude:
|
||||||
# - '/etc/**'
|
# - "/etc/**"
|
||||||
# - './out/**/*.json'
|
# - "./out/**/*.json"
|
||||||
exclude:
|
exclude:
|
||||||
|
|
||||||
# cataloging packages is exposed through the packages and power-user subcommands
|
# cataloging packages is exposed through the packages and power-user subcommands
|
||||||
|
|||||||
@ -50,7 +50,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
@ -182,7 +182,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
@ -251,8 +251,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
Error: fmt.Errorf("unable to create presenter"),
|
Error: fmt.Errorf("an exit error occured"),
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
@ -376,7 +376,7 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
|
|||||||
72
cmd/output_writer.go
Normal file
72
cmd/output_writer.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats"
|
||||||
|
"github.com/anchore/syft/internal/output"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 string) (sbom.Writer, error) {
|
||||||
|
outputOptions, err := parseOptions(outputs, defaultFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := output.MakeWriter(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 parseOptions(outputs []string, defaultFile string) (out []output.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, string(format.TableOption))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range outputs {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
|
||||||
|
// split to at most two parts for <format>=<file>
|
||||||
|
parts := strings.SplitN(name, "=", 2)
|
||||||
|
|
||||||
|
// the format option 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 option, use that
|
||||||
|
if len(parts) > 1 {
|
||||||
|
file = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
option := format.ParseOption(name)
|
||||||
|
if option == format.UnknownFormatOption {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := formats.ByOption(option)
|
||||||
|
if encoder == nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, output.WriterOption{
|
||||||
|
Format: *encoder,
|
||||||
|
Path: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, errs
|
||||||
|
}
|
||||||
78
cmd/output_writer_test.go
Normal file
78
cmd/output_writer_test.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOutputWriterConfig(t *testing.T) {
|
||||||
|
tmp := t.TempDir() + "/"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
outputs []string
|
||||||
|
file string
|
||||||
|
err bool
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
outputs: []string{},
|
||||||
|
expected: []string{""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"json"},
|
||||||
|
expected: []string{""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "test-1.json",
|
||||||
|
expected: []string{"test-1.json"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"json=test-2.json"},
|
||||||
|
expected: []string{"test-2.json"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"json=test-3-1.json", "spdx-json=test-3-2.json"},
|
||||||
|
expected: []string{"test-3-1.json", "test-3-2.json"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"text", "json=test-4.json"},
|
||||||
|
expected: []string{"", "test-4.json"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%s/%s", test.outputs, test.file), func(t *testing.T) {
|
||||||
|
outputs := test.outputs
|
||||||
|
for i, val := range outputs {
|
||||||
|
outputs[i] = strings.Replace(val, "=", "="+tmp, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := test.file
|
||||||
|
if file != "" {
|
||||||
|
file = tmp + file
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := makeWriter(test.outputs, file)
|
||||||
|
|
||||||
|
if test.err {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expected := range test.expected {
|
||||||
|
if expected != "" {
|
||||||
|
assert.FileExists(t, tmp+expected)
|
||||||
|
} else if file != "" {
|
||||||
|
assert.FileExists(t, file)
|
||||||
|
} else {
|
||||||
|
assert.NoFileExists(t, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/anchore"
|
"github.com/anchore/syft/internal/anchore"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/formats"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
@ -51,7 +50,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
packagesPresenterOpt format.Option
|
|
||||||
packagesCmd = &cobra.Command{
|
packagesCmd = &cobra.Command{
|
||||||
Use: "packages [SOURCE]",
|
Use: "packages [SOURCE]",
|
||||||
Short: "Generate a package SBOM",
|
Short: "Generate a package SBOM",
|
||||||
@ -63,14 +61,7 @@ var (
|
|||||||
Args: validateInputArgs,
|
Args: validateInputArgs,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||||
// set the presenter
|
|
||||||
presenterOption := format.ParseOption(appConfig.Output)
|
|
||||||
if presenterOption == format.UnknownFormatOption {
|
|
||||||
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
|
|
||||||
}
|
|
||||||
packagesPresenterOpt = presenterOption
|
|
||||||
|
|
||||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||||
}
|
}
|
||||||
@ -102,14 +93,14 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
|||||||
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
||||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringArrayP(
|
||||||
"output", "o", string(format.TableOption),
|
"output", "o", []string{string(format.TableOption)},
|
||||||
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
|
fmt.Sprintf("report output format, options=%v", format.AllOptions),
|
||||||
)
|
)
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"file", "", "",
|
"file", "", "",
|
||||||
"file to write the report output to (default is STDOUT)",
|
"file to write the default report output to (default is STDOUT)",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Upload options //////////////////////////////////////////////////////////
|
// Upload options //////////////////////////////////////////////////////////
|
||||||
@ -210,26 +201,26 @@ func validateInputArgs(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func packagesExec(_ *cobra.Command, args []string) error {
|
func packagesExec(_ *cobra.Command, args []string) error {
|
||||||
// could be an image or a directory, with or without a scheme
|
writer, err := makeWriter(appConfig.Output, appConfig.File)
|
||||||
userInput := args[0]
|
|
||||||
|
|
||||||
reporter, closer, err := reportWriter()
|
|
||||||
defer func() {
|
|
||||||
if err := closer(); err != nil {
|
|
||||||
log.Warnf("unable to write to report destination: %+v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
log.Warnf("unable to write to report destination: %w", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// could be an image or a directory, with or without a scheme
|
||||||
|
userInput := args[0]
|
||||||
|
|
||||||
return eventLoop(
|
return eventLoop(
|
||||||
packagesExecWorker(userInput),
|
packagesExecWorker(userInput, writer),
|
||||||
setupSignals(),
|
setupSignals(),
|
||||||
eventSubscription,
|
eventSubscription,
|
||||||
stereoscope.Cleanup,
|
stereoscope.Cleanup,
|
||||||
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
|
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +235,7 @@ func isVerbose() (result bool) {
|
|||||||
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
|
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
func packagesExecWorker(userInput string) <-chan error {
|
func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
@ -255,12 +246,6 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f := formats.ByOption(packagesPresenterOpt)
|
|
||||||
if f == nil {
|
|
||||||
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||||
@ -296,8 +281,8 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
Value: f.Presenter(s),
|
Value: func() error { return writer.Write(s) },
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
return errs
|
return errs
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/formats/syftjson"
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/internal/output"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
@ -73,9 +74,16 @@ func powerUserExec(_ *cobra.Command, args []string) error {
|
|||||||
// could be an image or a directory, with or without a scheme
|
// could be an image or a directory, with or without a scheme
|
||||||
userInput := args[0]
|
userInput := args[0]
|
||||||
|
|
||||||
reporter, closer, err := reportWriter()
|
writer, err := output.MakeWriter(output.WriterOption{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
Path: appConfig.File,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := closer(); err != nil {
|
if err := writer.Close(); err != nil {
|
||||||
log.Warnf("unable to write to report destination: %+v", err)
|
log.Warnf("unable to write to report destination: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,19 +92,15 @@ func powerUserExec(_ *cobra.Command, args []string) error {
|
|||||||
fmt.Fprintln(os.Stderr, deprecated)
|
fmt.Fprintln(os.Stderr, deprecated)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventLoop(
|
return eventLoop(
|
||||||
powerUserExecWorker(userInput),
|
powerUserExecWorker(userInput, writer),
|
||||||
setupSignals(),
|
setupSignals(),
|
||||||
eventSubscription,
|
eventSubscription,
|
||||||
stereoscope.Cleanup,
|
stereoscope.Cleanup,
|
||||||
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
|
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func powerUserExecWorker(userInput string) <-chan error {
|
func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
@ -140,8 +144,8 @@ func powerUserExecWorker(userInput string) <-chan error {
|
|||||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
Value: syftjson.Format().Presenter(s),
|
Value: func() error { return writer.Write(s) },
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func reportWriter() (io.Writer, func() error, error) {
|
|
||||||
nop := func() error { return nil }
|
|
||||||
path := strings.TrimSpace(appConfig.File)
|
|
||||||
|
|
||||||
switch len(path) {
|
|
||||||
case 0:
|
|
||||||
return os.Stdout, nop, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reportFile, func() error {
|
|
||||||
log.Infof("report written to file=%q", path)
|
|
||||||
|
|
||||||
return reportFile.Close()
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
go.mod
1
go.mod
@ -8,7 +8,6 @@ require (
|
|||||||
github.com/adrg/xdg v0.2.1
|
github.com/adrg/xdg v0.2.1
|
||||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa
|
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -101,8 +101,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
|||||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
||||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa h1:mDLUAkgXsV5Z8D0EEj8eS6FBekolV/A+Xxbs9054bPw=
|
|
||||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa/go.mod h1:29jwxTSAS6pBcrmuwf1U3r1Tqp1o1XpuiOJ0NT9NoGg=
|
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||||
|
|||||||
@ -25,7 +25,7 @@ type packageSBOMImportAPI interface {
|
|||||||
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
err := syftjson.Format().Presenter(s).Present(&buf)
|
err := syftjson.Format().Encode(&buf, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
|||||||
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
|
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
|
||||||
var model external.ImagePackageManifest
|
var model external.ImagePackageManifest
|
||||||
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
|
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
|
||||||
return nil, fmt.Errorf("unable to convert JSON presenter output to import model: %w", err)
|
return nil, fmt.Errorf("unable to convert JSON output to import model: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model, nil
|
return &model, nil
|
||||||
|
|||||||
@ -105,8 +105,7 @@ func TestPackageSbomToModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
pres := syftjson.Format().Presenter(s)
|
if err := syftjson.Format().Encode(&buf, s); err != nil {
|
||||||
if err := pres.Present(&buf); err != nil {
|
|
||||||
t.Fatalf("unable to get expected json: %+v", err)
|
t.Fatalf("unable to get expected json: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ type parser interface {
|
|||||||
// Application is the main syft application configuration.
|
// Application is the main syft application configuration.
|
||||||
type Application struct {
|
type Application struct {
|
||||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
Output []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||||
|
|||||||
@ -2,9 +2,10 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const (
|
|||||||
// ApplicationName is the non-capitalized name of the application (do not change this)
|
// ApplicationName is the non-capitalized name of the application (do not change this)
|
||||||
ApplicationName = "syft"
|
ApplicationName = "syft"
|
||||||
|
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||||
JSONSchemaVersion = "2.0.2"
|
JSONSchemaVersion = "2.0.2"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/anchore/go-presenter"
|
|
||||||
"github.com/anchore/go-testutils"
|
"github.com/anchore/go-testutils"
|
||||||
"github.com/anchore/stereoscope/pkg/filetree"
|
"github.com/anchore/stereoscope/pkg/filetree"
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/distro"
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
@ -32,7 +32,7 @@ func FromSnapshot() ImageOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
// grab the latest image contents and persist
|
// grab the latest image contents and persist
|
||||||
@ -40,11 +40,11 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := pres.Present(&buffer)
|
err := format.Encode(&buffer, sbom)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
// replace the expected snapshot contents with the current presenter contents
|
// replace the expected snapshot contents with the current encoder contents
|
||||||
if updateSnapshot {
|
if updateSnapshot {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
@ -66,14 +66,14 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
err := pres.Present(&buffer)
|
err := format.Encode(&buffer, sbom)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
// replace the expected snapshot contents with the current presenter contents
|
// replace the expected snapshot contents with the current encoder contents
|
||||||
if updateSnapshot {
|
if updateSnapshot {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,20 +8,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders")
|
||||||
|
|
||||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage),
|
||||||
testImage,
|
testImage,
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
|
|||||||
@ -8,20 +8,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders")
|
||||||
|
|
||||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage),
|
||||||
testImage,
|
testImage,
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
|
|||||||
@ -8,20 +8,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
|
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
|
||||||
|
|
||||||
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
func TestSPDXJSONDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateSpdxJson,
|
*updateSpdxJson,
|
||||||
spdxJsonRedactor,
|
spdxJsonRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
func TestSPDXJSONImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateSpdxJson,
|
*updateSpdxJson,
|
||||||
spdxJsonRedactor,
|
spdxJsonRedactor,
|
||||||
|
|||||||
@ -8,21 +8,23 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
|
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv encoders")
|
||||||
|
|
||||||
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
func TestSPDXTagValueDirectoryEncoder(t *testing.T) {
|
||||||
|
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateSpdxTagValue,
|
*updateSpdxTagValue,
|
||||||
spdxTagValueRedactor,
|
spdxTagValueRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
func TestSPDXTagValueImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateSpdxTagValue,
|
*updateSpdxTagValue,
|
||||||
spdxTagValueRedactor,
|
spdxTagValueRedactor,
|
||||||
|
|||||||
@ -16,19 +16,21 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json encoders")
|
||||||
|
|
||||||
func TestDirectoryPresenter(t *testing.T) {
|
func TestDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateJson,
|
*updateJson,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImagePresenter(t *testing.T) {
|
func TestImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateJson,
|
*updateJson,
|
||||||
)
|
)
|
||||||
@ -192,8 +194,9 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(s),
|
Format(),
|
||||||
|
s,
|
||||||
*updateJson,
|
*updateJson,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import (
|
|||||||
|
|
||||||
var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table format")
|
var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table format")
|
||||||
|
|
||||||
func TestTablePresenter(t *testing.T) {
|
func TestTableEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateTableGoldenFiles,
|
*updateTableGoldenFiles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,20 +7,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
var updateTextEncoderGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text encoder")
|
||||||
|
|
||||||
func TestTextDirectoryPresenter(t *testing.T) {
|
func TestTextDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
*updateTextPresenterGoldenFiles,
|
testutils.DirectoryInput(t),
|
||||||
|
*updateTextEncoderGoldenFiles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTextImagePresenter(t *testing.T) {
|
func TestTextImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateTextPresenterGoldenFiles,
|
*updateTextEncoderGoldenFiles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
116
internal/output/writer.go
Normal file
116
internal/output/writer.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||||
|
type streamWriter struct {
|
||||||
|
format format.Format
|
||||||
|
out io.Writer
|
||||||
|
close func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the provided SBOM to the data stream
|
||||||
|
func (w *streamWriter) Write(s sbom.SBOM) error {
|
||||||
|
return w.format.Encode(w.out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any resources, such as open files
|
||||||
|
func (w *streamWriter) Close() error {
|
||||||
|
if w.close != nil {
|
||||||
|
return w.close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
|
||||||
|
type multiWriter struct {
|
||||||
|
writers []sbom.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the SBOM to all writers
|
||||||
|
func (m *multiWriter) Write(s sbom.SBOM) (errs error) {
|
||||||
|
for _, w := range m.writers {
|
||||||
|
err := w.Write(s)
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all writers
|
||||||
|
func (m *multiWriter) Close() (errs error) {
|
||||||
|
for _, w := range m.writers {
|
||||||
|
err := w.Close()
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriterOption Format and path strings used to create sbom.Writer
|
||||||
|
type WriterOption struct {
|
||||||
|
Format format.Format
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeWriter create all report writers from input options; if a file is not specified, os.Stdout is used
|
||||||
|
func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) {
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, fmt.Errorf("no output options provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &multiWriter{}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if errs != nil {
|
||||||
|
// close any previously opened files; we can't really recover from any errors
|
||||||
|
_ = out.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
switch len(option.Path) {
|
||||||
|
case 0:
|
||||||
|
out.writers = append(out.writers, &streamWriter{
|
||||||
|
format: option.Format,
|
||||||
|
out: os.Stdout,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
// create any missing subdirectories
|
||||||
|
dir := path.Dir(option.Path)
|
||||||
|
if dir != "" {
|
||||||
|
s, err := os.Stat(dir)
|
||||||
|
if err != nil {
|
||||||
|
err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create report file: %w", err)
|
||||||
|
}
|
||||||
|
out.writers = append(out.writers, &streamWriter{
|
||||||
|
format: option.Format,
|
||||||
|
out: fileOut,
|
||||||
|
close: fileOut.Close,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
168
internal/output/writer_test.go
Normal file
168
internal/output/writer_test.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json"
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
|
"github.com/anchore/syft/internal/formats/table"
|
||||||
|
"github.com/anchore/syft/internal/formats/text"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type writerConfig struct {
|
||||||
|
format string
|
||||||
|
file string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputWriter(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
testName := func(options []WriterOption, err bool) string {
|
||||||
|
var out []string
|
||||||
|
for _, opt := range options {
|
||||||
|
out = append(out, string(opt.Format.Option)+"="+opt.Path)
|
||||||
|
}
|
||||||
|
errs := ""
|
||||||
|
if err {
|
||||||
|
errs = "(err)"
|
||||||
|
}
|
||||||
|
return strings.Join(out, ", ") + errs
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
outputs []WriterOption
|
||||||
|
err bool
|
||||||
|
expected []writerConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: table.Format(),
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
Path: "test-2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
file: "test-2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
Path: "test-3/1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: spdx22json.Format(),
|
||||||
|
Path: "test-3/2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
file: "test-3/1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "spdx-json",
|
||||||
|
file: "test-3/2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: text.Format(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: spdx22json.Format(),
|
||||||
|
Path: "test-4.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "spdx-json",
|
||||||
|
file: "test-4.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(testName(test.outputs, test.err), func(t *testing.T) {
|
||||||
|
outputs := test.outputs
|
||||||
|
for i := range outputs {
|
||||||
|
if outputs[i].Path != "" {
|
||||||
|
outputs[i].Path = tmp + outputs[i].Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := MakeWriter(outputs...)
|
||||||
|
|
||||||
|
if test.err {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mw := writer.(*multiWriter)
|
||||||
|
|
||||||
|
assert.Len(t, mw.writers, len(test.expected))
|
||||||
|
|
||||||
|
for i, e := range test.expected {
|
||||||
|
w := mw.writers[i].(*streamWriter)
|
||||||
|
|
||||||
|
assert.Equal(t, string(w.format.Option), e.format)
|
||||||
|
|
||||||
|
if e.file != "" {
|
||||||
|
assert.FileExists(t, tmp+e.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.file != "" {
|
||||||
|
assert.NotNil(t, w.out)
|
||||||
|
assert.NotNil(t, w.close)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, w.out)
|
||||||
|
assert.Nil(t, w.close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,23 +2,22 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
|
|
||||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
// handleExit is a UI function for processing the Exit bus event,
|
||||||
// via the given presenter to stdout.
|
// and calling the given function to output the contents.
|
||||||
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
|
func handleExit(event partybus.Event) error {
|
||||||
// show the report to stdout
|
// show the report to stdout
|
||||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
fn, err := syftEventParsers.ParseExit(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pres.Present(reportOutput); err != nil {
|
if err := fn(); err != nil {
|
||||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
return fmt.Errorf("unable to show package catalog report: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,16 +42,14 @@ type ephemeralTerminalUI struct {
|
|||||||
frame *frame.Frame
|
frame *frame.Frame
|
||||||
logBuffer *bytes.Buffer
|
logBuffer *bytes.Buffer
|
||||||
uiOutput *os.File
|
uiOutput *os.File
|
||||||
reportOutput io.Writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
|
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
|
||||||
func NewEphemeralTerminalUI(reportWriter io.Writer) UI {
|
func NewEphemeralTerminalUI() UI {
|
||||||
return &ephemeralTerminalUI{
|
return &ephemeralTerminalUI{
|
||||||
handler: ui.NewHandler(),
|
handler: ui.NewHandler(),
|
||||||
waitGroup: &sync.WaitGroup{},
|
waitGroup: &sync.WaitGroup{},
|
||||||
uiOutput: os.Stderr,
|
uiOutput: os.Stderr,
|
||||||
reportOutput: reportWriter,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,12 +80,12 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
|||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case event.Type == syftEvent.PresenterReady:
|
case event.Type == syftEvent.Exit:
|
||||||
// we need to close the screen now since signaling the the presenter is ready means that we
|
// we need to close the screen now since signaling the sbom is ready means that we
|
||||||
// are about to write bytes to stdout, so we should reset the terminal state first
|
// are about to write bytes to stdout, so we should reset the terminal state first
|
||||||
h.closeScreen(false)
|
h.closeScreen(false)
|
||||||
|
|
||||||
if err := handleCatalogerPresenterReady(event, h.reportOutput); err != nil {
|
if err := handleExit(event); err != nil {
|
||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
syftEvent "github.com/anchore/syft/syft/event"
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
@ -10,14 +8,11 @@ import (
|
|||||||
|
|
||||||
type loggerUI struct {
|
type loggerUI struct {
|
||||||
unsubscribe func() error
|
unsubscribe func() error
|
||||||
reportOutput io.Writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
||||||
func NewLoggerUI(reportWriter io.Writer) UI {
|
func NewLoggerUI() UI {
|
||||||
return &loggerUI{
|
return &loggerUI{}
|
||||||
reportOutput: reportWriter,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
||||||
@ -27,11 +22,11 @@ func (l *loggerUI) Setup(unsubscribe func() error) error {
|
|||||||
|
|
||||||
func (l loggerUI) Handle(event partybus.Event) error {
|
func (l loggerUI) Handle(event partybus.Event) error {
|
||||||
// ignore all events except for the final event
|
// ignore all events except for the final event
|
||||||
if event.Type != syftEvent.PresenterReady {
|
if event.Type != syftEvent.Exit {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handleCatalogerPresenterReady(event, l.reportOutput); err != nil {
|
if err := handleExit(event); err != nil {
|
||||||
log.Warnf("unable to show catalog image finished event: %+v", err)
|
log.Warnf("unable to show catalog image finished event: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
@ -16,16 +15,16 @@ import (
|
|||||||
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
||||||
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||||
// the final SBOM report.
|
// the final SBOM report.
|
||||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
func Select(verbose, quiet bool) (uis []UI) {
|
||||||
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
||||||
notATerminal := !isStderrATty && !isStdoutATty
|
notATerminal := !isStderrATty && !isStdoutATty
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||||
uis = append(uis, NewLoggerUI(reportWriter))
|
uis = append(uis, NewLoggerUI())
|
||||||
default:
|
default:
|
||||||
uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter))
|
uis = append(uis, NewEphemeralTerminalUI())
|
||||||
}
|
}
|
||||||
|
|
||||||
return uis
|
return uis
|
||||||
|
|||||||
@ -3,15 +3,11 @@
|
|||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Select is responsible for determining the specific UI function given select user option, the current platform
|
// Select is responsible for determining the specific UI function given select user option, the current platform
|
||||||
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
|
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
|
||||||
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
||||||
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||||
// the final SBOM report.
|
// the final SBOM report.
|
||||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
func Select(verbose, quiet bool) (uis []UI) {
|
||||||
return append(uis, NewLoggerUI(reportWriter))
|
return append(uis, NewLoggerUI())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,8 @@ const (
|
|||||||
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
||||||
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
|
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
|
||||||
|
|
||||||
// PresenterReady is a partybus event that occurs when an analysis result is ready for final presentation
|
// Exit is a partybus event that occurs when an analysis result is ready for final presentation
|
||||||
PresenterReady partybus.EventType = "syft-presenter-ready-event"
|
Exit partybus.EventType = "syft-exit-event"
|
||||||
|
|
||||||
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
||||||
ImportStarted partybus.EventType = "syft-import-started-event"
|
ImportStarted partybus.EventType = "syft-import-started-event"
|
||||||
|
|||||||
@ -6,7 +6,6 @@ package parsers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/anchore/go-presenter"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
@ -109,17 +108,17 @@ func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgress
|
|||||||
return path, prog, nil
|
return path, prog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParsePresenterReady(e partybus.Event) (presenter.Presenter, error) {
|
func ParseExit(e partybus.Event) (func() error, error) {
|
||||||
if err := checkEventType(e.Type, event.PresenterReady); err != nil {
|
if err := checkEventType(e.Type, event.Exit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pres, ok := e.Value.(presenter.Presenter)
|
fn, ok := e.Value.(func() error)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pres, nil
|
return fn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
||||||
|
|||||||
@ -50,10 +50,3 @@ func (f Format) Validate(reader io.Reader) error {
|
|||||||
|
|
||||||
return f.validator(reader)
|
return f.validator(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Format) Presenter(s sbom.SBOM) *Presenter {
|
|
||||||
if f.encoder == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return NewPresenter(f.encoder, s)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
package format
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Presenter struct {
|
|
||||||
sbom sbom.SBOM
|
|
||||||
encoder Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPresenter(encoder Encoder, s sbom.SBOM) *Presenter {
|
|
||||||
return &Presenter{
|
|
||||||
sbom: s,
|
|
||||||
encoder: encoder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
|
||||||
return pres.encoder(output, pres.sbom)
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@ Here is what the main execution path for syft does:
|
|||||||
|
|
||||||
1. Parse a user image string to get a stereoscope image.Source object
|
1. Parse a user image string to get a stereoscope image.Source object
|
||||||
2. Invoke all catalogers to catalog the image, adding discovered packages to a single catalog object
|
2. Invoke all catalogers to catalog the image, adding discovered packages to a single catalog object
|
||||||
3. Invoke a single presenter to show the contents of the catalog
|
3. Invoke one or more encoders to output contents of the catalog
|
||||||
|
|
||||||
A Source object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer),
|
A Source object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer),
|
||||||
providing a way to inspect paths and file content within the image. The Source object, not the image object, is used
|
providing a way to inspect paths and file content within the image. The Source object, not the image object, is used
|
||||||
|
|||||||
13
syft/sbom/writer.go
Normal file
13
syft/sbom/writer.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package sbom
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Writer an interface to write SBOMs
|
||||||
|
type Writer interface {
|
||||||
|
// Write writes the provided SBOM
|
||||||
|
Write(SBOM) error
|
||||||
|
|
||||||
|
// Closer a resource cleanup hook which will be called after SBOM
|
||||||
|
// is written or if an error occurs before Write is called
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
func TestPackagesCmdFlags(t *testing.T) {
|
func TestPackagesCmdFlags(t *testing.T) {
|
||||||
coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
|
coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
|
||||||
//badBinariesImage := "docker-archive:" + getFixtureImage(t, "image-bad-binaries")
|
//badBinariesImage := "docker-archive:" + getFixtureImage(t, "image-bad-binaries")
|
||||||
|
tmp := t.TempDir() + "/"
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -32,6 +33,15 @@ func TestPackagesCmdFlags(t *testing.T) {
|
|||||||
assertSuccessfulReturnCode,
|
assertSuccessfulReturnCode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "multiple-output-flags",
|
||||||
|
args: []string{"packages", "-o", "table", "-o", "json=" + tmp + ".tmp/multiple-output-flag-test.json", coverageImage},
|
||||||
|
assertions: []traitAssertion{
|
||||||
|
assertTableReport,
|
||||||
|
assertFileExists(tmp + ".tmp/multiple-output-flag-test.json"),
|
||||||
|
assertSuccessfulReturnCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
// I haven't been able to reproduce locally yet, but in CI this has proven to be unstable:
|
// I haven't been able to reproduce locally yet, but in CI this has proven to be unstable:
|
||||||
// For the same commit:
|
// For the same commit:
|
||||||
// pass: https://github.com/anchore/syft/runs/4611344142?check_suite_focus=true
|
// pass: https://github.com/anchore/syft/runs/4611344142?check_suite_focus=true
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -114,3 +115,12 @@ func assertSuccessfulReturnCode(tb testing.TB, _, _ string, rc int) {
|
|||||||
tb.Errorf("expected no failure but got rc=%d", rc)
|
tb.Errorf("expected no failure but got rc=%d", rc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertFileExists(file string) traitAssertion {
|
||||||
|
return func(tb testing.TB, _, _ string, _ int) {
|
||||||
|
tb.Helper()
|
||||||
|
if _, err := os.Stat(file); err != nil {
|
||||||
|
tb.Errorf("expected file to exist %s", file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func TestPackageOwnershipRelationships(t *testing.T) {
|
func TestPackageOwnershipRelationships(t *testing.T) {
|
||||||
|
|
||||||
// ensure that the json presenter is applying artifact ownership with an image that has expected ownership relationships
|
// ensure that the json encoder is applying artifact ownership with an image that has expected ownership relationships
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
fixture string
|
fixture string
|
||||||
}{
|
}{
|
||||||
@ -24,13 +24,8 @@ func TestPackageOwnershipRelationships(t *testing.T) {
|
|||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
sbom, _ := catalogFixtureImage(t, test.fixture)
|
sbom, _ := catalogFixtureImage(t, test.fixture)
|
||||||
|
|
||||||
p := syftjson.Format().Presenter(sbom)
|
|
||||||
if p == nil {
|
|
||||||
t.Fatal("unable to get presenter")
|
|
||||||
}
|
|
||||||
|
|
||||||
output := bytes.NewBufferString("")
|
output := bytes.NewBufferString("")
|
||||||
err := p.Present(output)
|
err := syftjson.Format().Encode(output, sbom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to present: %+v", err)
|
t.Fatalf("unable to present: %+v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user