github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/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 242 for _, test := range tests { 243 t.Run(test.name, func(t *testing.T) { 244 f, err := os.Open(test.fixturePath) 245 require.NoError(t, err) 246 t.Cleanup(func() { require.NoError(t, f.Close()) }) 247 248 reader := bufio.NewReader(f) 249 250 entries, err := parseDpkgStatus(reader) 251 require.NoError(t, err) 252 253 if diff := cmp.Diff(test.expected, entries); diff != "" { 254 t.Errorf("unexpected entry (-want +got):\n%s", diff) 255 } 256 }) 257 } 258 } 259 260 func TestSourceVersionExtract(t *testing.T) { 261 tests := []struct { 262 name string 263 input string 264 expected []string 265 }{ 266 { 267 name: "name and version", 268 input: "test (1.2.3)", 269 expected: []string{"test", "1.2.3"}, 270 }, 271 { 272 name: "only name", 273 input: "test", 274 expected: []string{"test", ""}, 275 }, 276 { 277 name: "empty", 278 input: "", 279 expected: []string{"", ""}, 280 }, 281 } 282 283 for _, test := range tests { 284 t.Run(test.name, func(t *testing.T) { 285 name, version := extractSourceVersion(test.input) 286 287 if name != test.expected[0] { 288 t.Errorf("mismatch name for %q : %q!=%q", test.input, name, test.expected[0]) 289 } 290 291 if version != test.expected[1] { 292 t.Errorf("mismatch version for %q : %q!=%q", test.input, version, test.expected[1]) 293 } 294 295 }) 296 } 297 } 298 299 func requireAs(expected error) require.ErrorAssertionFunc { 300 return func(t require.TestingT, err error, i ...interface{}) { 301 require.ErrorAs(t, err, &expected) 302 } 303 } 304 305 func Test_parseDpkgStatus_negativeCases(t *testing.T) { 306 tests := []struct { 307 name string 308 input string 309 want []pkg.Package 310 wantErr require.ErrorAssertionFunc 311 }{ 312 { 313 name: "no more packages", 314 input: `Package: apt`, 315 wantErr: require.NoError, 316 }, 317 { 318 name: "duplicated key", 319 input: `Package: apt 320 Package: apt-get 321 322 `, 323 wantErr: requireAs(errors.New("duplicate key discovered: Package")), 324 }, 325 { 326 name: "no match for continuation", 327 input: ` Package: apt 328 329 `, 330 wantErr: requireAs(errors.New("no match for continuation: line: ' Package: apt'")), 331 }, 332 { 333 name: "find keys", 334 input: `Package: apt 335 Status: install ok installed 336 Installed-Size: 10kib 337 338 `, 339 want: []pkg.Package{ 340 { 341 Name: "apt", 342 Type: "deb", 343 PURL: "pkg:deb/debian/apt?distro=debian-10", 344 Licenses: pkg.NewLicenseSet(), 345 Locations: file.NewLocationSet(file.NewLocation("place")), 346 Metadata: pkg.DpkgDBEntry{ 347 Package: "apt", 348 InstalledSize: 10240, 349 Files: []pkg.DpkgFileRecord{}, 350 }, 351 }, 352 }, 353 wantErr: require.NoError, 354 }, 355 } 356 357 for _, tt := range tests { 358 t.Run(tt.name, func(t *testing.T) { 359 pkgtest.NewCatalogTester(). 360 FromString("place", tt.input). 361 WithErrorAssertion(tt.wantErr). 362 WithLinuxRelease(linux.Release{ID: "debian", VersionID: "10"}). 363 Expects(tt.want, nil). 364 TestParser(t, parseDpkgDB) 365 }) 366 } 367 } 368 369 func Test_handleNewKeyValue(t *testing.T) { 370 tests := []struct { 371 name string 372 line string 373 wantKey string 374 wantVal interface{} 375 wantErr require.ErrorAssertionFunc 376 }{ 377 { 378 name: "cannot parse field", 379 line: "blabla", 380 wantErr: requireAs(errors.New("cannot parse field from line: 'blabla'")), 381 }, 382 { 383 name: "parse field", 384 line: "key: val", 385 wantKey: "key", 386 wantVal: "val", 387 wantErr: require.NoError, 388 }, 389 { 390 name: "parse installed size", 391 line: "InstalledSize: 128", 392 wantKey: "InstalledSize", 393 wantVal: 128, 394 wantErr: require.NoError, 395 }, 396 { 397 name: "parse installed kib size", 398 line: "InstalledSize: 1kib", 399 wantKey: "InstalledSize", 400 wantVal: 1024, 401 wantErr: require.NoError, 402 }, 403 { 404 name: "parse installed kb size", 405 line: "InstalledSize: 1kb", 406 wantKey: "InstalledSize", 407 wantVal: 1000, 408 wantErr: require.NoError, 409 }, 410 { 411 name: "parse installed-size mb", 412 line: "Installed-Size: 1 mb", 413 wantKey: "InstalledSize", 414 wantVal: 1000000, 415 wantErr: require.NoError, 416 }, 417 { 418 name: "fail parsing installed-size", 419 line: "Installed-Size: 1bla", 420 wantKey: "", 421 wantErr: requireAs(fmt.Errorf("unhandled size name: %s", "bla")), 422 }, 423 } 424 for _, tt := range tests { 425 t.Run(tt.name, func(t *testing.T) { 426 gotKey, gotVal, err := handleNewKeyValue(tt.line) 427 tt.wantErr(t, err, fmt.Sprintf("handleNewKeyValue(%v)", tt.line)) 428 429 assert.Equalf(t, tt.wantKey, gotKey, "handleNewKeyValue(%v)", tt.line) 430 assert.Equalf(t, tt.wantVal, gotVal, "handleNewKeyValue(%v)", tt.line) 431 }) 432 } 433 } 434 435 func abstractRelationships(t testing.TB, relationships []artifact.Relationship) map[string][]string { 436 t.Helper() 437 438 abstracted := make(map[string][]string) 439 for _, relationship := range relationships { 440 fromPkg, ok := relationship.From.(pkg.Package) 441 if !ok { 442 continue 443 } 444 toPkg, ok := relationship.To.(pkg.Package) 445 if !ok { 446 continue 447 } 448 449 // we build this backwards since we use DependencyOfRelationship instead of DependsOn 450 abstracted[toPkg.Name] = append(abstracted[toPkg.Name], fromPkg.Name) 451 } 452 453 return abstracted 454 }