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  }