github.com/CycloneDX/sbom-utility@v0.16.0/cmd/trim.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 "io" 23 "strings" 24 25 "github.com/CycloneDX/sbom-utility/common" 26 "github.com/CycloneDX/sbom-utility/schema" 27 "github.com/CycloneDX/sbom-utility/utils" 28 "github.com/spf13/cobra" 29 ) 30 31 // flags (do not translate) 32 const ( 33 FLAG_TRIM_FROM_PATHS = "from" 34 FLAG_TRIM_MAP_KEYS = "keys" 35 FLAG_TRIM_NORMALIZE = "normalize" 36 ) 37 38 // flag help (translate) 39 const ( 40 MSG_FLAG_TRIM_FROM_PATHS = "comma-separated list of dot-separated JSON document paths used to scope where trim is applied" + 41 "\n - if not present, the default `--from` path is the document \"root\"" 42 MSG_FLAG_TRIM_KEYS = "JSON map keys to trim (delete) (e.g., \"key1,key2,...,keyN\")" 43 ) 44 45 var TRIM_OUTPUT_SUPPORTED_FORMATS = MSG_SUPPORTED_OUTPUT_FORMATS_HELP + 46 strings.Join([]string{FORMAT_JSON}, ", ") 47 48 const ( 49 TRIM_KEYS_SEP = "," 50 TRIM_PATH_SEP = "." 51 TRIM_PATHS_SEP = "," 52 TRIM_FROM_TOKEN_WILDCARD = "*" 53 ) 54 55 func NewCommandTrim() *cobra.Command { 56 var command = new(cobra.Command) 57 command.Use = CMD_USAGE_TRIM 58 command.Short = "Trim elements from the BOM input file and write resultant BOM to output" 59 command.Long = "Trim elements from the BOM input file and write resultant BOM to output" 60 command.RunE = trimCmdImpl 61 command.PreRunE = func(cmd *cobra.Command, args []string) (err error) { 62 // Test for required flags (parameters) 63 err = preRunTestForInputFile(args) 64 return 65 } 66 initCommandTrimFlags(command) 67 68 return command 69 } 70 71 func initCommandTrimFlags(command *cobra.Command) (err error) { 72 getLogger().Enter() 73 defer getLogger().Exit() 74 75 command.PersistentFlags().StringVar(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_OUTPUT_FORMAT, FORMAT_JSON, 76 MSG_FLAG_OUTPUT_FORMAT+TRIM_OUTPUT_SUPPORTED_FORMATS) 77 command.PersistentFlags().BoolVar(&utils.GlobalFlags.PersistentFlags.OutputNormalize, FLAG_OUTPUT_NORMALIZE, false, MSG_FLAG_OUTPUT_NORMALIZE) 78 command.Flags().StringVarP(&utils.GlobalFlags.TrimFlags.RawPaths, FLAG_TRIM_FROM_PATHS, "", "", MSG_FLAG_TRIM_FROM_PATHS) 79 command.Flags().StringVarP(&utils.GlobalFlags.TrimFlags.RawKeys, FLAG_TRIM_MAP_KEYS, "", "", MSG_FLAG_TRIM_KEYS) 80 err = command.MarkFlagRequired(FLAG_TRIM_MAP_KEYS) 81 if err != nil { 82 err = getLogger().Errorf("unable to mark flag `%s` as required: %s", FLAG_TRIM_MAP_KEYS, err) 83 } 84 return 85 } 86 87 func trimCmdImpl(cmd *cobra.Command, args []string) (err error) { 88 getLogger().Enter(args) 89 defer getLogger().Exit() 90 91 // Create output writer 92 outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile 93 outputFile, writer, err := createOutputFile(outputFilename) 94 getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFilename, writer) 95 96 // use function closure to assure consistent error output based upon error type 97 defer func() { 98 // always close the output file 99 if outputFile != nil { 100 outputFile.Close() 101 getLogger().Infof("Closed output file: `%s`", outputFilename) 102 } 103 }() 104 105 // --keys parameter 106 if keys := utils.GlobalFlags.TrimFlags.RawKeys; keys != "" { 107 utils.GlobalFlags.TrimFlags.Keys = strings.Split(keys, TRIM_KEYS_SEP) 108 getLogger().Tracef("Trim: keys: `%v`\n", keys) 109 } else { 110 getLogger().Tracef("Trim: required parameter NOT found for `%s` flag", FLAG_TRIM_MAP_KEYS) 111 } 112 113 // --from parameter 114 if paths := utils.GlobalFlags.TrimFlags.RawPaths; paths != "" { 115 utils.GlobalFlags.TrimFlags.FromPaths = common.ParseFromPaths(paths) 116 getLogger().Tracef("Trim: paths: `%v`\n", paths) 117 } else { 118 getLogger().Tracef("Trim: required parameter NOT found for `%s` flag", FLAG_TRIM_FROM_PATHS) 119 } 120 121 if err == nil { 122 err = Trim(writer, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.TrimFlags) 123 } 124 125 return 126 } 127 128 // Assure all errors are logged 129 func processTrimResults(err error) { 130 if err != nil { 131 // No special processing at this time 132 getLogger().Error(err) 133 } 134 } 135 136 // NOTE: resourceType has already been validated 137 func Trim(writer io.Writer, persistentFlags utils.PersistentCommandFlags, trimFlags utils.TrimCommandFlags) (err error) { 138 getLogger().Enter() 139 defer getLogger().Exit() 140 141 // use function closure to assure consistent error output based upon error type 142 defer func() { 143 if err != nil { 144 processTrimResults(err) 145 } 146 }() 147 148 // Note: returns error if either file load or unmarshal to JSON map fails 149 var document *schema.BOM 150 if document, err = LoadInputBOMFileAndDetectSchema(); err != nil { 151 return 152 } 153 154 // At this time, fail SPDX format SBOMs as "unsupported" (for "any" format) 155 if !document.FormatInfo.IsCycloneDx() { 156 err = schema.NewUnsupportedFormatForCommandError( 157 document.FormatInfo.CanonicalName, 158 document.GetFilename(), 159 CMD_LICENSE, FORMAT_ANY) 160 return 161 } 162 163 // validate parameters 164 if len(trimFlags.Keys) == 0 && !persistentFlags.OutputNormalize { 165 // TODO create named error type in schema package 166 err = getLogger().Errorf("invalid parameter value: missing `keys` value from command") 167 return 168 } 169 170 // If no paths are passed, use BOM document root 171 if len(trimFlags.FromPaths) == 0 { 172 document.TrimBOMKeys(trimFlags.Keys) 173 } else { 174 // TODO: see if we can make this logic a method on BOM object 175 // else, loop through document paths provided by caller 176 qr := common.NewQueryRequest() 177 // Use query function to obtain BOM document subsets (as JSON maps) 178 // using --from path values 179 for _, path := range trimFlags.FromPaths { 180 qr.SetRawFromPaths(path) 181 result, errQuery := QueryJSONMap(document.GetJSONMap(), qr) 182 183 if errQuery != nil { 184 getLogger().Errorf("query error: invalid path: %s", path) 185 buffer, errEncode := utils.EncodeAnyToDefaultIndentedJSONStr(result) 186 if errEncode != nil { 187 getLogger().Tracef("result: %s", buffer.String()) 188 } 189 } 190 document.TrimEntityKeys(result, trimFlags.Keys) 191 } 192 } 193 194 // TODO: Investigate if we can simply Marshal the JSON map directly (performance). 195 // NOTE: Today we unmarshal() to ensure empty/zero fields are omitted via 196 // the custom marshal/unmarshal functions for CycloneDX. 197 // NOTE: If we do want to "validate" the BOM data at some point, we MAY 198 // need to unmarshal into CDX structures regardless. 199 // Fully unmarshal the SBOM into named structures 200 if err = document.UnmarshalCycloneDXBOM(); err != nil { 201 return 202 } 203 204 // Sort slices of BOM if "sort" flag set to true 205 if persistentFlags.OutputNormalize { 206 // Sort the slices of structures 207 if document.GetCdxBom() != nil { 208 bom := document.GetCdxBom() 209 if schema.NormalizeSupported(bom) { 210 document.GetCdxBom().Normalize() 211 } 212 } 213 } 214 215 // Output the "trimmed" version of the Input BOM 216 format := persistentFlags.OutputFormat 217 getLogger().Infof("Writing trimmed BOM (`%s` format)...", format) 218 switch format { 219 case FORMAT_JSON: 220 err = document.WriteAsEncodedJSONInt(writer, utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) 221 default: 222 // Default to Text output for anything else (set as flag default) 223 getLogger().Warningf("Trim not supported for `%s` format; defaulting to `%s` format...", 224 format, FORMAT_JSON) 225 err = document.WriteAsEncodedJSONInt(writer, utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) 226 } 227 228 return 229 }