github.com/CycloneDX/sbom-utility@v0.16.0/cmd/stats.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  	"fmt"
    23  	"io"
    24  	"sort"
    25  	"strings"
    26  	"text/tabwriter"
    27  
    28  	"github.com/CycloneDX/sbom-utility/schema"
    29  	"github.com/CycloneDX/sbom-utility/utils"
    30  	"github.com/spf13/cobra"
    31  )
    32  
    33  var STATS_LIST_OUTPUT_SUPPORTED_FORMATS = MSG_SUPPORTED_OUTPUT_FORMATS_HELP +
    34  	strings.Join([]string{FORMAT_TEXT, FORMAT_CSV, FORMAT_MARKDOWN}, ", ")
    35  
    36  func NewCommandStats() *cobra.Command {
    37  	var command = new(cobra.Command)
    38  	command.Use = CMD_USAGE_STATS_LIST
    39  	command.Short = "Show BOM input file statistics"
    40  	command.Long = "Show BOM input file statistics"
    41  	command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT,
    42  		STATS_LIST_OUTPUT_SUPPORTED_FORMATS)
    43  	command.RunE = statsCmdImpl
    44  	// TODO: command.ValidArgs = VALID_SUBCOMMANDS_S
    45  	command.PreRunE = func(cmd *cobra.Command, args []string) (err error) {
    46  		// Test for required flags (parameters)
    47  		err = preRunTestForInputFile(args)
    48  		return
    49  	}
    50  	return command
    51  }
    52  
    53  func statsCmdImpl(cmd *cobra.Command, args []string) (err error) {
    54  	getLogger().Enter(args)
    55  	defer getLogger().Exit()
    56  
    57  	// Create output writer
    58  	outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile
    59  	outputFile, writer, err := createOutputFile(outputFilename)
    60  	getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFilename, writer)
    61  
    62  	// use function closure to assure consistent error output based upon error type
    63  	defer func() {
    64  		// always close the output file
    65  		if outputFile != nil {
    66  			outputFile.Close()
    67  			getLogger().Infof("Closed output file: `%s`", outputFilename)
    68  		}
    69  	}()
    70  
    71  	if err == nil {
    72  		err = ListStats(writer, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.StatsFlags)
    73  	}
    74  
    75  	return
    76  }
    77  
    78  // Assure all errors are logged
    79  func processStatisticsResults(err error) {
    80  	if err != nil {
    81  		// No special processing at this time
    82  		getLogger().Error(err)
    83  	}
    84  }
    85  
    86  // NOTE: resourceType has already been validated
    87  func ListStats(writer io.Writer, persistentFlags utils.PersistentCommandFlags, statsFlags utils.StatsCommandFlags) (err error) {
    88  	getLogger().Enter()
    89  	defer getLogger().Exit()
    90  
    91  	// use function closure to assure consistent error output based upon error type
    92  	defer func() {
    93  		if err != nil {
    94  			processStatisticsResults(err)
    95  		}
    96  	}()
    97  
    98  	// Note: returns error if either file load or unmarshal to JSON map fails
    99  	var document *schema.BOM
   100  	document, err = LoadInputBOMFileAndDetectSchema()
   101  
   102  	if err != nil {
   103  		return
   104  	}
   105  
   106  	loadDocumentStatisticalEntities(document, statsFlags)
   107  
   108  	err = loadComponentStats(document)
   109  	if err != nil {
   110  		return
   111  	}
   112  
   113  	format := persistentFlags.OutputFormat
   114  	getLogger().Infof("Outputting listing (`%s` format)...", format)
   115  	switch format {
   116  	case FORMAT_TEXT:
   117  		DisplayStatsText(document, writer)
   118  	// case FORMAT_CSV:
   119  	// 	DisplayResourceListCSV(writer)
   120  	// case FORMAT_MARKDOWN:
   121  	// 	DisplayResourceListMarkdown(writer)
   122  	default:
   123  		// Default to Text output for anything else (set as flag default)
   124  		getLogger().Warningf("Stats not supported for `%s` format; defaulting to `%s` format...",
   125  			format, FORMAT_TEXT)
   126  		DisplayStatsText(document, writer)
   127  	}
   128  
   129  	return
   130  }
   131  
   132  func loadComponentStats(document *schema.BOM) (err error) {
   133  	if document == nil {
   134  		return getLogger().Errorf("invalid BOM document")
   135  	}
   136  
   137  	stats := document.Statistics
   138  
   139  	if stats == nil || stats.ComponentStats == nil {
   140  		return getLogger().Errorf("invalid BOM stats")
   141  	}
   142  
   143  	//componentStats := stats.ComponentStats
   144  	mapComponents := document.ComponentMap
   145  
   146  	if mapComponents == nil {
   147  		return getLogger().Errorf("invalid component map")
   148  	}
   149  
   150  	for _, key := range mapComponents.KeySet() {
   151  		aComponents, _ := mapComponents.Get(key)
   152  
   153  		if len(aComponents) > 1 {
   154  			// TODO: are they unique entries? or are DeepEqual() duplicates?
   155  			getLogger().Warningf("component `%v` has duplicate `%v` entries", key, len(aComponents))
   156  		}
   157  
   158  		// for _, c := range aComponents {
   159  		// 	fmt.Printf("comp=%v", c)
   160  		// }
   161  	}
   162  
   163  	return
   164  }
   165  
   166  func loadDocumentStatisticalEntities(document *schema.BOM, statsFlags utils.StatsCommandFlags) (err error) {
   167  	getLogger().Enter()
   168  	defer getLogger().Exit(err)
   169  
   170  	// At this time, fail SPDX format SBOMs as "unsupported" (for "any" format)
   171  	if !document.FormatInfo.IsCycloneDx() {
   172  		err = schema.NewUnsupportedFormatForCommandError(
   173  			document.FormatInfo.CanonicalName,
   174  			document.GetFilename(),
   175  			CMD_LICENSE, FORMAT_ANY)
   176  		return
   177  	}
   178  
   179  	// Before looking for license data, fully unmarshal the SBOM into named structures
   180  	if err = document.UnmarshalCycloneDXBOM(); err != nil {
   181  		return
   182  	}
   183  
   184  	err = document.HashmapComponentResources(nil)
   185  	if err != nil {
   186  		return
   187  	}
   188  
   189  	err = document.HashmapServiceResources(nil)
   190  	if err != nil {
   191  		return
   192  	}
   193  
   194  	err = document.HashmapVulnerabilityResources(nil)
   195  	if err != nil {
   196  		return
   197  	}
   198  
   199  	return
   200  }
   201  
   202  // NOTE: This list is NOT de-duplicated
   203  // TODO: Add a --no-title flag to skip title output
   204  func DisplayStatsText(bom *schema.BOM, writer io.Writer) {
   205  	getLogger().Enter()
   206  	defer getLogger().Exit()
   207  
   208  	// initialize tabwriter
   209  	w := new(tabwriter.Writer)
   210  	defer w.Flush()
   211  
   212  	// min-width, tab-width, padding, pad-char, flags
   213  	w.Init(writer, 8, 2, 2, ' ', 0)
   214  
   215  	// Display a warning "missing" in the actual output and return (short-circuit)
   216  	entries := bom.ResourceMap.Entries()
   217  
   218  	// Emit no license warning into output
   219  	if len(entries) == 0 {
   220  		fmt.Fprintf(w, "%s\n", MSG_OUTPUT_NO_RESOURCES_FOUND)
   221  		return
   222  	}
   223  
   224  	// Sort by Type then Name
   225  	sort.Slice(entries, func(i, j int) bool {
   226  		resource1 := (entries[i].Value).(schema.CDXResourceInfo)
   227  		resource2 := (entries[j].Value).(schema.CDXResourceInfo)
   228  		if resource1.ResourceType != resource2.ResourceType {
   229  			return resource1.ResourceType < resource2.ResourceType
   230  		}
   231  
   232  		return resource1.Name < resource2.Name
   233  	})
   234  
   235  	var resourceInfo schema.CDXResourceInfo
   236  
   237  	for _, entry := range entries {
   238  		value := entry.Value
   239  		resourceInfo = value.(schema.CDXResourceInfo)
   240  
   241  		// Format line and write to output
   242  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
   243  			resourceInfo.ResourceType,
   244  			resourceInfo.Name,
   245  			resourceInfo.Version,
   246  			resourceInfo.BOMRef)
   247  	}
   248  }