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  }