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  }