github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/cmd/syft/cli/options/writer.go (about)

     1  package options
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/mitchellh/go-homedir"
    13  
    14  	"github.com/anchore/syft/internal/bus"
    15  	"github.com/anchore/syft/internal/log"
    16  	"github.com/anchore/syft/syft/formats"
    17  	"github.com/anchore/syft/syft/formats/table"
    18  	"github.com/anchore/syft/syft/formats/template"
    19  	"github.com/anchore/syft/syft/sbom"
    20  )
    21  
    22  var _ sbom.Writer = (*sbomMultiWriter)(nil)
    23  
    24  var _ interface {
    25  	io.Closer
    26  	sbom.Writer
    27  } = (*sbomStreamWriter)(nil)
    28  
    29  // makeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
    30  // or an error but neither both and if there is no error, sbom.Writer.Close() should be called
    31  func makeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
    32  	outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  
    37  	writer, err := newSBOMMultiWriter(outputOptions...)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	return writer, nil
    43  }
    44  
    45  // makeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
    46  func makeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
    47  	writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path))
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	return writer, nil
    53  }
    54  
    55  // parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
    56  func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string) (out []sbomWriterDescription, errs error) {
    57  	// always should have one option -- we generally get the default of "table", but just make sure
    58  	if len(outputs) == 0 {
    59  		outputs = append(outputs, table.ID.String())
    60  	}
    61  
    62  	for _, name := range outputs {
    63  		name = strings.TrimSpace(name)
    64  
    65  		// split to at most two parts for <format>=<file>
    66  		parts := strings.SplitN(name, "=", 2)
    67  
    68  		// the format name is the first part
    69  		name = parts[0]
    70  
    71  		// default to the --file or empty string if not specified
    72  		file := defaultFile
    73  
    74  		// If a file is specified as part of the output formatName, use that
    75  		if len(parts) > 1 {
    76  			file = parts[1]
    77  		}
    78  
    79  		format := formats.ByName(name)
    80  		if format == nil {
    81  			errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formats.AllIDs()))
    82  			continue
    83  		}
    84  
    85  		if tmpl, ok := format.(template.OutputFormat); ok {
    86  			tmpl.SetTemplatePath(templateFilePath)
    87  			format = tmpl
    88  		}
    89  
    90  		out = append(out, newSBOMWriterDescription(format, file))
    91  	}
    92  	return out, errs
    93  }
    94  
    95  // sbomWriterDescription Format and path strings used to create sbom.Writer
    96  type sbomWriterDescription struct {
    97  	Format sbom.Format
    98  	Path   string
    99  }
   100  
   101  func newSBOMWriterDescription(f sbom.Format, p string) sbomWriterDescription {
   102  	expandedPath, err := homedir.Expand(p)
   103  	if err != nil {
   104  		log.Warnf("could not expand given writer output path=%q: %w", p, err)
   105  		// ignore errors
   106  		expandedPath = p
   107  	}
   108  	return sbomWriterDescription{
   109  		Format: f,
   110  		Path:   expandedPath,
   111  	}
   112  }
   113  
   114  // sbomMultiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
   115  type sbomMultiWriter struct {
   116  	writers []sbom.Writer
   117  }
   118  
   119  // newSBOMMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used
   120  func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) {
   121  	if len(options) == 0 {
   122  		return nil, fmt.Errorf("no output options provided")
   123  	}
   124  
   125  	out := &sbomMultiWriter{}
   126  
   127  	for _, option := range options {
   128  		switch len(option.Path) {
   129  		case 0:
   130  			out.writers = append(out.writers, &sbomPublisher{
   131  				format: option.Format,
   132  			})
   133  		default:
   134  			// create any missing subdirectories
   135  			dir := path.Dir(option.Path)
   136  			if dir != "" {
   137  				s, err := os.Stat(dir)
   138  				if err != nil {
   139  					err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
   140  					if err != nil {
   141  						return nil, err
   142  					}
   143  				} else if !s.IsDir() {
   144  					return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
   145  				}
   146  			}
   147  			fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
   148  			if err != nil {
   149  				return nil, fmt.Errorf("unable to create report file: %w", err)
   150  			}
   151  			out.writers = append(out.writers, &sbomStreamWriter{
   152  				format: option.Format,
   153  				out:    fileOut,
   154  			})
   155  		}
   156  	}
   157  
   158  	return out, nil
   159  }
   160  
   161  // Write writes the SBOM to all writers
   162  func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
   163  	for _, w := range m.writers {
   164  		err := w.Write(s)
   165  		if err != nil {
   166  			errs = multierror.Append(errs, fmt.Errorf("unable to write SBOM: %w", err))
   167  		}
   168  	}
   169  	return errs
   170  }
   171  
   172  // sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
   173  type sbomStreamWriter struct {
   174  	format sbom.Format
   175  	out    io.Writer
   176  }
   177  
   178  // Write the provided SBOM to the data stream
   179  func (w *sbomStreamWriter) Write(s sbom.SBOM) error {
   180  	defer w.Close()
   181  	return w.format.Encode(w.out, s)
   182  }
   183  
   184  // Close any resources, such as open files
   185  func (w *sbomStreamWriter) Close() error {
   186  	if closer, ok := w.out.(io.Closer); ok {
   187  		return closer.Close()
   188  	}
   189  	return nil
   190  }
   191  
   192  // sbomPublisher implements sbom.Writer that publishes results to the event bus
   193  type sbomPublisher struct {
   194  	format sbom.Format
   195  }
   196  
   197  // Write the provided SBOM to the data stream
   198  func (w *sbomPublisher) Write(s sbom.SBOM) error {
   199  	buf := &bytes.Buffer{}
   200  	if err := w.format.Encode(buf, s); err != nil {
   201  		return fmt.Errorf("unable to encode SBOM: %w", err)
   202  	}
   203  
   204  	bus.Report(buf.String())
   205  	return nil
   206  }