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  }