github.com/anchore/syft@v1.38.2/internal/relationship/by_file_ownership_test.go (about)

     1  package relationship
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/google/go-cmp/cmp"
     7  	"github.com/stretchr/testify/require"
     8  
     9  	"github.com/anchore/syft/internal/cmptest"
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/pkg"
    13  )
    14  
    15  type mockFR struct {
    16  	file.Resolver
    17  	translate map[string]string
    18  }
    19  
    20  func (m mockFR) FilesByPath(paths ...string) ([]file.Location, error) {
    21  	var results []file.Location
    22  	for _, p := range paths {
    23  		tPath, ok := m.translate[p]
    24  		if !ok {
    25  			tPath = p
    26  		}
    27  		results = append(results, file.NewLocation(tPath))
    28  	}
    29  	return results, nil
    30  }
    31  
    32  func TestOwnershipByFilesRelationship(t *testing.T) {
    33  
    34  	tests := []struct {
    35  		name     string
    36  		resolver file.Resolver
    37  		setup    func(t testing.TB) ([]pkg.Package, []artifact.Relationship)
    38  	}{
    39  		{
    40  			name: "owns-by-real-path",
    41  			setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
    42  				parent := pkg.Package{
    43  					Locations: file.NewLocationSet(
    44  						file.NewVirtualLocation("/a/path", "/another/path"),
    45  						file.NewVirtualLocation("/b/path", "/bee/path"),
    46  					),
    47  					Type: pkg.RpmPkg,
    48  					Metadata: pkg.RpmDBEntry{
    49  						Files: []pkg.RpmFileRecord{
    50  							{Path: "/owning/path/1"},
    51  							{Path: "/owning/path/2"},
    52  							{Path: "/d/path"},
    53  						},
    54  					},
    55  				}
    56  				parent.SetID()
    57  
    58  				child := pkg.Package{
    59  					Locations: file.NewLocationSet(
    60  						file.NewVirtualLocation("/c/path", "/another/path"),
    61  						file.NewVirtualLocation("/d/path", "/another/path"),
    62  					),
    63  					Type: pkg.NpmPkg,
    64  				}
    65  				child.SetID()
    66  
    67  				relationship := artifact.Relationship{
    68  					From: parent,
    69  					To:   child,
    70  					Type: artifact.OwnershipByFileOverlapRelationship,
    71  					Data: ownershipByFilesMetadata{
    72  						Files: []string{
    73  							"/d/path",
    74  						},
    75  					},
    76  				}
    77  
    78  				return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
    79  			},
    80  		},
    81  		{
    82  			name: "only-real-file-owner-gets-ownership-relationship-not-symlink-owner",
    83  			resolver: mockFR{
    84  				translate: map[string]string{
    85  					"/usr/bin/jenkins.war-symlink": "/usr/share/jenkins/jenkins.war", // symlink to the real file
    86  				},
    87  			},
    88  			setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
    89  				// Package that owns the symlink
    90  				symlinkOwner := pkg.Package{
    91  					Type: pkg.ApkPkg,
    92  					Metadata: pkg.ApkDBEntry{
    93  						Package: "jenkins-symlink-pkg",
    94  						Files: []pkg.ApkFileRecord{
    95  							{Path: "/usr/bin/jenkins.war-symlink"}, // this symlinks to the real file
    96  						},
    97  					},
    98  				}
    99  				symlinkOwner.SetID()
   100  
   101  				// Package that owns the real file
   102  				realFileOwner := pkg.Package{
   103  					Type: pkg.ApkPkg,
   104  					Metadata: pkg.ApkDBEntry{
   105  						Package: "jenkins-real-pkg",
   106  						Files: []pkg.ApkFileRecord{
   107  							{Path: "/usr/share/jenkins/jenkins.war"}, // the real file
   108  						},
   109  					},
   110  				}
   111  				realFileOwner.SetID()
   112  
   113  				// A third package that is "located at" the real file path
   114  				// This simulates a package being discovered at the jenkins.war location
   115  				consumerPackage := pkg.Package{
   116  					Type: pkg.JavaPkg,
   117  					Locations: file.NewLocationSet(
   118  						file.NewVirtualLocation("/usr/share/jenkins/jenkins.war", "/usr/share/jenkins/jenkins.war"),
   119  					),
   120  				}
   121  				consumerPackage.SetID()
   122  
   123  				// Only the real file owner should have an ownership relationship with the consumer package
   124  				// The symlink owner should NOT have a relationship with the consumer
   125  				relationship := artifact.Relationship{
   126  					From: realFileOwner,
   127  					To:   consumerPackage,
   128  					Type: artifact.OwnershipByFileOverlapRelationship,
   129  					Data: ownershipByFilesMetadata{
   130  						Files: []string{
   131  							"/usr/share/jenkins/jenkins.war",
   132  						},
   133  					},
   134  				}
   135  
   136  				return []pkg.Package{symlinkOwner, realFileOwner, consumerPackage}, []artifact.Relationship{relationship}
   137  			},
   138  		},
   139  		{
   140  			name: "misses-by-dead-symlink",
   141  			resolver: mockFR{
   142  				translate: map[string]string{
   143  					"/bin/gzip": "", // treat this as a dead symlink
   144  				},
   145  			},
   146  			setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
   147  				parent := pkg.Package{
   148  					Type: pkg.DebPkg,
   149  					Metadata: pkg.DpkgDBEntry{
   150  						Files: []pkg.DpkgFileRecord{
   151  							{Path: "/bin/gzip"}, // this symlinks to gzip via /bin -> /usr/bin
   152  						},
   153  					},
   154  				}
   155  				parent.SetID()
   156  
   157  				child := pkg.Package{
   158  					Locations: file.NewLocationSet(
   159  						file.NewVirtualLocation("/usr/bin/gzip", "/usr/bin/gzip"),
   160  					),
   161  					Type: pkg.BinaryPkg,
   162  				}
   163  				child.SetID()
   164  
   165  				return []pkg.Package{parent, child}, nil // importantly, no relationship is expected
   166  			},
   167  		},
   168  		{
   169  			name: "owns-by-symlink",
   170  			resolver: mockFR{
   171  				translate: map[string]string{
   172  					"/bin/gzip": "/usr/bin/gzip", // if there is a string path of /bin/gzip then return the real path of /usr/bin/gzip
   173  				},
   174  			},
   175  			setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
   176  				parent := pkg.Package{
   177  					Type: pkg.DebPkg,
   178  					Metadata: pkg.DpkgDBEntry{
   179  						Files: []pkg.DpkgFileRecord{
   180  							{Path: "/bin/gzip"}, // this symlinks to gzip via /bin -> /usr/bin
   181  						},
   182  					},
   183  				}
   184  				parent.SetID()
   185  
   186  				child := pkg.Package{
   187  					Locations: file.NewLocationSet(
   188  						file.NewVirtualLocation("/usr/bin/gzip", "/usr/bin/gzip"),
   189  					),
   190  					Type: pkg.BinaryPkg,
   191  				}
   192  				child.SetID()
   193  
   194  				relationship := artifact.Relationship{
   195  					From: parent,
   196  					To:   child,
   197  					Type: artifact.OwnershipByFileOverlapRelationship,
   198  					Data: ownershipByFilesMetadata{
   199  						Files: []string{
   200  							"/usr/bin/gzip",
   201  						},
   202  					},
   203  				}
   204  
   205  				return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
   206  			},
   207  		},
   208  		{
   209  			name: "owns-by-virtual-path",
   210  			setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
   211  				parent := pkg.Package{
   212  					Locations: file.NewLocationSet(
   213  						file.NewVirtualLocation("/a/path", "/some/other/path"),
   214  						file.NewVirtualLocation("/b/path", "/bee/path"),
   215  					),
   216  					Type: pkg.RpmPkg,
   217  					Metadata: pkg.RpmDBEntry{
   218  						Files: []pkg.RpmFileRecord{
   219  							{Path: "/owning/path/1"},
   220  							{Path: "/owning/path/2"},
   221  							{Path: "/another/path"},
   222  						},
   223  					},
   224  				}
   225  				parent.SetID()
   226  
   227  				child := pkg.Package{
   228  					Locations: file.NewLocationSet(
   229  						file.NewVirtualLocation("/c/path", "/another/path"),
   230  						file.NewLocation("/d/path"),
   231  					),
   232  					Type: pkg.NpmPkg,
   233  				}
   234  				child.SetID()
   235  
   236  				relationship := artifact.Relationship{
   237  					From: parent,
   238  					To:   child,
   239  					Type: artifact.OwnershipByFileOverlapRelationship,
   240  					Data: ownershipByFilesMetadata{
   241  						Files: []string{
   242  							"/another/path",
   243  						},
   244  					},
   245  				}
   246  				return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
   247  			},
   248  		},
   249  		{
   250  			name: "ignore-empty-path",
   251  			setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
   252  				parent := pkg.Package{
   253  					Locations: file.NewLocationSet(
   254  						file.NewVirtualLocation("/a/path", "/some/other/path"),
   255  						file.NewVirtualLocation("/b/path", "/bee/path"),
   256  					),
   257  					Type: pkg.RpmPkg,
   258  					Metadata: pkg.RpmDBEntry{
   259  						Files: []pkg.RpmFileRecord{
   260  							{Path: "/owning/path/1"},
   261  							{Path: "/owning/path/2"},
   262  							{Path: ""},
   263  						},
   264  					},
   265  				}
   266  
   267  				parent.SetID()
   268  
   269  				child := pkg.Package{
   270  					Locations: file.NewLocationSet(
   271  						file.NewVirtualLocation("/c/path", "/another/path"),
   272  						file.NewLocation("/d/path"),
   273  					),
   274  					Type: pkg.NpmPkg,
   275  				}
   276  
   277  				child.SetID()
   278  
   279  				return []pkg.Package{parent, child}, nil
   280  			},
   281  		},
   282  	}
   283  
   284  	for _, test := range tests {
   285  		t.Run(test.name, func(t *testing.T) {
   286  			pkgs, expectedRelations := test.setup(t)
   287  			c := pkg.NewCollection(pkgs...)
   288  			relationships := byFileOwnershipOverlap(test.resolver, c)
   289  
   290  			require.Len(t, relationships, len(expectedRelations))
   291  			for idx, expectedRelationship := range expectedRelations {
   292  				actualRelationship := relationships[idx]
   293  				if d := cmp.Diff(expectedRelationship, actualRelationship, cmptest.DefaultOptions()...); d != "" {
   294  					t.Errorf("unexpected relationship (-want, +got): %s", d)
   295  				}
   296  			}
   297  		})
   298  	}
   299  }