From 64159da32ca30a1f24e386d0d8099d31e12caf3c Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 14 Feb 2023 10:03:47 -0500 Subject: [PATCH] allow convert to take stdin (#1570) Signed-off-by: Alex Goodman --- cmd/syft/cli/convert.go | 5 ++- cmd/syft/cli/convert/convert.go | 23 +++++++---- test/cli/all_formats_convertible_test.go | 9 +---- test/cli/convert_cmd_test.go | 50 ++++++++++++++++++++++++ test/cli/utils_test.go | 44 ++++++++++++++++++++- 5 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 test/cli/convert_cmd_test.go diff --git a/cmd/syft/cli/convert.go b/cmd/syft/cli/convert.go index a2bf4c73749..a93cb304ae6 100644 --- a/cmd/syft/cli/convert.go +++ b/cmd/syft/cli/convert.go @@ -14,8 +14,9 @@ import ( ) const ( - convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout in table format, by default - {{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output goes to a file named img.cdx.json + convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout + {{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json"" + {{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json ` ) diff --git a/cmd/syft/cli/convert/convert.go b/cmd/syft/cli/convert/convert.go index 20817033772..50e9d4d800b 100644 --- a/cmd/syft/cli/convert/convert.go +++ b/cmd/syft/cli/convert/convert.go @@ -3,6 +3,7 @@ package convert import ( "context" "fmt" + "io" "os" "github.com/anchore/syft/cmd/syft/cli/options" @@ -26,15 +27,23 @@ func Run(_ context.Context, app *config.Application, args []string) error { // this can only be a SBOM file userInput := args[0] - f, err := os.Open(userInput) - if err != nil { - return fmt.Errorf("failed to open SBOM file: %w", err) + + var reader io.ReadCloser + + if userInput == "-" { + reader = os.Stdin + } else { + f, err := os.Open(userInput) + if err != nil { + return fmt.Errorf("failed to open SBOM file: %w", err) + } + defer func() { + _ = f.Close() + }() + reader = f } - defer func() { - _ = f.Close() - }() - sbom, _, err := formats.Decode(f) + sbom, _, err := formats.Decode(reader) if err != nil { return fmt.Errorf("failed to decode SBOM: %w", err) } diff --git a/test/cli/all_formats_convertible_test.go b/test/cli/all_formats_convertible_test.go index fe673ae3071..63709d12c76 100644 --- a/test/cli/all_formats_convertible_test.go +++ b/test/cli/all_formats_convertible_test.go @@ -10,14 +10,9 @@ import ( "github.com/stretchr/testify/require" ) -func TestConvertCmdFlags(t *testing.T) { +func TestAllFormatsConvertable(t *testing.T) { assertions := []traitAssertion{ - func(tb testing.TB, stdout, _ string, _ int) { - tb.Helper() - if len(stdout) < 1000 { - tb.Errorf("there may not be any report output (len=%d)", len(stdout)) - } - }, + assertStdoutLengthGreaterThan(1000), assertSuccessfulReturnCode, } diff --git a/test/cli/convert_cmd_test.go b/test/cli/convert_cmd_test.go new file mode 100644 index 00000000000..9a6932caea0 --- /dev/null +++ b/test/cli/convert_cmd_test.go @@ -0,0 +1,50 @@ +package cli + +import ( + "fmt" + "strings" + "testing" +) + +func TestConvertCmd(t *testing.T) { + assertions := []traitAssertion{ + assertInOutput("PackageName: musl-utils"), + assertSuccessfulReturnCode, + } + + tests := []struct { + from string + to string + }{ + {from: "syft-json", to: "spdx-tag-value"}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("from %s to %s", test.from, test.to), func(t *testing.T) { + sbomArgs := []string{"dir:./test-fixtures/image-pkg-coverage", "-o", test.from} + cmd, stdout, stderr := runSyft(t, nil, sbomArgs...) + if cmd.ProcessState.ExitCode() != 0 { + t.Log("STDOUT:\n", stdout) + t.Log("STDERR:\n", stderr) + t.Log("COMMAND:", strings.Join(cmd.Args, " ")) + t.Fatalf("failure executing syft creating an sbom") + return + } + + convertArgs := []string{"convert", "-", "-o", test.to} + cmd = getSyftCommand(t, convertArgs...) + + cmd.Stdin = strings.NewReader(stdout) + stdout, stderr = runCommandObj(t, cmd, nil, false) + + for _, traitFn := range assertions { + traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) + } + if t.Failed() { + t.Log("STDOUT:\n", stdout) + t.Log("STDERR:\n", stderr) + t.Log("COMMAND:", strings.Join(cmd.Args, " ")) + } + }) + } +} diff --git a/test/cli/utils_test.go b/test/cli/utils_test.go index 0ee1ac121be..faf8533cf51 100644 --- a/test/cli/utils_test.go +++ b/test/cli/utils_test.go @@ -177,7 +177,7 @@ func runSyftCommand(t testing.TB, env map[string]string, expectError bool, args t.Errorf("STDOUT: %s", stdout) t.Errorf("STDERR: %s", stderr) - // this probably indicates a timeout + // this probably indicates a timeout... lets run it again with more verbosity to help debug issues args = append(args, "-vv") cmd = getSyftCommand(t, args...) @@ -194,6 +194,48 @@ func runSyftCommand(t testing.TB, env map[string]string, expectError bool, args return cmd, stdout, stderr } +func runCommandObj(t testing.TB, cmd *exec.Cmd, env map[string]string, expectError bool) (string, string) { + cancel := make(chan bool, 1) + defer func() { + cancel <- true + }() + + if env == nil { + env = make(map[string]string) + } + + // we should not have tests reaching out for app update checks + env["SYFT_CHECK_FOR_APP_UPDATE"] = "false" + + timeout := func() { + select { + case <-cancel: + return + case <-time.After(60 * time.Second): + } + + if cmd != nil && cmd.Process != nil { + // get a stack trace printed + err := cmd.Process.Signal(syscall.SIGABRT) + if err != nil { + t.Errorf("error aborting: %+v", err) + } + } + } + + go timeout() + + stdout, stderr, err := runCommand(cmd, env) + + if !expectError && err != nil && stdout == "" { + t.Errorf("error running syft: %+v", err) + t.Errorf("STDOUT: %s", stdout) + t.Errorf("STDERR: %s", stderr) + } + + return stdout, stderr +} + func runCosign(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { cmd := getCommand(t, ".tmp/cosign", args...) if env == nil {