github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/spdxjson/encoder_test.go (about)

     1  package spdxjson
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/anchore/syft/syft/artifact"
    13  	"github.com/anchore/syft/syft/file"
    14  	"github.com/anchore/syft/syft/format/internal/spdxutil"
    15  	"github.com/anchore/syft/syft/format/internal/testutil"
    16  	"github.com/anchore/syft/syft/pkg"
    17  	"github.com/anchore/syft/syft/sbom"
    18  )
    19  
    20  var updateSnapshot = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
    21  var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
    22  
    23  func getEncoder(t testing.TB) sbom.FormatEncoder {
    24  	cfg := DefaultEncoderConfig()
    25  	cfg.Pretty = true
    26  
    27  	enc, err := NewFormatEncoderWithConfig(cfg)
    28  	require.NoError(t, err)
    29  	return enc
    30  }
    31  
    32  func TestPrettyOutput(t *testing.T) {
    33  	run := func(opt bool) string {
    34  		enc, err := NewFormatEncoderWithConfig(EncoderConfig{
    35  			Version: spdxutil.DefaultVersion,
    36  			Pretty:  opt,
    37  		})
    38  		require.NoError(t, err)
    39  
    40  		dir := t.TempDir()
    41  		s := testutil.DirectoryInput(t, dir)
    42  
    43  		var buffer bytes.Buffer
    44  		err = enc.Encode(&buffer, s)
    45  		require.NoError(t, err)
    46  
    47  		return strings.TrimSpace(buffer.String())
    48  	}
    49  
    50  	t.Run("pretty", func(t *testing.T) {
    51  		actual := run(true)
    52  		assert.Contains(t, actual, "\n")
    53  	})
    54  
    55  	t.Run("compact", func(t *testing.T) {
    56  		actual := run(false)
    57  		assert.NotContains(t, actual, "\n")
    58  	})
    59  }
    60  
    61  func TestEscapeHTML(t *testing.T) {
    62  	dir := t.TempDir()
    63  	s := testutil.DirectoryInput(t, dir)
    64  	s.Artifacts.Packages.Add(pkg.Package{
    65  		Name: "<html-package>",
    66  	})
    67  
    68  	// by default we do not escape HTML
    69  	t.Run("default", func(t *testing.T) {
    70  		cfg := DefaultEncoderConfig()
    71  
    72  		enc, err := NewFormatEncoderWithConfig(cfg)
    73  		require.NoError(t, err)
    74  
    75  		var buffer bytes.Buffer
    76  		err = enc.Encode(&buffer, s)
    77  		require.NoError(t, err)
    78  
    79  		actual := buffer.String()
    80  		assert.Contains(t, actual, "<html-package>")
    81  		assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
    82  	})
    83  
    84  }
    85  
    86  func TestSPDXJSONDirectoryEncoder(t *testing.T) {
    87  	dir := t.TempDir()
    88  	testutil.AssertEncoderAgainstGoldenSnapshot(t,
    89  		testutil.EncoderSnapshotTestConfig{
    90  			Subject:                     testutil.DirectoryInput(t, dir),
    91  			Format:                      getEncoder(t),
    92  			UpdateSnapshot:              *updateSnapshot,
    93  			PersistRedactionsInSnapshot: true,
    94  			IsJSON:                      true,
    95  			Redactor:                    redactor(dir),
    96  		},
    97  	)
    98  }
    99  
   100  func TestSPDXJSONImageEncoder(t *testing.T) {
   101  	testImage := "image-simple"
   102  	testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
   103  		testutil.ImageSnapshotTestConfig{
   104  			Image:               testImage,
   105  			UpdateImageSnapshot: *updateImage,
   106  		},
   107  		testutil.EncoderSnapshotTestConfig{
   108  			Subject:                     testutil.ImageInput(t, testImage, testutil.FromSnapshot()),
   109  			Format:                      getEncoder(t),
   110  			UpdateSnapshot:              *updateSnapshot,
   111  			PersistRedactionsInSnapshot: true,
   112  			IsJSON:                      true,
   113  			Redactor:                    redactor(),
   114  		},
   115  	)
   116  }
   117  
   118  func TestSPDX22JSONRequredProperties(t *testing.T) {
   119  	cfg := DefaultEncoderConfig()
   120  	cfg.Pretty = true
   121  	cfg.Version = "2.2"
   122  
   123  	enc, err := NewFormatEncoderWithConfig(cfg)
   124  	require.NoError(t, err)
   125  
   126  	coords := file.Coordinates{
   127  		RealPath:     "/some/file",
   128  		FileSystemID: "ac897d978b6c38749a1",
   129  	}
   130  
   131  	p1 := pkg.Package{
   132  		Name:      "files-analyzed-true",
   133  		Version:   "v1",
   134  		Locations: file.NewLocationSet(file.NewLocation(coords.RealPath)),
   135  		Licenses:  pkg.LicenseSet{},
   136  		Language:  pkg.Java,
   137  		Metadata: pkg.JavaArchive{
   138  			ArchiveDigests: []file.Digest{
   139  				{
   140  					Algorithm: "sha256",
   141  					Value:     "a9b87321a9879c79d87987987a97c97b9789ce978dffea987",
   142  				},
   143  			},
   144  			Parent: nil,
   145  		},
   146  	}
   147  	p1.SetID()
   148  
   149  	p2 := pkg.Package{
   150  		Name:    "files-analyzed-false",
   151  		Version: "v2",
   152  	}
   153  	p2.SetID()
   154  
   155  	testutil.AssertEncoderAgainstGoldenSnapshot(t,
   156  		testutil.EncoderSnapshotTestConfig{
   157  			Subject: sbom.SBOM{
   158  				Artifacts: sbom.Artifacts{
   159  					Packages: pkg.NewCollection(p1, p2),
   160  					FileDigests: map[file.Coordinates][]file.Digest{
   161  						coords: {
   162  							{
   163  								Algorithm: "sha1",
   164  								Value:     "3b4ab96c371d913e2a88c269844b6c5fb5cbe761",
   165  							},
   166  						},
   167  					},
   168  				},
   169  				Relationships: []artifact.Relationship{
   170  					{
   171  						From: p1,
   172  						To:   coords,
   173  						Type: artifact.ContainsRelationship,
   174  					},
   175  				},
   176  			},
   177  			Format:                      enc,
   178  			UpdateSnapshot:              *updateSnapshot,
   179  			PersistRedactionsInSnapshot: true,
   180  			IsJSON:                      true,
   181  			Redactor:                    redactor(),
   182  		},
   183  	)
   184  }
   185  
   186  func TestSPDXRelationshipOrder(t *testing.T) {
   187  	testImage := "image-simple"
   188  
   189  	s := testutil.ImageInput(t, testImage, testutil.FromSnapshot())
   190  	testutil.AddSampleFileRelationships(&s)
   191  
   192  	testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
   193  		testutil.ImageSnapshotTestConfig{
   194  			Image:               testImage,
   195  			UpdateImageSnapshot: *updateImage,
   196  		},
   197  		testutil.EncoderSnapshotTestConfig{
   198  			Subject:                     s,
   199  			Format:                      getEncoder(t),
   200  			UpdateSnapshot:              *updateSnapshot,
   201  			PersistRedactionsInSnapshot: true,
   202  			IsJSON:                      true,
   203  			Redactor:                    redactor(),
   204  		},
   205  	)
   206  }
   207  
   208  func redactor(values ...string) testutil.Redactor {
   209  	return testutil.NewRedactions().
   210  		WithValuesRedacted(values...).
   211  		WithPatternRedactors(
   212  			map[string]string{
   213  				// each SBOM reports the time it was generated, which is not useful during snapshot testing
   214  				`"created":\s+"[^"]*"`: `"created":"redacted"`,
   215  
   216  				// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
   217  				`"documentNamespace":\s+"[^"]*"`: `"documentNamespace":"redacted"`,
   218  
   219  				// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
   220  				`"licenseListVersion":\s+"[^"]*"`: `"licenseListVersion":"redacted"`,
   221  			},
   222  		)
   223  }
   224  
   225  func TestSupportedVersions(t *testing.T) {
   226  	encs := defaultFormatEncoders()
   227  	require.NotEmpty(t, encs)
   228  
   229  	versions := SupportedVersions()
   230  	require.Equal(t, len(versions), len(encs))
   231  
   232  	subject := testutil.DirectoryInput(t, t.TempDir())
   233  	dec := NewFormatDecoder()
   234  
   235  	relationshipOffsetPerVersion := map[string]int{
   236  		// the package representing the source gets a relationship from the source package to all other packages found
   237  		// these relationships cannot be removed until the primaryPackagePurpose info is available in 2.3
   238  		"2.1": 2,
   239  		"2.2": 2,
   240  		// the source-to-package relationships can be removed since the primaryPackagePurpose info is available in 2.3
   241  		"2.3": 0,
   242  	}
   243  
   244  	pkgCountOffsetPerVersion := map[string]int{
   245  		"2.1": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3
   246  		"2.2": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3
   247  		"2.3": 0, // the source package can be removed since the primaryPackagePurpose info is available
   248  	}
   249  
   250  	for _, enc := range encs {
   251  		t.Run(enc.Version(), func(t *testing.T) {
   252  			require.Contains(t, versions, enc.Version())
   253  
   254  			var buf bytes.Buffer
   255  			require.NoError(t, enc.Encode(&buf, subject))
   256  
   257  			id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
   258  			assert.Equal(t, enc.ID(), id)
   259  			assert.Equal(t, enc.Version(), version)
   260  
   261  			var s *sbom.SBOM
   262  			var err error
   263  			s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
   264  			require.NoError(t, err)
   265  
   266  			assert.Equal(t, enc.ID(), id)
   267  			assert.Equal(t, enc.Version(), version)
   268  
   269  			require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
   270  
   271  			offset := relationshipOffsetPerVersion[enc.Version()]
   272  
   273  			assert.Equal(t, len(subject.Relationships)+offset, len(s.Relationships), "mismatched relationship count")
   274  
   275  			offset = pkgCountOffsetPerVersion[enc.Version()]
   276  
   277  			if !assert.Equal(t, subject.Artifacts.Packages.PackageCount()+offset, s.Artifacts.Packages.PackageCount(), "mismatched package count") {
   278  				t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
   279  				for _, p := range subject.Artifacts.Packages.Sorted() {
   280  					t.Logf("  - %s", p.String())
   281  				}
   282  				t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
   283  				for _, p := range s.Artifacts.Packages.Sorted() {
   284  					t.Logf("  - %s", p.String())
   285  				}
   286  			}
   287  		})
   288  	}
   289  }
   290  
   291  func defaultFormatEncoders() []sbom.FormatEncoder {
   292  	var encs []sbom.FormatEncoder
   293  	for _, version := range SupportedVersions() {
   294  		enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
   295  		if err != nil {
   296  			panic(err)
   297  		}
   298  		encs = append(encs, enc)
   299  	}
   300  	return encs
   301  }