github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/internal/testutils/utils.go (about)

     1  package testutils
     2  
     3  import (
     4  	"bytes"
     5  	"math/rand"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/nextlinux/gosbom/gosbom/artifact"
    11  	"github.com/nextlinux/gosbom/gosbom/cpe"
    12  	"github.com/nextlinux/gosbom/gosbom/file"
    13  	"github.com/nextlinux/gosbom/gosbom/linux"
    14  	"github.com/nextlinux/gosbom/gosbom/pkg"
    15  	"github.com/nextlinux/gosbom/gosbom/sbom"
    16  	"github.com/nextlinux/gosbom/gosbom/source"
    17  	"github.com/sergi/go-diff/diffmatchpatch"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	"github.com/anchore/go-testutils"
    22  	"github.com/anchore/stereoscope/pkg/filetree"
    23  	"github.com/anchore/stereoscope/pkg/image"
    24  	"github.com/anchore/stereoscope/pkg/imagetest"
    25  )
    26  
    27  type redactor func(s []byte) []byte
    28  
    29  type imageCfg struct {
    30  	fromSnapshot bool
    31  }
    32  
    33  type ImageOption func(cfg *imageCfg)
    34  
    35  func FromSnapshot() ImageOption {
    36  	return func(cfg *imageCfg) {
    37  		cfg.fromSnapshot = true
    38  	}
    39  }
    40  
    41  func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, json bool, redactors ...redactor) {
    42  	var buffer bytes.Buffer
    43  
    44  	// grab the latest image contents and persist
    45  	if updateSnapshot {
    46  		imagetest.UpdateGoldenFixtureImage(t, testImage)
    47  	}
    48  
    49  	err := format.Encode(&buffer, sbom)
    50  	assert.NoError(t, err)
    51  	actual := buffer.Bytes()
    52  
    53  	// replace the expected snapshot contents with the current encoder contents
    54  	if updateSnapshot {
    55  		testutils.UpdateGoldenFileContents(t, actual)
    56  	}
    57  
    58  	actual = redact(actual, redactors...)
    59  	expected := redact(testutils.GetGoldenFileContents(t), redactors...)
    60  
    61  	if json {
    62  		require.JSONEq(t, string(expected), string(actual))
    63  	} else if !bytes.Equal(expected, actual) {
    64  		// assert that the golden file snapshot matches the actual contents
    65  		dmp := diffmatchpatch.New()
    66  		diffs := dmp.DiffMain(string(expected), string(actual), true)
    67  		t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
    68  	}
    69  }
    70  
    71  func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, json bool, redactors ...redactor) {
    72  	var buffer bytes.Buffer
    73  
    74  	err := format.Encode(&buffer, sbom)
    75  	assert.NoError(t, err)
    76  	actual := buffer.Bytes()
    77  
    78  	// replace the expected snapshot contents with the current encoder contents
    79  	if updateSnapshot {
    80  		testutils.UpdateGoldenFileContents(t, actual)
    81  	}
    82  
    83  	actual = redact(actual, redactors...)
    84  	expected := redact(testutils.GetGoldenFileContents(t), redactors...)
    85  
    86  	if json {
    87  		require.JSONEq(t, string(expected), string(actual))
    88  	} else if !bytes.Equal(expected, actual) {
    89  		dmp := diffmatchpatch.New()
    90  		diffs := dmp.DiffMain(string(expected), string(actual), true)
    91  		t.Logf("len: %d\nexpected: %s", len(expected), expected)
    92  		t.Logf("len: %d\nactual: %s", len(actual), actual)
    93  		t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
    94  	}
    95  }
    96  
    97  func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM {
    98  	t.Helper()
    99  	catalog := pkg.NewCollection()
   100  	var cfg imageCfg
   101  	var img *image.Image
   102  	for _, opt := range options {
   103  		opt(&cfg)
   104  	}
   105  
   106  	switch cfg.fromSnapshot {
   107  	case true:
   108  		img = imagetest.GetGoldenFixtureImage(t, testImage)
   109  	default:
   110  		img = imagetest.GetFixtureImage(t, "docker-archive", testImage)
   111  	}
   112  
   113  	populateImageCatalog(catalog, img)
   114  
   115  	// this is a hard coded value that is not given by the fixture helper and must be provided manually
   116  	img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
   117  
   118  	src, err := source.NewFromImage(img, "user-image-input")
   119  	assert.NoError(t, err)
   120  
   121  	return sbom.SBOM{
   122  		Artifacts: sbom.Artifacts{
   123  			Packages: catalog,
   124  			LinuxDistribution: &linux.Release{
   125  				PrettyName: "debian",
   126  				Name:       "debian",
   127  				ID:         "debian",
   128  				IDLike:     []string{"like!"},
   129  				Version:    "1.2.3",
   130  				VersionID:  "1.2.3",
   131  			},
   132  		},
   133  		Source: src.Metadata,
   134  		Descriptor: sbom.Descriptor{
   135  			Name:    "gosbom",
   136  			Version: "v0.42.0-bogus",
   137  			// the application configuration should be persisted here, however, we do not want to import
   138  			// the application configuration in this package (it's reserved only for ingestion by the cmd package)
   139  			Configuration: map[string]string{
   140  				"config-key": "config-value",
   141  			},
   142  		},
   143  	}
   144  }
   145  
   146  func carriageRedactor(s []byte) []byte {
   147  	msg := strings.ReplaceAll(string(s), "\r\n", "\n")
   148  	return []byte(msg)
   149  }
   150  
   151  func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
   152  	_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
   153  	_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
   154  
   155  	// populate catalog with test data
   156  	catalog.Add(pkg.Package{
   157  		Name:    "package-1",
   158  		Version: "1.0.1",
   159  		Locations: file.NewLocationSet(
   160  			file.NewLocationFromImage(string(ref1.RealPath), *ref1.Reference, img),
   161  		),
   162  		Type:         pkg.PythonPkg,
   163  		FoundBy:      "the-cataloger-1",
   164  		Language:     pkg.Python,
   165  		MetadataType: pkg.PythonPackageMetadataType,
   166  		Licenses: pkg.NewLicenseSet(
   167  			pkg.NewLicense("MIT"),
   168  		),
   169  		Metadata: pkg.PythonPackageMetadata{
   170  			Name:    "package-1",
   171  			Version: "1.0.1",
   172  		},
   173  		PURL: "a-purl-1", // intentionally a bad pURL for test fixtures
   174  		CPEs: []cpe.CPE{
   175  			cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
   176  		},
   177  	})
   178  	catalog.Add(pkg.Package{
   179  		Name:    "package-2",
   180  		Version: "2.0.1",
   181  		Locations: file.NewLocationSet(
   182  			file.NewLocationFromImage(string(ref2.RealPath), *ref2.Reference, img),
   183  		),
   184  		Type:         pkg.DebPkg,
   185  		FoundBy:      "the-cataloger-2",
   186  		MetadataType: pkg.DpkgMetadataType,
   187  		Metadata: pkg.DpkgMetadata{
   188  			Package: "package-2",
   189  			Version: "2.0.1",
   190  		},
   191  		PURL: "pkg:deb/debian/package-2@2.0.1",
   192  		CPEs: []cpe.CPE{
   193  			cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
   194  		},
   195  	})
   196  }
   197  
   198  func DirectoryInput(t testing.TB) sbom.SBOM {
   199  	catalog := newDirectoryCatalog()
   200  
   201  	src, err := source.NewFromDirectory("/some/path")
   202  	assert.NoError(t, err)
   203  
   204  	return sbom.SBOM{
   205  		Artifacts: sbom.Artifacts{
   206  			Packages: catalog,
   207  			LinuxDistribution: &linux.Release{
   208  				PrettyName: "debian",
   209  				Name:       "debian",
   210  				ID:         "debian",
   211  				IDLike:     []string{"like!"},
   212  				Version:    "1.2.3",
   213  				VersionID:  "1.2.3",
   214  			},
   215  		},
   216  		Source: src.Metadata,
   217  		Descriptor: sbom.Descriptor{
   218  			Name:    "gosbom",
   219  			Version: "v0.42.0-bogus",
   220  			// the application configuration should be persisted here, however, we do not want to import
   221  			// the application configuration in this package (it's reserved only for ingestion by the cmd package)
   222  			Configuration: map[string]string{
   223  				"config-key": "config-value",
   224  			},
   225  		},
   226  	}
   227  }
   228  
   229  func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM {
   230  	catalog := newDirectoryCatalogWithAuthorField()
   231  
   232  	src, err := source.NewFromDirectory("/some/path")
   233  	assert.NoError(t, err)
   234  
   235  	return sbom.SBOM{
   236  		Artifacts: sbom.Artifacts{
   237  			Packages: catalog,
   238  			LinuxDistribution: &linux.Release{
   239  				PrettyName: "debian",
   240  				Name:       "debian",
   241  				ID:         "debian",
   242  				IDLike:     []string{"like!"},
   243  				Version:    "1.2.3",
   244  				VersionID:  "1.2.3",
   245  			},
   246  		},
   247  		Source: src.Metadata,
   248  		Descriptor: sbom.Descriptor{
   249  			Name:    "gosbom",
   250  			Version: "v0.42.0-bogus",
   251  			// the application configuration should be persisted here, however, we do not want to import
   252  			// the application configuration in this package (it's reserved only for ingestion by the cmd package)
   253  			Configuration: map[string]string{
   254  				"config-key": "config-value",
   255  			},
   256  		},
   257  	}
   258  }
   259  
   260  func newDirectoryCatalog() *pkg.Collection {
   261  	catalog := pkg.NewCollection()
   262  
   263  	// populate catalog with test data
   264  	catalog.Add(pkg.Package{
   265  		Name:    "package-1",
   266  		Version: "1.0.1",
   267  		Type:    pkg.PythonPkg,
   268  		FoundBy: "the-cataloger-1",
   269  		Locations: file.NewLocationSet(
   270  			file.NewLocation("/some/path/pkg1"),
   271  		),
   272  		Language:     pkg.Python,
   273  		MetadataType: pkg.PythonPackageMetadataType,
   274  		Licenses: pkg.NewLicenseSet(
   275  			pkg.NewLicense("MIT"),
   276  		),
   277  		Metadata: pkg.PythonPackageMetadata{
   278  			Name:    "package-1",
   279  			Version: "1.0.1",
   280  			Files: []pkg.PythonFileRecord{
   281  				{
   282  					Path: "/some/path/pkg1/dependencies/foo",
   283  				},
   284  			},
   285  		},
   286  		PURL: "a-purl-2", // intentionally a bad pURL for test fixtures
   287  		CPEs: []cpe.CPE{
   288  			cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
   289  		},
   290  	})
   291  	catalog.Add(pkg.Package{
   292  		Name:    "package-2",
   293  		Version: "2.0.1",
   294  		Type:    pkg.DebPkg,
   295  		FoundBy: "the-cataloger-2",
   296  		Locations: file.NewLocationSet(
   297  			file.NewLocation("/some/path/pkg1"),
   298  		),
   299  		MetadataType: pkg.DpkgMetadataType,
   300  		Metadata: pkg.DpkgMetadata{
   301  			Package: "package-2",
   302  			Version: "2.0.1",
   303  		},
   304  		PURL: "pkg:deb/debian/package-2@2.0.1",
   305  		CPEs: []cpe.CPE{
   306  			cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
   307  		},
   308  	})
   309  
   310  	return catalog
   311  }
   312  
   313  func newDirectoryCatalogWithAuthorField() *pkg.Collection {
   314  	catalog := pkg.NewCollection()
   315  
   316  	// populate catalog with test data
   317  	catalog.Add(pkg.Package{
   318  		Name:    "package-1",
   319  		Version: "1.0.1",
   320  		Type:    pkg.PythonPkg,
   321  		FoundBy: "the-cataloger-1",
   322  		Locations: file.NewLocationSet(
   323  			file.NewLocation("/some/path/pkg1"),
   324  		),
   325  		Language:     pkg.Python,
   326  		MetadataType: pkg.PythonPackageMetadataType,
   327  		Licenses: pkg.NewLicenseSet(
   328  			pkg.NewLicense("MIT"),
   329  		),
   330  		Metadata: pkg.PythonPackageMetadata{
   331  			Name:    "package-1",
   332  			Version: "1.0.1",
   333  			Author:  "test-author",
   334  			Files: []pkg.PythonFileRecord{
   335  				{
   336  					Path: "/some/path/pkg1/dependencies/foo",
   337  				},
   338  			},
   339  		},
   340  		PURL: "a-purl-2", // intentionally a bad pURL for test fixtures
   341  		CPEs: []cpe.CPE{
   342  			cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
   343  		},
   344  	})
   345  	catalog.Add(pkg.Package{
   346  		Name:    "package-2",
   347  		Version: "2.0.1",
   348  		Type:    pkg.DebPkg,
   349  		FoundBy: "the-cataloger-2",
   350  		Locations: file.NewLocationSet(
   351  			file.NewLocation("/some/path/pkg1"),
   352  		),
   353  		MetadataType: pkg.DpkgMetadataType,
   354  		Metadata: pkg.DpkgMetadata{
   355  			Package: "package-2",
   356  			Version: "2.0.1",
   357  		},
   358  		PURL: "pkg:deb/debian/package-2@2.0.1",
   359  		CPEs: []cpe.CPE{
   360  			cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
   361  		},
   362  	})
   363  
   364  	return catalog
   365  }
   366  
   367  //nolint:gosec
   368  func AddSampleFileRelationships(s *sbom.SBOM) {
   369  	catalog := s.Artifacts.Packages.Sorted()
   370  	s.Artifacts.FileMetadata = map[file.Coordinates]file.Metadata{}
   371  
   372  	files := []string{"/f1", "/f2", "/d1/f3", "/d2/f4", "/z1/f5", "/a1/f6"}
   373  	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
   374  	rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] })
   375  
   376  	for _, f := range files {
   377  		meta := file.Metadata{}
   378  		coords := file.Coordinates{RealPath: f}
   379  		s.Artifacts.FileMetadata[coords] = meta
   380  
   381  		s.Relationships = append(s.Relationships, artifact.Relationship{
   382  			From: catalog[0],
   383  			To:   coords,
   384  			Type: artifact.ContainsRelationship,
   385  		})
   386  	}
   387  }
   388  
   389  // remove dynamic values, which should be tested independently
   390  func redact(b []byte, redactors ...redactor) []byte {
   391  	redactors = append(redactors, carriageRedactor)
   392  	for _, r := range redactors {
   393  		b = r(b)
   394  	}
   395  	return b
   396  }