github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/internal/testutils/utils.go (about) 1 package testutils 2 3 import ( 4 "bytes" 5 "math/rand" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/nextlinux/gosbom/gosbom/artifact" 11 "github.com/nextlinux/gosbom/gosbom/cpe" 12 "github.com/nextlinux/gosbom/gosbom/file" 13 "github.com/nextlinux/gosbom/gosbom/linux" 14 "github.com/nextlinux/gosbom/gosbom/pkg" 15 "github.com/nextlinux/gosbom/gosbom/sbom" 16 "github.com/nextlinux/gosbom/gosbom/source" 17 "github.com/sergi/go-diff/diffmatchpatch" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 21 "github.com/anchore/go-testutils" 22 "github.com/anchore/stereoscope/pkg/filetree" 23 "github.com/anchore/stereoscope/pkg/image" 24 "github.com/anchore/stereoscope/pkg/imagetest" 25 ) 26 27 type redactor func(s []byte) []byte 28 29 type imageCfg struct { 30 fromSnapshot bool 31 } 32 33 type ImageOption func(cfg *imageCfg) 34 35 func FromSnapshot() ImageOption { 36 return func(cfg *imageCfg) { 37 cfg.fromSnapshot = true 38 } 39 } 40 41 func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, json bool, redactors ...redactor) { 42 var buffer bytes.Buffer 43 44 // grab the latest image contents and persist 45 if updateSnapshot { 46 imagetest.UpdateGoldenFixtureImage(t, testImage) 47 } 48 49 err := format.Encode(&buffer, sbom) 50 assert.NoError(t, err) 51 actual := buffer.Bytes() 52 53 // replace the expected snapshot contents with the current encoder contents 54 if updateSnapshot { 55 testutils.UpdateGoldenFileContents(t, actual) 56 } 57 58 actual = redact(actual, redactors...) 59 expected := redact(testutils.GetGoldenFileContents(t), redactors...) 60 61 if json { 62 require.JSONEq(t, string(expected), string(actual)) 63 } else if !bytes.Equal(expected, actual) { 64 // assert that the golden file snapshot matches the actual contents 65 dmp := diffmatchpatch.New() 66 diffs := dmp.DiffMain(string(expected), string(actual), true) 67 t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) 68 } 69 } 70 71 func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, json bool, redactors ...redactor) { 72 var buffer bytes.Buffer 73 74 err := format.Encode(&buffer, sbom) 75 assert.NoError(t, err) 76 actual := buffer.Bytes() 77 78 // replace the expected snapshot contents with the current encoder contents 79 if updateSnapshot { 80 testutils.UpdateGoldenFileContents(t, actual) 81 } 82 83 actual = redact(actual, redactors...) 84 expected := redact(testutils.GetGoldenFileContents(t), redactors...) 85 86 if json { 87 require.JSONEq(t, string(expected), string(actual)) 88 } else if !bytes.Equal(expected, actual) { 89 dmp := diffmatchpatch.New() 90 diffs := dmp.DiffMain(string(expected), string(actual), true) 91 t.Logf("len: %d\nexpected: %s", len(expected), expected) 92 t.Logf("len: %d\nactual: %s", len(actual), actual) 93 t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) 94 } 95 } 96 97 func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM { 98 t.Helper() 99 catalog := pkg.NewCollection() 100 var cfg imageCfg 101 var img *image.Image 102 for _, opt := range options { 103 opt(&cfg) 104 } 105 106 switch cfg.fromSnapshot { 107 case true: 108 img = imagetest.GetGoldenFixtureImage(t, testImage) 109 default: 110 img = imagetest.GetFixtureImage(t, "docker-archive", testImage) 111 } 112 113 populateImageCatalog(catalog, img) 114 115 // this is a hard coded value that is not given by the fixture helper and must be provided manually 116 img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" 117 118 src, err := source.NewFromImage(img, "user-image-input") 119 assert.NoError(t, err) 120 121 return sbom.SBOM{ 122 Artifacts: sbom.Artifacts{ 123 Packages: catalog, 124 LinuxDistribution: &linux.Release{ 125 PrettyName: "debian", 126 Name: "debian", 127 ID: "debian", 128 IDLike: []string{"like!"}, 129 Version: "1.2.3", 130 VersionID: "1.2.3", 131 }, 132 }, 133 Source: src.Metadata, 134 Descriptor: sbom.Descriptor{ 135 Name: "gosbom", 136 Version: "v0.42.0-bogus", 137 // the application configuration should be persisted here, however, we do not want to import 138 // the application configuration in this package (it's reserved only for ingestion by the cmd package) 139 Configuration: map[string]string{ 140 "config-key": "config-value", 141 }, 142 }, 143 } 144 } 145 146 func carriageRedactor(s []byte) []byte { 147 msg := strings.ReplaceAll(string(s), "\r\n", "\n") 148 return []byte(msg) 149 } 150 151 func populateImageCatalog(catalog *pkg.Collection, img *image.Image) { 152 _, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks) 153 _, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks) 154 155 // populate catalog with test data 156 catalog.Add(pkg.Package{ 157 Name: "package-1", 158 Version: "1.0.1", 159 Locations: file.NewLocationSet( 160 file.NewLocationFromImage(string(ref1.RealPath), *ref1.Reference, img), 161 ), 162 Type: pkg.PythonPkg, 163 FoundBy: "the-cataloger-1", 164 Language: pkg.Python, 165 MetadataType: pkg.PythonPackageMetadataType, 166 Licenses: pkg.NewLicenseSet( 167 pkg.NewLicense("MIT"), 168 ), 169 Metadata: pkg.PythonPackageMetadata{ 170 Name: "package-1", 171 Version: "1.0.1", 172 }, 173 PURL: "a-purl-1", // intentionally a bad pURL for test fixtures 174 CPEs: []cpe.CPE{ 175 cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), 176 }, 177 }) 178 catalog.Add(pkg.Package{ 179 Name: "package-2", 180 Version: "2.0.1", 181 Locations: file.NewLocationSet( 182 file.NewLocationFromImage(string(ref2.RealPath), *ref2.Reference, img), 183 ), 184 Type: pkg.DebPkg, 185 FoundBy: "the-cataloger-2", 186 MetadataType: pkg.DpkgMetadataType, 187 Metadata: pkg.DpkgMetadata{ 188 Package: "package-2", 189 Version: "2.0.1", 190 }, 191 PURL: "pkg:deb/debian/package-2@2.0.1", 192 CPEs: []cpe.CPE{ 193 cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), 194 }, 195 }) 196 } 197 198 func DirectoryInput(t testing.TB) sbom.SBOM { 199 catalog := newDirectoryCatalog() 200 201 src, err := source.NewFromDirectory("/some/path") 202 assert.NoError(t, err) 203 204 return sbom.SBOM{ 205 Artifacts: sbom.Artifacts{ 206 Packages: catalog, 207 LinuxDistribution: &linux.Release{ 208 PrettyName: "debian", 209 Name: "debian", 210 ID: "debian", 211 IDLike: []string{"like!"}, 212 Version: "1.2.3", 213 VersionID: "1.2.3", 214 }, 215 }, 216 Source: src.Metadata, 217 Descriptor: sbom.Descriptor{ 218 Name: "gosbom", 219 Version: "v0.42.0-bogus", 220 // the application configuration should be persisted here, however, we do not want to import 221 // the application configuration in this package (it's reserved only for ingestion by the cmd package) 222 Configuration: map[string]string{ 223 "config-key": "config-value", 224 }, 225 }, 226 } 227 } 228 229 func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM { 230 catalog := newDirectoryCatalogWithAuthorField() 231 232 src, err := source.NewFromDirectory("/some/path") 233 assert.NoError(t, err) 234 235 return sbom.SBOM{ 236 Artifacts: sbom.Artifacts{ 237 Packages: catalog, 238 LinuxDistribution: &linux.Release{ 239 PrettyName: "debian", 240 Name: "debian", 241 ID: "debian", 242 IDLike: []string{"like!"}, 243 Version: "1.2.3", 244 VersionID: "1.2.3", 245 }, 246 }, 247 Source: src.Metadata, 248 Descriptor: sbom.Descriptor{ 249 Name: "gosbom", 250 Version: "v0.42.0-bogus", 251 // the application configuration should be persisted here, however, we do not want to import 252 // the application configuration in this package (it's reserved only for ingestion by the cmd package) 253 Configuration: map[string]string{ 254 "config-key": "config-value", 255 }, 256 }, 257 } 258 } 259 260 func newDirectoryCatalog() *pkg.Collection { 261 catalog := pkg.NewCollection() 262 263 // populate catalog with test data 264 catalog.Add(pkg.Package{ 265 Name: "package-1", 266 Version: "1.0.1", 267 Type: pkg.PythonPkg, 268 FoundBy: "the-cataloger-1", 269 Locations: file.NewLocationSet( 270 file.NewLocation("/some/path/pkg1"), 271 ), 272 Language: pkg.Python, 273 MetadataType: pkg.PythonPackageMetadataType, 274 Licenses: pkg.NewLicenseSet( 275 pkg.NewLicense("MIT"), 276 ), 277 Metadata: pkg.PythonPackageMetadata{ 278 Name: "package-1", 279 Version: "1.0.1", 280 Files: []pkg.PythonFileRecord{ 281 { 282 Path: "/some/path/pkg1/dependencies/foo", 283 }, 284 }, 285 }, 286 PURL: "a-purl-2", // intentionally a bad pURL for test fixtures 287 CPEs: []cpe.CPE{ 288 cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), 289 }, 290 }) 291 catalog.Add(pkg.Package{ 292 Name: "package-2", 293 Version: "2.0.1", 294 Type: pkg.DebPkg, 295 FoundBy: "the-cataloger-2", 296 Locations: file.NewLocationSet( 297 file.NewLocation("/some/path/pkg1"), 298 ), 299 MetadataType: pkg.DpkgMetadataType, 300 Metadata: pkg.DpkgMetadata{ 301 Package: "package-2", 302 Version: "2.0.1", 303 }, 304 PURL: "pkg:deb/debian/package-2@2.0.1", 305 CPEs: []cpe.CPE{ 306 cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), 307 }, 308 }) 309 310 return catalog 311 } 312 313 func newDirectoryCatalogWithAuthorField() *pkg.Collection { 314 catalog := pkg.NewCollection() 315 316 // populate catalog with test data 317 catalog.Add(pkg.Package{ 318 Name: "package-1", 319 Version: "1.0.1", 320 Type: pkg.PythonPkg, 321 FoundBy: "the-cataloger-1", 322 Locations: file.NewLocationSet( 323 file.NewLocation("/some/path/pkg1"), 324 ), 325 Language: pkg.Python, 326 MetadataType: pkg.PythonPackageMetadataType, 327 Licenses: pkg.NewLicenseSet( 328 pkg.NewLicense("MIT"), 329 ), 330 Metadata: pkg.PythonPackageMetadata{ 331 Name: "package-1", 332 Version: "1.0.1", 333 Author: "test-author", 334 Files: []pkg.PythonFileRecord{ 335 { 336 Path: "/some/path/pkg1/dependencies/foo", 337 }, 338 }, 339 }, 340 PURL: "a-purl-2", // intentionally a bad pURL for test fixtures 341 CPEs: []cpe.CPE{ 342 cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), 343 }, 344 }) 345 catalog.Add(pkg.Package{ 346 Name: "package-2", 347 Version: "2.0.1", 348 Type: pkg.DebPkg, 349 FoundBy: "the-cataloger-2", 350 Locations: file.NewLocationSet( 351 file.NewLocation("/some/path/pkg1"), 352 ), 353 MetadataType: pkg.DpkgMetadataType, 354 Metadata: pkg.DpkgMetadata{ 355 Package: "package-2", 356 Version: "2.0.1", 357 }, 358 PURL: "pkg:deb/debian/package-2@2.0.1", 359 CPEs: []cpe.CPE{ 360 cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), 361 }, 362 }) 363 364 return catalog 365 } 366 367 //nolint:gosec 368 func AddSampleFileRelationships(s *sbom.SBOM) { 369 catalog := s.Artifacts.Packages.Sorted() 370 s.Artifacts.FileMetadata = map[file.Coordinates]file.Metadata{} 371 372 files := []string{"/f1", "/f2", "/d1/f3", "/d2/f4", "/z1/f5", "/a1/f6"} 373 rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 374 rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) 375 376 for _, f := range files { 377 meta := file.Metadata{} 378 coords := file.Coordinates{RealPath: f} 379 s.Artifacts.FileMetadata[coords] = meta 380 381 s.Relationships = append(s.Relationships, artifact.Relationship{ 382 From: catalog[0], 383 To: coords, 384 Type: artifact.ContainsRelationship, 385 }) 386 } 387 } 388 389 // remove dynamic values, which should be tested independently 390 func redact(b []byte, redactors ...redactor) []byte { 391 redactors = append(redactors, carriageRedactor) 392 for _, r := range redactors { 393 b = r(b) 394 } 395 return b 396 }