github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/deb/parse_dpkg_db_test.go (about) 1 package deb 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/file" 15 "github.com/anchore/syft/syft/linux" 16 "github.com/anchore/syft/syft/pkg" 17 "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" 18 ) 19 20 func Test_parseDpkgStatus(t *testing.T) { 21 tests := []struct { 22 name string 23 expected []pkg.DpkgMetadata 24 fixturePath string 25 }{ 26 { 27 name: "single package", 28 fixturePath: "test-fixtures/status/single", 29 expected: []pkg.DpkgMetadata{ 30 { 31 Package: "apt", 32 Source: "apt-dev", 33 Version: "1.8.2", 34 Architecture: "amd64", 35 InstalledSize: 4064, 36 Maintainer: "APT Development Team <deity@lists.debian.org>", 37 Description: `commandline package manager 38 This package provides commandline tools for searching and 39 managing as well as querying information about packages 40 as a low-level access to all features of the libapt-pkg library. 41 . 42 These include: 43 * apt-get for retrieval of packages and information about them 44 from authenticated sources and for installation, upgrade and 45 removal of packages together with their dependencies 46 * apt-cache for querying available information about installed 47 as well as installable packages 48 * apt-cdrom to use removable media as a source for packages 49 * apt-config as an interface to the configuration settings 50 * apt-key as an interface to manage authentication keys`, 51 Files: []pkg.DpkgFileRecord{ 52 { 53 Path: "/etc/apt/apt.conf.d/01autoremove", 54 Digest: &file.Digest{ 55 Algorithm: "md5", 56 Value: "76120d358bc9037bb6358e737b3050b5", 57 }, 58 IsConfigFile: true, 59 }, 60 { 61 Path: "/etc/cron.daily/apt-compat", 62 Digest: &file.Digest{ 63 Algorithm: "md5", 64 Value: "49e9b2cfa17849700d4db735d04244f3", 65 }, 66 IsConfigFile: true, 67 }, 68 { 69 Path: "/etc/kernel/postinst.d/apt-auto-removal", 70 Digest: &file.Digest{ 71 Algorithm: "md5", 72 Value: "4ad976a68f045517cf4696cec7b8aa3a", 73 }, 74 IsConfigFile: true, 75 }, 76 { 77 Path: "/etc/logrotate.d/apt", 78 Digest: &file.Digest{ 79 Algorithm: "md5", 80 Value: "179f2ed4f85cbaca12fa3d69c2a4a1c3", 81 }, 82 IsConfigFile: true, 83 }, 84 }, 85 }, 86 }, 87 }, 88 { 89 name: "single package with installed size", 90 fixturePath: "test-fixtures/status/installed-size-4KB", 91 expected: []pkg.DpkgMetadata{ 92 { 93 Package: "apt", 94 Source: "apt-dev", 95 Version: "1.8.2", 96 Architecture: "amd64", 97 InstalledSize: 4000, 98 Maintainer: "APT Development Team <deity@lists.debian.org>", 99 Description: `commandline package manager 100 This package provides commandline tools for searching and 101 managing as well as querying information about packages 102 as a low-level access to all features of the libapt-pkg library. 103 . 104 These include: 105 * apt-get for retrieval of packages and information about them 106 from authenticated sources and for installation, upgrade and 107 removal of packages together with their dependencies 108 * apt-cache for querying available information about installed 109 as well as installable packages 110 * apt-cdrom to use removable media as a source for packages 111 * apt-config as an interface to the configuration settings 112 * apt-key as an interface to manage authentication keys`, 113 Files: []pkg.DpkgFileRecord{}, 114 }, 115 }, 116 }, 117 { 118 name: "multiple entries", 119 fixturePath: "test-fixtures/status/multiple", 120 expected: []pkg.DpkgMetadata{ 121 { 122 Package: "no-version", 123 Files: []pkg.DpkgFileRecord{}, 124 }, 125 { 126 Package: "tzdata", 127 Version: "2020a-0+deb10u1", 128 Source: "tzdata-dev", 129 Architecture: "all", 130 InstalledSize: 3036, 131 Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>", 132 Description: `time zone and daylight-saving time data 133 This package contains data required for the implementation of 134 standard local time for many representative locations around the 135 globe. It is updated periodically to reflect changes made by 136 political bodies to time zone boundaries, UTC offsets, and 137 daylight-saving rules.`, 138 Files: []pkg.DpkgFileRecord{}, 139 }, 140 { 141 Package: "util-linux", 142 Version: "2.33.1-0.1", 143 Architecture: "amd64", 144 InstalledSize: 4327, 145 Maintainer: "LaMont Jones <lamont@debian.org>", 146 Description: `miscellaneous system utilities 147 This package contains a number of important utilities, most of which 148 are oriented towards maintenance of your system. Some of the more 149 important utilities included in this package allow you to view kernel 150 messages, create new filesystems, view block device information, 151 interface with real time clock, etc.`, 152 Files: []pkg.DpkgFileRecord{ 153 { 154 Path: "/etc/default/hwclock", 155 Digest: &file.Digest{ 156 Algorithm: "md5", 157 Value: "3916544450533eca69131f894db0ca12", 158 }, 159 IsConfigFile: true, 160 }, 161 { 162 Path: "/etc/init.d/hwclock.sh", 163 Digest: &file.Digest{ 164 Algorithm: "md5", 165 Value: "1ca5c0743fa797ffa364db95bb8d8d8e", 166 }, 167 IsConfigFile: true, 168 }, 169 { 170 Path: "/etc/pam.d/runuser", 171 Digest: &file.Digest{ 172 Algorithm: "md5", 173 Value: "b8b44b045259525e0fae9e38fdb2aeeb", 174 }, 175 IsConfigFile: true, 176 }, 177 { 178 Path: "/etc/pam.d/runuser-l", 179 Digest: &file.Digest{ 180 Algorithm: "md5", 181 Value: "2106ea05877e8913f34b2c77fa02be45", 182 }, 183 IsConfigFile: true, 184 }, 185 { 186 Path: "/etc/pam.d/su", 187 Digest: &file.Digest{ 188 Algorithm: "md5", 189 Value: "ce6dcfda3b190a27a455bb38a45ff34a", 190 }, 191 IsConfigFile: true, 192 }, 193 { 194 Path: "/etc/pam.d/su-l", 195 Digest: &file.Digest{ 196 Algorithm: "md5", 197 Value: "756fef5687fecc0d986e5951427b0c4f", 198 }, 199 IsConfigFile: true, 200 }, 201 }, 202 }, 203 }, 204 }, 205 } 206 207 for _, test := range tests { 208 t.Run(test.name, func(t *testing.T) { 209 f, err := os.Open(test.fixturePath) 210 require.NoError(t, err) 211 t.Cleanup(func() { require.NoError(t, f.Close()) }) 212 213 reader := bufio.NewReader(f) 214 215 entries, err := parseDpkgStatus(reader) 216 require.NoError(t, err) 217 218 if diff := cmp.Diff(test.expected, entries); diff != "" { 219 t.Errorf("unexpected entry (-want +got):\n%s", diff) 220 } 221 }) 222 } 223 } 224 225 func TestSourceVersionExtract(t *testing.T) { 226 tests := []struct { 227 name string 228 input string 229 expected []string 230 }{ 231 { 232 name: "name and version", 233 input: "test (1.2.3)", 234 expected: []string{"test", "1.2.3"}, 235 }, 236 { 237 name: "only name", 238 input: "test", 239 expected: []string{"test", ""}, 240 }, 241 { 242 name: "empty", 243 input: "", 244 expected: []string{"", ""}, 245 }, 246 } 247 248 for _, test := range tests { 249 t.Run(test.name, func(t *testing.T) { 250 name, version := extractSourceVersion(test.input) 251 252 if name != test.expected[0] { 253 t.Errorf("mismatch name for %q : %q!=%q", test.input, name, test.expected[0]) 254 } 255 256 if version != test.expected[1] { 257 t.Errorf("mismatch version for %q : %q!=%q", test.input, version, test.expected[1]) 258 } 259 260 }) 261 } 262 } 263 264 func requireAs(expected error) require.ErrorAssertionFunc { 265 return func(t require.TestingT, err error, i ...interface{}) { 266 require.ErrorAs(t, err, &expected) 267 } 268 } 269 270 func Test_parseDpkgStatus_negativeCases(t *testing.T) { 271 tests := []struct { 272 name string 273 input string 274 want []pkg.Package 275 wantErr require.ErrorAssertionFunc 276 }{ 277 { 278 name: "no more packages", 279 input: `Package: apt`, 280 wantErr: require.NoError, 281 }, 282 { 283 name: "duplicated key", 284 input: `Package: apt 285 Package: apt-get 286 287 `, 288 wantErr: requireAs(errors.New("duplicate key discovered: Package")), 289 }, 290 { 291 name: "no match for continuation", 292 input: ` Package: apt 293 294 `, 295 wantErr: requireAs(errors.New("no match for continuation: line: ' Package: apt'")), 296 }, 297 { 298 name: "find keys", 299 input: `Package: apt 300 Status: install ok installed 301 Installed-Size: 10kib 302 303 `, 304 want: []pkg.Package{ 305 { 306 Name: "apt", 307 Type: "deb", 308 PURL: "pkg:deb/debian/apt?distro=debian-10", 309 Licenses: pkg.NewLicenseSet(), 310 Locations: file.NewLocationSet(file.NewLocation("place")), 311 MetadataType: "DpkgMetadata", 312 Metadata: pkg.DpkgMetadata{ 313 Package: "apt", 314 InstalledSize: 10240, 315 Files: []pkg.DpkgFileRecord{}, 316 }, 317 }, 318 }, 319 wantErr: require.NoError, 320 }, 321 } 322 323 for _, tt := range tests { 324 t.Run(tt.name, func(t *testing.T) { 325 pkgtest.NewCatalogTester(). 326 FromString("place", tt.input). 327 WithErrorAssertion(tt.wantErr). 328 WithLinuxRelease(linux.Release{ID: "debian", VersionID: "10"}). 329 Expects(tt.want, nil). 330 TestParser(t, parseDpkgDB) 331 }) 332 } 333 } 334 335 func Test_handleNewKeyValue(t *testing.T) { 336 tests := []struct { 337 name string 338 line string 339 wantKey string 340 wantVal interface{} 341 wantErr require.ErrorAssertionFunc 342 }{ 343 { 344 name: "cannot parse field", 345 line: "blabla", 346 wantErr: requireAs(errors.New("cannot parse field from line: 'blabla'")), 347 }, 348 { 349 name: "parse field", 350 line: "key: val", 351 wantKey: "key", 352 wantVal: "val", 353 wantErr: require.NoError, 354 }, 355 { 356 name: "parse installed size", 357 line: "InstalledSize: 128", 358 wantKey: "InstalledSize", 359 wantVal: 128, 360 wantErr: require.NoError, 361 }, 362 { 363 name: "parse installed kib size", 364 line: "InstalledSize: 1kib", 365 wantKey: "InstalledSize", 366 wantVal: 1024, 367 wantErr: require.NoError, 368 }, 369 { 370 name: "parse installed kb size", 371 line: "InstalledSize: 1kb", 372 wantKey: "InstalledSize", 373 wantVal: 1000, 374 wantErr: require.NoError, 375 }, 376 { 377 name: "parse installed-size mb", 378 line: "Installed-Size: 1 mb", 379 wantKey: "InstalledSize", 380 wantVal: 1000000, 381 wantErr: require.NoError, 382 }, 383 { 384 name: "fail parsing installed-size", 385 line: "Installed-Size: 1bla", 386 wantKey: "", 387 wantErr: requireAs(fmt.Errorf("unhandled size name: %s", "bla")), 388 }, 389 } 390 for _, tt := range tests { 391 t.Run(tt.name, func(t *testing.T) { 392 gotKey, gotVal, err := handleNewKeyValue(tt.line) 393 tt.wantErr(t, err, fmt.Sprintf("handleNewKeyValue(%v)", tt.line)) 394 395 assert.Equalf(t, tt.wantKey, gotKey, "handleNewKeyValue(%v)", tt.line) 396 assert.Equalf(t, tt.wantVal, gotVal, "handleNewKeyValue(%v)", tt.line) 397 }) 398 } 399 }