github.com/anchore/syft@v1.38.2/internal/relationship/binary/binary_dependencies_test.go (about) 1 package binary 2 3 import ( 4 "path" 5 "strings" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/google/go-cmp/cmp/cmpopts" 10 11 "github.com/anchore/syft/internal/sbomsync" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/pkg" 15 "github.com/anchore/syft/syft/sbom" 16 ) 17 18 func TestPackagesToRemove(t *testing.T) { 19 glibcCoordinate := file.NewCoordinates("/usr/lib64/libc.so.6", "") 20 glibCPackage := pkg.Package{ 21 Name: "glibc", 22 Version: "2.28-236.el8_9.12", 23 Locations: file.NewLocationSet( 24 file.NewLocation("path/to/rpmdb"), 25 ), 26 Type: pkg.RpmPkg, 27 Metadata: pkg.RpmDBEntry{ 28 Files: []pkg.RpmFileRecord{ 29 { 30 Path: glibcCoordinate.RealPath, 31 }, 32 }, 33 }, 34 } 35 glibCPackage.SetID() 36 37 glibCBinaryELFPackage := pkg.Package{ 38 Name: "glibc", 39 Locations: file.NewLocationSet( 40 file.NewLocation(glibcCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 41 ), 42 Type: pkg.BinaryPkg, 43 Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ 44 Type: "testfixture", 45 Vendor: "syft", 46 System: "syftsys", 47 SourceRepo: "https://github.com/someone/somewhere.git", 48 Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb", 49 }, 50 } 51 glibCBinaryELFPackage.SetID() 52 53 glibCBinaryELFPackageAsRPM := pkg.Package{ 54 Name: "glibc", 55 Locations: file.NewLocationSet( 56 file.NewLocation(glibcCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 57 ), 58 Type: pkg.RpmPkg, // note: the elf package claims it is a RPM, not binary 59 Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ 60 Type: "rpm", 61 Vendor: "syft", 62 System: "syftsys", 63 SourceRepo: "https://github.com/someone/somewhere.git", 64 Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb", 65 }, 66 } 67 glibCBinaryELFPackageAsRPM.SetID() 68 69 glibCBinaryClassifierPackage := pkg.Package{ 70 Name: "glibc", 71 Locations: file.NewLocationSet( 72 file.NewLocation(glibcCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation), 73 ), 74 Type: pkg.BinaryPkg, 75 Metadata: pkg.BinarySignature{}, 76 } 77 glibCBinaryClassifierPackage.SetID() 78 79 libCBinaryClassifierPackage := pkg.Package{ 80 Name: "libc", 81 Locations: file.NewLocationSet( 82 file.NewLocation(glibcCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 83 ), 84 Type: pkg.BinaryPkg, 85 Metadata: pkg.BinarySignature{}, 86 } 87 libCBinaryClassifierPackage.SetID() 88 89 tests := []struct { 90 name string 91 accessor sbomsync.Accessor 92 want []artifact.ID 93 }{ 94 { 95 name: "remove packages that are overlapping rpm --> binary", 96 accessor: newAccessor([]pkg.Package{glibCPackage, glibCBinaryELFPackage}, map[file.Coordinates]file.Executable{}, nil), 97 // this is surprising, right? the calling function reasons about if any generic binary package (regardless of it being an ELF package or not) 98 // should be deleted or kept based on the user configuration to do so. 99 want: []artifact.ID{}, 100 }, 101 { 102 name: "keep packages that are overlapping rpm --> binary when the binary self identifies as an RPM", 103 accessor: newAccessor([]pkg.Package{glibCPackage, glibCBinaryELFPackageAsRPM}, map[file.Coordinates]file.Executable{}, nil), 104 want: []artifact.ID{}, 105 }, 106 { 107 name: "remove no packages when there is a single binary package (or self identifying RPM)", 108 accessor: newAccessor([]pkg.Package{glibCBinaryELFPackage, glibCBinaryELFPackageAsRPM}, map[file.Coordinates]file.Executable{}, nil), 109 want: []artifact.ID{}, 110 }, 111 { 112 name: "remove packages when there is a single binary package and a classifier package", 113 accessor: newAccessor([]pkg.Package{glibCBinaryELFPackage, glibCBinaryClassifierPackage}, map[file.Coordinates]file.Executable{}, nil), 114 want: []artifact.ID{glibCBinaryClassifierPackage.ID()}, 115 }, 116 { 117 name: "ensure we're considering ELF packages, not just binary packages (supporting evidence)", 118 accessor: newAccessor([]pkg.Package{glibCBinaryClassifierPackage}, map[file.Coordinates]file.Executable{}, nil), 119 want: []artifact.ID{}, 120 }, 121 { 122 name: "ensure we're considering ELF packages, not just binary packages (primary evidence)", 123 accessor: newAccessor([]pkg.Package{libCBinaryClassifierPackage}, map[file.Coordinates]file.Executable{}, nil), 124 want: []artifact.ID{}, 125 }, 126 } 127 for _, tt := range tests { 128 t.Run(tt.name, func(t *testing.T) { 129 pkgsToDelete := PackagesToRemove(tt.accessor) 130 if diff := cmp.Diff(tt.want, pkgsToDelete); diff != "" { 131 t.Errorf("unexpected packages to delete (-want, +got): %s", diff) 132 } 133 }) 134 } 135 } 136 137 func TestNewDependencyRelationships(t *testing.T) { 138 // coordinates for the files under test 139 glibcCoordinate := file.NewCoordinates("/usr/lib64/libc.so.6", "") 140 secondGlibcCoordinate := file.NewCoordinates("/usr/local/lib64/libc.so.6", "") 141 nestedLibCoordinate := file.NewCoordinates("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib", "") 142 parallelLibCoordinate := file.NewCoordinates("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1", "") 143 144 // rpm package that was discovered in linked section of the ELF binary package 145 glibCPackage := pkg.Package{ 146 Name: "glibc", 147 Version: "2.28-236.el8_9.12", 148 Locations: file.NewLocationSet( 149 file.NewLocation(glibcCoordinate.RealPath), 150 file.NewLocation("some/other/path"), 151 ), 152 Type: pkg.RpmPkg, 153 Metadata: pkg.RpmDBEntry{ 154 Files: []pkg.RpmFileRecord{ 155 { 156 Path: glibcCoordinate.RealPath, 157 }, 158 { 159 Path: "some/other/path", 160 }, 161 }, 162 }, 163 } 164 glibCPackage.SetID() 165 166 // second rpm package that could be discovered in linked section of the ELF binary package (same base path as above) 167 glibCustomPackage := pkg.Package{ 168 Name: "glibc", 169 Version: "2.28-236.el8_9.12", 170 Locations: file.NewLocationSet(file.NewLocation(secondGlibcCoordinate.RealPath)), 171 Type: pkg.RpmPkg, 172 Metadata: pkg.RpmDBEntry{ 173 Files: []pkg.RpmFileRecord{ 174 { 175 Path: secondGlibcCoordinate.RealPath, 176 }, 177 }, 178 }, 179 } 180 glibCustomPackage.SetID() 181 182 // binary package that is an executable that can link against above rpm packages 183 syftTestFixturePackage := pkg.Package{ 184 Name: "syfttestfixture", 185 Version: "0.01", 186 PURL: "pkg:generic/syftsys/syfttestfixture@0.01", 187 FoundBy: "", 188 Locations: file.NewLocationSet( 189 file.NewLocation(nestedLibCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 190 file.NewLocation(parallelLibCoordinate.RealPath).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation), 191 ), 192 Language: "", 193 Type: pkg.RpmPkg, 194 Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ 195 Type: "rpm", 196 Vendor: "syft", 197 System: "syftsys", 198 SourceRepo: "https://github.com/someone/somewhere.git", 199 Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb", 200 }, 201 } 202 syftTestFixturePackage.SetID() 203 204 // dummy executable representation of glibc 205 glibcExecutable := file.Executable{ 206 Format: "elf", 207 HasExports: true, 208 HasEntrypoint: true, 209 ImportedLibraries: []string{}, 210 } 211 212 // executable representation of the syftTestFixturePackage 213 syftTestFixtureExecutable := file.Executable{ 214 Format: "elf", 215 HasExports: true, 216 HasEntrypoint: true, 217 ImportedLibraries: []string{ 218 path.Base(glibcCoordinate.RealPath), 219 }, 220 } 221 222 // second executable representation that has no parent package 223 syftTestFixtureExecutable2 := file.Executable{ 224 Format: "elf", 225 HasExports: true, 226 HasEntrypoint: true, 227 ImportedLibraries: []string{ 228 // this should not be a relationship because it is not a coordinate 229 "foo.so.6", 230 }, 231 } 232 233 tests := []struct { 234 name string 235 resolver file.Resolver 236 coordinateIndex map[file.Coordinates]file.Executable 237 packages []pkg.Package 238 prexistingRelationships []artifact.Relationship 239 want []artifact.Relationship 240 }{ 241 { 242 name: "blank sbom and accessor returns empty relationships", 243 resolver: nil, 244 coordinateIndex: map[file.Coordinates]file.Executable{}, 245 packages: []pkg.Package{}, 246 }, 247 { 248 name: "given a package that imports glibc, expect a relationship between the two packages when the package is an executable", 249 resolver: file.NewMockResolverForPaths( 250 glibcCoordinate.RealPath, 251 nestedLibCoordinate.RealPath, 252 parallelLibCoordinate.RealPath, 253 ), 254 // path -> executable (above mock resolver needs to be able to resolve to paths in this map) 255 coordinateIndex: map[file.Coordinates]file.Executable{ 256 glibcCoordinate: glibcExecutable, 257 nestedLibCoordinate: syftTestFixtureExecutable, 258 parallelLibCoordinate: syftTestFixtureExecutable2, 259 }, 260 packages: []pkg.Package{glibCPackage, syftTestFixturePackage}, 261 want: []artifact.Relationship{ 262 { 263 From: glibCPackage, 264 To: syftTestFixturePackage, 265 Type: artifact.DependencyOfRelationship, 266 }, 267 }, 268 }, 269 { 270 name: "given an executable maps to one base path represented by two RPM we make two relationships", 271 resolver: file.NewMockResolverForPaths( 272 glibcCoordinate.RealPath, 273 secondGlibcCoordinate.RealPath, 274 nestedLibCoordinate.RealPath, 275 parallelLibCoordinate.RealPath, 276 ), 277 coordinateIndex: map[file.Coordinates]file.Executable{ 278 glibcCoordinate: glibcExecutable, 279 secondGlibcCoordinate: glibcExecutable, 280 nestedLibCoordinate: syftTestFixtureExecutable, 281 parallelLibCoordinate: syftTestFixtureExecutable2, 282 }, 283 packages: []pkg.Package{glibCPackage, glibCustomPackage, syftTestFixturePackage}, 284 want: []artifact.Relationship{ 285 { 286 From: glibCPackage, 287 To: syftTestFixturePackage, 288 Type: artifact.DependencyOfRelationship, 289 }, 290 { 291 From: glibCustomPackage, 292 To: syftTestFixturePackage, 293 Type: artifact.DependencyOfRelationship, 294 }, 295 }, 296 }, 297 { 298 name: "given some dependency relationships already exist, expect no duplicate relationships to be created", 299 resolver: file.NewMockResolverForPaths( 300 glibcCoordinate.RealPath, 301 nestedLibCoordinate.RealPath, 302 parallelLibCoordinate.RealPath, 303 ), 304 coordinateIndex: map[file.Coordinates]file.Executable{ 305 glibcCoordinate: glibcExecutable, 306 nestedLibCoordinate: syftTestFixtureExecutable, 307 parallelLibCoordinate: syftTestFixtureExecutable2, 308 }, 309 packages: []pkg.Package{glibCPackage, glibCustomPackage, syftTestFixturePackage}, 310 prexistingRelationships: []artifact.Relationship{ 311 { 312 From: glibCPackage, 313 To: syftTestFixturePackage, 314 Type: artifact.DependencyOfRelationship, 315 }, 316 }, 317 }, 318 { 319 name: "given a package that imports a library that is not tracked by the resolver, expect no relationships to be created", 320 resolver: file.NewMockResolverForPaths(), 321 coordinateIndex: map[file.Coordinates]file.Executable{ 322 glibcCoordinate: glibcExecutable, 323 nestedLibCoordinate: syftTestFixtureExecutable, 324 parallelLibCoordinate: syftTestFixtureExecutable2, 325 }, 326 packages: []pkg.Package{glibCPackage, syftTestFixturePackage}, 327 }, 328 } 329 for _, tt := range tests { 330 t.Run(tt.name, func(t *testing.T) { 331 accessor := newAccessor(tt.packages, tt.coordinateIndex, tt.prexistingRelationships) 332 // given a resolver that knows about the paths of the packages and executables, 333 // and given an SBOM accessor that knows about the packages and executables, 334 // we should be able to create a set of relationships between the packages and executables 335 relationships := NewDependencyRelationships(tt.resolver, accessor) 336 if diff := relationshipComparer(tt.want, relationships); diff != "" { 337 t.Errorf("unexpected relationships (-want, +got): %s", diff) 338 } 339 }) 340 } 341 } 342 343 func relationshipComparer(x, y []artifact.Relationship) string { 344 return cmp.Diff(x, y, cmpopts.IgnoreUnexported( 345 pkg.Package{}, 346 artifact.Relationship{}, 347 file.LocationSet{}, 348 pkg.LicenseSet{}, 349 ), cmpopts.SortSlices(lessRelationships)) 350 } 351 352 func lessRelationships(r1, r2 artifact.Relationship) bool { 353 c := strings.Compare(string(r1.Type), string(r2.Type)) 354 if c != 0 { 355 return c < 0 356 } 357 c = strings.Compare(string(r1.From.ID()), string(r2.From.ID())) 358 if c != 0 { 359 return c < 0 360 } 361 c = strings.Compare(string(r1.To.ID()), string(r2.To.ID())) 362 return c < 0 363 } 364 365 func newAccessor(pkgs []pkg.Package, coordinateIndex map[file.Coordinates]file.Executable, preexistingRelationships []artifact.Relationship) sbomsync.Accessor { 366 sb := sbom.SBOM{ 367 Artifacts: sbom.Artifacts{ 368 Packages: pkg.NewCollection(), 369 }, 370 } 371 372 builder := sbomsync.NewBuilder(&sb) 373 builder.AddPackages(pkgs...) 374 375 accessor := builder.(sbomsync.Accessor) 376 accessor.WriteToSBOM(func(s *sbom.SBOM) { 377 s.Artifacts.Executables = coordinateIndex 378 if preexistingRelationships != nil { 379 s.Relationships = preexistingRelationships 380 } 381 }) 382 return accessor 383 }