github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/spdxjson/encoder_test.go (about) 1 package spdxjson 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 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/format/internal/spdxutil" 15 "github.com/anchore/syft/syft/format/internal/testutil" 16 "github.com/anchore/syft/syft/pkg" 17 "github.com/anchore/syft/syft/sbom" 18 ) 19 20 var updateSnapshot = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders") 21 var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing") 22 23 func getEncoder(t testing.TB) sbom.FormatEncoder { 24 cfg := DefaultEncoderConfig() 25 cfg.Pretty = true 26 27 enc, err := NewFormatEncoderWithConfig(cfg) 28 require.NoError(t, err) 29 return enc 30 } 31 32 func TestPrettyOutput(t *testing.T) { 33 run := func(opt bool) string { 34 enc, err := NewFormatEncoderWithConfig(EncoderConfig{ 35 Version: spdxutil.DefaultVersion, 36 Pretty: opt, 37 }) 38 require.NoError(t, err) 39 40 dir := t.TempDir() 41 s := testutil.DirectoryInput(t, dir) 42 43 var buffer bytes.Buffer 44 err = enc.Encode(&buffer, s) 45 require.NoError(t, err) 46 47 return strings.TrimSpace(buffer.String()) 48 } 49 50 t.Run("pretty", func(t *testing.T) { 51 actual := run(true) 52 assert.Contains(t, actual, "\n") 53 }) 54 55 t.Run("compact", func(t *testing.T) { 56 actual := run(false) 57 assert.NotContains(t, actual, "\n") 58 }) 59 } 60 61 func TestEscapeHTML(t *testing.T) { 62 dir := t.TempDir() 63 s := testutil.DirectoryInput(t, dir) 64 s.Artifacts.Packages.Add(pkg.Package{ 65 Name: "<html-package>", 66 }) 67 68 // by default we do not escape HTML 69 t.Run("default", func(t *testing.T) { 70 cfg := DefaultEncoderConfig() 71 72 enc, err := NewFormatEncoderWithConfig(cfg) 73 require.NoError(t, err) 74 75 var buffer bytes.Buffer 76 err = enc.Encode(&buffer, s) 77 require.NoError(t, err) 78 79 actual := buffer.String() 80 assert.Contains(t, actual, "<html-package>") 81 assert.NotContains(t, actual, "\\u003chtml-package\\u003e") 82 }) 83 84 } 85 86 func TestSPDXJSONDirectoryEncoder(t *testing.T) { 87 dir := t.TempDir() 88 testutil.AssertEncoderAgainstGoldenSnapshot(t, 89 testutil.EncoderSnapshotTestConfig{ 90 Subject: testutil.DirectoryInput(t, dir), 91 Format: getEncoder(t), 92 UpdateSnapshot: *updateSnapshot, 93 PersistRedactionsInSnapshot: true, 94 IsJSON: true, 95 Redactor: redactor(dir), 96 }, 97 ) 98 } 99 100 func TestSPDXJSONImageEncoder(t *testing.T) { 101 testImage := "image-simple" 102 testutil.AssertEncoderAgainstGoldenImageSnapshot(t, 103 testutil.ImageSnapshotTestConfig{ 104 Image: testImage, 105 UpdateImageSnapshot: *updateImage, 106 }, 107 testutil.EncoderSnapshotTestConfig{ 108 Subject: testutil.ImageInput(t, testImage, testutil.FromSnapshot()), 109 Format: getEncoder(t), 110 UpdateSnapshot: *updateSnapshot, 111 PersistRedactionsInSnapshot: true, 112 IsJSON: true, 113 Redactor: redactor(), 114 }, 115 ) 116 } 117 118 func TestSPDX22JSONRequredProperties(t *testing.T) { 119 cfg := DefaultEncoderConfig() 120 cfg.Pretty = true 121 cfg.Version = "2.2" 122 123 enc, err := NewFormatEncoderWithConfig(cfg) 124 require.NoError(t, err) 125 126 coords := file.Coordinates{ 127 RealPath: "/some/file", 128 FileSystemID: "ac897d978b6c38749a1", 129 } 130 131 p1 := pkg.Package{ 132 Name: "files-analyzed-true", 133 Version: "v1", 134 Locations: file.NewLocationSet(file.NewLocation(coords.RealPath)), 135 Licenses: pkg.LicenseSet{}, 136 Language: pkg.Java, 137 Metadata: pkg.JavaArchive{ 138 ArchiveDigests: []file.Digest{ 139 { 140 Algorithm: "sha256", 141 Value: "a9b87321a9879c79d87987987a97c97b9789ce978dffea987", 142 }, 143 }, 144 Parent: nil, 145 }, 146 } 147 p1.SetID() 148 149 p2 := pkg.Package{ 150 Name: "files-analyzed-false", 151 Version: "v2", 152 } 153 p2.SetID() 154 155 testutil.AssertEncoderAgainstGoldenSnapshot(t, 156 testutil.EncoderSnapshotTestConfig{ 157 Subject: sbom.SBOM{ 158 Artifacts: sbom.Artifacts{ 159 Packages: pkg.NewCollection(p1, p2), 160 FileDigests: map[file.Coordinates][]file.Digest{ 161 coords: { 162 { 163 Algorithm: "sha1", 164 Value: "3b4ab96c371d913e2a88c269844b6c5fb5cbe761", 165 }, 166 }, 167 }, 168 }, 169 Relationships: []artifact.Relationship{ 170 { 171 From: p1, 172 To: coords, 173 Type: artifact.ContainsRelationship, 174 }, 175 }, 176 }, 177 Format: enc, 178 UpdateSnapshot: *updateSnapshot, 179 PersistRedactionsInSnapshot: true, 180 IsJSON: true, 181 Redactor: redactor(), 182 }, 183 ) 184 } 185 186 func TestSPDXRelationshipOrder(t *testing.T) { 187 testImage := "image-simple" 188 189 s := testutil.ImageInput(t, testImage, testutil.FromSnapshot()) 190 testutil.AddSampleFileRelationships(&s) 191 192 testutil.AssertEncoderAgainstGoldenImageSnapshot(t, 193 testutil.ImageSnapshotTestConfig{ 194 Image: testImage, 195 UpdateImageSnapshot: *updateImage, 196 }, 197 testutil.EncoderSnapshotTestConfig{ 198 Subject: s, 199 Format: getEncoder(t), 200 UpdateSnapshot: *updateSnapshot, 201 PersistRedactionsInSnapshot: true, 202 IsJSON: true, 203 Redactor: redactor(), 204 }, 205 ) 206 } 207 208 func redactor(values ...string) testutil.Redactor { 209 return testutil.NewRedactions(). 210 WithValuesRedacted(values...). 211 WithPatternRedactors( 212 map[string]string{ 213 // each SBOM reports the time it was generated, which is not useful during snapshot testing 214 `"created":\s+"[^"]*"`: `"created":"redacted"`, 215 216 // each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing 217 `"documentNamespace":\s+"[^"]*"`: `"documentNamespace":"redacted"`, 218 219 // the license list will be updated periodically, the value here should not be directly tested in snapshot tests 220 `"licenseListVersion":\s+"[^"]*"`: `"licenseListVersion":"redacted"`, 221 }, 222 ) 223 } 224 225 func TestSupportedVersions(t *testing.T) { 226 encs := defaultFormatEncoders() 227 require.NotEmpty(t, encs) 228 229 versions := SupportedVersions() 230 require.Equal(t, len(versions), len(encs)) 231 232 subject := testutil.DirectoryInput(t, t.TempDir()) 233 dec := NewFormatDecoder() 234 235 relationshipOffsetPerVersion := map[string]int{ 236 // the package representing the source gets a relationship from the source package to all other packages found 237 // these relationships cannot be removed until the primaryPackagePurpose info is available in 2.3 238 "2.1": 2, 239 "2.2": 2, 240 // the source-to-package relationships can be removed since the primaryPackagePurpose info is available in 2.3 241 "2.3": 0, 242 } 243 244 pkgCountOffsetPerVersion := map[string]int{ 245 "2.1": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3 246 "2.2": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3 247 "2.3": 0, // the source package can be removed since the primaryPackagePurpose info is available 248 } 249 250 for _, enc := range encs { 251 t.Run(enc.Version(), func(t *testing.T) { 252 require.Contains(t, versions, enc.Version()) 253 254 var buf bytes.Buffer 255 require.NoError(t, enc.Encode(&buf, subject)) 256 257 id, version := dec.Identify(bytes.NewReader(buf.Bytes())) 258 assert.Equal(t, enc.ID(), id) 259 assert.Equal(t, enc.Version(), version) 260 261 var s *sbom.SBOM 262 var err error 263 s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes())) 264 require.NoError(t, err) 265 266 assert.Equal(t, enc.ID(), id) 267 assert.Equal(t, enc.Version(), version) 268 269 require.NotEmpty(t, s.Artifacts.Packages.PackageCount()) 270 271 offset := relationshipOffsetPerVersion[enc.Version()] 272 273 assert.Equal(t, len(subject.Relationships)+offset, len(s.Relationships), "mismatched relationship count") 274 275 offset = pkgCountOffsetPerVersion[enc.Version()] 276 277 if !assert.Equal(t, subject.Artifacts.Packages.PackageCount()+offset, s.Artifacts.Packages.PackageCount(), "mismatched package count") { 278 t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount()) 279 for _, p := range subject.Artifacts.Packages.Sorted() { 280 t.Logf(" - %s", p.String()) 281 } 282 t.Logf("actual: %d", s.Artifacts.Packages.PackageCount()) 283 for _, p := range s.Artifacts.Packages.Sorted() { 284 t.Logf(" - %s", p.String()) 285 } 286 } 287 }) 288 } 289 } 290 291 func defaultFormatEncoders() []sbom.FormatEncoder { 292 var encs []sbom.FormatEncoder 293 for _, version := range SupportedVersions() { 294 enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version}) 295 if err != nil { 296 panic(err) 297 } 298 encs = append(encs, enc) 299 } 300 return encs 301 }