github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/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 "io" 19 "io/fs" 20 "os" 21 "path" 22 "path/filepath" 23 "runtime" 24 "sort" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 "github.com/google/osv-scalibr/extractor" 32 "github.com/google/osv-scalibr/extractor/filesystem" 33 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 34 "github.com/google/osv-scalibr/extractor/filesystem/os/rpm" 35 rpmmeta "github.com/google/osv-scalibr/extractor/filesystem/os/rpm/metadata" 36 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 37 scalibrfs "github.com/google/osv-scalibr/fs" 38 "github.com/google/osv-scalibr/purl" 39 "github.com/google/osv-scalibr/stats" 40 "github.com/google/osv-scalibr/testing/fakefs" 41 "github.com/google/osv-scalibr/testing/testcollector" 42 ) 43 44 func TestFileRequired(t *testing.T) { 45 // supported OSes 46 if runtime.GOOS == "windows" { 47 t.Skipf("Test skipped, OS unsupported: %v", runtime.GOOS) 48 } 49 50 tests := []struct { 51 name string 52 path string 53 fileSizeBytes int64 54 maxFileSizeBytes int64 55 wantRequired bool 56 wantResultMetric stats.FileRequiredResult 57 }{ 58 // BDB 59 {path: "usr/lib/sysimage/rpm/Packages", wantRequired: true}, 60 {path: "var/lib/rpm/Packages", wantRequired: true}, 61 {path: "usr/share/rpm/Packages", wantRequired: true}, 62 // NDB 63 {path: "usr/lib/sysimage/rpm/Packages.db", wantRequired: true}, 64 {path: "var/lib/rpm/Packages.db", wantRequired: true}, 65 {path: "usr/share/rpm/Packages.db", wantRequired: true}, 66 // SQLite3 67 {path: "usr/lib/sysimage/rpm/rpmdb.sqlite", wantRequired: true}, 68 {path: "var/lib/rpm/rpmdb.sqlite", wantRequired: true}, 69 {path: "usr/share/rpm/rpmdb.sqlite", wantRequired: true}, 70 // invalid 71 {path: "rpm/rpmdb.sqlite", wantRequired: false}, 72 {path: "rpm/Packages.db", wantRequired: false}, 73 {path: "rpm/Packages", wantRequired: false}, 74 {path: "foo/var/lib/rpm/rpmdb.sqlite", wantRequired: false}, 75 {path: "foo/var/lib/rpm/Packages", wantRequired: false}, 76 {path: "/rpm/rpmdb.sqlite", wantRequired: false}, 77 {path: "/rpm/Packages.db", wantRequired: false}, 78 {path: "/rpm/Packages", wantRequired: false}, 79 {path: "/foo/var/lib/rpm/rpmdb.sqlite", wantRequired: false}, 80 {path: "/foo/var/lib/rpm/Packages", wantRequired: false}, 81 // File size limits 82 { 83 name: "Packages file required if file size < max file size", 84 path: "usr/lib/sysimage/rpm/Packages", 85 fileSizeBytes: 100 * units.KiB, 86 maxFileSizeBytes: 1000 * units.KiB, 87 wantRequired: true, 88 wantResultMetric: stats.FileRequiredResultOK, 89 }, 90 { 91 name: "Packages file required if file size == max file size", 92 path: "usr/lib/sysimage/rpm/Packages", 93 fileSizeBytes: 1000 * units.KiB, 94 maxFileSizeBytes: 1000 * units.KiB, 95 wantRequired: true, 96 wantResultMetric: stats.FileRequiredResultOK, 97 }, 98 { 99 name: "Packages file not required if file size > max file size", 100 path: "usr/lib/sysimage/rpm/Packages", 101 fileSizeBytes: 1000 * units.KiB, 102 maxFileSizeBytes: 100 * units.KiB, 103 wantRequired: false, 104 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 105 }, 106 { 107 name: "Packages file required if max file size set to 0", 108 path: "usr/lib/sysimage/rpm/Packages", 109 fileSizeBytes: 100 * units.KiB, 110 maxFileSizeBytes: 0, 111 wantRequired: true, 112 wantResultMetric: stats.FileRequiredResultOK, 113 }, 114 } 115 116 for _, tt := range tests { 117 desc := tt.name 118 if desc == "" { 119 desc = tt.path 120 } 121 122 t.Run(desc, func(t *testing.T) { 123 collector := testcollector.New() 124 var e filesystem.Extractor = rpm.New(rpm.Config{ 125 Stats: collector, 126 MaxFileSizeBytes: tt.maxFileSizeBytes, 127 }) 128 129 // Set a default file size if not specified. 130 fileSizeBytes := tt.fileSizeBytes 131 if fileSizeBytes == 0 { 132 fileSizeBytes = 1000 133 } 134 135 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 136 FileName: filepath.Base(tt.path), 137 FileMode: fs.ModePerm, 138 FileSize: fileSizeBytes, 139 })) 140 if isRequired != tt.wantRequired { 141 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 142 } 143 144 wantResultMetric := tt.wantResultMetric 145 if wantResultMetric == "" && tt.wantRequired { 146 wantResultMetric = stats.FileRequiredResultOK 147 } 148 gotResultMetric := collector.FileRequiredResult(tt.path) 149 if wantResultMetric != "" && gotResultMetric != wantResultMetric { 150 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, wantResultMetric) 151 } 152 }) 153 } 154 } 155 156 const fedora38 = `NAME="Fedora Linux" 157 VERSION="38 (Container Image)" 158 ID=fedora 159 VERSION_ID=38 160 VERSION_CODENAME="" 161 PLATFORM_ID="platform:f38" 162 PRETTY_NAME="Fedora Linux 38 (Container Image)" 163 CPE_NAME="cpe:/o:fedoraproject:fedora:38" 164 DEFAULT_HOSTNAME="fedora" 165 REDHAT_BUGZILLA_PRODUCT="Fedora" 166 REDHAT_BUGZILLA_PRODUCT_VERSION=38 167 REDHAT_SUPPORT_PRODUCT="Fedora" 168 REDHAT_SUPPORT_PRODUCT_VERSION=38 169 SUPPORT_END=2024-05-14 170 VARIANT="Container Image"` 171 172 func TestExtract(t *testing.T) { 173 // supported OSes 174 if runtime.GOOS == "windows" { 175 t.Skipf("Test skipped, OS unsupported: %v", runtime.GOOS) 176 } 177 178 tests := []struct { 179 name string 180 path string 181 osrelease string 182 timeoutval time.Duration 183 // rpm -qa --qf "%{NAME}@%{VERSION}-%{RELEASE}\n" |sort |head -n 3 184 wantPackages []*extractor.Package 185 // rpm -qa | wc -l 186 wantResults int 187 wantErr error 188 wantResultMetric stats.FileExtractedResult 189 }{ 190 { 191 name: "opensuse/leap:15.5_Packages.db_file_(NDB)", 192 // docker run --rm --entrypoint cat opensuse/leap:15.5 /var/lib/rpm/Packages.db > third_party/scalibr/extractor/filesystem/os/rpm/testdata/Packages.db 193 path: "testdata/Packages.db", 194 osrelease: fedora38, 195 wantResultMetric: stats.FileExtractedResultSuccess, 196 wantPackages: []*extractor.Package{ 197 { 198 Locations: []string{"testdata/Packages.db"}, 199 Name: "aaa_base", 200 Version: "84.87+git20180409.04c9dae-150300.10.3.1", 201 PURLType: purl.TypeRPM, 202 Metadata: &rpmmeta.Metadata{ 203 PackageName: "aaa_base", 204 Epoch: 0, 205 SourceRPM: "aaa_base-84.87+git20180409.04c9dae-150300.10.3.1.src.rpm", 206 OSID: "fedora", 207 OSVersionID: "38", 208 OSName: "Fedora Linux", 209 OSPrettyName: "Fedora Linux 38 (Container Image)", 210 Vendor: "SUSE LLC <https://www.suse.com/>", 211 Architecture: "x86_64", 212 }, 213 Licenses: []string{"GPL-2.0+"}, 214 }, 215 { 216 Locations: []string{"testdata/Packages.db"}, 217 Name: "bash", 218 Version: "4.4-150400.25.22", 219 PURLType: purl.TypeRPM, 220 Metadata: &rpmmeta.Metadata{ 221 PackageName: "bash", 222 Epoch: 0, 223 OSName: "Fedora Linux", 224 OSPrettyName: "Fedora Linux 38 (Container Image)", 225 SourceRPM: "bash-4.4-150400.25.22.src.rpm", 226 OSID: "fedora", 227 OSVersionID: "38", 228 Vendor: "SUSE LLC <https://www.suse.com/>", 229 Architecture: "x86_64", 230 }, 231 Licenses: []string{"GPL-3.0-or-later"}, 232 }, 233 { 234 Locations: []string{"testdata/Packages.db"}, 235 Name: "bash-sh", 236 Version: "4.4-150400.25.22", 237 PURLType: purl.TypeRPM, 238 Metadata: &rpmmeta.Metadata{ 239 PackageName: "bash-sh", 240 Epoch: 0, 241 SourceRPM: "bash-4.4-150400.25.22.src.rpm", 242 OSID: "fedora", 243 OSVersionID: "38", 244 OSName: "Fedora Linux", 245 OSPrettyName: "Fedora Linux 38 (Container Image)", 246 Vendor: "SUSE LLC <https://www.suse.com/>", 247 Architecture: "x86_64", 248 }, 249 Licenses: []string{"GPL-3.0-or-later"}, 250 }, 251 }, 252 wantResults: 137, 253 }, 254 { 255 name: "CentOS_7.9.2009_Packages_file_(Berkley_DB)", 256 // docker run --rm --entrypoint cat centos:centos7.9.2009 /var/lib/rpm/Packages > third_party/scalibr/extractor/filesystem/os/rpm/testdata/Packages 257 path: "testdata/Packages", 258 osrelease: fedora38, 259 wantResultMetric: stats.FileExtractedResultSuccess, 260 wantPackages: []*extractor.Package{ 261 { 262 Locations: []string{"testdata/Packages"}, 263 Name: "acl", 264 Version: "2.2.51-15.el7", 265 PURLType: purl.TypeRPM, 266 Metadata: &rpmmeta.Metadata{ 267 PackageName: "acl", 268 Epoch: 0, 269 SourceRPM: "acl-2.2.51-15.el7.src.rpm", 270 OSID: "fedora", 271 OSVersionID: "38", 272 OSName: "Fedora Linux", 273 OSPrettyName: "Fedora Linux 38 (Container Image)", 274 Vendor: "CentOS", 275 Architecture: "x86_64", 276 }, 277 Licenses: []string{"GPLv2+"}, 278 }, 279 { 280 Locations: []string{"testdata/Packages"}, 281 Name: "audit-libs", 282 Version: "2.8.5-4.el7", 283 PURLType: purl.TypeRPM, 284 Metadata: &rpmmeta.Metadata{ 285 PackageName: "audit-libs", 286 Epoch: 0, 287 SourceRPM: "audit-2.8.5-4.el7.src.rpm", 288 OSID: "fedora", 289 OSVersionID: "38", 290 OSName: "Fedora Linux", 291 OSPrettyName: "Fedora Linux 38 (Container Image)", 292 Vendor: "CentOS", 293 Architecture: "x86_64", 294 }, 295 Licenses: []string{"LGPLv2+"}, 296 }, 297 { 298 Locations: []string{"testdata/Packages"}, 299 Name: "basesystem", 300 Version: "10.0-7.el7.centos", 301 PURLType: purl.TypeRPM, 302 Metadata: &rpmmeta.Metadata{ 303 PackageName: "basesystem", 304 Epoch: 0, 305 SourceRPM: "basesystem-10.0-7.el7.centos.src.rpm", 306 OSID: "fedora", 307 OSVersionID: "38", 308 OSName: "Fedora Linux", 309 OSPrettyName: "Fedora Linux 38 (Container Image)", 310 Vendor: "CentOS", 311 Architecture: "noarch", 312 }, 313 Licenses: []string{"Public Domain"}, 314 }, 315 }, 316 wantResults: 148, 317 }, 318 { 319 name: "file not found", 320 path: "testdata/foobar", 321 wantPackages: nil, 322 wantResults: 0, 323 wantErr: os.ErrNotExist, 324 wantResultMetric: stats.FileExtractedResultErrorUnknown, 325 }, 326 { 327 name: "empty", 328 path: "testdata/empty.sqlite", 329 wantPackages: nil, 330 wantResults: 0, 331 wantErr: io.EOF, 332 wantResultMetric: stats.FileExtractedResultErrorUnknown, 333 }, 334 { 335 name: "invalid", 336 path: "testdata/invalid", 337 wantPackages: nil, 338 wantResults: 0, 339 wantErr: io.ErrUnexpectedEOF, 340 wantResultMetric: stats.FileExtractedResultErrorUnknown, 341 }, 342 { 343 name: "corrupt db times out", 344 path: "testdata/timeout/Packages", 345 timeoutval: 1 * time.Second, 346 wantPackages: nil, 347 wantResults: 0, 348 wantErr: cmpopts.AnyError, 349 wantResultMetric: stats.FileExtractedResultErrorUnknown, 350 }, 351 { 352 name: "RockyLinux_9.2.20230513_rpmdb.sqlite_file_(sqlite3)", 353 // docker run --rm --entrypoint cat rockylinux:9.2.20230513 /var/lib/rpm/rpmdb.sqlite > third_party/scalibr/extractor/filesystem/os/rpm/testdata/rpmdb.sqlite 354 path: "testdata/rpmdb.sqlite", 355 osrelease: fedora38, 356 wantResultMetric: stats.FileExtractedResultSuccess, 357 wantPackages: []*extractor.Package{ 358 { 359 Locations: []string{"testdata/rpmdb.sqlite"}, 360 Name: "alternatives", 361 Version: "1.20-2.el9", 362 PURLType: purl.TypeRPM, 363 Metadata: &rpmmeta.Metadata{ 364 PackageName: "alternatives", 365 Epoch: 0, 366 SourceRPM: "chkconfig-1.20-2.el9.src.rpm", 367 OSID: "fedora", 368 OSVersionID: "38", 369 OSName: "Fedora Linux", 370 OSPrettyName: "Fedora Linux 38 (Container Image)", 371 Vendor: "Rocky Enterprise Software Foundation", 372 Architecture: "x86_64", 373 }, 374 Licenses: []string{"GPLv2"}, 375 }, 376 { 377 Locations: []string{"testdata/rpmdb.sqlite"}, 378 Name: "audit-libs", 379 Version: "3.0.7-103.el9", 380 PURLType: purl.TypeRPM, 381 Metadata: &rpmmeta.Metadata{ 382 PackageName: "audit-libs", 383 Epoch: 0, 384 SourceRPM: "audit-3.0.7-103.el9.src.rpm", 385 OSID: "fedora", 386 OSVersionID: "38", 387 OSName: "Fedora Linux", 388 OSPrettyName: "Fedora Linux 38 (Container Image)", 389 Vendor: "Rocky Enterprise Software Foundation", 390 Architecture: "x86_64", 391 }, 392 Licenses: []string{"LGPLv2+"}, 393 }, 394 { 395 Locations: []string{"testdata/rpmdb.sqlite"}, 396 Name: "basesystem", 397 Version: "11-13.el9", 398 PURLType: purl.TypeRPM, 399 Metadata: &rpmmeta.Metadata{ 400 PackageName: "basesystem", 401 Epoch: 0, 402 SourceRPM: "basesystem-11-13.el9.src.rpm", 403 OSID: "fedora", 404 OSVersionID: "38", 405 OSName: "Fedora Linux", 406 OSPrettyName: "Fedora Linux 38 (Container Image)", 407 Vendor: "Rocky Enterprise Software Foundation", 408 Architecture: "noarch", 409 }, 410 Licenses: []string{"Public Domain"}, 411 }, 412 }, 413 wantResults: 141, 414 }, 415 { 416 name: "osrelease:_no_version_id", 417 // docker run --rm --entrypoint cat rockylinux:9.2.20230513 /var/lib/rpm/rpmdb.sqlite > third_party/scalibr/extractor/filesystem/os/rpm/testdata/rpmdb.sqlite 418 path: "testdata/rpmdb.sqlite", 419 osrelease: `ID=fedora 420 BUILD_ID=asdf`, 421 wantResultMetric: stats.FileExtractedResultSuccess, 422 wantPackages: []*extractor.Package{ 423 { 424 Locations: []string{"testdata/rpmdb.sqlite"}, 425 Name: "alternatives", 426 Version: "1.20-2.el9", 427 PURLType: purl.TypeRPM, 428 Metadata: &rpmmeta.Metadata{ 429 PackageName: "alternatives", 430 Epoch: 0, 431 SourceRPM: "chkconfig-1.20-2.el9.src.rpm", 432 OSID: "fedora", 433 OSBuildID: "asdf", 434 Vendor: "Rocky Enterprise Software Foundation", 435 Architecture: "x86_64", 436 }, 437 Licenses: []string{"GPLv2"}, 438 }, 439 { 440 Locations: []string{"testdata/rpmdb.sqlite"}, 441 Name: "audit-libs", 442 Version: "3.0.7-103.el9", 443 PURLType: purl.TypeRPM, 444 Metadata: &rpmmeta.Metadata{ 445 PackageName: "audit-libs", 446 Epoch: 0, 447 SourceRPM: "audit-3.0.7-103.el9.src.rpm", 448 OSID: "fedora", 449 OSBuildID: "asdf", 450 Vendor: "Rocky Enterprise Software Foundation", 451 Architecture: "x86_64", 452 }, 453 Licenses: []string{"LGPLv2+"}, 454 }, 455 { 456 Locations: []string{"testdata/rpmdb.sqlite"}, 457 Name: "basesystem", 458 Version: "11-13.el9", 459 PURLType: purl.TypeRPM, 460 Metadata: &rpmmeta.Metadata{ 461 PackageName: "basesystem", 462 Epoch: 0, 463 SourceRPM: "basesystem-11-13.el9.src.rpm", 464 OSID: "fedora", 465 OSBuildID: "asdf", 466 Vendor: "Rocky Enterprise Software Foundation", 467 Architecture: "noarch", 468 }, 469 Licenses: []string{"Public Domain"}, 470 }, 471 }, 472 wantResults: 141, 473 }, 474 { 475 name: "custom_rpm", 476 // https://www.redhat.com/sysadmin/create-rpm-package 477 path: "testdata/Packages_epoch", 478 osrelease: `NAME=Fedora 479 VERSION="32 (Container Image)" 480 ID=fedora 481 VERSION_ID=32 482 VERSION_CODENAME="" 483 PLATFORM_ID="platform:f32" 484 PRETTY_NAME="Fedora 32 (Container Image)" 485 CPE_NAME="cpe:/o:fedoraproject:fedora:32"`, 486 wantResultMetric: stats.FileExtractedResultSuccess, 487 wantPackages: []*extractor.Package{ 488 { 489 Locations: []string{"testdata/Packages"}, 490 Name: "hello", 491 Version: "0.0.1-rls", 492 PURLType: purl.TypeRPM, 493 Metadata: &rpmmeta.Metadata{ 494 PackageName: "hello", 495 Epoch: 1, 496 SourceRPM: "hello-0.0.1-rls.src.rpm", 497 OSID: "fedora", 498 OSName: "Fedora", 499 OSPrettyName: "Fedora 32 (Container Image)", 500 OSVersionID: "32", 501 Architecture: "x86_64", 502 }, 503 Licenses: []string{"GPL"}, 504 }, 505 }, 506 wantResults: 1, 507 }, 508 } 509 510 for _, tt := range tests { 511 t.Run(tt.name, func(t *testing.T) { 512 d := t.TempDir() 513 createOsRelease(t, d, tt.osrelease) 514 515 // Copy files to a temp directory, as sqlite can't open them directly. 516 tmpPath, err := CopyFileToTempDir(t, tt.path, d) 517 if err != nil { 518 t.Fatalf("CopyFileToTempDir(%s) error: %v\n", tt.path, err) 519 } 520 521 info, err := os.Stat(tmpPath) 522 if err != nil && !os.IsNotExist(err) { 523 t.Fatalf("Failed to stat test file: %v", err) 524 } 525 526 collector := testcollector.New() 527 var e filesystem.Extractor = rpm.New(rpm.Config{ 528 Stats: collector, 529 Timeout: tt.timeoutval, 530 }) 531 532 input := &filesystem.ScanInput{ 533 FS: scalibrfs.DirFS(filepath.Dir(tmpPath)), 534 Path: filepath.Base(tmpPath), 535 Root: filepath.Dir(tmpPath), 536 Info: info, 537 } 538 got, err := e.Extract(t.Context(), input) 539 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 540 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tmpPath, err, tt.wantErr) 541 } 542 543 // Update location with the temp path. 544 for _, p := range tt.wantPackages { 545 p.Locations = []string{filepath.Base(tmpPath)} 546 } 547 548 pkgs := got.Packages 549 sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name }) 550 gotFirst3 := pkgs[:min(len(pkgs), 3)] 551 if diff := cmp.Diff(tt.wantPackages, gotFirst3); diff != "" { 552 t.Errorf("Extract(%s) (-want +got):\n%s", tmpPath, diff) 553 } 554 555 if len(pkgs) != tt.wantResults { 556 t.Errorf("Extract(%s): got %d results, want %d\n", tmpPath, len(pkgs), tt.wantResults) 557 } 558 559 gotResultMetric := collector.FileExtractedResult(filepath.Base(tmpPath)) 560 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 561 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tmpPath, gotResultMetric, tt.wantResultMetric) 562 } 563 564 var wantFileSize int64 565 if info != nil { 566 wantFileSize = info.Size() 567 } 568 gotFileSizeMetric := collector.FileExtractedFileSize(filepath.Base(tmpPath)) 569 if gotFileSizeMetric != wantFileSize { 570 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tmpPath, gotFileSizeMetric, wantFileSize) 571 } 572 }) 573 } 574 } 575 576 func TestExtract_VirtualFilesystem(t *testing.T) { 577 // supported OSes 578 if runtime.GOOS == "windows" { 579 t.Skipf("Test skipped, OS unsupported: %v", runtime.GOOS) 580 } 581 582 tests := []struct { 583 name string 584 path string 585 osrelease string 586 timeoutval time.Duration 587 // rpm -qa --qf "%{NAME}@%{VERSION}-%{RELEASE}\n" |sort |head -n 3 588 wantPackages []*extractor.Package 589 // rpm -qa | wc -l 590 wantResults int 591 wantErr error 592 }{ 593 { 594 name: "opensuse/leap:15.5_Packages.db_file_(NDB)", 595 // docker run --rm --entrypoint cat opensuse/leap:15.5 /var/lib/rpm/Packages.db > third_party/scalibr/extractor/filesystem/os/rpm/testdata/Packages.db 596 path: "testdata/Packages.db", 597 osrelease: fedora38, 598 wantPackages: []*extractor.Package{ 599 { 600 Locations: []string{"testdata/Packages.db"}, 601 Name: "aaa_base", 602 Version: "84.87+git20180409.04c9dae-150300.10.3.1", 603 PURLType: purl.TypeRPM, 604 Metadata: &rpmmeta.Metadata{ 605 PackageName: "aaa_base", 606 Epoch: 0, 607 SourceRPM: "aaa_base-84.87+git20180409.04c9dae-150300.10.3.1.src.rpm", 608 OSID: "fedora", 609 OSVersionID: "38", 610 OSName: "Fedora Linux", 611 OSPrettyName: "Fedora Linux 38 (Container Image)", 612 Vendor: "SUSE LLC <https://www.suse.com/>", 613 Architecture: "x86_64", 614 }, 615 Licenses: []string{"GPL-2.0+"}, 616 }, 617 { 618 Locations: []string{"testdata/Packages.db"}, 619 Name: "bash", 620 Version: "4.4-150400.25.22", 621 PURLType: purl.TypeRPM, 622 Metadata: &rpmmeta.Metadata{ 623 PackageName: "bash", 624 Epoch: 0, 625 OSName: "Fedora Linux", 626 OSPrettyName: "Fedora Linux 38 (Container Image)", 627 SourceRPM: "bash-4.4-150400.25.22.src.rpm", 628 OSID: "fedora", 629 OSVersionID: "38", 630 Vendor: "SUSE LLC <https://www.suse.com/>", 631 Architecture: "x86_64", 632 }, 633 Licenses: []string{"GPL-3.0-or-later"}, 634 }, 635 { 636 Locations: []string{"testdata/Packages.db"}, 637 Name: "bash-sh", 638 Version: "4.4-150400.25.22", 639 PURLType: purl.TypeRPM, 640 Metadata: &rpmmeta.Metadata{ 641 PackageName: "bash-sh", 642 Epoch: 0, 643 SourceRPM: "bash-4.4-150400.25.22.src.rpm", 644 OSID: "fedora", 645 OSVersionID: "38", 646 OSName: "Fedora Linux", 647 OSPrettyName: "Fedora Linux 38 (Container Image)", 648 Vendor: "SUSE LLC <https://www.suse.com/>", 649 Architecture: "x86_64", 650 }, 651 Licenses: []string{"GPL-3.0-or-later"}, 652 }, 653 }, 654 wantResults: 137, 655 }, 656 { 657 name: "CentOS_7.9.2009_Packages_file_(Berkley_DB)", 658 // docker run --rm --entrypoint cat centos:centos7.9.2009 /var/lib/rpm/Packages > third_party/scalibr/extractor/filesystem/os/rpm/testdata/Packages 659 path: "testdata/Packages", 660 osrelease: fedora38, 661 wantPackages: []*extractor.Package{ 662 { 663 Locations: []string{"testdata/Packages"}, 664 Name: "acl", 665 Version: "2.2.51-15.el7", 666 PURLType: purl.TypeRPM, 667 Metadata: &rpmmeta.Metadata{ 668 PackageName: "acl", 669 Epoch: 0, 670 SourceRPM: "acl-2.2.51-15.el7.src.rpm", 671 OSID: "fedora", 672 OSVersionID: "38", 673 OSName: "Fedora Linux", 674 OSPrettyName: "Fedora Linux 38 (Container Image)", 675 Vendor: "CentOS", 676 Architecture: "x86_64", 677 }, 678 Licenses: []string{"GPLv2+"}, 679 }, 680 { 681 Locations: []string{"testdata/Packages"}, 682 Name: "audit-libs", 683 Version: "2.8.5-4.el7", 684 PURLType: purl.TypeRPM, 685 Metadata: &rpmmeta.Metadata{ 686 PackageName: "audit-libs", 687 Epoch: 0, 688 SourceRPM: "audit-2.8.5-4.el7.src.rpm", 689 OSID: "fedora", 690 OSVersionID: "38", 691 OSName: "Fedora Linux", 692 OSPrettyName: "Fedora Linux 38 (Container Image)", 693 Vendor: "CentOS", 694 Architecture: "x86_64", 695 }, 696 Licenses: []string{"LGPLv2+"}, 697 }, 698 { 699 Locations: []string{"testdata/Packages"}, 700 Name: "basesystem", 701 Version: "10.0-7.el7.centos", 702 PURLType: purl.TypeRPM, 703 Metadata: &rpmmeta.Metadata{ 704 PackageName: "basesystem", 705 Epoch: 0, 706 SourceRPM: "basesystem-10.0-7.el7.centos.src.rpm", 707 OSID: "fedora", 708 OSVersionID: "38", 709 OSName: "Fedora Linux", 710 OSPrettyName: "Fedora Linux 38 (Container Image)", 711 Vendor: "CentOS", 712 Architecture: "noarch", 713 }, 714 Licenses: []string{"Public Domain"}, 715 }, 716 }, 717 wantResults: 148, 718 }, 719 { 720 name: "RockyLinux_9.2.20230513_rpmdb.sqlite_file_(sqlite3)", 721 // docker run --rm --entrypoint cat rockylinux:9.2.20230513 /var/lib/rpm/rpmdb.sqlite > third_party/scalibr/extractor/filesystem/os/rpm/testdata/rpmdb.sqlite 722 path: "testdata/rpmdb.sqlite", 723 osrelease: fedora38, 724 wantPackages: []*extractor.Package{ 725 { 726 Locations: []string{"testdata/rpmdb.sqlite"}, 727 Name: "alternatives", 728 Version: "1.20-2.el9", 729 PURLType: purl.TypeRPM, 730 Metadata: &rpmmeta.Metadata{ 731 PackageName: "alternatives", 732 Epoch: 0, 733 SourceRPM: "chkconfig-1.20-2.el9.src.rpm", 734 OSID: "fedora", 735 OSVersionID: "38", 736 OSName: "Fedora Linux", 737 OSPrettyName: "Fedora Linux 38 (Container Image)", 738 Vendor: "Rocky Enterprise Software Foundation", 739 Architecture: "x86_64", 740 }, 741 Licenses: []string{"GPLv2"}, 742 }, 743 { 744 Locations: []string{"testdata/rpmdb.sqlite"}, 745 Name: "audit-libs", 746 Version: "3.0.7-103.el9", 747 PURLType: purl.TypeRPM, 748 Metadata: &rpmmeta.Metadata{ 749 PackageName: "audit-libs", 750 Epoch: 0, 751 SourceRPM: "audit-3.0.7-103.el9.src.rpm", 752 OSID: "fedora", 753 OSVersionID: "38", 754 OSName: "Fedora Linux", 755 OSPrettyName: "Fedora Linux 38 (Container Image)", 756 Vendor: "Rocky Enterprise Software Foundation", 757 Architecture: "x86_64", 758 }, 759 Licenses: []string{"LGPLv2+"}, 760 }, 761 { 762 Locations: []string{"testdata/rpmdb.sqlite"}, 763 Name: "basesystem", 764 Version: "11-13.el9", 765 PURLType: purl.TypeRPM, 766 Metadata: &rpmmeta.Metadata{ 767 PackageName: "basesystem", 768 Epoch: 0, 769 SourceRPM: "basesystem-11-13.el9.src.rpm", 770 OSID: "fedora", 771 OSVersionID: "38", 772 OSName: "Fedora Linux", 773 OSPrettyName: "Fedora Linux 38 (Container Image)", 774 Vendor: "Rocky Enterprise Software Foundation", 775 Architecture: "noarch", 776 }, 777 Licenses: []string{"Public Domain"}, 778 }, 779 }, 780 wantResults: 141, 781 }, 782 { 783 name: "custom_rpm", 784 // https://www.redhat.com/sysadmin/create-rpm-package 785 path: "testdata/Packages_epoch", 786 osrelease: `NAME=Fedora 787 VERSION="32 (Container Image)" 788 ID=fedora 789 VERSION_ID=32 790 VERSION_CODENAME="" 791 PLATFORM_ID="platform:f32" 792 PRETTY_NAME="Fedora 32 (Container Image)" 793 CPE_NAME="cpe:/o:fedoraproject:fedora:32"`, 794 795 wantPackages: []*extractor.Package{ 796 { 797 Locations: []string{"testdata/Packages_epoch"}, 798 Name: "hello", 799 Version: "0.0.1-rls", 800 PURLType: purl.TypeRPM, 801 Metadata: &rpmmeta.Metadata{ 802 PackageName: "hello", 803 Epoch: 1, 804 SourceRPM: "hello-0.0.1-rls.src.rpm", 805 OSID: "fedora", 806 OSName: "Fedora", 807 OSPrettyName: "Fedora 32 (Container Image)", 808 OSVersionID: "32", 809 Architecture: "x86_64", 810 }, 811 Licenses: []string{"GPL"}, 812 }, 813 }, 814 wantResults: 1, 815 }, 816 { 817 name: "empty", 818 path: "testdata/empty.sqlite", 819 wantPackages: nil, 820 wantResults: 0, 821 wantErr: io.EOF, 822 }, 823 { 824 name: "invalid", 825 path: "testdata/invalid", 826 wantPackages: nil, 827 wantResults: 0, 828 wantErr: io.ErrUnexpectedEOF, 829 }, 830 } 831 832 for _, tt := range tests { 833 t.Run(tt.name, func(t *testing.T) { 834 d := t.TempDir() 835 createOsRelease(t, d, tt.osrelease) 836 837 // Need to record scalibr files found in /tmp before the rpm extractor runs, as it may create 838 // some. This is needed to compare the files found after the extractor runs. 839 filesInTmpWant := scalibrFilesInTmp(t) 840 841 r, err := os.Open(tt.path) 842 defer func() { 843 if err = r.Close(); err != nil { 844 t.Errorf("Close(): %v", err) 845 } 846 }() 847 if err != nil { 848 t.Fatal(err) 849 } 850 851 info, err := os.Stat(tt.path) 852 if err != nil { 853 t.Fatalf("Failed to stat test file: %v", err) 854 } 855 856 input := &filesystem.ScanInput{ 857 FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Info: info, 858 } 859 860 got, err := rpm.New(rpm.Config{}).Extract(t.Context(), input) 861 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 862 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr) 863 } 864 865 pkgs := got.Packages 866 sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name }) 867 gotFirst3 := pkgs[:min(len(pkgs), 3)] 868 if diff := cmp.Diff(tt.wantPackages, gotFirst3); diff != "" { 869 t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff) 870 } 871 872 if len(pkgs) != tt.wantResults { 873 t.Errorf("Extract(%s): got %d results, want %d\n", tt.path, len(pkgs), tt.wantResults) 874 } 875 876 // Check that no scalibr files remain in /tmp. 877 filesInTmpGot := scalibrFilesInTmp(t) 878 less := func(a, b string) bool { return a < b } 879 if diff := cmp.Diff(filesInTmpWant, filesInTmpGot, cmpopts.SortSlices(less)); diff != "" { 880 t.Errorf("returned unexpected diff (-want +got):\n%s", diff) 881 } 882 }) 883 } 884 } 885 886 // CopyFileToTempDir copies the passed in file to a temporary directory, then returns the new file path. 887 func CopyFileToTempDir(t *testing.T, filepath, root string) (string, error) { 888 t.Helper() 889 890 filename := path.Base(filepath) 891 newfile := path.Join(root, filename) 892 893 bytes, err := os.ReadFile(filepath) 894 if os.IsNotExist(err) { 895 return newfile, nil 896 } 897 if err != nil { 898 return "", err 899 } 900 if err := os.WriteFile(newfile, bytes, 0400); err != nil { 901 return "", err 902 } 903 return newfile, nil 904 } 905 906 func createOsRelease(t *testing.T, root string, content string) { 907 t.Helper() 908 _ = os.MkdirAll(filepath.Join(root, "etc"), 0755) 909 err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644) 910 if err != nil { 911 t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err) 912 } 913 } 914 915 // scalibrFilesInTmp returns the list of filenames in /tmp that start with "scalibr-". 916 func scalibrFilesInTmp(t *testing.T) []string { 917 t.Helper() 918 919 filenames := []string{} 920 files, err := os.ReadDir(os.TempDir()) 921 if err != nil { 922 t.Fatalf("os.ReadDir('%q') error: %v", os.TempDir(), err) 923 } 924 925 for _, f := range files { 926 name := f.Name() 927 if strings.HasPrefix(name, "scalibr-") { 928 filenames = append(filenames, f.Name()) 929 } 930 } 931 return filenames 932 }