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  }