mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Simplify the SBOM writer interface (#1892)
* remove sbom.writer bytes call and consolidate helpers to options pkg Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * dont close stdout Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove close operation from multiwriter Signed-off-by: Alex Goodman <alex.goodman@anchore.com> --------- Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
7de7a7990a
commit
25ce245c03
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/anchore/syft/cmd/syft/cli/packages"
|
"github.com/anchore/syft/cmd/syft/cli/packages"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
@ -33,17 +34,6 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to write to report destination: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := writer.Close(); err != nil {
|
|
||||||
fmt.Printf("unable to close report destination: %+v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// could be an image or a directory, with or without a scheme
|
// could be an image or a directory, with or without a scheme
|
||||||
// TODO: validate that source is image
|
// TODO: validate that source is image
|
||||||
userInput := args[0]
|
userInput := args[0]
|
||||||
@ -62,7 +52,7 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
|||||||
subscription := eventBus.Subscribe()
|
subscription := eventBus.Subscribe()
|
||||||
|
|
||||||
return eventloop.EventLoop(
|
return eventloop.EventLoop(
|
||||||
execWorker(app, *si, writer),
|
execWorker(app, *si),
|
||||||
eventloop.SetupSignals(),
|
eventloop.SetupSignals(),
|
||||||
subscription,
|
subscription,
|
||||||
stereoscope.Cleanup,
|
stereoscope.Cleanup,
|
||||||
@ -70,7 +60,7 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, errs chan error) ([]byte, error) {
|
func buildSBOM(app *config.Application, si source.Input, errs chan error) (*sbom.SBOM, error) {
|
||||||
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
|
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
|
||||||
if cleanup != nil {
|
if cleanup != nil {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@ -88,103 +78,120 @@ func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, err
|
|||||||
return nil, fmt.Errorf("no SBOM produced for %q", si.UserInput)
|
return nil, fmt.Errorf("no SBOM produced for %q", si.UserInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: only works for single format no multi writer support
|
return s, nil
|
||||||
sBytes, err := writer.Bytes(*s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to build SBOM bytes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sBytes, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:funlen
|
//nolint:funlen
|
||||||
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
|
func execWorker(app *config.Application, si source.Input) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
sBytes, err := buildSBOM(app, si, writer, errs)
|
defer bus.Publish(partybus.Event{Type: event.Exit})
|
||||||
|
|
||||||
|
s, err := buildSBOM(app, si, errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- fmt.Errorf("unable to build SBOM: %w", err)
|
errs <- fmt.Errorf("unable to build SBOM: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add multi writer support
|
// note: ValidateOutputOptions ensures that there is no more than one output type
|
||||||
for _, o := range app.Outputs {
|
o := app.Outputs[0]
|
||||||
f, err := os.CreateTemp("", o)
|
|
||||||
if err != nil {
|
|
||||||
errs <- fmt.Errorf("unable to create temp file: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
f, err := os.CreateTemp("", o)
|
||||||
defer os.Remove(f.Name())
|
if err != nil {
|
||||||
|
errs <- fmt.Errorf("unable to create temp file: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
if _, err := f.Write(sBytes); err != nil {
|
writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath)
|
||||||
errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err)
|
if err != nil {
|
||||||
return
|
errs <- fmt.Errorf("unable to create SBOM writer: %w", err)
|
||||||
}
|
return
|
||||||
|
|
||||||
// TODO: what other validation here besides binary name?
|
|
||||||
cmd := "cosign"
|
|
||||||
if !commandExists(cmd) {
|
|
||||||
errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select Cosign predicate type based on defined output type
|
|
||||||
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
|
||||||
var predicateType string
|
|
||||||
switch strings.ToLower(o) {
|
|
||||||
case "cyclonedx-json":
|
|
||||||
predicateType = "cyclonedx"
|
|
||||||
case "spdx-tag-value":
|
|
||||||
predicateType = "spdx"
|
|
||||||
case "spdx-json":
|
|
||||||
predicateType = "spdxjson"
|
|
||||||
default:
|
|
||||||
predicateType = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"attest", si.UserInput, "--predicate", f.Name(), "--type", predicateType}
|
|
||||||
if app.Attest.Key != "" {
|
|
||||||
args = append(args, "--key", app.Attest.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
execCmd := exec.Command(cmd, args...)
|
|
||||||
execCmd.Env = os.Environ()
|
|
||||||
if app.Attest.Key != "" {
|
|
||||||
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password))
|
|
||||||
} else {
|
|
||||||
// no key provided, use cosign's keyless mode
|
|
||||||
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// bus adapter for ui to hook into stdout via an os pipe
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
errs <- fmt.Errorf("unable to create os pipe: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
b := &busWriter{r: r, w: w, mon: progress.NewManual(-1)}
|
|
||||||
execCmd.Stdout = b
|
|
||||||
execCmd.Stderr = b
|
|
||||||
defer b.mon.SetCompleted()
|
|
||||||
|
|
||||||
// attest the SBOM
|
|
||||||
err = execCmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
b.mon.SetError(err)
|
|
||||||
errs <- fmt.Errorf("unable to attest SBOM: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
if err := writer.Write(*s); err != nil {
|
||||||
Type: event.Exit,
|
errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err)
|
||||||
Value: func() error { return nil },
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// TODO: what other validation here besides binary name?
|
||||||
|
cmd := "cosign"
|
||||||
|
if !commandExists(cmd) {
|
||||||
|
errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select Cosign predicate type based on defined output type
|
||||||
|
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
||||||
|
var predicateType string
|
||||||
|
switch strings.ToLower(o) {
|
||||||
|
case "cyclonedx-json":
|
||||||
|
predicateType = "cyclonedx"
|
||||||
|
case "spdx-tag-value", "spdx-tv":
|
||||||
|
predicateType = "spdx"
|
||||||
|
case "spdx-json", "json":
|
||||||
|
predicateType = "spdxjson"
|
||||||
|
default:
|
||||||
|
predicateType = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"attest", si.UserInput, "--predicate", f.Name(), "--type", predicateType}
|
||||||
|
if app.Attest.Key != "" {
|
||||||
|
args = append(args, "--key", app.Attest.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
execCmd := exec.Command(cmd, args...)
|
||||||
|
execCmd.Env = os.Environ()
|
||||||
|
if app.Attest.Key != "" {
|
||||||
|
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password))
|
||||||
|
} else {
|
||||||
|
// no key provided, use cosign's keyless mode
|
||||||
|
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation")
|
||||||
|
|
||||||
|
// bus adapter for ui to hook into stdout via an os pipe
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
errs <- fmt.Errorf("unable to create os pipe: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
mon := progress.NewManual(-1)
|
||||||
|
|
||||||
|
bus.Publish(
|
||||||
|
partybus.Event{
|
||||||
|
Type: event.AttestationStarted,
|
||||||
|
Source: monitor.GenericTask{
|
||||||
|
Title: monitor.Title{
|
||||||
|
Default: "Create attestation",
|
||||||
|
WhileRunning: "Creating attestation",
|
||||||
|
OnSuccess: "Created attestation",
|
||||||
|
},
|
||||||
|
Context: "cosign",
|
||||||
|
},
|
||||||
|
Value: &monitor.ShellProgress{
|
||||||
|
Reader: r,
|
||||||
|
Manual: mon,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
execCmd.Stdout = w
|
||||||
|
execCmd.Stderr = w
|
||||||
|
|
||||||
|
// attest the SBOM
|
||||||
|
err = execCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
mon.SetError(err)
|
||||||
|
errs <- fmt.Errorf("unable to attest SBOM: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mon.SetCompleted()
|
||||||
}()
|
}()
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
@ -207,37 +214,6 @@ func ValidateOutputOptions(app *config.Application) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type busWriter struct {
|
|
||||||
w *os.File
|
|
||||||
r *os.File
|
|
||||||
hasWritten bool
|
|
||||||
mon *progress.Manual
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *busWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if !b.hasWritten {
|
|
||||||
b.hasWritten = true
|
|
||||||
bus.Publish(
|
|
||||||
partybus.Event{
|
|
||||||
Type: event.AttestationStarted,
|
|
||||||
Source: monitor.GenericTask{
|
|
||||||
Title: monitor.Title{
|
|
||||||
Default: "Create attestation",
|
|
||||||
WhileRunning: "Creating attestation",
|
|
||||||
OnSuccess: "Created attestation",
|
|
||||||
},
|
|
||||||
Context: "cosign",
|
|
||||||
},
|
|
||||||
Value: &monitor.ShellProgress{
|
|
||||||
Reader: b.r,
|
|
||||||
Manual: b.mon,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return b.w.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
func commandExists(cmd string) bool {
|
||||||
_, err := exec.LookPath(cmd)
|
_, err := exec.LookPath(cmd)
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|||||||
@ -14,17 +14,11 @@ import (
|
|||||||
|
|
||||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
||||||
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// this can only be a SBOM file
|
// this can only be a SBOM file
|
||||||
userInput := args[0]
|
userInput := args[0]
|
||||||
|
|
||||||
|
|||||||
@ -2,25 +2,37 @@ package options
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/formats"
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
"github.com/anchore/syft/syft/formats/table"
|
||||||
"github.com/anchore/syft/syft/formats/template"
|
"github.com/anchore/syft/syft/formats/template"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
var _ sbom.Writer = (*sbomMultiWriter)(nil)
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
io.Closer
|
||||||
|
sbom.Writer
|
||||||
|
} = (*sbomStreamWriter)(nil)
|
||||||
|
|
||||||
|
// MakeSBOMWriter 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
|
// 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) {
|
func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
|
||||||
outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath)
|
outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
writer, err := sbom.NewWriter(outputOptions...)
|
writer, err := newSBOMMultiWriter(outputOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -28,8 +40,18 @@ func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Wr
|
|||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
|
// MakeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
|
||||||
func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) {
|
func MakeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
|
||||||
|
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||||
|
func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string) (out []sbomWriterDescription, errs error) {
|
||||||
// always should have one option -- we generally get the default of "table", but just make sure
|
// always should have one option -- we generally get the default of "table", but just make sure
|
||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
outputs = append(outputs, table.ID.String())
|
outputs = append(outputs, table.ID.String())
|
||||||
@ -63,7 +85,113 @@ func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out [
|
|||||||
format = tmpl
|
format = tmpl
|
||||||
}
|
}
|
||||||
|
|
||||||
out = append(out, sbom.NewWriterOption(format, file))
|
out = append(out, newSBOMWriterDescription(format, file))
|
||||||
}
|
}
|
||||||
return out, errs
|
return out, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sbomWriterDescription Format and path strings used to create sbom.Writer
|
||||||
|
type sbomWriterDescription struct {
|
||||||
|
Format sbom.Format
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSBOMWriterDescription(f sbom.Format, p string) sbomWriterDescription {
|
||||||
|
expandedPath, err := homedir.Expand(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
||||||
|
// ignore errors
|
||||||
|
expandedPath = p
|
||||||
|
}
|
||||||
|
return sbomWriterDescription{
|
||||||
|
Format: f,
|
||||||
|
Path: expandedPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sbomMultiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
|
||||||
|
type sbomMultiWriter struct {
|
||||||
|
writers []sbom.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
type nopWriteCloser struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n nopWriteCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSBOMMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used
|
||||||
|
func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) {
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, fmt.Errorf("no output options provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &sbomMultiWriter{}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
switch len(option.Path) {
|
||||||
|
case 0:
|
||||||
|
out.writers = append(out.writers, &sbomStreamWriter{
|
||||||
|
format: option.Format,
|
||||||
|
out: nopWriteCloser{Writer: 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, &sbomStreamWriter{
|
||||||
|
format: option.Format,
|
||||||
|
out: fileOut,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the SBOM to all writers
|
||||||
|
func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
|
||||||
|
for _, w := range m.writers {
|
||||||
|
err := w.Write(s)
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("unable to write SBOM: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||||
|
type sbomStreamWriter struct {
|
||||||
|
format sbom.Format
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the provided SBOM to the data stream
|
||||||
|
func (w *sbomStreamWriter) Write(s sbom.SBOM) error {
|
||||||
|
defer w.Close()
|
||||||
|
return w.format.Encode(w.out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any resources, such as open files
|
||||||
|
func (w *sbomStreamWriter) Close() error {
|
||||||
|
if closer, ok := w.out.(io.Closer); ok {
|
||||||
|
return closer.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/homedir"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsSupportedFormat(t *testing.T) {
|
func Test_MakeSBOMWriter(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
outputs []string
|
outputs []string
|
||||||
wantErr assert.ErrorAssertionFunc
|
wantErr assert.ErrorAssertionFunc
|
||||||
@ -28,7 +34,198 @@ func TestIsSupportedFormat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
_, err := MakeWriter(tt.outputs, "", "")
|
_, err := MakeSBOMWriter(tt.outputs, "", "")
|
||||||
tt.wantErr(t, err)
|
tt.wantErr(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dummyEncoder(io.Writer, sbom.SBOM) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dummyFormat(name string) sbom.Format {
|
||||||
|
return sbom.NewFormat(sbom.AnyVersion, dummyEncoder, nil, nil, sbom.FormatID(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newSBOMMultiWriter(t *testing.T) {
|
||||||
|
type writerConfig struct {
|
||||||
|
format string
|
||||||
|
file string
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
testName := func(options []sbomWriterDescription, err bool) string {
|
||||||
|
var out []string
|
||||||
|
for _, opt := range options {
|
||||||
|
out = append(out, string(opt.Format.ID())+"="+opt.Path)
|
||||||
|
}
|
||||||
|
errs := ""
|
||||||
|
if err {
|
||||||
|
errs = "(err)"
|
||||||
|
}
|
||||||
|
return strings.Join(out, ", ") + errs
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
outputs []sbomWriterDescription
|
||||||
|
err bool
|
||||||
|
expected []writerConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
outputs: []sbomWriterDescription{},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []sbomWriterDescription{
|
||||||
|
{
|
||||||
|
Format: dummyFormat("table"),
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []sbomWriterDescription{
|
||||||
|
{
|
||||||
|
Format: dummyFormat("json"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []sbomWriterDescription{
|
||||||
|
{
|
||||||
|
Format: dummyFormat("json"),
|
||||||
|
Path: "test-2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
file: "test-2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []sbomWriterDescription{
|
||||||
|
{
|
||||||
|
Format: dummyFormat("json"),
|
||||||
|
Path: "test-3/1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: dummyFormat("spdx-json"),
|
||||||
|
Path: "test-3/2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
file: "test-3/1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "spdx-json",
|
||||||
|
file: "test-3/2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []sbomWriterDescription{
|
||||||
|
{
|
||||||
|
Format: dummyFormat("text"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: dummyFormat("spdx-json"),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mw, err := newSBOMMultiWriter(outputs...)
|
||||||
|
|
||||||
|
if test.err {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, mw.writers, len(test.expected))
|
||||||
|
|
||||||
|
for i, e := range test.expected {
|
||||||
|
switch w := mw.writers[i].(type) {
|
||||||
|
case *sbomStreamWriter:
|
||||||
|
assert.Equal(t, string(w.format.ID()), e.format)
|
||||||
|
if e.file != "" {
|
||||||
|
assert.NotNil(t, w.out)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, w.out)
|
||||||
|
}
|
||||||
|
if e.file != "" {
|
||||||
|
assert.FileExists(t, tmp+e.file)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown writer type: %T", w)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newSBOMWriterDescription(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "expand home dir",
|
||||||
|
path: "~/place.txt",
|
||||||
|
expected: filepath.Join(homedir.Get(), "place.txt"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "passthrough other paths",
|
||||||
|
path: "/other/place.txt",
|
||||||
|
expected: "/other/place.txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no path",
|
||||||
|
path: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
o := newSBOMWriterDescription(dummyFormat("table"), tt.path)
|
||||||
|
assert.Equal(t, tt.expected, o.Path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"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"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
@ -29,17 +28,11 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||||
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
|
// could be an image or a directory, with or without a scheme
|
||||||
userInput := args[0]
|
userInput := args[0]
|
||||||
si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource)
|
si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource)
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"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"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
@ -28,19 +27,11 @@ import (
|
|||||||
|
|
||||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||||
f := syftjson.Format()
|
f := syftjson.Format()
|
||||||
writer, err := sbom.NewWriter(sbom.WriterOption{
|
writer, err := options.MakeSBOMWriterForFormat(f, app.File)
|
||||||
Format: f,
|
|
||||||
Path: app.File,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := writer.Close(); err != nil {
|
|
||||||
log.Warnf("unable to write to report destination: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// inform user at end of run that command will be removed
|
// inform user at end of run that command will be removed
|
||||||
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
|
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
|
||||||
fmt.Fprintln(os.Stderr, deprecated)
|
fmt.Fprintln(os.Stderr, deprecated)
|
||||||
|
|||||||
@ -56,13 +56,13 @@ func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Collection,
|
|||||||
// otherwise conditionally use the correct set of loggers based on the input type (container image or directory)
|
// otherwise conditionally use the correct set of loggers based on the input type (container image or directory)
|
||||||
switch src.Metadata.Scheme {
|
switch src.Metadata.Scheme {
|
||||||
case source.ImageScheme:
|
case source.ImageScheme:
|
||||||
log.Info("cataloging image")
|
log.Info("cataloging an image")
|
||||||
catalogers = cataloger.ImageCatalogers(cfg)
|
catalogers = cataloger.ImageCatalogers(cfg)
|
||||||
case source.FileScheme:
|
case source.FileScheme:
|
||||||
log.Info("cataloging file")
|
log.Info("cataloging a file")
|
||||||
catalogers = cataloger.AllCatalogers(cfg)
|
catalogers = cataloger.AllCatalogers(cfg)
|
||||||
case source.DirectoryScheme:
|
case source.DirectoryScheme:
|
||||||
log.Info("cataloging directory")
|
log.Info("cataloging a directory")
|
||||||
catalogers = cataloger.DirectoryCatalogers(cfg)
|
catalogers = cataloger.DirectoryCatalogers(cfg)
|
||||||
default:
|
default:
|
||||||
return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
|
return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
|
||||||
|
|||||||
@ -1,123 +0,0 @@
|
|||||||
package sbom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
|
|
||||||
type multiWriter struct {
|
|
||||||
writers []Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriterOption Format and path strings used to create sbom.Writer
|
|
||||||
type WriterOption struct {
|
|
||||||
Format Format
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriterOption(f Format, p string) WriterOption {
|
|
||||||
expandedPath, err := homedir.Expand(p)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
|
||||||
// ignore errors
|
|
||||||
expandedPath = p
|
|
||||||
}
|
|
||||||
return WriterOption{
|
|
||||||
Format: f,
|
|
||||||
Path: expandedPath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWriter create all report writers from input options; if a file is not specified, os.Stdout is used
|
|
||||||
func NewWriter(options ...WriterOption) (_ Writer, err error) {
|
|
||||||
if len(options) == 0 {
|
|
||||||
return nil, fmt.Errorf("no output options provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
out := &multiWriter{}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
// close any previously opened files; we can't really recover from any errors
|
|
||||||
if err := out.Close(); err != nil {
|
|
||||||
log.Warnf("unable to close sbom writers: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes the SBOM to all writers
|
|
||||||
func (m *multiWriter) Write(s SBOM) (errs error) {
|
|
||||||
for _, w := range m.writers {
|
|
||||||
err := w.Write(s)
|
|
||||||
if err != nil {
|
|
||||||
errs = multierror.Append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the bytes of the SBOM that would be written
|
|
||||||
func (m *multiWriter) Bytes(s SBOM) (bytes []byte, err error) {
|
|
||||||
for _, w := range m.writers {
|
|
||||||
b, err := w.Bytes(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bytes = append(bytes, b...)
|
|
||||||
}
|
|
||||||
return bytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
package sbom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/homedir"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dummyEncoder(io.Writer, SBOM) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dummyFormat(name string) Format {
|
|
||||||
return NewFormat(AnyVersion, dummyEncoder, nil, nil, FormatID(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
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.ID())+"="+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: dummyFormat("table"),
|
|
||||||
Path: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: []writerConfig{
|
|
||||||
{
|
|
||||||
format: "table",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
outputs: []WriterOption{
|
|
||||||
{
|
|
||||||
Format: dummyFormat("json"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: []writerConfig{
|
|
||||||
{
|
|
||||||
format: "json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
outputs: []WriterOption{
|
|
||||||
{
|
|
||||||
Format: dummyFormat("json"),
|
|
||||||
Path: "test-2.json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: []writerConfig{
|
|
||||||
{
|
|
||||||
format: "json",
|
|
||||||
file: "test-2.json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
outputs: []WriterOption{
|
|
||||||
{
|
|
||||||
Format: dummyFormat("json"),
|
|
||||||
Path: "test-3/1.json",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Format: dummyFormat("spdx-json"),
|
|
||||||
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: dummyFormat("text"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Format: dummyFormat("spdx-json"),
|
|
||||||
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 := NewWriter(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.ID()), 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewWriterOption(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "expand home dir",
|
|
||||||
path: "~/place.txt",
|
|
||||||
expected: filepath.Join(homedir.Get(), "place.txt"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "passthrough other paths",
|
|
||||||
path: "/other/place.txt",
|
|
||||||
expected: "/other/place.txt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no path",
|
|
||||||
path: "",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
o := NewWriterOption(dummyFormat("table"), tt.path)
|
|
||||||
assert.Equal(t, tt.expected, o.Path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package sbom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
|
||||||
type streamWriter struct {
|
|
||||||
format Format
|
|
||||||
out io.Writer
|
|
||||||
close func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the provided SBOM to the data stream
|
|
||||||
func (w *streamWriter) Write(s SBOM) error {
|
|
||||||
return w.format.Encode(w.out, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the bytes of the SBOM that would be written
|
|
||||||
func (w *streamWriter) Bytes(s SBOM) ([]byte, error) {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
err := w.format.Encode(&buffer, s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close any resources, such as open files
|
|
||||||
func (w *streamWriter) Close() error {
|
|
||||||
if w.close != nil {
|
|
||||||
return w.close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,16 +1,6 @@
|
|||||||
package sbom
|
package sbom
|
||||||
|
|
||||||
import "io"
|
// Writer an interface to write SBOMs to a destination
|
||||||
|
|
||||||
// Writer an interface to write SBOMs
|
|
||||||
type Writer interface {
|
type Writer interface {
|
||||||
// Write writes the provided SBOM
|
|
||||||
Write(SBOM) error
|
Write(SBOM) error
|
||||||
|
|
||||||
// Bytes returns the bytes of the SBOM that would be written
|
|
||||||
Bytes(SBOM) ([]byte, 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user