github.com/CycloneDX/sbom-utility@v0.16.0/cmd/normalize_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  	"io"
    25  	"log"
    26  	"os"
    27  	"testing"
    28  
    29  	"github.com/CycloneDX/sbom-utility/utils"
    30  )
    31  
    32  const (
    33  	TEST_CDX_1_5_NORMALIZE_COMPONENTS               = "test/normalize/cdx-1-5-components.bom.json"
    34  	TEST_CDX_1_5_NORMALIZE_SERVICES                 = "test/normalize/cdx-1-5-services.bom.json"
    35  	TEST_CDX_1_5_NORMALIZE_LICENSES                 = "test/normalize/cdx-1-5-licenses.bom.json"
    36  	TEST_CDX_1_5_NORMALIZE_DEPENDENCIES             = "test/normalize/cdx-1-5-dependencies.bom.json"
    37  	TEST_CDX_1_5_NORMALIZE_EXTERNAL_REFERENCES      = "test/normalize/cdx-1-5-external-references.bom.json"
    38  	TEST_CDX_1_5_NORMALIZE_VULNERABILITIES          = "test/normalize/cdx-1-5-vulnerabilities.bom.json"
    39  	TEST_CDX_1_5_NORMALIZE_VULNERABILITIES_NATS_BOX = "test/normalize/cdx-1-5-vulnerabilities-container-nats-box.bom.json"
    40  	TEST_CDX_1_4_NORMALIZE_COMPONENTS_XXL           = "test/normalize/cdx-1-4-components-xxl.bom.json"
    41  	TEST_CDX_1_2_NORMALIZE_COMPONENTS_PROTON        = "test/normalize/cdx-1-2-components-protonmail.bom.json"
    42  )
    43  
    44  type NormalizeTestInfo struct {
    45  	CommonTestInfo
    46  	Keys      []string
    47  	FromPaths []string
    48  	Normalize bool
    49  }
    50  
    51  func (ti *NormalizeTestInfo) String() string {
    52  	buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti)
    53  	return buffer.String()
    54  }
    55  
    56  func NewNormalizeTestInfo(inputFile string, resultExpectedError error) *NormalizeTestInfo {
    57  	var ti = new(NormalizeTestInfo)
    58  	// Set to test normalization by default
    59  	ti.Normalize = true
    60  	var pCommon = &ti.CommonTestInfo
    61  	pCommon.InitBasic(inputFile, FORMAT_JSON, resultExpectedError)
    62  	return ti
    63  }
    64  
    65  func innerTestNormalize(t *testing.T, testInfo *NormalizeTestInfo) (outputBuffer bytes.Buffer, basicTestInfo string, err error) {
    66  	getLogger().Tracef("TestInfo: %s", testInfo)
    67  
    68  	// Mock stdin if requested
    69  	if testInfo.MockStdin == true {
    70  		utils.GlobalFlags.PersistentFlags.InputFile = INPUT_TYPE_STDIN
    71  		file, err := os.Open(testInfo.InputFile) // For read access.
    72  		if err != nil {
    73  			log.Fatal(err)
    74  		}
    75  
    76  		// convert byte slice to io.Reader
    77  		savedStdIn := os.Stdin
    78  		// !!!Important restore stdin
    79  		defer func() { os.Stdin = savedStdIn }()
    80  		os.Stdin = file
    81  	}
    82  
    83  	// invoke resource list command with a byte buffer
    84  	outputBuffer, err = innerBufferedTestNormalize(testInfo)
    85  	// if the command resulted in a failure
    86  	if err != nil {
    87  		// if tests asks us to report a FAIL to the test framework
    88  		cti := &testInfo.CommonTestInfo
    89  		if cti.Autofail {
    90  			encodedTestInfo, _ := utils.EncodeAnyToDefaultIndentedJSONStr(testInfo)
    91  			t.Errorf("%s: failed: %v\n%s", cti.InputFile, err, encodedTestInfo.String())
    92  		}
    93  		return
    94  	}
    95  
    96  	return
    97  }
    98  
    99  func innerBufferedTestNormalize(testInfo *NormalizeTestInfo) (outputBuffer bytes.Buffer, err error) {
   100  
   101  	// The command looks for the input & output filename in global flags struct
   102  	utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile
   103  	utils.GlobalFlags.PersistentFlags.OutputFile = testInfo.OutputFile
   104  	utils.GlobalFlags.PersistentFlags.OutputFormat = testInfo.OutputFormat
   105  	utils.GlobalFlags.PersistentFlags.OutputIndent = testInfo.OutputIndent
   106  	utils.GlobalFlags.PersistentFlags.OutputNormalize = testInfo.Normalize // NOTE: default=true
   107  	utils.GlobalFlags.TrimFlags.Keys = testInfo.Keys
   108  	utils.GlobalFlags.TrimFlags.FromPaths = testInfo.FromPaths
   109  	var outputWriter io.Writer
   110  	var outputFile *os.File
   111  
   112  	// TODO: centralize this logic to a function all Commands can use...
   113  	// Note: Any "Mocking" of os.Stdin/os.Stdout should be done in functions that call this one
   114  	if testInfo.OutputFile == "" {
   115  		// Declare an output outputBuffer/outputWriter to use used during tests
   116  		bufferedWriter := bufio.NewWriter(&outputBuffer)
   117  		outputWriter = bufferedWriter
   118  		// MUST ensure all data is written to buffer before further testing
   119  		defer bufferedWriter.Flush()
   120  	} else {
   121  		outputFile, outputWriter, err = createOutputFile(testInfo.OutputFile)
   122  		getLogger().Tracef("outputFile: `%v`; writer: `%v`", testInfo.OutputFile, outputWriter)
   123  
   124  		// use function closure to assure consistent error output based upon error type
   125  		defer func() {
   126  			// always close the output file (even if error, as long as file handle returned)
   127  			if outputFile != nil {
   128  				outputFile.Close()
   129  				getLogger().Infof("Closed output file: `%s`", testInfo.OutputFile)
   130  			}
   131  		}()
   132  
   133  		if err != nil {
   134  			return
   135  		}
   136  	}
   137  
   138  	// NOTE: We use the Trim() command to test the Normalize() functionality for now
   139  	// TODO: Ideally, we want a top-level command "Normalize()" with other flag options
   140  	// BUT, also want to allow normalization any time ANY command writes BOM as output
   141  	// so the Trim() command is a great first impl. towards those goals.
   142  	err = Trim(outputWriter, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.TrimFlags)
   143  	return
   144  }
   145  
   146  func TestNormalizeCdx15Components(t *testing.T) {
   147  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_COMPONENTS, nil)
   148  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_COMPONENTS)
   149  	// ti.FromPaths = []string{"components"}
   150  	innerTestNormalize(t, ti)
   151  	document, err := LoadBOMOutputFile(ti.CommonTestInfo)
   152  	if err != nil {
   153  		t.Error(err)
   154  	}
   155  
   156  	// Before looking for license data, fully unmarshal the SBOM into named structures
   157  	if err = document.UnmarshalCycloneDXBOM(); err != nil {
   158  		return
   159  	}
   160  }
   161  
   162  func TestNormalizeCdx15Services(t *testing.T) {
   163  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_SERVICES, nil)
   164  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_SERVICES)
   165  	innerTestNormalize(t, ti)
   166  }
   167  
   168  func TestNormalizeCdx15Dependencies(t *testing.T) {
   169  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_DEPENDENCIES, nil)
   170  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_DEPENDENCIES)
   171  	innerTestNormalize(t, ti)
   172  }
   173  
   174  func TestNormalizeCdx15ExternalReferences(t *testing.T) {
   175  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_EXTERNAL_REFERENCES, nil)
   176  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_EXTERNAL_REFERENCES)
   177  	innerTestNormalize(t, ti)
   178  }
   179  
   180  func TestNormalizeCdx15Vulnerabilities(t *testing.T) {
   181  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_VULNERABILITIES, nil)
   182  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_VULNERABILITIES)
   183  	innerTestNormalize(t, ti)
   184  }
   185  
   186  func TestNormalizeCdx15Licenses(t *testing.T) {
   187  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_LICENSES, nil)
   188  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_LICENSES)
   189  	innerTestNormalize(t, ti)
   190  }
   191  
   192  // XXL Sort tests
   193  func TestNormalizeCdx12ComponentsProtonMail(t *testing.T) {
   194  	ti := NewNormalizeTestInfo(TEST_CDX_1_2_NORMALIZE_COMPONENTS_PROTON, nil)
   195  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_2_NORMALIZE_COMPONENTS_PROTON)
   196  	innerTestNormalize(t, ti)
   197  }
   198  
   199  func TestNormalizeCdx14ComponentsXXL(t *testing.T) {
   200  	ti := NewNormalizeTestInfo(TEST_CDX_1_4_NORMALIZE_COMPONENTS_XXL, nil)
   201  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_4_NORMALIZE_COMPONENTS_XXL)
   202  	innerTestNormalize(t, ti)
   203  }
   204  
   205  func TestNormalizeCdx15VulnerabilitiesNatsBox(t *testing.T) {
   206  	ti := NewNormalizeTestInfo(TEST_CDX_1_5_NORMALIZE_VULNERABILITIES_NATS_BOX, nil)
   207  	ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_CDX_1_5_NORMALIZE_VULNERABILITIES_NATS_BOX)
   208  	innerTestNormalize(t, ti)
   209  }
   210  
   211  // EXPERIMENTAL:
   212  // TODO: see if we can create a function to loop through all nested structures and
   213  // report one which ones do NOT have support for the Normalizer interface.
   214  // This could be used to verify always have code to normalize any structure as
   215  // new ones are added release-to-release
   216  // func TestNormalizeReflect(t *testing.T) {
   217  // 	document, err := LoadBOMFile(TEST_CDX_1_5_NORMALIZE_COMPONENTS)
   218  // 	if err != nil {
   219  // 		return
   220  // 	}
   221  
   222  // 	if err = document.UnmarshalCycloneDXBOM(); err != nil {
   223  // 		return
   224  // 	}
   225  
   226  // 	// Test reflect.New using an existing instance
   227  // 	//ptrBom := reflect.New(reflect.TypeOf(*document))
   228  // 	//fmt.Printf("New *schema.BOM: %+v\n", ptrBom)
   229  
   230  // 	// Test reflect.New using an existing (empty) instance
   231  // 	//bom2Type := reflect.TypeOf(schema.BOM{})
   232  // 	//ptrBom2 := reflect.New(bom2Type)
   233  // 	//fmt.Printf("New *schema.BOM: %+v\n", ptrBom2)
   234  
   235  // 	// Assure we can
   236  // 	ptrCdxBom := document.GetCdxBom()
   237  // 	//fmt.Printf("*schema.CDXBom: %+v\n", ptrCdxBom)
   238  
   239  // 	ListFields(ptrCdxBom)
   240  // }
   241  
   242  // func ListFields(itfc interface{}) {
   243  // 	// NOTE: we can immediately use ValueOf() to dereference the interface{}
   244  // 	// NOTE: Elem() will panic if reflect.ValueOf(itfc).Kind() != reflect.Ptr || reflect.Interface
   245  // 	rvoItfc := reflect.ValueOf(itfc)
   246  
   247  // 	// Deref. if needed to get the ACTUAL type we want to list fields for
   248  // 	if rvoItfc.Kind() == reflect.Pointer || rvoItfc.Kind() == reflect.Interface {
   249  // 		rvoItfc = reflect.ValueOf(itfc).Elem()
   250  // 	}
   251  
   252  // 	// Immediately grab the Type of the dereferenced interface{}
   253  // 	rvoType := rvoItfc.Type()
   254  // 	fmt.Printf("Interface: Type: `%v`, Kind: `%v`\n", rvoType.String(), rvoType.Kind())
   255  
   256  // 	if rvoType.Kind() == reflect.Struct {
   257  // 		// Iterate over all fields of the Struct type (if any)
   258  // 		for i := 0; i < rvoType.NumField(); i++ {
   259  // 			field := rvoType.Field(i)
   260  
   261  // 			// Indirect returns the fieldValue that v points to.
   262  // 			// - If <input> is a nil pointer, Indirect returns a zero Value.
   263  // 			// - If <input> is not a pointer, Indirect returns <input> (no dereference using Elem() method).
   264  // 			fieldName := field.Name
   265  // 			fieldValue := reflect.Indirect(rvoItfc).FieldByName(fieldName)
   266  // 			fvKind := fieldValue.Kind()
   267  // 			fvValueOf := reflect.ValueOf(fieldValue)
   268  // 			// TODO: explore `field.PkgPath`
   269  // 			fmt.Printf(">> Field(%v): `%s`, Kind: `%s`, Tags: `%s`, Value: `%v`\n", i, fieldName, fvKind.String(), field.Tag, fvValueOf)
   270  
   271  // 			// TODO: explore use of isItfc := field.CanInterface()
   272  // 			if fvKind == reflect.Ptr || fvKind == reflect.Interface {
   273  // 				if !fieldValue.IsNil() {
   274  // 					// NOTE:  temp.Elem() could be reflect.Struct, reflect.Map, reflect.Slice, etc.
   275  // 					ListFields(fieldValue.Interface())
   276  // 				}
   277  // 			}
   278  // 		}
   279  // 	} else {
   280  // 		fmt.Printf("!!! Unhandled Kind(): `%v`", rvoType.Kind())
   281  // 	}
   282  // }