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