github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/syftjson/encoder_test.go (about)

     1  package syftjson
     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  	stereoFile "github.com/anchore/stereoscope/pkg/file"
    13  	"github.com/anchore/syft/syft/artifact"
    14  	"github.com/anchore/syft/syft/cpe"
    15  	"github.com/anchore/syft/syft/file"
    16  	"github.com/anchore/syft/syft/linux"
    17  	"github.com/anchore/syft/syft/pkg"
    18  	"github.com/anchore/syft/syft/sbom"
    19  	"github.com/anchore/syft/syft/source"
    20  	"github.com/lineaje-labs/syft/internal"
    21  	"github.com/lineaje-labs/syft/syft/format/internal/testutil"
    22  )
    23  
    24  var updateSnapshot = flag.Bool("update-json", false, "update the *.golden files for json encoders")
    25  var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
    26  
    27  func TestDefaultNameAndVersion(t *testing.T) {
    28  	expectedID, expectedVersion := ID, internal.JSONSchemaVersion
    29  	enc := NewFormatEncoder()
    30  	if enc.ID() != expectedID {
    31  		t.Errorf("expected ID %q, got %q", expectedID, enc.ID())
    32  	}
    33  
    34  	if enc.Version() != expectedVersion {
    35  		t.Errorf("expected version %q, got %q", expectedVersion, enc.Version())
    36  	}
    37  }
    38  
    39  func TestPrettyOutput(t *testing.T) {
    40  	run := func(opt bool) string {
    41  		enc, err := NewFormatEncoderWithConfig(EncoderConfig{
    42  			Pretty: opt,
    43  		})
    44  		require.NoError(t, err)
    45  
    46  		dir := t.TempDir()
    47  		s := testutil.DirectoryInput(t, dir)
    48  
    49  		var buffer bytes.Buffer
    50  		err = enc.Encode(&buffer, s)
    51  		require.NoError(t, err)
    52  
    53  		return strings.TrimSpace(buffer.String())
    54  	}
    55  
    56  	t.Run("pretty", func(t *testing.T) {
    57  		actual := run(true)
    58  		assert.Contains(t, actual, "\n")
    59  	})
    60  
    61  	t.Run("compact", func(t *testing.T) {
    62  		actual := run(false)
    63  		assert.NotContains(t, actual, "\n")
    64  	})
    65  }
    66  
    67  func TestEscapeHTML(t *testing.T) {
    68  	dir := t.TempDir()
    69  	s := testutil.DirectoryInput(t, dir)
    70  	s.Artifacts.Packages.Add(pkg.Package{
    71  		Name: "<html-package>",
    72  	})
    73  
    74  	// by default we do not escape HTML
    75  	t.Run("default", func(t *testing.T) {
    76  		cfg := DefaultEncoderConfig()
    77  
    78  		enc, err := NewFormatEncoderWithConfig(cfg)
    79  		require.NoError(t, err)
    80  
    81  		var buffer bytes.Buffer
    82  		err = enc.Encode(&buffer, s)
    83  		require.NoError(t, err)
    84  
    85  		actual := buffer.String()
    86  		assert.Contains(t, actual, "<html-package>")
    87  		assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
    88  	})
    89  }
    90  
    91  func TestDirectoryEncoder(t *testing.T) {
    92  	cfg := DefaultEncoderConfig()
    93  	cfg.Pretty = true
    94  	enc, err := NewFormatEncoderWithConfig(cfg)
    95  	require.NoError(t, err)
    96  
    97  	dir := t.TempDir()
    98  	testutil.AssertEncoderAgainstGoldenSnapshot(t,
    99  		testutil.EncoderSnapshotTestConfig{
   100  			Subject:                     testutil.DirectoryInput(t, dir),
   101  			Format:                      enc,
   102  			UpdateSnapshot:              *updateSnapshot,
   103  			PersistRedactionsInSnapshot: true,
   104  			IsJSON:                      true,
   105  			Redactor:                    redactor(dir),
   106  		},
   107  	)
   108  }
   109  
   110  func TestImageEncoder(t *testing.T) {
   111  	cfg := DefaultEncoderConfig()
   112  	cfg.Pretty = true
   113  	enc, err := NewFormatEncoderWithConfig(cfg)
   114  	require.NoError(t, err)
   115  
   116  	testImage := "image-simple"
   117  	testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
   118  		testutil.ImageSnapshotTestConfig{
   119  			Image:               testImage,
   120  			UpdateImageSnapshot: *updateImage,
   121  		},
   122  		testutil.EncoderSnapshotTestConfig{
   123  			Subject:                     testutil.ImageInput(t, testImage, testutil.FromSnapshot()),
   124  			Format:                      enc,
   125  			UpdateSnapshot:              *updateSnapshot,
   126  			PersistRedactionsInSnapshot: true,
   127  			IsJSON:                      true,
   128  			Redactor:                    redactor(),
   129  		},
   130  	)
   131  }
   132  
   133  func TestEncodeFullJSONDocument(t *testing.T) {
   134  	catalog := pkg.NewCollection()
   135  
   136  	p1 := pkg.Package{
   137  		Name:    "package-1",
   138  		Version: "1.0.1",
   139  		Locations: file.NewLocationSet(
   140  			file.NewLocationFromCoordinates(file.Coordinates{
   141  				RealPath: "/a/place/a",
   142  			}),
   143  		),
   144  		Type:     pkg.PythonPkg,
   145  		FoundBy:  "the-cataloger-1",
   146  		Language: pkg.Python,
   147  		Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
   148  		Metadata: pkg.PythonPackage{
   149  			Name:    "package-1",
   150  			Version: "1.0.1",
   151  			Files:   []pkg.PythonFileRecord{},
   152  		},
   153  		PURL: "a-purl-1",
   154  		CPEs: []cpe.CPE{
   155  			cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
   156  		},
   157  	}
   158  
   159  	p2 := pkg.Package{
   160  		Name:    "package-2",
   161  		Version: "2.0.1",
   162  		Locations: file.NewLocationSet(
   163  			file.NewLocationFromCoordinates(file.Coordinates{
   164  				RealPath: "/b/place/b",
   165  			}),
   166  		),
   167  		Type:    pkg.DebPkg,
   168  		FoundBy: "the-cataloger-2",
   169  		Metadata: pkg.DpkgDBEntry{
   170  			Package: "package-2",
   171  			Version: "2.0.1",
   172  			Files:   []pkg.DpkgFileRecord{},
   173  		},
   174  		PURL: "a-purl-2",
   175  		CPEs: []cpe.CPE{
   176  			cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
   177  		},
   178  	}
   179  
   180  	catalog.Add(p1)
   181  	catalog.Add(p2)
   182  
   183  	s := sbom.SBOM{
   184  		Artifacts: sbom.Artifacts{
   185  			Packages: catalog,
   186  			FileMetadata: map[file.Coordinates]file.Metadata{
   187  				file.NewVirtualLocation("/a/place", "/a/symlink/to/place").Coordinates: {
   188  					FileInfo: stereoFile.ManualInfo{
   189  						NameValue: "/a/place",
   190  						ModeValue: 0775,
   191  					},
   192  					Type:    stereoFile.TypeDirectory,
   193  					UserID:  0,
   194  					GroupID: 0,
   195  				},
   196  				file.NewLocation("/a/place/a").Coordinates: {
   197  					FileInfo: stereoFile.ManualInfo{
   198  						NameValue: "/a/place/a",
   199  						ModeValue: 0775,
   200  					},
   201  					Type:    stereoFile.TypeRegular,
   202  					UserID:  0,
   203  					GroupID: 0,
   204  				},
   205  				file.NewLocation("/b").Coordinates: {
   206  					FileInfo: stereoFile.ManualInfo{
   207  						NameValue: "/b",
   208  						ModeValue: 0775,
   209  					},
   210  					Type:            stereoFile.TypeSymLink,
   211  					LinkDestination: "/c",
   212  					UserID:          0,
   213  					GroupID:         0,
   214  				},
   215  				file.NewLocation("/b/place/b").Coordinates: {
   216  					FileInfo: stereoFile.ManualInfo{
   217  						NameValue: "/b/place/b",
   218  						ModeValue: 0644,
   219  					},
   220  					Type:    stereoFile.TypeRegular,
   221  					UserID:  1,
   222  					GroupID: 2,
   223  				},
   224  			},
   225  			FileDigests: map[file.Coordinates][]file.Digest{
   226  				file.NewLocation("/a/place/a").Coordinates: {
   227  					{
   228  						Algorithm: "sha256",
   229  						Value:     "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
   230  					},
   231  				},
   232  				file.NewLocation("/b/place/b").Coordinates: {
   233  					{
   234  						Algorithm: "sha256",
   235  						Value:     "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c",
   236  					},
   237  				},
   238  			},
   239  			FileContents: map[file.Coordinates]string{
   240  				file.NewLocation("/a/place/a").Coordinates: "the-contents",
   241  			},
   242  			LinuxDistribution: &linux.Release{
   243  				ID:        "redhat",
   244  				Version:   "7",
   245  				VersionID: "7",
   246  				IDLike: []string{
   247  					"rhel",
   248  				},
   249  			},
   250  		},
   251  		Relationships: []artifact.Relationship{
   252  			{
   253  				From: p1,
   254  				To:   p2,
   255  				Type: artifact.OwnershipByFileOverlapRelationship,
   256  				Data: map[string]string{
   257  					"file": "path",
   258  				},
   259  			},
   260  		},
   261  		Source: source.Description{
   262  			ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
   263  			Metadata: source.StereoscopeImageSourceMetadata{
   264  				UserInput:      "user-image-input",
   265  				ID:             "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
   266  				ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
   267  				MediaType:      "application/vnd.docker.distribution.manifest.v2+json",
   268  				Tags: []string{
   269  					"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
   270  				},
   271  				Size: 38,
   272  				Layers: []source.StereoscopeLayerMetadata{
   273  					{
   274  						MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
   275  						Digest:    "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
   276  						Size:      22,
   277  					},
   278  					{
   279  						MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
   280  						Digest:    "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
   281  						Size:      16,
   282  					},
   283  				},
   284  				RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."),
   285  				RawConfig:   []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."),
   286  				RepoDigests: []string{},
   287  			},
   288  		},
   289  		Descriptor: sbom.Descriptor{
   290  			Name:    "syft",
   291  			Version: "v0.42.0-bogus",
   292  			// the application configuration should be persisted here, however, we do not want to import
   293  			// the application configuration in this package (it's reserved only for ingestion by the cmd package)
   294  			Configuration: map[string]string{
   295  				"config-key": "config-value",
   296  			},
   297  		},
   298  	}
   299  
   300  	testutil.AssertEncoderAgainstGoldenSnapshot(t,
   301  		testutil.EncoderSnapshotTestConfig{
   302  			Subject:                     s,
   303  			Format:                      NewFormatEncoder(),
   304  			UpdateSnapshot:              *updateSnapshot,
   305  			PersistRedactionsInSnapshot: true,
   306  			IsJSON:                      true,
   307  			Redactor:                    redactor(),
   308  		},
   309  	)
   310  }
   311  
   312  func redactor(values ...string) testutil.Redactor {
   313  	return testutil.NewRedactions().
   314  		WithValuesRedacted(values...).
   315  		WithPatternRedactors(
   316  			map[string]string{
   317  				// remove schema version (don't even show the key or value)
   318  				`,?\s*"schema":\s*\{[^}]*}`: "",
   319  			},
   320  		)
   321  }