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