github.com/anchore/syft@v1.38.2/syft/format/syftjson/encoder_test.go (about)

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