github.com/CycloneDX/sbom-utility@v0.16.0/cmd/trim_test.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  	"bufio"
    23  	"bytes"
    24  	"fmt"
    25  	"io"
    26  	"log"
    27  	"os"
    28  	"strings"
    29  	"testing"
    30  
    31  	"github.com/CycloneDX/sbom-utility/common"
    32  	"github.com/CycloneDX/sbom-utility/utils"
    33  )
    34  
    35  const (
    36  	// Trim test BOM files
    37  	TEST_TRIM_CDX_1_4_ENCODED_CHARS           = "test/trim/trim-cdx-1-4-sample-encoded-chars.sbom.json"
    38  	TEST_TRIM_CDX_1_4_SAMPLE_XXL_1            = "test/trim/trim-cdx-1-4-sample-xxl-1.sbom.json"
    39  	TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY = "test/trim/trim-cdx-1-5-sample-small-components-only.sbom.json"
    40  	TEST_TRIM_CDX_1_4_SAMPLE_VEX              = "test/trim/trim-cdx-1-4-sample-vex.json"
    41  	TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1         = "test/trim/trim-cdx-1-5-sample-medium-1.sbom.json"
    42  	TEST_TRIM_CDX_1_5_COMPONENTS_NORMALIZE    = "test/trim/trim-cdx-1-5-sample-components-normalize.sbom.json"
    43  )
    44  
    45  type TrimTestInfo struct {
    46  	CommonTestInfo
    47  	Keys      []string
    48  	FromPaths []string
    49  }
    50  
    51  func (ti *TrimTestInfo) String() string {
    52  	buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti)
    53  	return buffer.String()
    54  }
    55  
    56  func NewTrimTestInfo(inputFile string, resultExpectedError error) *TrimTestInfo {
    57  	var ti = new(TrimTestInfo)
    58  	var pCommon = &ti.CommonTestInfo
    59  	pCommon.InitBasic(inputFile, FORMAT_JSON, resultExpectedError)
    60  	return ti
    61  }
    62  
    63  // -------------------------------------------
    64  // test helper functions
    65  // -------------------------------------------
    66  
    67  func innerTestTrim(t *testing.T, testInfo *TrimTestInfo) (outputBuffer bytes.Buffer, basicTestInfo string, err error) {
    68  	getLogger().Tracef("TestInfo: %s", testInfo)
    69  
    70  	// Mock stdin if requested
    71  	if testInfo.MockStdin == true {
    72  		utils.GlobalFlags.PersistentFlags.InputFile = INPUT_TYPE_STDIN
    73  		file, err := os.Open(testInfo.InputFile) // For read access.
    74  		if err != nil {
    75  			log.Fatal(err)
    76  		}
    77  
    78  		// convert byte slice to io.Reader
    79  		savedStdIn := os.Stdin
    80  		// !!!Important restore stdin
    81  		defer func() { os.Stdin = savedStdIn }()
    82  		os.Stdin = file
    83  	}
    84  
    85  	// invoke resource list command with a byte buffer
    86  	outputBuffer, err = innerBufferedTestTrim(testInfo)
    87  	// if the command resulted in a failure
    88  	if err != nil {
    89  		// if tests asks us to report a FAIL to the test framework
    90  		cti := &testInfo.CommonTestInfo
    91  		if cti.Autofail {
    92  			encodedTestInfo, _ := utils.EncodeAnyToDefaultIndentedJSONStr(testInfo)
    93  			t.Errorf("%s: failed: %v\n%s", cti.InputFile, err, encodedTestInfo.String())
    94  		}
    95  		return
    96  	}
    97  
    98  	return
    99  }
   100  
   101  func innerBufferedTestTrim(testInfo *TrimTestInfo) (outputBuffer bytes.Buffer, err error) {
   102  
   103  	// The command looks for the input & output filename in global flags struct
   104  	utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile
   105  	utils.GlobalFlags.PersistentFlags.OutputFile = testInfo.OutputFile
   106  	utils.GlobalFlags.PersistentFlags.OutputFormat = testInfo.OutputFormat
   107  	utils.GlobalFlags.PersistentFlags.OutputIndent = testInfo.OutputIndent
   108  	utils.GlobalFlags.TrimFlags.Keys = testInfo.Keys
   109  	utils.GlobalFlags.TrimFlags.FromPaths = testInfo.FromPaths
   110  	var outputWriter io.Writer
   111  	var outputFile *os.File
   112  
   113  	// TODO: centralize this logic to a function all Commands can use...
   114  	// Note: Any "Mocking" of os.Stdin/os.Stdout should be done in functions that call this one
   115  	if testInfo.OutputFile == "" {
   116  		// Declare an output outputBuffer/outputWriter to use used during tests
   117  		bufferedWriter := bufio.NewWriter(&outputBuffer)
   118  		outputWriter = bufferedWriter
   119  		// MUST ensure all data is written to buffer before further testing
   120  		defer bufferedWriter.Flush()
   121  	} else {
   122  		outputFile, outputWriter, err = createOutputFile(testInfo.OutputFile)
   123  		getLogger().Tracef("outputFile: `%v`; writer: `%v`", testInfo.OutputFile, outputWriter)
   124  
   125  		// use function closure to assure consistent error output based upon error type
   126  		defer func() {
   127  			// always close the output file (even if error, as long as file handle returned)
   128  			if outputFile != nil {
   129  				outputFile.Close()
   130  				getLogger().Infof("Closed output file: `%s`", testInfo.OutputFile)
   131  			}
   132  		}()
   133  
   134  		if err != nil {
   135  			return
   136  		}
   137  	}
   138  
   139  	err = Trim(outputWriter, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.TrimFlags)
   140  	return
   141  }
   142  
   143  func VerifyTrimOutputFileResult(t *testing.T, originalTest TrimTestInfo) (err error) {
   144  
   145  	// Create a new test info. structure copying in data from the original test
   146  	queryTestInfo := NewCommonTestInfo()
   147  	queryTestInfo.InputFile = originalTest.OutputFile
   148  
   149  	// Load and Query temporary "trimmed" output BOM file using the "from" path
   150  	// Default to "root" (i.e,, "") path if none selected.
   151  	fromPath := ""
   152  	if len(originalTest.FromPaths) > 0 {
   153  		fromPath = originalTest.FromPaths[0]
   154  	}
   155  
   156  	request, err := common.NewQueryRequestSelectFromWhere(
   157  		common.QUERY_TOKEN_WILDCARD, fromPath, "")
   158  	if err != nil {
   159  		t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err)
   160  		return
   161  	}
   162  
   163  	// Verify each key was removed
   164  	var pResult interface{}
   165  	for _, key := range originalTest.Keys {
   166  
   167  		// use a buffered query on the temp. output file on the (parent) path
   168  		pResult, _, err = innerQuery(t, queryTestInfo, request)
   169  		if err != nil {
   170  			t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err)
   171  			return
   172  		}
   173  
   174  		// short-circuit if the "from" path dereferenced to a non-existent key
   175  		if pResult == nil {
   176  			t.Errorf("empty (nil) found at from clause: %s", fromPath)
   177  			return
   178  		}
   179  
   180  		// verify the "key" was removed from the (parent) JSON map
   181  		err = VerifyTrimmed(pResult, key)
   182  	}
   183  
   184  	return
   185  }
   186  
   187  func VerifyTrimmed(pResult interface{}, key string) (err error) {
   188  	// verify the "key" was removed from the (parent) JSON map
   189  	if pResult != nil {
   190  		switch typedValue := pResult.(type) {
   191  		case map[string]interface{}:
   192  			// verify map key was removed
   193  			if _, ok := typedValue[key]; ok {
   194  				formattedValue, _ := utils.MarshalAnyToFormattedJsonString(typedValue)
   195  				err = getLogger().Errorf("trim failed. Key `%s`, found in: `%s`", key, formattedValue)
   196  				return
   197  			}
   198  		case []interface{}:
   199  			if len(typedValue) == 0 {
   200  				err = getLogger().Errorf("empty slice found at from clause.")
   201  				return
   202  			}
   203  			// Verify all elements of slice
   204  			for _, value := range typedValue {
   205  				err = VerifyTrimmed(value, key)
   206  				return err
   207  			}
   208  		default:
   209  			err = getLogger().Errorf("trim failed. Unexpected JSON type: `%T`", typedValue)
   210  			return
   211  		}
   212  	}
   213  	return
   214  }
   215  
   216  // ----------------------------------------
   217  // Trim with encoded chars
   218  // ----------------------------------------
   219  
   220  // NOTE: The JSON Marshal(), by default, encodes chars (assumes JSON docs are being transmitted over HTML streams)
   221  // which is not true for BOM documents as stream (wire) transmission encodings
   222  // are specified for both formats.  We need to assure any commands that
   223  // rewrite BOMs (after edits) preserve original characters.
   224  func TestTrimCdx14PreserveUnencodedChars(t *testing.T) {
   225  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_ENCODED_CHARS, nil)
   226  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_4_ENCODED_CHARS)
   227  	ti.Keys = append(ti.Keys, "name")
   228  	outputBuffer, _ := innerBufferedTestTrim(ti)
   229  	TEST1 := "<guillem@debian.org>"
   230  	TEST2 := "<adduser@packages.debian.org>"
   231  
   232  	outputString := outputBuffer.String()
   233  
   234  	if strings.Contains(outputString, TEST1) {
   235  		t.Errorf("removed expected utf8 characters from string: `%s`", TEST1)
   236  	}
   237  
   238  	if strings.Contains(outputString, TEST2) {
   239  		t.Errorf("removed expected utf8 characters from string: `%s`", TEST2)
   240  	}
   241  }
   242  
   243  // ----------------------------------------
   244  // Trim "keys" globally (entire BOM)
   245  // ----------------------------------------
   246  func TestTrimCdx14ComponentPropertiesSampleXXLBuffered(t *testing.T) {
   247  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1, nil)
   248  	ti.Keys = append(ti.Keys, "properties")
   249  	ti.ResultExpectedByteSize = 8121420
   250  	outputBuffer, _ := innerBufferedTestTrim(ti)
   251  	// verify "after" trim lengths and content have removed properties
   252  	getLogger().Tracef("Len(outputBuffer): `%v`\n", outputBuffer.Len())
   253  	if ti.ResultExpectedByteSize > 0 {
   254  		if outputBuffer.Len() != ti.ResultExpectedByteSize {
   255  			t.Error(fmt.Errorf("invalid trim result size (bytes): expected: %v, actual: %v", ti.ResultExpectedByteSize, outputBuffer.Len()))
   256  		}
   257  	}
   258  }
   259  
   260  // TODO: enable for when we have a "from" parameter to limit trim scope
   261  func TestTrimCdx14ComponentPropertiesSampleXXL(t *testing.T) {
   262  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1, nil)
   263  	ti.Keys = append(ti.Keys, "properties")
   264  	ti.FromPaths = []string{"metadata.component"}
   265  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1)
   266  	innerTestTrim(t, ti)
   267  	// Assure JSON map does not contain the trimmed key(s)
   268  	err := VerifyTrimOutputFileResult(t, *ti)
   269  	if err != nil {
   270  		t.Error(err)
   271  	}
   272  }
   273  
   274  func TestTrimCdx15MultipleKeys(t *testing.T) {
   275  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY, nil)
   276  	ti.Keys = append(ti.Keys, "properties", "hashes", "version", "description", "name")
   277  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY)
   278  	innerTestTrim(t, ti)
   279  	// Assure JSON map does not contain the trimmed key(s)
   280  	err := VerifyTrimOutputFileResult(t, *ti)
   281  	if err != nil {
   282  		t.Error(err)
   283  	}
   284  }
   285  
   286  func TestTrimCdx15Properties(t *testing.T) {
   287  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil)
   288  	ti.Keys = append(ti.Keys, "properties")
   289  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1)
   290  	innerTestTrim(t, ti)
   291  	// Assure JSON map does not contain the trimmed key(s)
   292  	err := VerifyTrimOutputFileResult(t, *ti)
   293  	if err != nil {
   294  		t.Error(err)
   295  	}
   296  }
   297  
   298  // ----------------------------------------
   299  // Trim "keys" only under specified "paths"
   300  // ----------------------------------------
   301  
   302  func TestTrimCdx15PropertiesFromMetadataComponent(t *testing.T) {
   303  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil)
   304  	ti.Keys = append(ti.Keys, "properties")
   305  	ti.FromPaths = []string{"metadata.component"}
   306  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1)
   307  	innerTestTrim(t, ti)
   308  	// Assure JSON map does not contain the trimmed key(s)
   309  	err := VerifyTrimOutputFileResult(t, *ti)
   310  	if err != nil {
   311  		t.Error(err)
   312  	}
   313  }
   314  
   315  func TestTrimCdx15HashesFromTools(t *testing.T) {
   316  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil)
   317  	ti.Keys = append(ti.Keys, "hashes")
   318  	ti.FromPaths = []string{"metadata.tools"}
   319  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1)
   320  	innerTestTrim(t, ti)
   321  	// Assure JSON map does not contain the trimmed key(s)
   322  	err := VerifyTrimOutputFileResult(t, *ti)
   323  	if err != nil {
   324  		t.Error(err)
   325  	}
   326  }
   327  
   328  func TestTrimCdx15AllIncrementallyFromSmallSample(t *testing.T) {
   329  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY, nil)
   330  	ti.Keys = append(ti.Keys, "type", "purl", "bom-ref", "serialNumber", "components", "name", "description", "properties")
   331  	ti.FromPaths = []string{""}
   332  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY)
   333  	_, _, err := innerTestTrim(t, ti)
   334  	if err != nil {
   335  		t.Error(err)
   336  	}
   337  	// Assure JSON map does not contain the trimmed key(s)
   338  	err = VerifyTrimOutputFileResult(t, *ti)
   339  	if err != nil {
   340  		t.Error(err)
   341  	}
   342  }
   343  
   344  func TestTrimCdx15FooFromToolsAndTestJsonIndent(t *testing.T) {
   345  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil)
   346  	ti.Keys = append(ti.Keys, "foo")
   347  	ti.FromPaths = []string{"metadata.tools"}
   348  	ti.OutputIndent = 2 // Matches the space indent of the test input file
   349  	ti.ResultExpectedByteSize = 4292
   350  	ti.ResultExpectedLineCount = 194
   351  	ti.ResultExpectedIndentLength = int(ti.OutputIndent)
   352  	ti.ResultExpectedIndentAtLineNum = 1
   353  
   354  	buffer, _, err := innerTestTrim(t, ti)
   355  	if err != nil {
   356  		t.Error(err)
   357  	}
   358  
   359  	// Validate expected output file size in bytes (assumes 2-space indent)
   360  	if actualSize := buffer.Len(); actualSize != ti.ResultExpectedByteSize {
   361  		t.Error(fmt.Errorf("invalid trim result (output size (byte)): expected size: %v, actual size: %v", ti.ResultExpectedByteSize, actualSize))
   362  	}
   363  
   364  	// validate test-specific strings still exist
   365  	TEST_STRING_1 := "\"name\": \"urn:example.com:identifier:product\""
   366  	contains := bufferContainsValues(buffer, TEST_STRING_1)
   367  	if !contains {
   368  		t.Error(fmt.Errorf("invalid trim result: string not found: %s", TEST_STRING_1))
   369  	}
   370  
   371  	verifyFileLineCountAndIndentation(t, buffer, &ti.CommonTestInfo)
   372  
   373  	// verify indent continues to use multiples of 2
   374  	ti.ResultExpectedIndentLength = 4
   375  	ti.ResultExpectedIndentAtLineNum = 6
   376  	verifyFileLineCountAndIndentation(t, buffer, &ti.CommonTestInfo)
   377  	ti.ResultExpectedIndentLength = 6
   378  	ti.ResultExpectedIndentAtLineNum = 8
   379  	verifyFileLineCountAndIndentation(t, buffer, &ti.CommonTestInfo)
   380  	ti.ResultExpectedIndentLength = 4
   381  	ti.ResultExpectedIndentAtLineNum = 30
   382  	verifyFileLineCountAndIndentation(t, buffer, &ti.CommonTestInfo)
   383  }
   384  
   385  func TestTrimCdx14SourceFromVulnerabilities(t *testing.T) {
   386  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_SAMPLE_VEX, nil)
   387  	ti.Keys = append(ti.Keys, "source")
   388  	ti.FromPaths = []string{"vulnerabilities"}
   389  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_4_SAMPLE_VEX)
   390  
   391  	buffer, _, err := innerTestTrim(t, ti)
   392  	s := buffer.String()
   393  	if err != nil {
   394  		getLogger().Debugf("result: %s", s)
   395  		t.Error(err)
   396  	}
   397  
   398  	// Assure JSON map does not contain the trimmed key(s)
   399  	err = VerifyTrimOutputFileResult(t, *ti)
   400  	if err != nil {
   401  		t.Error(err)
   402  	}
   403  }
   404  
   405  // ----------------------------------------
   406  // Trim "properties" and --normalize
   407  // ----------------------------------------
   408  
   409  func TestTrimCdx15ComponentsPropertiesAndNormalize(t *testing.T) {
   410  	ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_COMPONENTS_NORMALIZE, nil)
   411  	ti.Keys = append(ti.Keys, "properties")
   412  	ti.FromPaths = []string{""}
   413  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_COMPONENTS_NORMALIZE)
   414  	_, _, err := innerTestTrim(t, ti)
   415  	if err != nil {
   416  		t.Error(err)
   417  	}
   418  	// Assure JSON map does not contain the trimmed key(s)
   419  	err = VerifyTrimOutputFileResult(t, *ti)
   420  	if err != nil {
   421  		t.Error(err)
   422  	}
   423  }