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 }