github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/common/cyclonedxhelpers/decoder_test.go (about) 1 package cyclonedxhelpers 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "testing" 8 9 "github.com/CycloneDX/cyclonedx-go" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/anchore/syft/syft/artifact" 14 "github.com/anchore/syft/syft/pkg" 15 "github.com/anchore/syft/syft/sbom" 16 ) 17 18 func Test_decode(t *testing.T) { 19 type expected struct { 20 os string 21 pkg string 22 ver string 23 relation string 24 purl string 25 cpe string 26 } 27 tests := []struct { 28 name string 29 input cyclonedx.BOM 30 expected []expected 31 }{ 32 { 33 name: "basic mapping from cyclonedx", 34 input: cyclonedx.BOM{ 35 Metadata: nil, 36 Components: &[]cyclonedx.Component{ 37 { 38 BOMRef: "p1", 39 Type: cyclonedx.ComponentTypeLibrary, 40 Name: "package-1", 41 Version: "1.0.1", 42 Description: "", 43 Hashes: nil, 44 Licenses: &cyclonedx.Licenses{ 45 { 46 License: &cyclonedx.License{ 47 ID: "MIT", 48 }, 49 }, 50 }, 51 CPE: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", 52 PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", 53 ExternalReferences: &[]cyclonedx.ExternalReference{ 54 { 55 URL: "", 56 Comment: "", 57 Hashes: nil, 58 Type: "", 59 }, 60 }, 61 Properties: &[]cyclonedx.Property{ 62 { 63 Name: "foundBy", 64 Value: "the-cataloger-1", 65 }, 66 { 67 Name: "language", 68 Value: "python", 69 }, 70 { 71 Name: "type", 72 Value: "python", 73 }, 74 { 75 Name: "metadataType", 76 Value: "PythonPackageMetadata", 77 }, 78 { 79 Name: "path", 80 Value: "/some/path/pkg1", 81 }, 82 }, 83 Components: nil, 84 Evidence: nil, 85 }, 86 { 87 BOMRef: "p2", 88 Type: cyclonedx.ComponentTypeLibrary, 89 Name: "package-2", 90 Version: "2.0.2", 91 Hashes: nil, 92 Licenses: &cyclonedx.Licenses{ 93 { 94 License: &cyclonedx.License{ 95 ID: "MIT", 96 }, 97 }, 98 }, 99 CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*", 100 PackageURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2", 101 Properties: &[]cyclonedx.Property{ 102 103 { 104 Name: "foundBy", 105 Value: "apkdb-cataloger", 106 }, 107 { 108 Name: "type", 109 Value: "apk", 110 }, 111 { 112 Name: "metadataType", 113 Value: "ApkMetadata", 114 }, 115 { 116 Name: "path", 117 Value: "/lib/apk/db/installed", 118 }, 119 { 120 Name: "layerID", 121 Value: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", 122 }, 123 { 124 Name: "originPackage", 125 Value: "zlib", 126 }, 127 { 128 Name: "size", 129 Value: "51213", 130 }, 131 { 132 Name: "installedSize", 133 Value: "110592", 134 }, 135 { 136 Name: "pullDependencies", 137 Value: "so:libc.musl-x86_64.so.1", 138 }, 139 { 140 Name: "pullChecksum", 141 Value: "Q1uss4DfpvL16Nw2YUTwmzGBABz3Y=", 142 }, 143 { 144 Name: "gitCommitOfApkPort", 145 Value: "d2bfb22c8e8f67ad7d8d02704f35ec4d2a19f9b9", 146 }, 147 }, 148 }, 149 { 150 Type: cyclonedx.ComponentTypeOS, 151 Name: "debian", 152 Version: "1.2.3", 153 Hashes: nil, 154 Licenses: &cyclonedx.Licenses{ 155 { 156 License: &cyclonedx.License{ 157 ID: "MIT", 158 }, 159 }, 160 }, 161 Properties: &[]cyclonedx.Property{ 162 { 163 Name: "prettyName", 164 Value: "debian", 165 }, 166 { 167 Name: "id", 168 Value: "debian", 169 }, 170 { 171 Name: "versionID", 172 Value: "1.2.3", 173 }, 174 }, 175 Components: nil, 176 Evidence: nil, 177 }, 178 }, 179 Dependencies: &[]cyclonedx.Dependency{ 180 { 181 Ref: "p1", 182 Dependencies: &[]string{"p2"}, 183 }, 184 }, 185 }, 186 expected: []expected{ 187 { 188 os: "debian", 189 ver: "1.2.3", 190 }, 191 { 192 pkg: "package-1", 193 ver: "1.0.1", 194 cpe: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", 195 purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", 196 }, 197 { 198 pkg: "package-2", 199 ver: "2.0.2", 200 purl: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2", 201 relation: "package-1", 202 }, 203 }, 204 }, 205 } 206 for _, test := range tests { 207 t.Run(test.name, func(t *testing.T) { 208 sbom, err := ToSyftModel(&test.input) 209 assert.NoError(t, err) 210 211 test: 212 for _, e := range test.expected { 213 if e.os != "" { 214 assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID) 215 assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID) 216 } 217 if e.pkg != "" { 218 for p := range sbom.Artifacts.Packages.Enumerate() { 219 if e.pkg != p.Name { 220 continue 221 } 222 223 assert.Equal(t, e.ver, p.Version) 224 225 if e.cpe != "" { 226 foundCPE := false 227 for _, c := range p.CPEs { 228 cstr := c.BindToFmtString() 229 if e.cpe == cstr { 230 foundCPE = true 231 break 232 } 233 } 234 if !foundCPE { 235 assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe)) 236 } 237 } 238 239 if e.purl != "" { 240 assert.Equal(t, e.purl, p.PURL) 241 } 242 243 if e.relation != "" { 244 foundRelation := false 245 for _, r := range sbom.Relationships { 246 p := sbom.Artifacts.Packages.Package(r.To.ID()) 247 if e.relation == p.Name { 248 foundRelation = true 249 break 250 } 251 } 252 if !foundRelation { 253 assert.Fail(t, fmt.Sprintf("relation not found: %s", e.relation)) 254 } 255 } 256 continue test 257 } 258 assert.Fail(t, fmt.Sprintf("package should be present: %s", e.pkg)) 259 } 260 } 261 }) 262 } 263 } 264 265 func Test_relationshipDirection(t *testing.T) { 266 cyclonedx_bom := cyclonedx.BOM{Metadata: nil, 267 Components: &[]cyclonedx.Component{ 268 { 269 BOMRef: "p1", 270 Type: cyclonedx.ComponentTypeLibrary, 271 Name: "package-1", 272 Version: "1.0.1", 273 PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", 274 }, 275 { 276 BOMRef: "p2", 277 Type: cyclonedx.ComponentTypeLibrary, 278 Name: "package-2", 279 Version: "2.0.2", 280 PackageURL: "pkg:some/package-2@2.0.2?arch=arm64&upstream=upstream1&distro=alpine-1", 281 }, 282 }, 283 Dependencies: &[]cyclonedx.Dependency{ 284 { 285 Ref: "p1", 286 Dependencies: &[]string{"p2"}, 287 }, 288 }} 289 sbom, err := ToSyftModel(&cyclonedx_bom) 290 assert.Nil(t, err) 291 assert.Len(t, sbom.Relationships, 1) 292 relationship := sbom.Relationships[0] 293 294 // check that p2 -- dependency of --> p1 295 // same as p1 -- depends on --> p2 296 assert.Equal(t, artifact.DependencyOfRelationship, relationship.Type) 297 assert.Equal(t, "package-2", packageNameFromIdentifier(sbom, relationship.From)) 298 assert.Equal(t, "package-1", packageNameFromIdentifier(sbom, relationship.To)) 299 } 300 301 func packageNameFromIdentifier(model *sbom.SBOM, identifier artifact.Identifiable) string { 302 return model.Artifacts.Packages.Package(identifier.ID()).Name 303 } 304 305 func Test_missingDataDecode(t *testing.T) { 306 bom := &cyclonedx.BOM{ 307 Metadata: nil, 308 Components: &[]cyclonedx.Component{}, 309 SpecVersion: cyclonedx.SpecVersion1_4, 310 } 311 312 _, err := ToSyftModel(bom) 313 assert.NoError(t, err) 314 315 bom.Metadata = &cyclonedx.Metadata{} 316 317 _, err = ToSyftModel(bom) 318 assert.NoError(t, err) 319 320 pkg := decodeComponent(&cyclonedx.Component{ 321 Licenses: &cyclonedx.Licenses{ 322 { 323 License: nil, 324 }, 325 }, 326 }) 327 assert.Equal(t, pkg.Licenses.Empty(), true) 328 } 329 330 func Test_missingComponentsDecode(t *testing.T) { 331 bom := &cyclonedx.BOM{ 332 SpecVersion: cyclonedx.SpecVersion1_4, 333 } 334 bomBytes, _ := json.Marshal(&bom) 335 decode := GetDecoder(cyclonedx.BOMFileFormatJSON) 336 337 _, err := decode(bytes.NewReader(bomBytes)) 338 339 assert.NoError(t, err) 340 } 341 342 func Test_decodeDependencies(t *testing.T) { 343 c1 := cyclonedx.Component{ 344 Name: "c1", 345 } 346 347 c2 := cyclonedx.Component{ 348 Name: "c2", 349 } 350 351 c3 := cyclonedx.Component{ 352 Name: "c3", 353 } 354 355 for _, c := range []*cyclonedx.Component{&c1, &c2, &c3} { 356 c.BOMRef = c.Name 357 } 358 359 setTypes := func(typ cyclonedx.ComponentType, components ...cyclonedx.Component) *[]cyclonedx.Component { 360 var out []cyclonedx.Component 361 for _, c := range components { 362 c.Type = typ 363 out = append(out, c) 364 } 365 return &out 366 } 367 368 tests := []struct { 369 name string 370 sbom cyclonedx.BOM 371 expected []string 372 }{ 373 { 374 name: "dependencies decoded as dependencyOf relationships", 375 sbom: cyclonedx.BOM{ 376 Components: setTypes(cyclonedx.ComponentTypeLibrary, 377 c1, 378 c2, 379 c3, 380 ), 381 Dependencies: &[]cyclonedx.Dependency{ 382 { 383 Ref: c1.BOMRef, 384 Dependencies: &[]string{ 385 c2.BOMRef, 386 c3.BOMRef, 387 }, 388 }, 389 }, 390 }, 391 expected: []string{c2.Name, c3.Name}, 392 }, 393 { 394 name: "dependencies skipped with unhandled components", 395 sbom: cyclonedx.BOM{ 396 Components: setTypes("", 397 c1, 398 c2, 399 c3, 400 ), 401 Dependencies: &[]cyclonedx.Dependency{ 402 { 403 Ref: c1.BOMRef, 404 Dependencies: &[]string{ 405 c2.BOMRef, 406 c3.BOMRef, 407 }, 408 }, 409 }, 410 }, 411 expected: nil, 412 }, 413 } 414 415 for _, test := range tests { 416 t.Run(test.name, func(t *testing.T) { 417 s, err := ToSyftModel(&test.sbom) 418 require.NoError(t, err) 419 require.NotNil(t, s) 420 421 var deps []string 422 if s != nil { 423 for _, r := range s.Relationships { 424 if r.Type != artifact.DependencyOfRelationship { 425 continue 426 } 427 if p, ok := r.To.(pkg.Package); !ok || p.Name != c1.Name { 428 continue 429 } 430 if p, ok := r.From.(pkg.Package); ok { 431 deps = append(deps, p.Name) 432 } 433 } 434 } 435 require.Equal(t, test.expected, deps) 436 }) 437 } 438 }