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 }