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  }