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 }