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  }