github.com/google/osv-scalibr@v0.4.1/annotator/osduplicate/rpm/rpm_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package rpm_test 16 17 import ( 18 "context" 19 "os" 20 "path/filepath" 21 "runtime" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 "github.com/google/go-cpy/cpy" 27 "github.com/google/osv-scalibr/annotator" 28 "github.com/google/osv-scalibr/annotator/osduplicate/rpm" 29 "github.com/google/osv-scalibr/extractor" 30 scalibrfs "github.com/google/osv-scalibr/fs" 31 "github.com/google/osv-scalibr/inventory" 32 "github.com/google/osv-scalibr/inventory/vex" 33 "google.golang.org/protobuf/proto" 34 ) 35 36 func TestAnnotate(t *testing.T) { 37 if runtime.GOOS != "linux" { 38 t.Skipf("Test skipped, OS unsupported: %v", runtime.GOOS) 39 } 40 41 cancelledContext, cancel := context.WithCancel(t.Context()) 42 cancel() 43 44 copier := cpy.New( 45 cpy.Func(proto.Clone), 46 cpy.IgnoreAllUnexported(), 47 ) 48 49 tests := []struct { 50 desc string 51 packages []*extractor.Package 52 dbPaths map[string]string 53 //nolint:containedctx 54 ctx context.Context 55 wantErr error 56 wantPackages []*extractor.Package 57 }{ 58 { 59 desc: "no_rpm_dbs", 60 packages: []*extractor.Package{ 61 { 62 Name: "pyxattr", 63 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 64 }, 65 }, 66 wantPackages: []*extractor.Package{ 67 { 68 Name: "pyxattr", 69 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 70 }, 71 }, 72 }, 73 { 74 desc: "some_pkgs_found_in_Packages", 75 dbPaths: map[string]string{ 76 "usr/lib/sysimage/rpm/Packages": "testdata/Packages", 77 }, 78 packages: []*extractor.Package{ 79 { 80 Name: "pyxattr", 81 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 82 }, 83 { 84 Name: "not-in-db", 85 Locations: []string{"path/not/in/db"}, 86 }, 87 }, 88 wantPackages: []*extractor.Package{ 89 { 90 Name: "pyxattr", 91 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 92 ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{ 93 Plugin: rpm.Name, 94 Justification: vex.ComponentNotPresent, 95 MatchesAllVulns: true, 96 }}, 97 }, 98 { 99 Name: "not-in-db", 100 Locations: []string{"path/not/in/db"}, 101 }, 102 }, 103 }, 104 { 105 desc: "some_pkg_found_in_Packages.db", 106 dbPaths: map[string]string{ 107 "var/lib/rpm/Packages.db": "testdata/Packages.db", 108 }, 109 packages: []*extractor.Package{ 110 { 111 Name: "cracklib", 112 Locations: []string{"usr/sbin/cracklib-check"}, 113 }, 114 { 115 Name: "not-in-db", 116 Locations: []string{"path/not/in/db"}, 117 }, 118 }, 119 wantPackages: []*extractor.Package{ 120 { 121 Name: "cracklib", 122 Locations: []string{"usr/sbin/cracklib-check"}, 123 ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{ 124 Plugin: rpm.Name, 125 Justification: vex.ComponentNotPresent, 126 MatchesAllVulns: true, 127 }}, 128 }, 129 { 130 Name: "not-in-db", 131 Locations: []string{"path/not/in/db"}, 132 }, 133 }, 134 }, 135 { 136 desc: "some_pkg_found_in_rpmdb.sqlite", 137 dbPaths: map[string]string{ 138 "usr/share/rpm/rpmdb.sqlite": "testdata/rpmdb.sqlite", 139 }, 140 packages: []*extractor.Package{ 141 { 142 Name: "python3-gpg", 143 Locations: []string{"usr/lib64/python3.9/site-packages/gpg-1.15.1-py3.9.egg-info"}, 144 }, 145 { 146 Name: "not-in-db", 147 Locations: []string{"path/not/in/db"}, 148 }, 149 }, 150 wantPackages: []*extractor.Package{ 151 { 152 Name: "python3-gpg", 153 Locations: []string{"usr/lib64/python3.9/site-packages/gpg-1.15.1-py3.9.egg-info"}, 154 ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{ 155 Plugin: rpm.Name, 156 Justification: vex.ComponentNotPresent, 157 MatchesAllVulns: true, 158 }}, 159 }, 160 { 161 Name: "not-in-db", 162 Locations: []string{"path/not/in/db"}, 163 }, 164 }, 165 }, 166 { 167 desc: "some_pkg_found_in_multiple_dbs", 168 dbPaths: map[string]string{ 169 "var/lib/rpm/Packages": "testdata/Packages", 170 "usr/lib/sysimage/rpm/Packages.db": "testdata/Packages.db", 171 "usr/lib/sysimage/rpm/rpmdb.sqlite": "testdata/rpmdb.sqlite", 172 }, 173 packages: []*extractor.Package{ 174 { 175 // From Packages 176 Name: "pyxattr", 177 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 178 }, 179 { 180 // From Packages.db 181 Name: "cracklib", 182 Locations: []string{"usr/sbin/cracklib-check"}, 183 }, 184 { 185 // From rpmdb.sqlite 186 Name: "python3-gpg", 187 Locations: []string{"usr/lib64/python3.9/site-packages/gpg-1.15.1-py3.9.egg-info"}, 188 }, 189 { 190 Name: "not-in-db", 191 Locations: []string{"path/not/in/db"}, 192 }, 193 }, 194 wantPackages: []*extractor.Package{ 195 { 196 Name: "pyxattr", 197 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 198 ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{ 199 Plugin: rpm.Name, 200 Justification: vex.ComponentNotPresent, 201 MatchesAllVulns: true, 202 }}, 203 }, 204 { 205 Name: "cracklib", 206 Locations: []string{"usr/sbin/cracklib-check"}, 207 ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{ 208 Plugin: rpm.Name, 209 Justification: vex.ComponentNotPresent, 210 MatchesAllVulns: true, 211 }}, 212 }, 213 { 214 Name: "python3-gpg", 215 Locations: []string{"usr/lib64/python3.9/site-packages/gpg-1.15.1-py3.9.egg-info"}, 216 ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{ 217 Plugin: rpm.Name, 218 Justification: vex.ComponentNotPresent, 219 MatchesAllVulns: true, 220 }}, 221 }, 222 { 223 Name: "not-in-db", 224 Locations: []string{"path/not/in/db"}, 225 }, 226 }, 227 }, 228 { 229 desc: "ctx_cancelled", 230 ctx: cancelledContext, 231 dbPaths: map[string]string{ 232 "usr/lib/sysimage/rpm/Packages": "testdata/Packages", 233 }, 234 packages: []*extractor.Package{ 235 { 236 Name: "pyxattr", 237 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 238 }, 239 }, 240 wantPackages: []*extractor.Package{ 241 { 242 Name: "pyxattr", 243 Locations: []string{"usr/lib64/python2.7/site-packages/pyxattr-0.5.1-py2.7.egg-info"}, 244 // No annotations 245 }, 246 }, 247 wantErr: cmpopts.AnyError, 248 }, 249 } 250 251 for _, fsType := range []string{"virtual_fs", "real_fs"} { 252 for _, tt := range tests { 253 t.Run(tt.desc+"_"+fsType, func(t *testing.T) { 254 if tt.ctx == nil { 255 tt.ctx = t.Context() 256 } 257 258 tmpPath := setupRPMDBs(t, tt.dbPaths) 259 input := &annotator.ScanInput{ 260 ScanRoot: scalibrfs.RealFSScanRoot(tmpPath), 261 } 262 263 if fsType == "virtual_fs" { 264 // Simulate a virtual FS by hiding the root path. 265 input.ScanRoot.Path = "" 266 } 267 268 // Deep copy the packages to avoid modifying the original inventory that is used in other tests. 269 packages := copier.Copy(tt.packages).([]*extractor.Package) 270 inv := &inventory.Inventory{Packages: packages} 271 272 err := rpm.NewDefault().Annotate(tt.ctx, input, inv) 273 if !cmp.Equal(tt.wantErr, err, cmpopts.EquateErrors()) { 274 t.Fatalf("Annotate(%v) error: %v, want %v", tt.packages, err, tt.wantErr) 275 } 276 277 want := &inventory.Inventory{Packages: tt.wantPackages} 278 if diff := cmp.Diff(want, inv); diff != "" { 279 t.Errorf("Annotate(%v): unexpected diff (-want +got): %v", tt.packages, diff) 280 } 281 }) 282 } 283 } 284 } 285 286 // setupRPMDBs creates a temporary test directory with the RPM database paths 287 // and contents specified in the supplied path -> content map. 288 // Returns the path of the created tmp dir. 289 func setupRPMDBs(t *testing.T, dbPaths map[string]string) string { 290 t.Helper() 291 root := t.TempDir() 292 for dbPath, contentFile := range dbPaths { 293 dbDir := filepath.Join(root, filepath.Dir(dbPath)) 294 if err := os.MkdirAll(dbDir, 0777); err != nil { 295 t.Fatalf("error creating directory %q: %v", dbDir, err) 296 } 297 298 content, err := os.ReadFile(contentFile) 299 if err != nil { 300 t.Fatalf("Error reading content file %q: %v", contentFile, err) 301 } 302 303 dbFile := filepath.Join(root, dbPath) 304 if err := os.WriteFile(dbFile, content, 0644); err != nil { 305 t.Fatalf("Error creating file %q: %v", dbFile, err) 306 } 307 } 308 309 return root 310 }