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