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 }