github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/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/generic"
    19  	"github.com/lineaje-labs/syft/syft/pkg/cataloger/internal/pkgtest"
    20  )
    21  
    22  func Test_parseDpkgStatus(t *testing.T) {
    23  	tests := []struct {
    24  		name        string
    25  		expected    []pkg.DpkgDBEntry
    26  		fixturePath string
    27  	}{
    28  		{
    29  			name:        "single package",
    30  			fixturePath: "test-fixtures/status/single",
    31  			expected: []pkg.DpkgDBEntry{
    32  				{
    33  					Package:       "apt",
    34  					Source:        "apt-dev",
    35  					Version:       "1.8.2",
    36  					Architecture:  "amd64",
    37  					InstalledSize: 4064,
    38  					Maintainer:    "APT Development Team <deity@lists.debian.org>",
    39  					Description: `commandline package manager
    40   This package provides commandline tools for searching and
    41   managing as well as querying information about packages
    42   as a low-level access to all features of the libapt-pkg library.
    43   .
    44   These include:
    45   * apt-get for retrieval of packages and information about them
    46   from authenticated sources and for installation, upgrade and
    47   removal of packages together with their dependencies
    48   * apt-cache for querying available information about installed
    49   as well as installable packages
    50   * apt-cdrom to use removable media as a source for packages
    51   * apt-config as an interface to the configuration settings
    52   * apt-key as an interface to manage authentication keys`,
    53  					Provides: []string{"apt-transport-https (= 1.8.2)"},
    54  					Depends: []string{
    55  						"adduser",
    56  						"gpgv | gpgv2 | gpgv1",
    57  						"debian-archive-keyring",
    58  						"libapt-pkg5.0 (>= 1.7.0~alpha3~)",
    59  						"libc6 (>= 2.15)",
    60  						"libgcc1 (>= 1:3.0)",
    61  						"libgnutls30 (>= 3.6.6)",
    62  						"libseccomp2 (>= 1.0.1)",
    63  						"libstdc++6 (>= 5.2)",
    64  					},
    65  					Files: []pkg.DpkgFileRecord{
    66  						{
    67  							Path: "/etc/apt/apt.conf.d/01autoremove",
    68  							Digest: &file.Digest{
    69  								Algorithm: "md5",
    70  								Value:     "76120d358bc9037bb6358e737b3050b5",
    71  							},
    72  							IsConfigFile: true,
    73  						},
    74  						{
    75  							Path: "/etc/cron.daily/apt-compat",
    76  							Digest: &file.Digest{
    77  								Algorithm: "md5",
    78  								Value:     "49e9b2cfa17849700d4db735d04244f3",
    79  							},
    80  							IsConfigFile: true,
    81  						},
    82  						{
    83  							Path: "/etc/kernel/postinst.d/apt-auto-removal",
    84  							Digest: &file.Digest{
    85  								Algorithm: "md5",
    86  								Value:     "4ad976a68f045517cf4696cec7b8aa3a",
    87  							},
    88  							IsConfigFile: true,
    89  						},
    90  						{
    91  							Path: "/etc/logrotate.d/apt",
    92  							Digest: &file.Digest{
    93  								Algorithm: "md5",
    94  								Value:     "179f2ed4f85cbaca12fa3d69c2a4a1c3",
    95  							},
    96  							IsConfigFile: true,
    97  						},
    98  					},
    99  				},
   100  			},
   101  		},
   102  		{
   103  			name:        "single package with installed size",
   104  			fixturePath: "test-fixtures/status/installed-size-4KB",
   105  			expected: []pkg.DpkgDBEntry{
   106  				{
   107  					Package:       "apt",
   108  					Source:        "apt-dev",
   109  					Version:       "1.8.2",
   110  					Architecture:  "amd64",
   111  					InstalledSize: 4000,
   112  					Maintainer:    "APT Development Team <deity@lists.debian.org>",
   113  					Description: `commandline package manager
   114   This package provides commandline tools for searching and
   115   managing as well as querying information about packages
   116   as a low-level access to all features of the libapt-pkg library.
   117   .
   118   These include:
   119   * apt-get for retrieval of packages and information about them
   120   from authenticated sources and for installation, upgrade and
   121   removal of packages together with their dependencies
   122   * apt-cache for querying available information about installed
   123   as well as installable packages
   124   * apt-cdrom to use removable media as a source for packages
   125   * apt-config as an interface to the configuration settings
   126   * apt-key as an interface to manage authentication keys`,
   127  					Provides: []string{"apt-transport-https (= 1.8.2)"},
   128  					Depends: []string{
   129  						"adduser",
   130  						"gpgv | gpgv2 | gpgv1",
   131  						"debian-archive-keyring",
   132  						"libapt-pkg5.0 (>= 1.7.0~alpha3~)",
   133  						"libc6 (>= 2.15)",
   134  						"libgcc1 (>= 1:3.0)",
   135  						"libgnutls30 (>= 3.6.6)",
   136  						"libseccomp2 (>= 1.0.1)",
   137  						"libstdc++6 (>= 5.2)",
   138  					},
   139  					Files: []pkg.DpkgFileRecord{},
   140  				},
   141  			},
   142  		},
   143  		{
   144  			name:        "multiple entries",
   145  			fixturePath: "test-fixtures/status/multiple",
   146  			expected: []pkg.DpkgDBEntry{
   147  				{
   148  					Package: "no-version",
   149  					Files:   []pkg.DpkgFileRecord{},
   150  				},
   151  				{
   152  					Package:       "tzdata",
   153  					Version:       "2020a-0+deb10u1",
   154  					Source:        "tzdata-dev",
   155  					Architecture:  "all",
   156  					InstalledSize: 3036,
   157  					Maintainer:    "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
   158  					Description: `time zone and daylight-saving time data
   159   This package contains data required for the implementation of
   160   standard local time for many representative locations around the
   161   globe. It is updated periodically to reflect changes made by
   162   political bodies to time zone boundaries, UTC offsets, and
   163   daylight-saving rules.`,
   164  					Provides: []string{"tzdata-buster"},
   165  					Depends:  []string{"debconf (>= 0.5) | debconf-2.0"},
   166  					Files:    []pkg.DpkgFileRecord{},
   167  				},
   168  				{
   169  					Package:       "util-linux",
   170  					Version:       "2.33.1-0.1",
   171  					Architecture:  "amd64",
   172  					InstalledSize: 4327,
   173  					Maintainer:    "LaMont Jones <lamont@debian.org>",
   174  					Description: `miscellaneous system utilities
   175   This package contains a number of important utilities, most of which
   176   are oriented towards maintenance of your system. Some of the more
   177   important utilities included in this package allow you to view kernel
   178   messages, create new filesystems, view block device information,
   179   interface with real time clock, etc.`,
   180  					Depends: []string{"fdisk", "login (>= 1:4.5-1.1~)"},
   181  					PreDepends: []string{
   182  						"libaudit1 (>= 1:2.2.1)", "libblkid1 (>= 2.31.1)", "libc6 (>= 2.25)",
   183  						"libcap-ng0 (>= 0.7.9)", "libmount1 (>= 2.25)", "libpam0g (>= 0.99.7.1)",
   184  						"libselinux1 (>= 2.6-3~)", "libsmartcols1 (>= 2.33)", "libsystemd0",
   185  						"libtinfo6 (>= 6)", "libudev1 (>= 183)", "libuuid1 (>= 2.16)",
   186  						"zlib1g (>= 1:1.1.4)",
   187  					},
   188  					Files: []pkg.DpkgFileRecord{
   189  						{
   190  							Path: "/etc/default/hwclock",
   191  							Digest: &file.Digest{
   192  								Algorithm: "md5",
   193  								Value:     "3916544450533eca69131f894db0ca12",
   194  							},
   195  							IsConfigFile: true,
   196  						},
   197  						{
   198  							Path: "/etc/init.d/hwclock.sh",
   199  							Digest: &file.Digest{
   200  								Algorithm: "md5",
   201  								Value:     "1ca5c0743fa797ffa364db95bb8d8d8e",
   202  							},
   203  							IsConfigFile: true,
   204  						},
   205  						{
   206  							Path: "/etc/pam.d/runuser",
   207  							Digest: &file.Digest{
   208  								Algorithm: "md5",
   209  								Value:     "b8b44b045259525e0fae9e38fdb2aeeb",
   210  							},
   211  							IsConfigFile: true,
   212  						},
   213  						{
   214  							Path: "/etc/pam.d/runuser-l",
   215  							Digest: &file.Digest{
   216  								Algorithm: "md5",
   217  								Value:     "2106ea05877e8913f34b2c77fa02be45",
   218  							},
   219  							IsConfigFile: true,
   220  						},
   221  						{
   222  							Path: "/etc/pam.d/su",
   223  							Digest: &file.Digest{
   224  								Algorithm: "md5",
   225  								Value:     "ce6dcfda3b190a27a455bb38a45ff34a",
   226  							},
   227  							IsConfigFile: true,
   228  						},
   229  						{
   230  							Path: "/etc/pam.d/su-l",
   231  							Digest: &file.Digest{
   232  								Algorithm: "md5",
   233  								Value:     "756fef5687fecc0d986e5951427b0c4f",
   234  							},
   235  							IsConfigFile: true,
   236  						},
   237  					},
   238  				},
   239  			},
   240  		},
   241  	}
   242  
   243  	for _, test := range tests {
   244  		t.Run(test.name, func(t *testing.T) {
   245  			f, err := os.Open(test.fixturePath)
   246  			require.NoError(t, err)
   247  			t.Cleanup(func() { require.NoError(t, f.Close()) })
   248  
   249  			reader := bufio.NewReader(f)
   250  
   251  			entries, err := parseDpkgStatus(reader)
   252  			require.NoError(t, err)
   253  
   254  			if diff := cmp.Diff(test.expected, entries); diff != "" {
   255  				t.Errorf("unexpected entry (-want +got):\n%s", diff)
   256  			}
   257  		})
   258  	}
   259  }
   260  
   261  func TestSourceVersionExtract(t *testing.T) {
   262  	tests := []struct {
   263  		name     string
   264  		input    string
   265  		expected []string
   266  	}{
   267  		{
   268  			name:     "name and version",
   269  			input:    "test (1.2.3)",
   270  			expected: []string{"test", "1.2.3"},
   271  		},
   272  		{
   273  			name:     "only name",
   274  			input:    "test",
   275  			expected: []string{"test", ""},
   276  		},
   277  		{
   278  			name:     "empty",
   279  			input:    "",
   280  			expected: []string{"", ""},
   281  		},
   282  	}
   283  
   284  	for _, test := range tests {
   285  		t.Run(test.name, func(t *testing.T) {
   286  			name, version := extractSourceVersion(test.input)
   287  
   288  			if name != test.expected[0] {
   289  				t.Errorf("mismatch name for %q : %q!=%q", test.input, name, test.expected[0])
   290  			}
   291  
   292  			if version != test.expected[1] {
   293  				t.Errorf("mismatch version for %q : %q!=%q", test.input, version, test.expected[1])
   294  			}
   295  
   296  		})
   297  	}
   298  }
   299  
   300  func requireAs(expected error) require.ErrorAssertionFunc {
   301  	return func(t require.TestingT, err error, i ...interface{}) {
   302  		require.ErrorAs(t, err, &expected)
   303  	}
   304  }
   305  
   306  func Test_parseDpkgStatus_negativeCases(t *testing.T) {
   307  	tests := []struct {
   308  		name    string
   309  		input   string
   310  		want    []pkg.Package
   311  		wantErr require.ErrorAssertionFunc
   312  	}{
   313  		{
   314  			name:    "no more packages",
   315  			input:   `Package: apt`,
   316  			wantErr: require.NoError,
   317  		},
   318  		{
   319  			name: "duplicated key",
   320  			input: `Package: apt
   321  Package: apt-get
   322  
   323  `,
   324  			wantErr: requireAs(errors.New("duplicate key discovered: Package")),
   325  		},
   326  		{
   327  			name: "no match for continuation",
   328  			input: `  Package: apt
   329  
   330  `,
   331  			wantErr: requireAs(errors.New("no match for continuation: line: '  Package: apt'")),
   332  		},
   333  		{
   334  			name: "find keys",
   335  			input: `Package: apt
   336  Status: install ok installed
   337  Installed-Size: 10kib
   338  
   339  `,
   340  			want: []pkg.Package{
   341  				{
   342  					Name:      "apt",
   343  					Type:      "deb",
   344  					PURL:      "pkg:deb/debian/apt?distro=debian-10",
   345  					Licenses:  pkg.NewLicenseSet(),
   346  					Locations: file.NewLocationSet(file.NewLocation("place")),
   347  					Metadata: pkg.DpkgDBEntry{
   348  						Package:       "apt",
   349  						InstalledSize: 10240,
   350  						Files:         []pkg.DpkgFileRecord{},
   351  					},
   352  				},
   353  			},
   354  			wantErr: require.NoError,
   355  		},
   356  	}
   357  
   358  	for _, tt := range tests {
   359  		t.Run(tt.name, func(t *testing.T) {
   360  			pkgtest.NewCatalogTester().
   361  				FromString("place", tt.input).
   362  				WithErrorAssertion(tt.wantErr).
   363  				WithLinuxRelease(linux.Release{ID: "debian", VersionID: "10"}).
   364  				Expects(tt.want, nil).
   365  				TestParser(t, parseDpkgDB)
   366  		})
   367  	}
   368  }
   369  
   370  func Test_handleNewKeyValue(t *testing.T) {
   371  	tests := []struct {
   372  		name    string
   373  		line    string
   374  		wantKey string
   375  		wantVal interface{}
   376  		wantErr require.ErrorAssertionFunc
   377  	}{
   378  		{
   379  			name:    "cannot parse field",
   380  			line:    "blabla",
   381  			wantErr: requireAs(errors.New("cannot parse field from line: 'blabla'")),
   382  		},
   383  		{
   384  			name:    "parse field",
   385  			line:    "key: val",
   386  			wantKey: "key",
   387  			wantVal: "val",
   388  			wantErr: require.NoError,
   389  		},
   390  		{
   391  			name:    "parse installed size",
   392  			line:    "InstalledSize: 128",
   393  			wantKey: "InstalledSize",
   394  			wantVal: 128,
   395  			wantErr: require.NoError,
   396  		},
   397  		{
   398  			name:    "parse installed kib size",
   399  			line:    "InstalledSize: 1kib",
   400  			wantKey: "InstalledSize",
   401  			wantVal: 1024,
   402  			wantErr: require.NoError,
   403  		},
   404  		{
   405  			name:    "parse installed kb size",
   406  			line:    "InstalledSize: 1kb",
   407  			wantKey: "InstalledSize",
   408  			wantVal: 1000,
   409  			wantErr: require.NoError,
   410  		},
   411  		{
   412  			name:    "parse installed-size mb",
   413  			line:    "Installed-Size: 1 mb",
   414  			wantKey: "InstalledSize",
   415  			wantVal: 1000000,
   416  			wantErr: require.NoError,
   417  		},
   418  		{
   419  			name:    "fail parsing installed-size",
   420  			line:    "Installed-Size: 1bla",
   421  			wantKey: "",
   422  			wantErr: requireAs(fmt.Errorf("unhandled size name: %s", "bla")),
   423  		},
   424  	}
   425  	for _, tt := range tests {
   426  		t.Run(tt.name, func(t *testing.T) {
   427  			gotKey, gotVal, err := handleNewKeyValue(tt.line)
   428  			tt.wantErr(t, err, fmt.Sprintf("handleNewKeyValue(%v)", tt.line))
   429  
   430  			assert.Equalf(t, tt.wantKey, gotKey, "handleNewKeyValue(%v)", tt.line)
   431  			assert.Equalf(t, tt.wantVal, gotVal, "handleNewKeyValue(%v)", tt.line)
   432  		})
   433  	}
   434  }
   435  
   436  func Test_stripVersionSpecifier(t *testing.T) {
   437  
   438  	tests := []struct {
   439  		name  string
   440  		input string
   441  		want  string
   442  	}{
   443  		{
   444  			name:  "package name only",
   445  			input: "test",
   446  			want:  "test",
   447  		},
   448  		{
   449  			name:  "with version",
   450  			input: "test (1.2.3)",
   451  			want:  "test",
   452  		},
   453  		{
   454  			name:  "multiple packages",
   455  			input: "test | other",
   456  			want:  "test | other",
   457  		},
   458  		{
   459  			name:  "with architecture specifiers",
   460  			input: "test [amd64 i386]",
   461  			want:  "test",
   462  		},
   463  	}
   464  	for _, tt := range tests {
   465  		t.Run(tt.name, func(t *testing.T) {
   466  			assert.Equal(t, tt.want, stripVersionSpecifier(tt.input))
   467  		})
   468  	}
   469  }
   470  
   471  func Test_associateRelationships(t *testing.T) {
   472  	tests := []struct {
   473  		name              string
   474  		fixture           string
   475  		wantRelationships map[string][]string
   476  	}{
   477  		{
   478  			name:    "relationships for coreutils",
   479  			fixture: "test-fixtures/status/coreutils-relationships",
   480  			wantRelationships: map[string][]string{
   481  				"coreutils":    {"libacl1", "libattr1", "libc6", "libgmp10", "libselinux1"},
   482  				"libacl1":      {"libc6"},
   483  				"libattr1":     {"libc6"},
   484  				"libc6":        {"libgcc-s1"},
   485  				"libgcc-s1":    {"gcc-12-base", "libc6"},
   486  				"libgmp10":     {"libc6"},
   487  				"libpcre2-8-0": {"libc6"},
   488  				"libselinux1":  {"libc6", "libpcre2-8-0"},
   489  			},
   490  		},
   491  		{
   492  			name:    "relationships from dpkg example docs",
   493  			fixture: "test-fixtures/status/doc-examples",
   494  			wantRelationships: map[string][]string{
   495  				"made-up-package-1": {"kernel-headers-2.2.10", "hurd-dev", "gnumach-dev"},
   496  				"made-up-package-2": {"libluajit5.1-dev", "liblua5.1-dev"},
   497  				"made-up-package-3": {"foo", "bar"},
   498  				// note that the "made-up-package-4" depends on "made-up-package-5" but not via the direct
   499  				// package name, but through the "provides" virtual package name "virtual-package-5".
   500  				"made-up-package-4": {"made-up-package-5"},
   501  				// note that though there is a "default-mta | mail-transport-agent | not-installed"
   502  				// dependency choice we raise up the packages that are installed for every choice.
   503  				// In this case that means that "default-mta" and "mail-transport-agent".
   504  				"mutt": {"libc6", "default-mta", "mail-transport-agent"},
   505  			},
   506  		},
   507  		{
   508  			name:    "relationships for libpam-runtime",
   509  			fixture: "test-fixtures/status/libpam-runtime",
   510  			wantRelationships: map[string][]string{
   511  				"libpam-runtime": {"debconf1", "debconf-2.0", "debconf2", "cdebconf", "libpam-modules"},
   512  			},
   513  		},
   514  	}
   515  	for _, tt := range tests {
   516  		t.Run(tt.name, func(t *testing.T) {
   517  			f, err := os.Open(tt.fixture)
   518  			require.NoError(t, err)
   519  
   520  			reader := file.NewLocationReadCloser(file.NewLocation(tt.fixture), f)
   521  
   522  			pkgs, relationships, err := parseDpkgDB(nil, &generic.Environment{}, reader)
   523  			require.NotEmpty(t, pkgs)
   524  			require.NotEmpty(t, relationships)
   525  			require.NoError(t, err)
   526  
   527  			if d := cmp.Diff(tt.wantRelationships, abstractRelationships(t, relationships)); d != "" {
   528  				t.Errorf("unexpected relationships (-want +got):\n%s", d)
   529  			}
   530  		})
   531  	}
   532  }
   533  
   534  func abstractRelationships(t testing.TB, relationships []artifact.Relationship) map[string][]string {
   535  	t.Helper()
   536  
   537  	abstracted := make(map[string][]string)
   538  	for _, relationship := range relationships {
   539  		fromPkg, ok := relationship.From.(pkg.Package)
   540  		if !ok {
   541  			continue
   542  		}
   543  		toPkg, ok := relationship.To.(pkg.Package)
   544  		if !ok {
   545  			continue
   546  		}
   547  
   548  		// we build this backwards since we use DependencyOfRelationship instead of DependsOn
   549  		abstracted[toPkg.Name] = append(abstracted[toPkg.Name], fromPkg.Name)
   550  	}
   551  
   552  	return abstracted
   553  }