github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/pacman/pacman_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 pacman_test
    16  
    17  import (
    18  	"io/fs"
    19  	"os"
    20  	"path/filepath"
    21  	"reflect"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/os/pacman"
    29  	pacmanmeta "github.com/google/osv-scalibr/extractor/filesystem/os/pacman/metadata"
    30  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    31  	scalibrfs "github.com/google/osv-scalibr/fs"
    32  	"github.com/google/osv-scalibr/inventory"
    33  	"github.com/google/osv-scalibr/purl"
    34  	"github.com/google/osv-scalibr/stats"
    35  	"github.com/google/osv-scalibr/testing/fakefs"
    36  	"github.com/google/osv-scalibr/testing/testcollector"
    37  )
    38  
    39  func TestNew(t *testing.T) {
    40  	tests := []struct {
    41  		name    string
    42  		cfg     pacman.Config
    43  		wantCfg pacman.Config
    44  	}{
    45  		{
    46  			name: "default",
    47  			cfg:  pacman.DefaultConfig(),
    48  			wantCfg: pacman.Config{
    49  				MaxFileSizeBytes: 100 * units.MiB,
    50  			},
    51  		},
    52  		{
    53  			name: "custom",
    54  			cfg: pacman.Config{
    55  				MaxFileSizeBytes: 10,
    56  			},
    57  			wantCfg: pacman.Config{
    58  				MaxFileSizeBytes: 10,
    59  			},
    60  		},
    61  	}
    62  
    63  	for _, tt := range tests {
    64  		t.Run(tt.name, func(t *testing.T) {
    65  			got := pacman.New(tt.cfg)
    66  			if !reflect.DeepEqual(got.Config(), tt.wantCfg) {
    67  				t.Errorf("New(%+v).Config(): got %+v, want %+v", tt.cfg, got.Config(), tt.wantCfg)
    68  			}
    69  		})
    70  	}
    71  }
    72  
    73  func TestFileRequired(t *testing.T) {
    74  	tests := []struct {
    75  		name             string
    76  		path             string
    77  		fileSizeBytes    int64
    78  		maxFileSizeBytes int64
    79  		wantRequired     bool
    80  		wantResultMetric stats.FileRequiredResult
    81  	}{
    82  		{
    83  			name:             "desc file",
    84  			path:             "var/lib/pacman/local/pacmanlinux-keyring-20241015-1/desc",
    85  			wantRequired:     true,
    86  			wantResultMetric: stats.FileRequiredResultOK,
    87  		},
    88  		{
    89  			name:             "desc file required if file size < max file size",
    90  			path:             "var/lib/pacman/local/argon2-20190702-6/desc",
    91  			fileSizeBytes:    100 * units.KiB,
    92  			maxFileSizeBytes: 1000 * units.KiB,
    93  			wantRequired:     true,
    94  			wantResultMetric: stats.FileRequiredResultOK,
    95  		},
    96  		{
    97  			name:             "desc file required if file size == max file size",
    98  			path:             "var/lib/pacman/local/audit-4.0.2-2/desc",
    99  			fileSizeBytes:    1000 * units.KiB,
   100  			maxFileSizeBytes: 1000 * units.KiB,
   101  			wantRequired:     true,
   102  			wantResultMetric: stats.FileRequiredResultOK,
   103  		},
   104  		{
   105  			name:             "desc file not required if file size > max file size",
   106  			path:             "var/lib/pacman/local/argon2-20190702-6/desc",
   107  			fileSizeBytes:    1000 * units.KiB,
   108  			maxFileSizeBytes: 100 * units.KiB,
   109  			wantRequired:     false,
   110  			wantResultMetric: stats.FileRequiredResultSizeLimitExceeded,
   111  		},
   112  		{
   113  			name:             "desc file required if max file size set to 0",
   114  			path:             "var/lib/pacman/local/audit-4.0.2-2/desc",
   115  			fileSizeBytes:    100 * units.KiB,
   116  			maxFileSizeBytes: 0,
   117  			wantRequired:     true,
   118  			wantResultMetric: stats.FileRequiredResultOK,
   119  		},
   120  		{
   121  			name:         "invalid file",
   122  			path:         "var/lib/pacman/local/pacmanlinux-keyring-20241015-1/foodesc",
   123  			wantRequired: false,
   124  		},
   125  		{
   126  			name:         "invalid file",
   127  			path:         "var/lib/pacman/local/pacmanlinux-keyring-20241015-1/desc/foo",
   128  			wantRequired: false,
   129  		},
   130  		{
   131  			name:         "invalid file",
   132  			path:         "var/lib/pacman/localfoo/desc",
   133  			wantRequired: false,
   134  		},
   135  	}
   136  
   137  	for _, tt := range tests {
   138  		t.Run(tt.name, func(t *testing.T) {
   139  			collector := testcollector.New()
   140  			var e filesystem.Extractor = pacman.New(pacman.Config{
   141  				Stats:            collector,
   142  				MaxFileSizeBytes: tt.maxFileSizeBytes,
   143  			})
   144  
   145  			fileSizeBytes := tt.fileSizeBytes
   146  			if fileSizeBytes == 0 {
   147  				fileSizeBytes = 1000
   148  			}
   149  
   150  			isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{
   151  				FileName: filepath.Base(tt.path),
   152  				FileMode: fs.ModePerm,
   153  				FileSize: fileSizeBytes,
   154  			}))
   155  			if isRequired != tt.wantRequired {
   156  				t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired)
   157  			}
   158  
   159  			gotResultMetric := collector.FileRequiredResult(tt.path)
   160  			if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric {
   161  				t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric)
   162  			}
   163  		})
   164  	}
   165  }
   166  
   167  const ArchRolling = `NAME="Arch Linux"
   168  PRETTY_NAME="Arch Linux"
   169  ID=arch
   170  BUILD_ID=rolling
   171  VERSION_ID=20241201.0.284684
   172  ANSI_COLOR="38;2;23;147;209"
   173  HOME_URL="https://archlinux.org/"
   174  DOCUMENTATION_URL="https://wiki.archlinux.org/"
   175  SUPPORT_URL="https://bbs.archlinux.org/"
   176  BUG_REPORT_URL="https://gitlab.archlinux.org/groups/archlinux/-/issues"
   177  PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
   178  LOGO=archlinux-logo
   179  `
   180  
   181  func TestExtract(t *testing.T) {
   182  	tests := []struct {
   183  		name             string
   184  		path             string
   185  		osrelease        string
   186  		cfg              pacman.Config
   187  		wantPackages     []*extractor.Package
   188  		wantErr          error
   189  		wantResultMetric stats.FileExtractedResult
   190  	}{
   191  		{
   192  			name:      "valid desc file",
   193  			path:      "testdata/valid",
   194  			osrelease: ArchRolling,
   195  			wantPackages: []*extractor.Package{
   196  				{
   197  					Name:     "gawk",
   198  					Version:  "5.3.1-1",
   199  					PURLType: purl.TypePacman,
   200  					Metadata: &pacmanmeta.Metadata{
   201  						PackageName:         "gawk",
   202  						PackageVersion:      "5.3.1-1",
   203  						OSID:                "arch",
   204  						OSVersionID:         "20241201.0.284684",
   205  						PackageDependencies: "sh, glibc, mpfr",
   206  					},
   207  					Locations: []string{"testdata/valid"},
   208  				},
   209  			},
   210  			wantResultMetric: stats.FileExtractedResultSuccess,
   211  		},
   212  		{
   213  			name:      "valid desc file one dependency",
   214  			path:      "testdata/valid_one_dep",
   215  			osrelease: ArchRolling,
   216  			wantPackages: []*extractor.Package{
   217  				{
   218  					Name:     "filesystem",
   219  					Version:  "2024.11.21-1",
   220  					PURLType: purl.TypePacman,
   221  					Metadata: &pacmanmeta.Metadata{
   222  						PackageName:         "filesystem",
   223  						PackageVersion:      "2024.11.21-1",
   224  						OSID:                "arch",
   225  						OSVersionID:         "20241201.0.284684",
   226  						PackageDependencies: "iana-etc",
   227  					},
   228  					Locations: []string{"testdata/valid_one_dep"},
   229  				},
   230  			},
   231  			wantResultMetric: stats.FileExtractedResultSuccess,
   232  		},
   233  		{
   234  			name:      "valid desc file no dependencies",
   235  			path:      "testdata/valid_no_dep",
   236  			osrelease: ArchRolling,
   237  			wantPackages: []*extractor.Package{
   238  				{
   239  					Name:     "libxml2",
   240  					Version:  "2.13.5-1",
   241  					PURLType: purl.TypePacman,
   242  					Metadata: &pacmanmeta.Metadata{
   243  						PackageName:    "libxml2",
   244  						PackageVersion: "2.13.5-1",
   245  						OSID:           "arch",
   246  						OSVersionID:    "20241201.0.284684",
   247  					},
   248  					Locations: []string{"testdata/valid_no_dep"},
   249  				},
   250  			},
   251  			wantResultMetric: stats.FileExtractedResultSuccess,
   252  		},
   253  		{
   254  			name:      "no os version",
   255  			path:      "testdata/valid",
   256  			osrelease: `ID=arch`,
   257  			wantPackages: []*extractor.Package{
   258  				{
   259  					Name:     "gawk",
   260  					Version:  "5.3.1-1",
   261  					PURLType: purl.TypePacman,
   262  					Metadata: &pacmanmeta.Metadata{
   263  						PackageName:         "gawk",
   264  						PackageVersion:      "5.3.1-1",
   265  						OSID:                "arch",
   266  						PackageDependencies: "sh, glibc, mpfr",
   267  					},
   268  					Locations: []string{"testdata/valid"},
   269  				},
   270  			},
   271  			wantResultMetric: stats.FileExtractedResultSuccess,
   272  		},
   273  		{
   274  			name: "missing_osrelease",
   275  			path: "testdata/valid",
   276  			wantPackages: []*extractor.Package{
   277  				{
   278  					Name:     "gawk",
   279  					Version:  "5.3.1-1",
   280  					PURLType: purl.TypePacman,
   281  					Metadata: &pacmanmeta.Metadata{
   282  						PackageName:         "gawk",
   283  						PackageVersion:      "5.3.1-1",
   284  						PackageDependencies: "sh, glibc, mpfr",
   285  					},
   286  					Locations: []string{"testdata/valid"},
   287  				},
   288  			},
   289  			wantResultMetric: stats.FileExtractedResultSuccess,
   290  		},
   291  		{
   292  			name:         "invalid value eof",
   293  			path:         "testdata/invalid_value_eof",
   294  			osrelease:    ArchRolling,
   295  			wantPackages: []*extractor.Package{},
   296  		},
   297  		{
   298  			name:      "eof after dependencies",
   299  			path:      "testdata/eof_after_dependencies",
   300  			osrelease: ArchRolling,
   301  			wantPackages: []*extractor.Package{
   302  				{
   303  					Name:     "gawk",
   304  					Version:  "5.3.1-1",
   305  					PURLType: purl.TypePacman,
   306  					Metadata: &pacmanmeta.Metadata{
   307  						PackageName:         "gawk",
   308  						PackageVersion:      "5.3.1-1",
   309  						OSID:                "arch",
   310  						OSVersionID:         "20241201.0.284684",
   311  						PackageDependencies: "sh, glibc, mpfr",
   312  					},
   313  					Locations: []string{"testdata/eof_after_dependencies"},
   314  				},
   315  			},
   316  			wantResultMetric: stats.FileExtractedResultSuccess,
   317  		},
   318  	}
   319  
   320  	for _, tt := range tests {
   321  		t.Run(tt.name, func(t *testing.T) {
   322  			collector := testcollector.New()
   323  			var e filesystem.Extractor = pacman.New(pacman.Config{
   324  				Stats:            collector,
   325  				MaxFileSizeBytes: 100,
   326  			})
   327  
   328  			d := t.TempDir()
   329  			createOsRelease(t, d, tt.osrelease)
   330  
   331  			// Opening and Reading the Test File
   332  			r, err := os.Open(tt.path)
   333  			defer func() {
   334  				if err = r.Close(); err != nil {
   335  					t.Errorf("Close(): %v", err)
   336  				}
   337  			}()
   338  			if err != nil {
   339  				t.Fatal(err)
   340  			}
   341  
   342  			info, err := os.Stat(tt.path)
   343  			if err != nil {
   344  				t.Fatalf("Failed to stat test file: %v", err)
   345  			}
   346  
   347  			input := &filesystem.ScanInput{
   348  				FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info,
   349  			}
   350  
   351  			got, err := e.Extract(t.Context(), input)
   352  
   353  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
   354  			if diff := cmp.Diff(wantInv, got); diff != "" {
   355  				t.Errorf("Package mismatch (-want +got):\n%s", diff)
   356  			}
   357  		})
   358  	}
   359  }
   360  
   361  func createOsRelease(t *testing.T, root string, content string) {
   362  	t.Helper()
   363  	_ = os.MkdirAll(filepath.Join(root, "etc"), 0755)
   364  	err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644)
   365  	if err != nil {
   366  		t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err)
   367  	}
   368  }