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 }