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  }