github.com/anchore/syft@v1.38.2/cmd/syft/internal/test/integration/encode_decode_cycle_test.go (about) 1 package integration 2 3 import ( 4 "bytes" 5 "os" 6 "path/filepath" 7 "strings" 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/sergi/go-diff/diffmatchpatch" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 15 "github.com/anchore/syft/cmd/syft/internal/options" 16 "github.com/anchore/syft/syft/format" 17 "github.com/anchore/syft/syft/format/syftjson" 18 "github.com/anchore/syft/syft/source" 19 ) 20 21 // TestEncodeDecodeEncodeCycleComparison is testing for differences in how SBOM documents get encoded on multiple cycles. 22 // By encoding and decoding the sbom we can compare the differences between the set of resulting objects. However, 23 // this requires specific comparisons being done, and select redactions/omissions being made. Additionally, there are 24 // already unit tests on each format encoder-decoder for properly functioning comparisons in depth, so there is no need 25 // to do an object-to-object comparison. For this reason this test focuses on a bytes-to-bytes comparison after an 26 // encode-decode-encode loop which will detect lossy behavior in both directions. 27 func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { 28 // use second image for relationships 29 images := []string{ 30 "image-pkg-coverage", 31 "image-owning-package", 32 } 33 tests := []struct { 34 name string 35 redactor func(in []byte) []byte 36 json bool 37 }{ 38 { 39 name: syftjson.ID.String(), 40 redactor: func(in []byte) []byte { 41 // no redactions necessary 42 return in 43 }, 44 json: true, 45 }, 46 // TODO: ignoring the `ref` field though does create stable results to compare, but the SBOM is fundamentally gutted and not worth comparing (find a better redaction or compare method) 47 //{ 48 // name: cyclonedxjson.ID.String(), 49 // redactor: func(in []byte) []byte { 50 // // unstable values 51 // in = regexp.MustCompile(`"(timestamp|serialNumber|bom-ref|ref)":\s*"(\n|[^"])+"`).ReplaceAll(in, []byte(`"$1": "redacted"`)) 52 // in = regexp.MustCompile(`"(dependsOn)":\s*\[(?:\s|[^]])+]`).ReplaceAll(in, []byte(`"$1": []`)) 53 // return in 54 // }, 55 // json: true, 56 //}, 57 //{ 58 // name: cyclonedxxml.ID.String(), 59 // redactor: func(in []byte) []byte { 60 // // unstable values 61 // in = regexp.MustCompile(`(serialNumber|bom-ref|ref)="[^"]+"`).ReplaceAll(in, []byte{}) 62 // in = regexp.MustCompile(`<timestamp>[^<]+</timestamp>`).ReplaceAll(in, []byte{}) 63 // 64 // return in 65 // }, 66 //}, 67 } 68 69 opts := options.DefaultOutput() 70 require.NoError(t, opts.PostLoad()) 71 encoderList, err := opts.Encoders() 72 require.NoError(t, err) 73 74 encoders := format.NewEncoderCollection(encoderList...) 75 decoders := format.NewDecoderCollection(format.Decoders()...) 76 77 for _, test := range tests { 78 t.Run(test.name, func(t *testing.T) { 79 for _, image := range images { 80 originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope) 81 // we need a way to inject supplier into this test 82 // supplier is not available as part of the SBOM Config API since the flag 83 // is used in conjunction with the SourceConfig which is injected into generateSBOM during scan 84 originalSBOM.Source.Supplier = "anchore" 85 f := encoders.GetByString(test.name) 86 require.NotNil(t, f) 87 88 var buff1 bytes.Buffer 89 err := f.Encode(&buff1, originalSBOM) 90 require.NoError(t, err) 91 92 newSBOM, formatID, formatVersion, err := decoders.Decode(bytes.NewReader(buff1.Bytes())) 93 require.NoError(t, err) 94 require.Equal(t, f.ID(), formatID) 95 require.Equal(t, f.Version(), formatVersion) 96 97 var buff2 bytes.Buffer 98 err = f.Encode(&buff2, *newSBOM) 99 require.NoError(t, err) 100 101 by1 := buff1.Bytes() 102 by2 := buff2.Bytes() 103 if test.redactor != nil { 104 by1 = test.redactor(by1) 105 by2 = test.redactor(by2) 106 } 107 108 if test.json { 109 s1 := string(by1) 110 s2 := string(by2) 111 if diff := cmp.Diff(s1, s2); diff != "" { 112 t.Errorf("Encode/Decode mismatch (-want +got) [image %q]:\n%s", image, diff) 113 } 114 } else if !assert.True(t, bytes.Equal(by1, by2)) { 115 dmp := diffmatchpatch.New() 116 diffs := dmp.DiffMain(string(by1), string(by2), true) 117 t.Errorf("diff: %s", dmp.DiffPrettyText(diffs)) 118 } 119 120 // write raw IMAGE@NAME-start and IMAGE@NAME-finish to files within the results dir 121 // ... this is helpful for debugging 122 require.NoError(t, os.MkdirAll("results", 0700)) 123 124 suffix := "sbom" 125 switch { 126 case strings.Contains(test.name, "json"): 127 suffix = "json" 128 case strings.Contains(test.name, "xml"): 129 suffix = "xml" 130 } 131 132 require.NoError(t, os.WriteFile(filepath.Join("results", image+"@"+test.name+"-start."+suffix), by1, 0600)) 133 require.NoError(t, os.WriteFile(filepath.Join("results", image+"@"+test.name+"-finish."+suffix), by2, 0600)) 134 } 135 }) 136 } 137 }