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 }