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 }