github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/debian/parse_dpkg_db_test.go (about) 1 package debian 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "os" 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 14 "github.com/anchore/syft/syft/artifact" 15 "github.com/anchore/syft/syft/file" 16 "github.com/anchore/syft/syft/linux" 17 "github.com/anchore/syft/syft/pkg" 18 "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" 19 ) 20 21 func Test_parseDpkgStatus(t *testing.T) { 22 tests := []struct { 23 name string 24 expected []pkg.DpkgDBEntry 25 fixturePath string 26 }{ 27 { 28 name: "single package", 29 fixturePath: "test-fixtures/var/lib/dpkg/status.d/single", 30 expected: []pkg.DpkgDBEntry{ 31 { 32 Package: "apt", 33 Source: "apt-dev", 34 Version: "1.8.2", 35 Architecture: "amd64", 36 InstalledSize: 4064, 37 Maintainer: "APT Development Team <deity@lists.debian.org>", 38 Description: `commandline package manager 39 This package provides commandline tools for searching and 40 managing as well as querying information about packages 41 as a low-level access to all features of the libapt-pkg library. 42 . 43 These include: 44 * apt-get for retrieval of packages and information about them 45 from authenticated sources and for installation, upgrade and 46 removal of packages together with their dependencies 47 * apt-cache for querying available information about installed 48 as well as installable packages 49 * apt-cdrom to use removable media as a source for packages 50 * apt-config as an interface to the configuration settings 51 * apt-key as an interface to manage authentication keys`, 52 Provides: []string{"apt-transport-https (= 1.8.2)"}, 53 Depends: []string{ 54 "adduser", 55 "gpgv | gpgv2 | gpgv1", 56 "debian-archive-keyring", 57 "libapt-pkg5.0 (>= 1.7.0~alpha3~)", 58 "libc6 (>= 2.15)", 59 "libgcc1 (>= 1:3.0)", 60 "libgnutls30 (>= 3.6.6)", 61 "libseccomp2 (>= 1.0.1)", 62 "libstdc++6 (>= 5.2)", 63 }, 64 Files: []pkg.DpkgFileRecord{ 65 { 66 Path: "/etc/apt/apt.conf.d/01autoremove", 67 Digest: &file.Digest{ 68 Algorithm: "md5", 69 Value: "76120d358bc9037bb6358e737b3050b5", 70 }, 71 IsConfigFile: true, 72 }, 73 { 74 Path: "/etc/cron.daily/apt-compat", 75 Digest: &file.Digest{ 76 Algorithm: "md5", 77 Value: "49e9b2cfa17849700d4db735d04244f3", 78 }, 79 IsConfigFile: true, 80 }, 81 { 82 Path: "/etc/kernel/postinst.d/apt-auto-removal", 83 Digest: &file.Digest{ 84 Algorithm: "md5", 85 Value: "4ad976a68f045517cf4696cec7b8aa3a", 86 }, 87 IsConfigFile: true, 88 }, 89 { 90 Path: "/etc/logrotate.d/apt", 91 Digest: &file.Digest{ 92 Algorithm: "md5", 93 Value: "179f2ed4f85cbaca12fa3d69c2a4a1c3", 94 }, 95 IsConfigFile: true, 96 }, 97 }, 98 }, 99 }, 100 }, 101 { 102 name: "single package with installed size", 103 fixturePath: "test-fixtures/var/lib/dpkg/status.d/installed-size-4KB", 104 expected: []pkg.DpkgDBEntry{ 105 { 106 Package: "apt", 107 Source: "apt-dev", 108 Version: "1.8.2", 109 Architecture: "amd64", 110 InstalledSize: 4000, 111 Maintainer: "APT Development Team <deity@lists.debian.org>", 112 Description: `commandline package manager 113 This package provides commandline tools for searching and 114 managing as well as querying information about packages 115 as a low-level access to all features of the libapt-pkg library. 116 . 117 These include: 118 * apt-get for retrieval of packages and information about them 119 from authenticated sources and for installation, upgrade and 120 removal of packages together with their dependencies 121 * apt-cache for querying available information about installed 122 as well as installable packages 123 * apt-cdrom to use removable media as a source for packages 124 * apt-config as an interface to the configuration settings 125 * apt-key as an interface to manage authentication keys`, 126 Provides: []string{"apt-transport-https (= 1.8.2)"}, 127 Depends: []string{ 128 "adduser", 129 "gpgv | gpgv2 | gpgv1", 130 "debian-archive-keyring", 131 "libapt-pkg5.0 (>= 1.7.0~alpha3~)", 132 "libc6 (>= 2.15)", 133 "libgcc1 (>= 1:3.0)", 134 "libgnutls30 (>= 3.6.6)", 135 "libseccomp2 (>= 1.0.1)", 136 "libstdc++6 (>= 5.2)", 137 }, 138 Files: []pkg.DpkgFileRecord{}, 139 }, 140 }, 141 }, 142 { 143 name: "multiple entries", 144 fixturePath: "test-fixtures/var/lib/dpkg/status.d/multiple", 145 expected: []pkg.DpkgDBEntry{ 146 { 147 Package: "no-version", 148 Files: []pkg.DpkgFileRecord{}, 149 }, 150 { 151 Package: "tzdata", 152 Version: "2020a-0+deb10u1", 153 Source: "tzdata-dev", 154 Architecture: "all", 155 InstalledSize: 3036, 156 Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>", 157 Description: `time zone and daylight-saving time data 158 This package contains data required for the implementation of 159 standard local time for many representative locations around the 160 globe. It is updated periodically to reflect changes made by 161 political bodies to time zone boundaries, UTC offsets, and 162 daylight-saving rules.`, 163 Provides: []string{"tzdata-buster"}, 164 Depends: []string{"debconf (>= 0.5) | debconf-2.0"}, 165 Files: []pkg.DpkgFileRecord{}, 166 }, 167 { 168 Package: "util-linux", 169 Version: "2.33.1-0.1", 170 Architecture: "amd64", 171 InstalledSize: 4327, 172 Maintainer: "LaMont Jones <lamont@debian.org>", 173 Description: `miscellaneous system utilities 174 This package contains a number of important utilities, most of which 175 are oriented towards maintenance of your system. Some of the more 176 important utilities included in this package allow you to view kernel 177 messages, create new filesystems, view block device information, 178 interface with real time clock, etc.`, 179 Depends: []string{"fdisk", "login (>= 1:4.5-1.1~)"}, 180 PreDepends: []string{ 181 "libaudit1 (>= 1:2.2.1)", "libblkid1 (>= 2.31.1)", "libc6 (>= 2.25)", 182 "libcap-ng0 (>= 0.7.9)", "libmount1 (>= 2.25)", "libpam0g (>= 0.99.7.1)", 183 "libselinux1 (>= 2.6-3~)", "libsmartcols1 (>= 2.33)", "libsystemd0", 184 "libtinfo6 (>= 6)", "libudev1 (>= 183)", "libuuid1 (>= 2.16)", 185 "zlib1g (>= 1:1.1.4)", 186 }, 187 Files: []pkg.DpkgFileRecord{ 188 { 189 Path: "/etc/default/hwclock", 190 Digest: &file.Digest{ 191 Algorithm: "md5", 192 Value: "3916544450533eca69131f894db0ca12", 193 }, 194 IsConfigFile: true, 195 }, 196 { 197 Path: "/etc/init.d/hwclock.sh", 198 Digest: &file.Digest{ 199 Algorithm: "md5", 200 Value: "1ca5c0743fa797ffa364db95bb8d8d8e", 201 }, 202 IsConfigFile: true, 203 }, 204 { 205 Path: "/etc/pam.d/runuser", 206 Digest: &file.Digest{ 207 Algorithm: "md5", 208 Value: "b8b44b045259525e0fae9e38fdb2aeeb", 209 }, 210 IsConfigFile: true, 211 }, 212 { 213 Path: "/etc/pam.d/runuser-l", 214 Digest: &file.Digest{ 215 Algorithm: "md5", 216 Value: "2106ea05877e8913f34b2c77fa02be45", 217 }, 218 IsConfigFile: true, 219 }, 220 { 221 Path: "/etc/pam.d/su", 222 Digest: &file.Digest{ 223 Algorithm: "md5", 224 Value: "ce6dcfda3b190a27a455bb38a45ff34a", 225 }, 226 IsConfigFile: true, 227 }, 228 { 229 Path: "/etc/pam.d/su-l", 230 Digest: &file.Digest{ 231 Algorithm: "md5", 232 Value: "756fef5687fecc0d986e5951427b0c4f", 233 }, 234 IsConfigFile: true, 235 }, 236 }, 237 }, 238 }, 239 }, 240 { 241 name: "deinstall status packages are ignored", 242 fixturePath: "test-fixtures/var/lib/dpkg/status.d/deinstall", 243 expected: []pkg.DpkgDBEntry{ 244 { 245 Package: "linux-image-6.14.0-1012-aws", 246 Source: "linux-signed-aws-6.14", 247 Version: "6.14.0-1012.12~24.04.1", 248 Architecture: "amd64", 249 InstalledSize: 15221, 250 Maintainer: "Canonical Kernel Team <kernel-team@lists.ubuntu.com>", 251 Description: `Signed kernel image aws 252 A kernel image for aws. This version of it is signed with 253 Canonical's signing key.`, 254 Provides: []string{"fuse-module", 255 "linux-image", 256 "spl-dkms", 257 "spl-modules", 258 "v4l2loopback-dkms", 259 "v4l2loopback-modules", 260 "zfs-dkms", 261 "zfs-modules"}, 262 Depends: []string{ 263 "kmod", 264 "linux-base (>= 4.5ubuntu1~16.04.1)", 265 "linux-modules-6.14.0-1012-aws", 266 }, 267 Files: []pkg.DpkgFileRecord{}, 268 }, 269 }, 270 }, 271 } 272 273 for _, test := range tests { 274 t.Run(test.name, func(t *testing.T) { 275 f, err := os.Open(test.fixturePath) 276 require.NoError(t, err) 277 t.Cleanup(func() { require.NoError(t, f.Close()) }) 278 279 reader := bufio.NewReader(f) 280 281 entries, err := parseDpkgStatus(reader) 282 require.NoError(t, err) 283 284 if diff := cmp.Diff(test.expected, entries); diff != "" { 285 t.Errorf("unexpected entry (-want +got):\n%s", diff) 286 } 287 }) 288 } 289 } 290 291 func Test_corruptEntry(t *testing.T) { 292 f, err := os.Open("test-fixtures/var/lib/dpkg/status.d/corrupt") 293 require.NoError(t, err) 294 t.Cleanup(func() { require.NoError(t, f.Close()) }) 295 296 reader := bufio.NewReader(f) 297 298 _, err = parseDpkgStatus(reader) 299 require.Error(t, err) 300 } 301 302 func TestSourceVersionExtract(t *testing.T) { 303 tests := []struct { 304 name string 305 input string 306 expected []string 307 }{ 308 { 309 name: "name and version", 310 input: "test (1.2.3)", 311 expected: []string{"test", "1.2.3"}, 312 }, 313 { 314 name: "only name", 315 input: "test", 316 expected: []string{"test", ""}, 317 }, 318 { 319 name: "empty", 320 input: "", 321 expected: []string{"", ""}, 322 }, 323 } 324 325 for _, test := range tests { 326 t.Run(test.name, func(t *testing.T) { 327 name, version := extractSourceVersion(test.input) 328 329 if name != test.expected[0] { 330 t.Errorf("mismatch name for %q : %q!=%q", test.input, name, test.expected[0]) 331 } 332 333 if version != test.expected[1] { 334 t.Errorf("mismatch version for %q : %q!=%q", test.input, version, test.expected[1]) 335 } 336 337 }) 338 } 339 } 340 341 func requireAs(expected error) require.ErrorAssertionFunc { 342 return func(t require.TestingT, err error, i ...interface{}) { 343 require.ErrorAs(t, err, &expected) 344 } 345 } 346 347 func Test_parseDpkgStatus_negativeCases(t *testing.T) { 348 tests := []struct { 349 name string 350 input string 351 want []pkg.Package 352 wantErr require.ErrorAssertionFunc 353 }{ 354 { 355 name: "no more packages", 356 input: `Package: apt`, 357 wantErr: requireAs(errors.New("unable to determine packages")), 358 }, 359 { 360 name: "duplicated key", 361 input: `Package: apt 362 Package: apt-get 363 364 `, 365 wantErr: requireAs(errors.New("duplicate key discovered: Package")), 366 }, 367 { 368 name: "no match for continuation", 369 input: ` Package: apt 370 371 `, 372 wantErr: requireAs(errors.New("no match for continuation: line: ' Package: apt'")), 373 }, 374 { 375 name: "find keys", 376 input: `Package: apt 377 Status: install ok installed 378 Installed-Size: 10kib 379 380 `, 381 want: []pkg.Package{ 382 { 383 Name: "apt", 384 Type: "deb", 385 PURL: "pkg:deb/debian/apt?distro=debian-10", 386 Licenses: pkg.NewLicenseSet(), 387 Locations: file.NewLocationSet(file.NewLocation("place")), 388 Metadata: pkg.DpkgDBEntry{ 389 Package: "apt", 390 InstalledSize: 10240, 391 Files: []pkg.DpkgFileRecord{}, 392 }, 393 }, 394 }, 395 wantErr: require.NoError, 396 }, 397 } 398 399 for _, tt := range tests { 400 t.Run(tt.name, func(t *testing.T) { 401 pkgtest.NewCatalogTester(). 402 FromString("place", tt.input). 403 WithErrorAssertion(tt.wantErr). 404 WithLinuxRelease(linux.Release{ID: "debian", VersionID: "10"}). 405 Expects(tt.want, nil). 406 TestParser(t, parseDpkgDB) 407 }) 408 } 409 } 410 411 func Test_handleNewKeyValue(t *testing.T) { 412 tests := []struct { 413 name string 414 line string 415 wantKey string 416 wantVal interface{} 417 wantErr require.ErrorAssertionFunc 418 }{ 419 { 420 name: "cannot parse field", 421 line: "blabla", 422 wantErr: requireAs(errors.New("cannot parse field from line: 'blabla'")), 423 }, 424 { 425 name: "parse field", 426 line: "key: val", 427 wantKey: "key", 428 wantVal: "val", 429 wantErr: require.NoError, 430 }, 431 { 432 name: "parse installed size", 433 line: "InstalledSize: 128", 434 wantKey: "InstalledSize", 435 wantVal: 128, 436 wantErr: require.NoError, 437 }, 438 { 439 name: "parse installed kib size", 440 line: "InstalledSize: 1kib", 441 wantKey: "InstalledSize", 442 wantVal: 1024, 443 wantErr: require.NoError, 444 }, 445 { 446 name: "parse installed kb size", 447 line: "InstalledSize: 1kb", 448 wantKey: "InstalledSize", 449 wantVal: 1000, 450 wantErr: require.NoError, 451 }, 452 { 453 name: "parse installed-size mb", 454 line: "Installed-Size: 1 mb", 455 wantKey: "InstalledSize", 456 wantVal: 1000000, 457 wantErr: require.NoError, 458 }, 459 { 460 name: "fail parsing installed-size", 461 line: "Installed-Size: 1bla", 462 wantKey: "", 463 wantErr: requireAs(fmt.Errorf("unhandled size name: %s", "bla")), 464 }, 465 } 466 for _, tt := range tests { 467 t.Run(tt.name, func(t *testing.T) { 468 gotKey, gotVal, err := handleNewKeyValue(tt.line) 469 tt.wantErr(t, err, fmt.Sprintf("handleNewKeyValue(%v)", tt.line)) 470 471 assert.Equalf(t, tt.wantKey, gotKey, "handleNewKeyValue(%v)", tt.line) 472 assert.Equalf(t, tt.wantVal, gotVal, "handleNewKeyValue(%v)", tt.line) 473 }) 474 } 475 } 476 477 func abstractRelationships(t testing.TB, relationships []artifact.Relationship) map[string][]string { 478 t.Helper() 479 480 abstracted := make(map[string][]string) 481 for _, relationship := range relationships { 482 fromPkg, ok := relationship.From.(pkg.Package) 483 if !ok { 484 continue 485 } 486 toPkg, ok := relationship.To.(pkg.Package) 487 if !ok { 488 continue 489 } 490 491 // we build this backwards since we use DependencyOfRelationship instead of DependsOn 492 abstracted[toPkg.Name] = append(abstracted[toPkg.Name], fromPkg.Name) 493 } 494 495 return abstracted 496 }