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  }