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  }