github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/apk/apk_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 apk_test 16 17 import ( 18 "fmt" 19 "io/fs" 20 "os" 21 "path/filepath" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 "github.com/google/osv-scalibr/extractor" 27 "github.com/google/osv-scalibr/extractor/filesystem" 28 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 29 "github.com/google/osv-scalibr/extractor/filesystem/os/apk" 30 apkmeta "github.com/google/osv-scalibr/extractor/filesystem/os/apk/metadata" 31 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 32 scalibrfs "github.com/google/osv-scalibr/fs" 33 "github.com/google/osv-scalibr/inventory" 34 "github.com/google/osv-scalibr/purl" 35 "github.com/google/osv-scalibr/stats" 36 "github.com/google/osv-scalibr/testing/fakefs" 37 "github.com/google/osv-scalibr/testing/testcollector" 38 ) 39 40 func TestFileRequired(t *testing.T) { 41 tests := []struct { 42 name string 43 path string 44 fileSizeBytes int64 45 maxFileSizeBytes int64 46 wantRequired bool 47 wantResultMetric stats.FileRequiredResult 48 }{ 49 { 50 name: "installed file", 51 path: "lib/apk/db/installed", 52 wantRequired: true, 53 wantResultMetric: stats.FileRequiredResultOK, 54 }, 55 { 56 name: "installed file in /usr", 57 path: "usr/lib/apk/db/installed", 58 wantRequired: true, 59 wantResultMetric: stats.FileRequiredResultOK, 60 }, 61 { 62 name: "installed file in /var", 63 path: "var/lib/apk/db/installed", 64 wantRequired: true, 65 wantResultMetric: stats.FileRequiredResultOK, 66 }, 67 { 68 name: "sub file", 69 path: "lib/apk/db/installed/test", 70 wantRequired: false, 71 }, 72 { 73 name: "inside other dir", 74 path: "foo/lib/apk/db/installed", 75 wantRequired: false, 76 }, 77 { 78 name: "installed file required if file size < max file size", 79 path: "lib/apk/db/installed", 80 fileSizeBytes: 100 * units.KiB, 81 maxFileSizeBytes: 1000 * units.KiB, 82 wantRequired: true, 83 wantResultMetric: stats.FileRequiredResultOK, 84 }, 85 { 86 name: "installed file required if file size == max file size", 87 path: "lib/apk/db/installed", 88 fileSizeBytes: 100 * units.KiB, 89 maxFileSizeBytes: 100 * units.KiB, 90 wantRequired: true, 91 wantResultMetric: stats.FileRequiredResultOK, 92 }, 93 { 94 name: "installed file not required if file size > max file size", 95 path: "lib/apk/db/installed", 96 fileSizeBytes: 1000 * units.KiB, 97 maxFileSizeBytes: 100 * units.KiB, 98 wantRequired: false, 99 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 100 }, 101 { 102 name: "installed file required if max file size set to 0", 103 path: "lib/apk/db/installed", 104 fileSizeBytes: 100 * units.KiB, 105 maxFileSizeBytes: 0, 106 wantRequired: true, 107 wantResultMetric: stats.FileRequiredResultOK, 108 }, 109 } 110 111 for _, tt := range tests { 112 t.Run(tt.name, func(t *testing.T) { 113 collector := testcollector.New() 114 var e filesystem.Extractor = apk.New(apk.Config{ 115 Stats: collector, 116 MaxFileSizeBytes: tt.maxFileSizeBytes, 117 }) 118 119 // Set a default file size if not specified. 120 fileSizeBytes := tt.fileSizeBytes 121 if fileSizeBytes == 0 { 122 fileSizeBytes = 1000 123 } 124 125 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 126 FileName: filepath.Base(tt.path), 127 FileMode: fs.ModePerm, 128 FileSize: fileSizeBytes, 129 })) 130 if isRequired != tt.wantRequired { 131 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 132 } 133 134 gotResultMetric := collector.FileRequiredResult(tt.path) 135 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 136 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 137 } 138 }) 139 } 140 } 141 142 const alpine = `NAME="Alpine Linux" 143 ID=alpine 144 VERSION_ID=3.18.0 145 PRETTY_NAME="Alpine Linux v3.18" 146 HOME_URL="https://alpinelinux.org/" 147 BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"` 148 149 func TestExtract(t *testing.T) { 150 tests := []struct { 151 name string 152 path string 153 osrelease string 154 wantPackages []*extractor.Package 155 wantErr error 156 wantResultMetric stats.FileExtractedResult 157 }{ 158 { 159 name: "alpine latest", 160 path: "testdata/installed", 161 osrelease: alpine, 162 wantPackages: []*extractor.Package{ 163 getPackage("testdata/installed", "alpine-baselayout", "alpine-baselayout", "3.4.3-r1", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "65502ca9379dd29d1ac4b0bf0dcf03a3dd1b324a"), 164 getPackage("testdata/installed", "alpine-baselayout-data", "alpine-baselayout", "3.4.3-r1", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "65502ca9379dd29d1ac4b0bf0dcf03a3dd1b324a"), 165 getPackage("testdata/installed", "alpine-keys", "alpine-keys", "2.4-r1", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "MIT", "aab68f8c9ab434a46710de8e12fb3206e2930a59"), 166 getPackage("testdata/installed", "apk-tools", "apk-tools", "2.14.0-r0", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "028d34f678a5386c3dc488cc3b62467c7a9d1a0b"), 167 getPackage("testdata/installed", "busybox", "busybox", "1.36.0-r9", "alpine", "3.18.0", "Sören Tempel <soeren+alpine@soeren-tempel.net>", "x86_64", "GPL-2.0-only", "b5c719c244319df3c72ab1f1ee994c2143cab7f0"), 168 getPackage("testdata/installed", "busybox-binsh", "busybox", "1.36.0-r9", "alpine", "3.18.0", "Sören Tempel <soeren+alpine@soeren-tempel.net>", "x86_64", "GPL-2.0-only", "b5c719c244319df3c72ab1f1ee994c2143cab7f0"), 169 getPackage("testdata/installed", "ca-certificates-bundle", "ca-certificates", "20230506-r0", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "MPL-2.0 AND MIT", "59534a02716a92a10d177a118c34066162eff4a6"), 170 getPackage("testdata/installed", "libc-utils", "libc-dev", "0.7.2-r5", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "BSD-2-Clause AND BSD-3-Clause", "988f183cc9d6699930c3e18ccf4a9e36010afb56"), 171 getPackage("testdata/installed", "libcrypto3", "openssl", "3.1.0-r4", "alpine", "3.18.0", "Ariadne Conill <ariadne@dereferenced.org>", "x86_64", "Apache-2.0", "730b75e01c670e3dba5d6c05420b5f605edb6201"), 172 getPackage("testdata/installed", "libssl3", "openssl", "3.1.0-r4", "alpine", "3.18.0", "Ariadne Conill <ariadne@dereferenced.org>", "x86_64", "Apache-2.0", "730b75e01c670e3dba5d6c05420b5f605edb6201"), 173 getPackage("testdata/installed", "musl", "musl", "1.2.4-r0", "alpine", "3.18.0", "Timo Teräs <timo.teras@iki.fi>", "x86_64", "MIT", "b0d8a9d948174e28a4aefcee4ef6be872225ccce"), 174 getPackage("testdata/installed", "musl-utils", "musl", "1.2.4-r0", "alpine", "3.18.0", "Timo Teräs <timo.teras@iki.fi>", "x86_64", "MIT AND BSD-2-Clause AND GPL-2.0-or-later", "b0d8a9d948174e28a4aefcee4ef6be872225ccce"), 175 getPackage("testdata/installed", "scanelf", "pax-utils", "1.3.7-r1", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "84a227baf001b6e0208e3352b294e4d7a40e93de"), 176 getPackage("testdata/installed", "ssl_client", "busybox", "1.36.0-r9", "alpine", "3.18.0", "Sören Tempel <soeren+alpine@soeren-tempel.net>", "x86_64", "GPL-2.0-only", "b5c719c244319df3c72ab1f1ee994c2143cab7f0"), 177 getPackage("testdata/installed", "zlib", "zlib", "1.2.13-r1", "alpine", "3.18.0", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "Zlib", "84a227baf001b6e0208e3352b294e4d7a40e93de"), 178 }, 179 wantResultMetric: stats.FileExtractedResultSuccess, 180 }, 181 { 182 name: "origin not set", 183 path: "testdata/no-origin", 184 osrelease: alpine, 185 wantPackages: []*extractor.Package{ 186 getPackage("testdata/no-origin", "pkgname", "", "1.2.3", "alpine", "3.18.0", "", "x86_64", "GPL-2.0-only", ""), 187 }, 188 wantResultMetric: stats.FileExtractedResultSuccess, 189 }, 190 { 191 name: "empty", 192 path: "testdata/empty", 193 wantPackages: []*extractor.Package{}, 194 wantResultMetric: stats.FileExtractedResultSuccess, 195 }, 196 { 197 name: "invalid", 198 path: "testdata/invalid", 199 wantPackages: nil, 200 wantErr: cmpopts.AnyError, 201 wantResultMetric: stats.FileExtractedResultErrorUnknown, 202 }, 203 { 204 name: "osrelease_openwrt", 205 path: "testdata/single", 206 osrelease: `ID=openwrt 207 VERSION_ID=1.2.3`, 208 wantPackages: []*extractor.Package{ 209 getPackage("testdata/single", "alpine-baselayout-data", "alpine-baselayout", "3.4.3-r1", "openwrt", "1.2.3", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "65502ca9379dd29d1ac4b0bf0dcf03a3dd1b324a"), 210 }, 211 wantResultMetric: stats.FileExtractedResultSuccess, 212 }, 213 { 214 name: "osrelease no version", 215 path: "testdata/single", 216 osrelease: "ID=openwrt", 217 wantPackages: []*extractor.Package{ 218 getPackage("testdata/single", "alpine-baselayout-data", "alpine-baselayout", "3.4.3-r1", "openwrt", "", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "65502ca9379dd29d1ac4b0bf0dcf03a3dd1b324a"), 219 }, 220 wantResultMetric: stats.FileExtractedResultSuccess, 221 }, 222 { 223 name: "no osrelease", 224 path: "testdata/single", 225 osrelease: "", 226 wantPackages: []*extractor.Package{ 227 getPackage("testdata/single", "alpine-baselayout-data", "alpine-baselayout", "3.4.3-r1", "", "", "Natanael Copa <ncopa@alpinelinux.org>", "x86_64", "GPL-2.0-only", "65502ca9379dd29d1ac4b0bf0dcf03a3dd1b324a"), 228 }, 229 wantResultMetric: stats.FileExtractedResultSuccess, 230 }, 231 { 232 name: "different arch", 233 path: "testdata/different-arch", 234 osrelease: "", 235 wantPackages: []*extractor.Package{ 236 getPackage("testdata/different-arch", "alpine-baselayout-data", "alpine-baselayout", "3.4.3-r1", "", "", "Natanael Copa <ncopa@alpinelinux.org>", "noarch", "GPL-2.0-only", "65502ca9379dd29d1ac4b0bf0dcf03a3dd1b324a"), 237 }, 238 wantResultMetric: stats.FileExtractedResultSuccess, 239 }, 240 } 241 242 for _, tt := range tests { 243 t.Run(tt.name, func(t *testing.T) { 244 collector := testcollector.New() 245 var e filesystem.Extractor = apk.New(apk.Config{ 246 Stats: collector, 247 }) 248 249 d := t.TempDir() 250 createOsRelease(t, d, tt.osrelease) 251 252 r, err := os.Open(tt.path) 253 defer func() { 254 if err = r.Close(); err != nil { 255 t.Errorf("Close(): %v", err) 256 } 257 }() 258 if err != nil { 259 t.Fatal(err) 260 } 261 262 info, err := os.Stat(tt.path) 263 if err != nil { 264 t.Fatalf("Failed to stat test file: %v", err) 265 } 266 267 input := &filesystem.ScanInput{ 268 FS: scalibrfs.DirFS(d), 269 Path: tt.path, 270 Reader: r, 271 Root: d, 272 Info: info, 273 } 274 275 got, err := e.Extract(t.Context(), input) 276 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 277 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.name, err, tt.wantErr) 278 } 279 280 ignoreOrder := cmpopts.SortSlices(func(a, b any) bool { 281 return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b) 282 }) 283 wantInv := inventory.Inventory{Packages: tt.wantPackages} 284 if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" { 285 t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff) 286 } 287 288 gotResultMetric := collector.FileExtractedResult(tt.path) 289 if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { 290 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 291 } 292 293 gotFileSizeMetric := collector.FileExtractedFileSize(tt.path) 294 if gotFileSizeMetric != info.Size() { 295 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size()) 296 } 297 }) 298 } 299 } 300 301 func getPackage(path, pkgName, origin, version, osID, osVersionID, maintainer, arch, license string, commit string) *extractor.Package { 302 p := &extractor.Package{ 303 Locations: []string{path}, 304 Name: pkgName, 305 Version: version, 306 PURLType: purl.TypeApk, 307 Metadata: &apkmeta.Metadata{ 308 PackageName: pkgName, 309 OriginName: origin, 310 OSID: osID, 311 OSVersionID: osVersionID, 312 Maintainer: maintainer, 313 Architecture: arch, 314 }, 315 Licenses: []string{license}, 316 } 317 if commit != "" { 318 p.SourceCode = &extractor.SourceCodeIdentifier{ 319 Commit: commit, 320 } 321 } 322 return p 323 } 324 325 func createOsRelease(t *testing.T, root string, content string) { 326 t.Helper() 327 _ = os.MkdirAll(filepath.Join(root, "etc"), 0755) 328 err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644) 329 if err != nil { 330 t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err) 331 } 332 }