github.com/CycloneDX/sbom-utility@v0.16.0/cmd/schema.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 /* 3 * Licensed to the Apache Software Foundation (ASF) under one or more 4 * contributor license agreements. See the NOTICE file distributed with 5 * this work for additional information regarding copyright ownership. 6 * The ASF licenses this file to You under the Apache License, Version 2.0 7 * (the "License"); you may not use this file except in compliance with 8 * the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package cmd 20 21 import ( 22 "encoding/csv" 23 "fmt" 24 "io" 25 "sort" 26 "strings" 27 "text/tabwriter" 28 29 "github.com/CycloneDX/sbom-utility/common" 30 "github.com/CycloneDX/sbom-utility/schema" 31 "github.com/CycloneDX/sbom-utility/utils" 32 "github.com/spf13/cobra" 33 ) 34 35 const ( 36 SUBCOMMAND_SCHEMA_LIST = "list" 37 ) 38 39 const ( 40 MSG_OUTPUT_NO_SCHEMAS_FOUND = "[WARN] no schemas found in configuration (i.e., \"config.json\")" 41 ) 42 43 var VALID_SUBCOMMANDS_SCHEMA = []string{SUBCOMMAND_SCHEMA_LIST} 44 45 // Subcommand flags 46 const ( 47 FLAG_SCHEMA_OUTPUT_FORMAT_HELP = "format output using the specified type" 48 ) 49 50 const ( 51 SCHEMA_DATA_KEY_KEY_FILE = "file" // summary 52 SCHEMA_DATA_KEY_KEY_FORMAT = "format" // summary 53 SCHEMA_DATA_KEY_KEY_NAME = "name" // summary 54 SCHEMA_DATA_KEY_KEY_SOURCE = "url" // summary 55 SCHEMA_DATA_KEY_KEY_VARIANT = "variant" // summary 56 SCHEMA_DATA_KEY_KEY_VERSION = "version" // summary 57 ) 58 59 // NOTE: columns will be output in order they are listed here: 60 var SCHEMA_LIST_ROW_DATA = []ColumnFormatData{ 61 *NewColumnFormatData(SCHEMA_DATA_KEY_KEY_NAME, REPORT_DO_NOT_TRUNCATE, REPORT_SUMMARY_DATA, false), 62 *NewColumnFormatData(SCHEMA_DATA_KEY_KEY_VARIANT, REPORT_DO_NOT_TRUNCATE, REPORT_SUMMARY_DATA, false), 63 *NewColumnFormatData(SCHEMA_DATA_KEY_KEY_FORMAT, REPORT_DO_NOT_TRUNCATE, REPORT_SUMMARY_DATA, false), 64 *NewColumnFormatData(SCHEMA_DATA_KEY_KEY_VERSION, REPORT_DO_NOT_TRUNCATE, REPORT_SUMMARY_DATA, false), 65 *NewColumnFormatData(SCHEMA_DATA_KEY_KEY_FILE, REPORT_DO_NOT_TRUNCATE, REPORT_SUMMARY_DATA, false), 66 *NewColumnFormatData(SCHEMA_DATA_KEY_KEY_SOURCE, REPORT_DO_NOT_TRUNCATE, REPORT_SUMMARY_DATA, false), 67 } 68 69 // Command help formatting 70 var SCHEMA_LIST_SUPPORTED_FORMATS = MSG_SUPPORTED_OUTPUT_FORMATS_HELP + 71 strings.Join([]string{FORMAT_TEXT, FORMAT_CSV, FORMAT_MARKDOWN}, ", ") 72 73 func NewCommandSchema() *cobra.Command { 74 var command = new(cobra.Command) 75 command.Use = CMD_USAGE_SCHEMA_LIST // "schema" 76 command.Short = "View supported SBOM schemas" 77 command.Long = fmt.Sprintf("View built-in BOM schemas supported by the utility. The default command produces a list based upon `%s`.", DEFAULT_SCHEMA_CONFIG) 78 command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, 79 FLAG_SCHEMA_OUTPUT_FORMAT_HELP+SCHEMA_LIST_SUPPORTED_FORMATS) 80 command.Flags().StringP(FLAG_REPORT_WHERE, "", "", FLAG_REPORT_WHERE_HELP) 81 command.RunE = schemaCmdImpl 82 command.PreRunE = func(cmd *cobra.Command, args []string) (err error) { 83 84 // TODO: pre-validate if --where keys are valid for this command 85 86 // the command requires at least 1 valid subcommand (argument) 87 if len(args) > 1 { 88 return getLogger().Errorf("Too many arguments provided: %v", args) 89 } 90 91 // Make sure (optional) subcommand is known/valid 92 if len(args) == 1 { 93 if !preRunTestForSubcommand(VALID_SUBCOMMANDS_SCHEMA, args[0]) { 94 return getLogger().Errorf("Subcommand provided is not valid: `%v`", args[0]) 95 } 96 } 97 98 if len(args) == 0 { 99 getLogger().Tracef("No subcommands provided; defaulting to: `%s` subcommand", SUBCOMMAND_SCHEMA_LIST) 100 } 101 return 102 } 103 return command 104 } 105 106 func schemaCmdImpl(cmd *cobra.Command, args []string) (err error) { 107 getLogger().Enter() 108 defer getLogger().Exit() 109 110 // Create output writer 111 outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile 112 outputFile, writer, err := createOutputFile(outputFilename) 113 getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer) 114 115 // use function closure to assure consistent error output based upon error type 116 defer func() { 117 // always close the output file 118 if outputFile != nil { 119 err = outputFile.Close() 120 getLogger().Infof("Closed output file: `%s`", outputFilename) 121 } 122 }() 123 124 // process filters supplied on the --where command flag 125 whereFilters, err := processWhereFlag(cmd) 126 if err != nil { 127 return 128 } 129 130 err = ListSchemas(writer, utils.GlobalFlags.PersistentFlags, whereFilters) 131 return 132 } 133 134 func flattenFormatSchemas(sliceFormatSchemas []schema.FormatSchema) (flattenedFormatSchemas []schema.FormatSchemaInstance) { 135 for _, format := range sliceFormatSchemas { 136 for _, schema := range format.Schemas { 137 schema.Format = format.CanonicalName 138 flattenedFormatSchemas = append(flattenedFormatSchemas, schema) 139 } 140 } 141 return 142 } 143 144 func filterFormatSchemas(whereFilters []common.WhereFilter) (filteredFormats []schema.FormatSchemaInstance, err error) { 145 getLogger().Enter() 146 defer getLogger().Exit(err) 147 148 // Get format array 149 sliceFormats := SupportedFormatConfig.Formats 150 151 // flatten structs 152 sliceSchemas := flattenFormatSchemas(sliceFormats) 153 154 for _, schema := range sliceSchemas { 155 var match bool = true 156 157 if len(whereFilters) > 0 { 158 mapFormat, _ := utils.MarshalStructToJsonMap(schema) 159 match, _ = whereFilterMatch(mapFormat, whereFilters) 160 } 161 162 if match { 163 filteredFormats = append(filteredFormats, schema) 164 165 getLogger().Tracef("append: %s\n", 166 schema.Name) 167 } 168 } 169 return 170 } 171 172 func sortFormatSchemaInstances(filteredSchemas []schema.FormatSchemaInstance) []schema.FormatSchemaInstance { 173 // Sort by Format, Version, Variant 174 sort.Slice(filteredSchemas, func(i, j int) bool { 175 schema1 := filteredSchemas[i] 176 schema2 := filteredSchemas[j] 177 178 if schema1.Format != schema2.Format { 179 return schema1.Format < schema2.Format 180 } 181 182 if schema1.Version != schema2.Version { 183 return schema1.Version > schema2.Version 184 } 185 186 return schema1.Variant < schema2.Variant 187 }) 188 return filteredSchemas 189 } 190 191 func ListSchemas(writer io.Writer, persistentFlags utils.PersistentCommandFlags, whereFilters []common.WhereFilter) (err error) { 192 getLogger().Enter() 193 defer getLogger().Exit() 194 195 // Hash all filtered list of schemas within input file 196 getLogger().Infof("Scanning document for vulnerabilities...") 197 var filteredSchemas []schema.FormatSchemaInstance 198 filteredSchemas, err = filterFormatSchemas(whereFilters) 199 200 if err != nil { 201 return 202 } 203 204 // default output (writer) to standard out 205 format := persistentFlags.OutputFormat 206 switch format { 207 case FORMAT_DEFAULT: 208 // defaults to text if no explicit `--format` parameter 209 err = DisplaySchemasTabbedText(writer, filteredSchemas) 210 case FORMAT_TEXT: 211 err = DisplaySchemasTabbedText(writer, filteredSchemas) 212 case FORMAT_CSV: 213 err = DisplaySchemasCSV(writer, filteredSchemas) 214 case FORMAT_MARKDOWN: 215 err = DisplaySchemasMarkdown(writer, filteredSchemas) 216 default: 217 // default to text format for anything else 218 getLogger().Warningf("unsupported format: `%s`; using default format.", format) 219 err = DisplaySchemasTabbedText(writer, filteredSchemas) 220 } 221 return 222 } 223 224 // TODO: Add a --no-title flag to skip title output 225 func DisplaySchemasTabbedText(writer io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { 226 getLogger().Enter() 227 defer getLogger().Exit() 228 229 // initialize tabwriter 230 w := new(tabwriter.Writer) 231 232 // min-width, tab-width, padding, pad-char, flags 233 w.Init(writer, 2, 2, 2, ' ', 0) 234 defer w.Flush() 235 236 // Emit no schemas found warning into output 237 if len(filteredSchemas) == 0 { 238 getLogger().Warningf("No supported built-in schemas found in `%s`.\n", DEFAULT_SCHEMA_CONFIG) 239 return 240 } 241 242 // create title row and underline row from slices of optional and compulsory titles 243 titles, underlines := prepareReportTitleData(SCHEMA_LIST_ROW_DATA, false) 244 245 // Create title row and add tabs between column titles for the tabWRiter 246 fmt.Fprintf(w, "%s\n", strings.Join(titles, "\t")) 247 fmt.Fprintf(w, "%s\n", strings.Join(underlines, "\t")) 248 249 // Sort by Format, Version, Variant 250 filteredSchemas = sortFormatSchemaInstances(filteredSchemas) 251 252 // Emit row data 253 var line []string 254 for _, schemaInstance := range filteredSchemas { 255 // Supply variant name "latest" (the default name), if not otherwise declared in schema definition 256 schemaInstance.Variant = schema.FormatSchemaVariant(schemaInstance.Variant) 257 line, err = prepareReportLineData( 258 schemaInstance, 259 SCHEMA_LIST_ROW_DATA, 260 true, 261 ) 262 // Only emit line if no error 263 if err != nil { 264 return 265 } 266 fmt.Fprintf(w, "%s\n", strings.Join(line, "\t")) 267 } 268 return 269 } 270 271 // TODO: Add a --no-title flag to skip title output 272 func DisplaySchemasMarkdown(writer io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { 273 getLogger().Enter() 274 defer getLogger().Exit() 275 276 // Create title row data as []string, include all columns that are flagged "summary" data 277 titles, _ := prepareReportTitleData(SCHEMA_LIST_ROW_DATA, true) 278 titleRow := createMarkdownRow(titles) 279 fmt.Fprintf(writer, "%s\n", titleRow) 280 281 // create alignment row, include all columns that are flagged "summary" data 282 alignments := createMarkdownColumnAlignmentRow(SCHEMA_LIST_ROW_DATA, true) 283 alignmentRow := createMarkdownRow(alignments) 284 fmt.Fprintf(writer, "%s\n", alignmentRow) 285 286 // Emit no schemas found warning into output 287 if len(filteredSchemas) == 0 { 288 fmt.Fprintf(writer, "%s\n", MSG_OUTPUT_NO_SCHEMAS_FOUND) 289 return fmt.Errorf(MSG_OUTPUT_NO_SCHEMAS_FOUND) 290 } 291 292 // Sort by Format, Version, Variant 293 filteredSchemas = sortFormatSchemaInstances(filteredSchemas) 294 295 var line []string 296 var lineRow string 297 for _, schemaInstance := range filteredSchemas { 298 // Supply variant name "latest" (the default name), if not otherwise declared in schema definition 299 schemaInstance.Variant = schema.FormatSchemaVariant(schemaInstance.Variant) 300 line, err = prepareReportLineData( 301 schemaInstance, 302 SCHEMA_LIST_ROW_DATA, 303 true, 304 ) 305 // Only emit line if no error 306 if err != nil { 307 return 308 } 309 lineRow = createMarkdownRow(line) 310 fmt.Fprintf(writer, "%s\n", lineRow) 311 } 312 return 313 } 314 315 // TODO: Add a --no-title flag to skip title output 316 func DisplaySchemasCSV(writer io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { 317 getLogger().Enter() 318 defer getLogger().Exit() 319 320 // initialize writer and prepare the list of entries (i.e., the "rows") 321 w := csv.NewWriter(writer) 322 defer w.Flush() 323 324 // create title row from slices of optional and compulsory titles 325 titles, _ := prepareReportTitleData(SCHEMA_LIST_ROW_DATA, false) 326 327 if err = w.Write(titles); err != nil { 328 return getLogger().Errorf("error writing to output (%v): %s", titles, err) 329 } 330 331 // Emit no schemas found warning into output 332 if len(filteredSchemas) == 0 { 333 currentRow := []string{MSG_OUTPUT_NO_SCHEMAS_FOUND} 334 if err = w.Write(currentRow); err != nil { 335 return getLogger().Errorf("error writing to output (%v): %s", currentRow, err) 336 } 337 return fmt.Errorf(currentRow[0]) 338 } 339 340 // Sort by Format, Version, Variant 341 filteredSchemas = sortFormatSchemaInstances(filteredSchemas) 342 343 var line []string 344 for _, schemaInstance := range filteredSchemas { 345 // Supply variant name "latest" (the default name), if not otherwise declared in schema definition 346 schemaInstance.Variant = schema.FormatSchemaVariant(schemaInstance.Variant) 347 line, err = prepareReportLineData( 348 schemaInstance, 349 SCHEMA_LIST_ROW_DATA, 350 true) 351 352 // Only emit line if no error 353 if err != nil { 354 return 355 } 356 if err = w.Write(line); err != nil { 357 err = getLogger().Errorf("csv.Write: %w", err) 358 } 359 } 360 return 361 }