github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/dpkg/dpkg_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dpkg_test
    16  
    17  import (
    18  	"fmt"
    19  	"io/fs"
    20  	golog "log"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/google/go-cmp/cmp/cmpopts"
    29  	"github.com/google/osv-scalibr/extractor"
    30  	"github.com/google/osv-scalibr/extractor/filesystem"
    31  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    32  	"github.com/google/osv-scalibr/extractor/filesystem/os/dpkg"
    33  	dpkgmeta "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg/metadata"
    34  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    35  	scalibrfs "github.com/google/osv-scalibr/fs"
    36  	"github.com/google/osv-scalibr/inventory"
    37  	"github.com/google/osv-scalibr/inventory/vex"
    38  	scalibrlog "github.com/google/osv-scalibr/log"
    39  	"github.com/google/osv-scalibr/purl"
    40  	"github.com/google/osv-scalibr/stats"
    41  	"github.com/google/osv-scalibr/testing/fakefs"
    42  	"github.com/google/osv-scalibr/testing/testcollector"
    43  )
    44  
    45  func TestNew(t *testing.T) {
    46  	tests := []struct {
    47  		name    string
    48  		cfg     dpkg.Config
    49  		wantCfg dpkg.Config
    50  	}{
    51  		{
    52  			name: "default",
    53  			cfg:  dpkg.DefaultConfig(),
    54  			wantCfg: dpkg.Config{
    55  				MaxFileSizeBytes:    100 * units.MiB,
    56  				IncludeNotInstalled: false,
    57  			},
    58  		},
    59  		{
    60  			name: "custom",
    61  			cfg: dpkg.Config{
    62  				MaxFileSizeBytes:    10,
    63  				IncludeNotInstalled: true,
    64  			},
    65  			wantCfg: dpkg.Config{
    66  				MaxFileSizeBytes:    10,
    67  				IncludeNotInstalled: true,
    68  			},
    69  		},
    70  	}
    71  
    72  	for _, tt := range tests {
    73  		t.Run(tt.name, func(t *testing.T) {
    74  			got := dpkg.New(tt.cfg)
    75  			if !reflect.DeepEqual(got.Config(), tt.wantCfg) {
    76  				t.Errorf("New(%+v).Config(): got %+v, want %+v", tt.cfg, got.Config(), tt.wantCfg)
    77  			}
    78  		})
    79  	}
    80  }
    81  
    82  func TestFileRequired(t *testing.T) {
    83  	tests := []struct {
    84  		name             string
    85  		path             string
    86  		fileSizeBytes    int64
    87  		maxFileSizeBytes int64
    88  		wantRequired     bool
    89  		wantResultMetric stats.FileRequiredResult
    90  	}{
    91  		{
    92  			name:             "status file",
    93  			path:             "var/lib/dpkg/status",
    94  			wantRequired:     true,
    95  			wantResultMetric: stats.FileRequiredResultOK,
    96  		},
    97  		{
    98  			name:             "file in status.d",
    99  			path:             "var/lib/dpkg/status.d/foo",
   100  			wantRequired:     true,
   101  			wantResultMetric: stats.FileRequiredResultOK,
   102  		},
   103  		{
   104  			name:         "ignore md5sums file",
   105  			path:         "var/lib/dpkg/status.d/foo.md5sums",
   106  			wantRequired: false,
   107  		},
   108  		{
   109  			name:         "status.d as a file",
   110  			path:         "var/lib/dpkg/status.d",
   111  			wantRequired: false,
   112  		},
   113  		{
   114  			name:             "status file required if file size < max file size",
   115  			path:             "var/lib/dpkg/status",
   116  			fileSizeBytes:    100 * units.KiB,
   117  			maxFileSizeBytes: 1000 * units.KiB,
   118  			wantRequired:     true,
   119  			wantResultMetric: stats.FileRequiredResultOK,
   120  		},
   121  		{
   122  			name:             "status file required if file size == max file size",
   123  			path:             "var/lib/dpkg/status",
   124  			fileSizeBytes:    1000 * units.KiB,
   125  			maxFileSizeBytes: 1000 * units.KiB,
   126  			wantRequired:     true,
   127  			wantResultMetric: stats.FileRequiredResultOK,
   128  		},
   129  		{
   130  			name:             "status file not required if file size > max file size",
   131  			path:             "var/lib/dpkg/status",
   132  			fileSizeBytes:    1000 * units.KiB,
   133  			maxFileSizeBytes: 100 * units.KiB,
   134  			wantRequired:     false,
   135  			wantResultMetric: stats.FileRequiredResultSizeLimitExceeded,
   136  		},
   137  		{
   138  			name:             "status file required if max file size set to 0",
   139  			path:             "var/lib/dpkg/status",
   140  			fileSizeBytes:    100 * units.KiB,
   141  			maxFileSizeBytes: 0,
   142  			wantRequired:     true,
   143  			wantResultMetric: stats.FileRequiredResultOK,
   144  		},
   145  		{
   146  			name:             "status file",
   147  			path:             "usr/lib/opkg/status",
   148  			wantRequired:     true,
   149  			wantResultMetric: stats.FileRequiredResultOK,
   150  		},
   151  		{
   152  			name:         "status as a directory",
   153  			path:         "usr/lib/opkg/status/foo",
   154  			wantRequired: false,
   155  		},
   156  	}
   157  
   158  	for _, tt := range tests {
   159  		// Note the subtest here
   160  		t.Run(tt.name, func(t *testing.T) {
   161  			collector := testcollector.New()
   162  			var e filesystem.Extractor = dpkg.New(dpkg.Config{
   163  				Stats:            collector,
   164  				MaxFileSizeBytes: tt.maxFileSizeBytes,
   165  			})
   166  
   167  			// Set a default file size if not specified.
   168  			fileSizeBytes := tt.fileSizeBytes
   169  			if fileSizeBytes == 0 {
   170  				fileSizeBytes = 1000
   171  			}
   172  
   173  			isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{
   174  				FileName: filepath.Base(tt.path),
   175  				FileMode: fs.ModePerm,
   176  				FileSize: fileSizeBytes,
   177  			}))
   178  			if isRequired != tt.wantRequired {
   179  				t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired)
   180  			}
   181  
   182  			gotResultMetric := collector.FileRequiredResult(tt.path)
   183  			if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric {
   184  				t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric)
   185  			}
   186  		})
   187  	}
   188  }
   189  
   190  const DebianBookworm = `PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
   191  NAME="Debian GNU/Linux"
   192  VERSION_ID="12"
   193  VERSION="12 (bookworm)"
   194  VERSION_CODENAME=bookworm
   195  ID=debian`
   196  
   197  const OpkgRelease = `NAME="OpenWrt"
   198  VERSION="21.02.1"
   199  ID=openwrt
   200  VERSION_ID="21.02.1"
   201  VERSION_CODENAME="openwrt-21.02.1"
   202  PRETTY_NAME="OpenWrt 21.02.1"
   203  BUILD_ID="r16279-5cc53c7f44"`
   204  
   205  func TestExtract(t *testing.T) {
   206  	tests := []struct {
   207  		name             string
   208  		path             string
   209  		osrelease        string
   210  		cfg              dpkg.Config
   211  		isOPKG           bool
   212  		wantPackages     []*extractor.Package
   213  		wantErr          error
   214  		wantResultMetric stats.FileExtractedResult
   215  		wantLogWarn      int
   216  		wantLogErr       int
   217  	}{
   218  		{
   219  			name:      "valid status file",
   220  			path:      "testdata/dpkg/valid",
   221  			osrelease: DebianBookworm,
   222  			wantPackages: []*extractor.Package{
   223  				{
   224  					Name:     "accountsservice",
   225  					Version:  "22.08.8-6",
   226  					PURLType: purl.TypeDebian,
   227  					Metadata: &dpkgmeta.Metadata{
   228  						PackageName:       "accountsservice",
   229  						PackageVersion:    "22.08.8-6",
   230  						Status:            "install ok installed",
   231  						OSID:              "debian",
   232  						OSVersionCodename: "bookworm",
   233  						OSVersionID:       "12",
   234  						Maintainer:        "Debian freedesktop.org maintainers <pkg-freedesktop-maintainers@lists.alioth.debian.org>",
   235  						Architecture:      "amd64",
   236  					},
   237  					Locations: []string{"var/lib/dpkg/status"},
   238  				},
   239  				{
   240  					Name:     "acl",
   241  					Version:  "2.3.1-3",
   242  					PURLType: purl.TypeDebian,
   243  					Metadata: &dpkgmeta.Metadata{
   244  						PackageName:       "acl",
   245  						PackageVersion:    "2.3.1-3",
   246  						Status:            "install ok installed",
   247  						OSID:              "debian",
   248  						OSVersionCodename: "bookworm",
   249  						OSVersionID:       "12",
   250  						Maintainer:        "Guillem Jover <guillem@debian.org>",
   251  						Architecture:      "amd64",
   252  					},
   253  					Locations: []string{"var/lib/dpkg/status"},
   254  				},
   255  				{
   256  					Name:     "adduser",
   257  					Version:  "3.131",
   258  					PURLType: purl.TypeDebian,
   259  					Metadata: &dpkgmeta.Metadata{
   260  						PackageName:       "adduser",
   261  						PackageVersion:    "3.131",
   262  						Status:            "install ok installed",
   263  						OSID:              "debian",
   264  						OSVersionCodename: "bookworm",
   265  						OSVersionID:       "12",
   266  						Maintainer:        "Debian Adduser Developers <adduser@packages.debian.org>",
   267  						Architecture:      "all",
   268  					},
   269  					Locations: []string{"var/lib/dpkg/status"},
   270  				},
   271  				{
   272  					Name:     "admin-session",
   273  					Version:  "2023.06.26.c543406313-00",
   274  					PURLType: purl.TypeDebian,
   275  					Metadata: &dpkgmeta.Metadata{
   276  						PackageName:       "admin-session",
   277  						PackageVersion:    "2023.06.26.c543406313-00",
   278  						Status:            "install ok installed",
   279  						OSID:              "debian",
   280  						OSVersionCodename: "bookworm",
   281  						OSVersionID:       "12",
   282  						Maintainer:        "nobody@google.com",
   283  						Architecture:      "amd64",
   284  					},
   285  					Locations: []string{"var/lib/dpkg/status"},
   286  				},
   287  				{
   288  					Name:     "attr",
   289  					Version:  "1:2.5.1-4",
   290  					PURLType: purl.TypeDebian,
   291  					Metadata: &dpkgmeta.Metadata{
   292  						PackageName:       "attr",
   293  						PackageVersion:    "1:2.5.1-4",
   294  						Status:            "install ok installed",
   295  						OSID:              "debian",
   296  						OSVersionCodename: "bookworm",
   297  						OSVersionID:       "12",
   298  						Maintainer:        "Guillem Jover <guillem@debian.org>",
   299  						Architecture:      "amd64",
   300  					},
   301  					Locations: []string{"var/lib/dpkg/status"},
   302  				},
   303  				// Expect source name.
   304  				{
   305  					Name:     "libacl1",
   306  					Version:  "2.3.1-3",
   307  					PURLType: purl.TypeDebian,
   308  					Metadata: &dpkgmeta.Metadata{
   309  						PackageName:       "libacl1",
   310  						PackageVersion:    "2.3.1-3",
   311  						Status:            "install ok installed",
   312  						SourceName:        "acl",
   313  						OSID:              "debian",
   314  						OSVersionCodename: "bookworm",
   315  						OSVersionID:       "12",
   316  						Maintainer:        "Guillem Jover <guillem@debian.org>",
   317  						Architecture:      "amd64",
   318  					},
   319  					Locations: []string{"var/lib/dpkg/status"},
   320  				},
   321  				// Expect source name and version.
   322  				{
   323  					Name:     "util-linux-extra",
   324  					Version:  "2.38.1-5+b1",
   325  					PURLType: purl.TypeDebian,
   326  					Metadata: &dpkgmeta.Metadata{
   327  						PackageName:       "util-linux-extra",
   328  						PackageVersion:    "2.38.1-5+b1",
   329  						Status:            "install ok installed",
   330  						SourceName:        "util-linux",
   331  						SourceVersion:     "2.38.1-5",
   332  						OSID:              "debian",
   333  						OSVersionCodename: "bookworm",
   334  						OSVersionID:       "12",
   335  						Maintainer:        "util-linux packagers <util-linux@packages.debian.org>",
   336  						Architecture:      "amd64",
   337  					},
   338  					Locations: []string{"var/lib/dpkg/status"},
   339  				},
   340  			},
   341  			wantResultMetric: stats.FileExtractedResultSuccess,
   342  		},
   343  		{
   344  			name:      "packages with no version set are skipped",
   345  			path:      "testdata/dpkg/noversion",
   346  			osrelease: DebianBookworm,
   347  			wantPackages: []*extractor.Package{
   348  				{
   349  					Name:     "foo",
   350  					Version:  "1.0",
   351  					PURLType: purl.TypeDebian,
   352  					Metadata: &dpkgmeta.Metadata{
   353  						PackageName:       "foo",
   354  						PackageVersion:    "1.0",
   355  						Status:            "install ok installed",
   356  						OSID:              "debian",
   357  						OSVersionCodename: "bookworm",
   358  						OSVersionID:       "12",
   359  					},
   360  					Locations: []string{"var/lib/dpkg/status"},
   361  				},
   362  				{
   363  					Name:     "bar",
   364  					Version:  "2.0",
   365  					PURLType: purl.TypeDebian,
   366  					Metadata: &dpkgmeta.Metadata{
   367  						PackageName:       "bar",
   368  						PackageVersion:    "2.0",
   369  						Status:            "install ok installed",
   370  						OSID:              "debian",
   371  						OSVersionCodename: "bookworm",
   372  						OSVersionID:       "12",
   373  					},
   374  					Locations: []string{"var/lib/dpkg/status"},
   375  				},
   376  			},
   377  			wantResultMetric: stats.FileExtractedResultSuccess,
   378  			wantLogWarn:      1,
   379  		},
   380  		{
   381  			name:      "packages with no name set are skipped",
   382  			path:      "testdata/dpkg/nopackage",
   383  			osrelease: DebianBookworm,
   384  			wantPackages: []*extractor.Package{
   385  				{
   386  					Name:     "foo",
   387  					Version:  "1.0",
   388  					PURLType: purl.TypeDebian,
   389  					Metadata: &dpkgmeta.Metadata{
   390  						PackageName:       "foo",
   391  						PackageVersion:    "1.0",
   392  						Status:            "install ok installed",
   393  						OSID:              "debian",
   394  						OSVersionCodename: "bookworm",
   395  						OSVersionID:       "12",
   396  					},
   397  					Locations: []string{"var/lib/dpkg/status"},
   398  				},
   399  				{
   400  					Name:     "bar",
   401  					Version:  "2.0",
   402  					PURLType: purl.TypeDebian,
   403  					Metadata: &dpkgmeta.Metadata{
   404  						PackageName:       "bar",
   405  						PackageVersion:    "2.0",
   406  						Status:            "install ok installed",
   407  						OSID:              "debian",
   408  						OSVersionCodename: "bookworm",
   409  						OSVersionID:       "12",
   410  					},
   411  					Locations: []string{"var/lib/dpkg/status"},
   412  				},
   413  			},
   414  			wantResultMetric: stats.FileExtractedResultSuccess,
   415  			wantLogWarn:      1,
   416  		},
   417  		{
   418  			name:      "statusfield",
   419  			path:      "testdata/dpkg/statusfield",
   420  			osrelease: DebianBookworm,
   421  			wantPackages: []*extractor.Package{
   422  				{
   423  					Name:     "wantinstall_installed",
   424  					Version:  "1.0",
   425  					PURLType: purl.TypeDebian,
   426  					Metadata: &dpkgmeta.Metadata{
   427  						PackageName:       "wantinstall_installed",
   428  						PackageVersion:    "1.0",
   429  						Status:            "install ok installed",
   430  						OSID:              "debian",
   431  						OSVersionCodename: "bookworm",
   432  						OSVersionID:       "12",
   433  					},
   434  					Locations: []string{"var/lib/dpkg/status"},
   435  				},
   436  				{
   437  					Name:     "wantdeinstall_installed",
   438  					Version:  "1.0",
   439  					PURLType: purl.TypeDebian,
   440  					Metadata: &dpkgmeta.Metadata{
   441  						PackageName:       "wantdeinstall_installed",
   442  						PackageVersion:    "1.0",
   443  						Status:            "deinstall reinstreq installed",
   444  						OSID:              "debian",
   445  						OSVersionCodename: "bookworm",
   446  						OSVersionID:       "12",
   447  					},
   448  					Locations: []string{"var/lib/dpkg/status"},
   449  				},
   450  				{
   451  					Name:     "wantpurge_installed",
   452  					Version:  "1.0",
   453  					PURLType: purl.TypeDebian,
   454  					Metadata: &dpkgmeta.Metadata{
   455  						PackageName:       "wantpurge_installed",
   456  						PackageVersion:    "1.0",
   457  						Status:            "purge ok installed",
   458  						OSID:              "debian",
   459  						OSVersionCodename: "bookworm",
   460  						OSVersionID:       "12",
   461  					},
   462  					Locations: []string{"var/lib/dpkg/status"},
   463  				},
   464  			},
   465  			wantResultMetric: stats.FileExtractedResultSuccess,
   466  			wantLogWarn:      1,
   467  		},
   468  		{
   469  			name:      "statusfield including not installed",
   470  			path:      "testdata/dpkg/statusfield",
   471  			osrelease: DebianBookworm,
   472  			cfg: dpkg.Config{
   473  				IncludeNotInstalled: true,
   474  			},
   475  			wantPackages: []*extractor.Package{
   476  				{
   477  					Name:     "wantinstall_installed",
   478  					Version:  "1.0",
   479  					PURLType: purl.TypeDebian,
   480  					Metadata: &dpkgmeta.Metadata{
   481  						PackageName:       "wantinstall_installed",
   482  						PackageVersion:    "1.0",
   483  						Status:            "install ok installed",
   484  						OSID:              "debian",
   485  						OSVersionCodename: "bookworm",
   486  						OSVersionID:       "12",
   487  					},
   488  					Locations: []string{"var/lib/dpkg/status"},
   489  				},
   490  				{
   491  					Name:     "wantdeinstall_installed",
   492  					Version:  "1.0",
   493  					PURLType: purl.TypeDebian,
   494  					Metadata: &dpkgmeta.Metadata{
   495  						PackageName:       "wantdeinstall_installed",
   496  						PackageVersion:    "1.0",
   497  						Status:            "deinstall reinstreq installed",
   498  						OSID:              "debian",
   499  						OSVersionCodename: "bookworm",
   500  						OSVersionID:       "12",
   501  					},
   502  					Locations: []string{"var/lib/dpkg/status"},
   503  				},
   504  				{
   505  					Name:     "wantdeinstall_configfiles",
   506  					Version:  "1.0",
   507  					PURLType: purl.TypeDebian,
   508  					Metadata: &dpkgmeta.Metadata{
   509  						PackageName:       "wantdeinstall_configfiles",
   510  						PackageVersion:    "1.0",
   511  						Status:            "deinstall ok config-files",
   512  						OSID:              "debian",
   513  						OSVersionCodename: "bookworm",
   514  						OSVersionID:       "12",
   515  					},
   516  					Locations: []string{"var/lib/dpkg/status"},
   517  				},
   518  				{
   519  					Name:     "wantinstall_unpacked",
   520  					Version:  "1.0",
   521  					PURLType: purl.TypeDebian,
   522  					Metadata: &dpkgmeta.Metadata{
   523  						PackageName:       "wantinstall_unpacked",
   524  						PackageVersion:    "1.0",
   525  						Status:            "install ok unpacked",
   526  						OSID:              "debian",
   527  						OSVersionCodename: "bookworm",
   528  						OSVersionID:       "12",
   529  					},
   530  					Locations: []string{"var/lib/dpkg/status"},
   531  				},
   532  				{
   533  					Name:     "wantpurge_installed",
   534  					Version:  "1.0",
   535  					PURLType: purl.TypeDebian,
   536  					Metadata: &dpkgmeta.Metadata{
   537  						PackageName:       "wantpurge_installed",
   538  						PackageVersion:    "1.0",
   539  						Status:            "purge ok installed",
   540  						OSID:              "debian",
   541  						OSVersionCodename: "bookworm",
   542  						OSVersionID:       "12",
   543  					},
   544  					Locations: []string{"var/lib/dpkg/status"},
   545  				},
   546  				{
   547  					Name:     "wantinstall_halfinstalled",
   548  					Version:  "1.0",
   549  					PURLType: purl.TypeDebian,
   550  					Metadata: &dpkgmeta.Metadata{
   551  						PackageName:       "wantinstall_halfinstalled",
   552  						PackageVersion:    "1.0",
   553  						Status:            "install reinstreq half-installed",
   554  						OSID:              "debian",
   555  						OSVersionCodename: "bookworm",
   556  						OSVersionID:       "12",
   557  					},
   558  					Locations: []string{"var/lib/dpkg/status"},
   559  				},
   560  				{
   561  					Name:     "wantnostatus",
   562  					Version:  "1.0",
   563  					PURLType: purl.TypeDebian,
   564  					Metadata: &dpkgmeta.Metadata{
   565  						PackageName:       "wantnostatus",
   566  						PackageVersion:    "1.0",
   567  						OSID:              "debian",
   568  						OSVersionCodename: "bookworm",
   569  						OSVersionID:       "12",
   570  					},
   571  					Locations: []string{"var/lib/dpkg/status"},
   572  				},
   573  			},
   574  			wantResultMetric: stats.FileExtractedResultSuccess,
   575  		},
   576  		{
   577  			name:             "empty",
   578  			path:             "testdata/dpkg/empty",
   579  			osrelease:        DebianBookworm,
   580  			wantPackages:     []*extractor.Package{},
   581  			wantResultMetric: stats.FileExtractedResultSuccess,
   582  		},
   583  		{
   584  			name:             "invalid",
   585  			path:             "testdata/dpkg/invalid",
   586  			osrelease:        DebianBookworm,
   587  			wantPackages:     []*extractor.Package{},
   588  			wantErr:          cmpopts.AnyError,
   589  			wantResultMetric: stats.FileExtractedResultErrorUnknown,
   590  		},
   591  		{
   592  			name: "VERSION_CODENAME_not_set,_fallback_to_VERSION_ID",
   593  			path: "testdata/dpkg/single",
   594  			osrelease: `VERSION_ID="12"
   595  			ID=debian`,
   596  			wantPackages: []*extractor.Package{
   597  				{
   598  					Name:     "acl",
   599  					Version:  "2.3.1-3",
   600  					PURLType: purl.TypeDebian,
   601  					Metadata: &dpkgmeta.Metadata{
   602  						PackageName:    "acl",
   603  						PackageVersion: "2.3.1-3",
   604  						Status:         "install ok installed",
   605  						OSID:           "debian",
   606  						OSVersionID:    "12",
   607  						Maintainer:     "Guillem Jover <guillem@debian.org>",
   608  						Architecture:   "amd64",
   609  					},
   610  					Locations: []string{"var/lib/dpkg/status"},
   611  				},
   612  			},
   613  			wantResultMetric: stats.FileExtractedResultSuccess,
   614  		},
   615  		{
   616  			name:      "no version",
   617  			path:      "testdata/dpkg/single",
   618  			osrelease: `ID=debian`,
   619  			wantPackages: []*extractor.Package{
   620  				{
   621  					Name:     "acl",
   622  					Version:  "2.3.1-3",
   623  					PURLType: purl.TypeDebian,
   624  					Metadata: &dpkgmeta.Metadata{
   625  						PackageName:    "acl",
   626  						PackageVersion: "2.3.1-3",
   627  						Status:         "install ok installed",
   628  						OSID:           "debian",
   629  						Maintainer:     "Guillem Jover <guillem@debian.org>",
   630  						Architecture:   "amd64",
   631  					},
   632  					Locations: []string{"var/lib/dpkg/status"},
   633  				},
   634  			},
   635  			wantResultMetric: stats.FileExtractedResultSuccess,
   636  		},
   637  		{
   638  			name:      "osrelease id not set",
   639  			path:      "testdata/dpkg/single",
   640  			osrelease: "VERSION_CODENAME=bookworm",
   641  			wantPackages: []*extractor.Package{
   642  				{
   643  					Name:     "acl",
   644  					Version:  "2.3.1-3",
   645  					PURLType: purl.TypeDebian,
   646  					Metadata: &dpkgmeta.Metadata{
   647  						PackageName:       "acl",
   648  						PackageVersion:    "2.3.1-3",
   649  						Status:            "install ok installed",
   650  						OSVersionCodename: "bookworm",
   651  						Maintainer:        "Guillem Jover <guillem@debian.org>",
   652  						Architecture:      "amd64",
   653  					},
   654  					Locations: []string{"var/lib/dpkg/status"},
   655  				},
   656  			},
   657  			wantResultMetric: stats.FileExtractedResultSuccess,
   658  		},
   659  		{
   660  			name: "ubuntu",
   661  			path: "testdata/dpkg/single",
   662  			osrelease: `VERSION_ID="22.04"
   663  			VERSION_CODENAME=jammy
   664  			ID=ubuntu
   665  			ID_LIKE=debian`,
   666  			wantPackages: []*extractor.Package{
   667  				{
   668  					Name:     "acl",
   669  					Version:  "2.3.1-3",
   670  					PURLType: purl.TypeDebian,
   671  					Metadata: &dpkgmeta.Metadata{
   672  						PackageName:       "acl",
   673  						PackageVersion:    "2.3.1-3",
   674  						Status:            "install ok installed",
   675  						OSID:              "ubuntu",
   676  						OSVersionCodename: "jammy",
   677  						OSVersionID:       "22.04",
   678  						Maintainer:        "Guillem Jover <guillem@debian.org>",
   679  						Architecture:      "amd64",
   680  					},
   681  					Locations: []string{"var/lib/dpkg/status"},
   682  				},
   683  			},
   684  			wantResultMetric: stats.FileExtractedResultSuccess,
   685  		},
   686  		{
   687  			name: "ubuntu",
   688  			path: "testdata/dpkg/trailingnewlines",
   689  			osrelease: `VERSION_ID="22.04"
   690  			VERSION_CODENAME=jammy
   691  			ID=ubuntu
   692  			ID_LIKE=debian`,
   693  			wantPackages: []*extractor.Package{
   694  				{
   695  					Name:     "acl",
   696  					Version:  "2.3.1-3",
   697  					PURLType: purl.TypeDebian,
   698  					Metadata: &dpkgmeta.Metadata{
   699  						PackageName:       "acl",
   700  						PackageVersion:    "2.3.1-3",
   701  						Status:            "install ok installed",
   702  						OSID:              "ubuntu",
   703  						OSVersionCodename: "jammy",
   704  						OSVersionID:       "22.04",
   705  						Maintainer:        "Guillem Jover <guillem@debian.org>",
   706  						Architecture:      "amd64",
   707  					},
   708  					Locations: []string{"var/lib/dpkg/status"},
   709  				},
   710  			},
   711  			wantResultMetric: stats.FileExtractedResultSuccess,
   712  			wantLogWarn:      0,
   713  			wantLogErr:       0,
   714  		},
   715  		{
   716  			name:      "status.d file without Status field set should work",
   717  			path:      "testdata/dpkg/status.d/foo",
   718  			osrelease: DebianBookworm,
   719  			wantPackages: []*extractor.Package{
   720  				{
   721  					Name:     "foo",
   722  					Version:  "1.2.3",
   723  					PURLType: purl.TypeDebian,
   724  					Metadata: &dpkgmeta.Metadata{
   725  						PackageName:       "foo",
   726  						PackageVersion:    "1.2.3",
   727  						OSID:              "debian",
   728  						OSVersionCodename: "bookworm",
   729  						OSVersionID:       "12",
   730  						Maintainer:        "someone",
   731  						Architecture:      "amd64",
   732  					},
   733  					Locations: []string{"var/lib/dpkg/status.d/foo"},
   734  				},
   735  			},
   736  			wantResultMetric: stats.FileExtractedResultSuccess,
   737  		},
   738  		{
   739  			name:             "status.d file without Status field set should work",
   740  			path:             "testdata/dpkg/status.d/foo.md5sums",
   741  			osrelease:        DebianBookworm,
   742  			wantPackages:     []*extractor.Package{},
   743  			wantResultMetric: stats.FileExtractedResultSuccess,
   744  			wantLogWarn:      1,
   745  		},
   746  		{
   747  			name:      "transitional packages should be annotated",
   748  			path:      "testdata/dpkg/transitional",
   749  			osrelease: DebianBookworm,
   750  			wantPackages: []*extractor.Package{
   751  				{
   752  					Name:     "iceweasel",
   753  					Version:  "78.13.0esr-1~deb10u1",
   754  					PURLType: purl.TypeDebian,
   755  					Metadata: &dpkgmeta.Metadata{
   756  						PackageName:       "iceweasel",
   757  						Status:            "install ok installed",
   758  						PackageVersion:    "78.13.0esr-1~deb10u1",
   759  						SourceName:        "firefox-esr",
   760  						OSID:              "debian",
   761  						OSVersionCodename: "bookworm",
   762  						OSVersionID:       "12",
   763  						Maintainer:        "Maintainers of Mozilla-related packages <team+pkg-mozilla@tracker.debian.org>",
   764  						Architecture:      "all",
   765  					},
   766  					Locations: []string{"var/lib/dpkg/status"},
   767  					ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{
   768  						Plugin:          dpkg.Name,
   769  						Justification:   vex.ComponentNotPresent,
   770  						MatchesAllVulns: true,
   771  					}},
   772  				},
   773  			},
   774  			wantResultMetric: stats.FileExtractedResultSuccess,
   775  		},
   776  		{
   777  			name:      "transitional dummy packages should be annotated",
   778  			path:      "testdata/dpkg/transitional_dummy",
   779  			osrelease: DebianBookworm,
   780  			wantPackages: []*extractor.Package{
   781  				{
   782  					Name:     "git-core",
   783  					Version:  "1:2.14.2-1",
   784  					PURLType: purl.TypeDebian,
   785  					Metadata: &dpkgmeta.Metadata{
   786  						PackageName:       "git-core",
   787  						Status:            "install ok installed",
   788  						PackageVersion:    "1:2.14.2-1",
   789  						SourceName:        "git",
   790  						OSID:              "debian",
   791  						OSVersionCodename: "bookworm",
   792  						OSVersionID:       "12",
   793  						Maintainer:        "Gerrit Pape <pape@smarden.org>",
   794  						Architecture:      "all",
   795  					},
   796  					Locations: []string{"var/lib/dpkg/status"},
   797  					ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{
   798  						Plugin:          dpkg.Name,
   799  						Justification:   vex.ComponentNotPresent,
   800  						MatchesAllVulns: true,
   801  					}},
   802  				},
   803  			},
   804  			wantResultMetric: stats.FileExtractedResultSuccess,
   805  		},
   806  		{
   807  			name:      "transitional empty packages should be annotated",
   808  			path:      "testdata/dpkg/transitional_empty",
   809  			osrelease: DebianBookworm,
   810  			wantPackages: []*extractor.Package{
   811  				{
   812  					Name:     "runit-systemd",
   813  					Version:  "2.1.2-54+usrmerge",
   814  					PURLType: purl.TypeDebian,
   815  					Metadata: &dpkgmeta.Metadata{
   816  						PackageName:       "runit-systemd",
   817  						Status:            "install ok installed",
   818  						PackageVersion:    "2.1.2-54+usrmerge",
   819  						SourceName:        "runit",
   820  						OSID:              "debian",
   821  						OSVersionCodename: "bookworm",
   822  						OSVersionID:       "12",
   823  						Maintainer:        "Lorenzo Puliti <plorenzo@disroot.org>",
   824  						Architecture:      "all",
   825  					},
   826  					Locations: []string{"var/lib/dpkg/status"},
   827  					ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{
   828  						Plugin:          dpkg.Name,
   829  						Justification:   vex.ComponentNotPresent,
   830  						MatchesAllVulns: true,
   831  					}},
   832  				},
   833  			},
   834  			wantResultMetric: stats.FileExtractedResultSuccess,
   835  		},
   836  		{
   837  			name:      "valid opkg status file",
   838  			path:      "testdata/opkg/valid", // Path to your OPKG status file in the test data
   839  			osrelease: OpkgRelease,           // You can mock the os-release data as needed
   840  			isOPKG:    true,
   841  			wantPackages: []*extractor.Package{
   842  				{
   843  					Name:     "ubus",
   844  					Version:  "2024.10.20~252a9b0c-r1",
   845  					PURLType: purl.TypeOpkg,
   846  					Metadata: &dpkgmeta.Metadata{
   847  						PackageName:       "ubus",
   848  						PackageVersion:    "2024.10.20~252a9b0c-r1",
   849  						Status:            "install ok installed",
   850  						Architecture:      "x86_64",
   851  						OSID:              "openwrt",
   852  						OSVersionCodename: "openwrt-21.02.1",
   853  						OSVersionID:       "21.02.1",
   854  					},
   855  					Locations: []string{"usr/lib/opkg/status"},
   856  				},
   857  				{
   858  					Name:     "libuci20130104",
   859  					Version:  "2023.08.10~5781664d-r1",
   860  					PURLType: purl.TypeOpkg,
   861  					Metadata: &dpkgmeta.Metadata{
   862  						PackageName:       "libuci20130104",
   863  						PackageVersion:    "2023.08.10~5781664d-r1",
   864  						Status:            "install ok installed",
   865  						Architecture:      "x86_64",
   866  						OSID:              "openwrt",
   867  						OSVersionCodename: "openwrt-21.02.1",
   868  						OSVersionID:       "21.02.1",
   869  					},
   870  					Locations: []string{"usr/lib/opkg/status"},
   871  				},
   872  				{
   873  					Name:     "busybox",
   874  					Version:  "1.36.1-r2",
   875  					PURLType: purl.TypeOpkg,
   876  					Metadata: &dpkgmeta.Metadata{
   877  						PackageName:       "busybox",
   878  						PackageVersion:    "1.36.1-r2",
   879  						Status:            "install ok installed",
   880  						Architecture:      "x86_64",
   881  						OSID:              "openwrt",
   882  						OSVersionCodename: "openwrt-21.02.1",
   883  						OSVersionID:       "21.02.1",
   884  					},
   885  					Locations: []string{"usr/lib/opkg/status"},
   886  				},
   887  			},
   888  			wantResultMetric: stats.FileExtractedResultSuccess,
   889  		},
   890  		{
   891  			name:      "packages with no version set are skipped",
   892  			path:      "testdata/opkg/noversion",
   893  			osrelease: OpkgRelease,
   894  			isOPKG:    true,
   895  			wantPackages: []*extractor.Package{
   896  				{
   897  					Name:     "ubus",
   898  					Version:  "2024.10.20~252a9b0c-r1",
   899  					PURLType: purl.TypeOpkg,
   900  					Metadata: &dpkgmeta.Metadata{
   901  						PackageName:       "ubus",
   902  						PackageVersion:    "2024.10.20~252a9b0c-r1",
   903  						Status:            "install ok installed",
   904  						Architecture:      "x86_64",
   905  						OSID:              "openwrt",
   906  						OSVersionCodename: "openwrt-21.02.1",
   907  						OSVersionID:       "21.02.1",
   908  					},
   909  					Locations: []string{"usr/lib/opkg/status"},
   910  				},
   911  				{
   912  					Name:     "busybox",
   913  					Version:  "1.36.1-r2",
   914  					PURLType: purl.TypeOpkg,
   915  					Metadata: &dpkgmeta.Metadata{
   916  						PackageName:       "busybox",
   917  						PackageVersion:    "1.36.1-r2",
   918  						Status:            "install ok installed",
   919  						Architecture:      "x86_64",
   920  						OSID:              "openwrt",
   921  						OSVersionCodename: "openwrt-21.02.1",
   922  						OSVersionID:       "21.02.1",
   923  					},
   924  					Locations: []string{"usr/lib/opkg/status"},
   925  				},
   926  			},
   927  			wantResultMetric: stats.FileExtractedResultSuccess,
   928  			wantLogWarn:      1,
   929  		},
   930  		{
   931  			name:      "packages with no name set are skipped",
   932  			path:      "testdata/opkg/nopackage",
   933  			osrelease: OpkgRelease,
   934  			isOPKG:    true,
   935  			wantPackages: []*extractor.Package{
   936  				{
   937  					Name:     "ubus",
   938  					Version:  "2024.10.20~252a9b0c-r1",
   939  					PURLType: purl.TypeOpkg,
   940  					Metadata: &dpkgmeta.Metadata{
   941  						PackageName:       "ubus",
   942  						PackageVersion:    "2024.10.20~252a9b0c-r1",
   943  						Status:            "install ok installed",
   944  						Architecture:      "x86_64",
   945  						OSID:              "openwrt",
   946  						OSVersionCodename: "openwrt-21.02.1",
   947  						OSVersionID:       "21.02.1",
   948  					},
   949  					Locations: []string{"usr/lib/opkg/status"},
   950  				},
   951  				{
   952  					Name:     "busybox",
   953  					Version:  "1.36.1-r2",
   954  					PURLType: purl.TypeOpkg,
   955  					Metadata: &dpkgmeta.Metadata{
   956  						PackageName:       "busybox",
   957  						PackageVersion:    "1.36.1-r2",
   958  						Status:            "install ok installed",
   959  						Architecture:      "x86_64",
   960  						OSID:              "openwrt",
   961  						OSVersionCodename: "openwrt-21.02.1",
   962  						OSVersionID:       "21.02.1",
   963  					},
   964  					Locations: []string{"usr/lib/opkg/status"},
   965  				},
   966  			},
   967  			wantResultMetric: stats.FileExtractedResultSuccess,
   968  			wantLogWarn:      1,
   969  		},
   970  		{
   971  			name:      "statusfield",
   972  			path:      "testdata/opkg/statusfield", // Path to your OPKG status file in the test data
   973  			osrelease: OpkgRelease,                 // You can mock the os-release data as needed
   974  			isOPKG:    true,
   975  			wantPackages: []*extractor.Package{
   976  				{
   977  					Name:     "wantinstall_installed",
   978  					Version:  "1.0",
   979  					PURLType: purl.TypeOpkg,
   980  					Metadata: &dpkgmeta.Metadata{
   981  						PackageName:       "wantinstall_installed",
   982  						PackageVersion:    "1.0",
   983  						Status:            "install ok installed",
   984  						OSID:              "openwrt",
   985  						OSVersionCodename: "openwrt-21.02.1",
   986  						OSVersionID:       "21.02.1",
   987  					},
   988  					Locations: []string{"usr/lib/opkg/status"},
   989  				},
   990  				{
   991  					Name:     "wantpurge_installed",
   992  					Version:  "1.0",
   993  					PURLType: purl.TypeOpkg,
   994  					Metadata: &dpkgmeta.Metadata{
   995  						PackageName:       "wantpurge_installed",
   996  						PackageVersion:    "1.0",
   997  						Status:            "purge ok installed",
   998  						OSID:              "openwrt",
   999  						OSVersionCodename: "openwrt-21.02.1",
  1000  						OSVersionID:       "21.02.1",
  1001  					},
  1002  					Locations: []string{"usr/lib/opkg/status"},
  1003  				},
  1004  			},
  1005  			wantResultMetric: stats.FileExtractedResultSuccess,
  1006  			wantLogWarn:      1,
  1007  		},
  1008  		{
  1009  			name:      "statusfield including not installed",
  1010  			path:      "testdata/opkg/statusfield", // Path to your OPKG status file in the test data
  1011  			osrelease: OpkgRelease,                 // You can mock the os-release data as needed
  1012  			isOPKG:    true,
  1013  			cfg: dpkg.Config{
  1014  				IncludeNotInstalled: true,
  1015  			},
  1016  			wantPackages: []*extractor.Package{
  1017  				{
  1018  					Name:     "wantinstall_installed",
  1019  					Version:  "1.0",
  1020  					PURLType: purl.TypeOpkg,
  1021  					Metadata: &dpkgmeta.Metadata{
  1022  						PackageName:       "wantinstall_installed",
  1023  						PackageVersion:    "1.0",
  1024  						Status:            "install ok installed",
  1025  						OSID:              "openwrt",
  1026  						OSVersionCodename: "openwrt-21.02.1",
  1027  						OSVersionID:       "21.02.1",
  1028  					},
  1029  					Locations: []string{"usr/lib/opkg/status"},
  1030  				},
  1031  				{
  1032  					Name:     "wantdeinstall_configfiles",
  1033  					Version:  "1.0",
  1034  					PURLType: purl.TypeOpkg,
  1035  					Metadata: &dpkgmeta.Metadata{
  1036  						PackageName:       "wantdeinstall_configfiles",
  1037  						PackageVersion:    "1.0",
  1038  						Status:            "deinstall ok config-files",
  1039  						OSID:              "openwrt",
  1040  						OSVersionCodename: "openwrt-21.02.1",
  1041  						OSVersionID:       "21.02.1",
  1042  					},
  1043  					Locations: []string{"usr/lib/opkg/status"},
  1044  				},
  1045  				{
  1046  					Name:     "wantinstall_unpacked",
  1047  					Version:  "1.0",
  1048  					PURLType: purl.TypeOpkg,
  1049  					Metadata: &dpkgmeta.Metadata{
  1050  						PackageName:       "wantinstall_unpacked",
  1051  						PackageVersion:    "1.0",
  1052  						Status:            "install ok unpacked",
  1053  						OSID:              "openwrt",
  1054  						OSVersionCodename: "openwrt-21.02.1",
  1055  						OSVersionID:       "21.02.1",
  1056  					},
  1057  					Locations: []string{"usr/lib/opkg/status"},
  1058  				},
  1059  				{
  1060  					Name:     "wantpurge_installed",
  1061  					Version:  "1.0",
  1062  					PURLType: purl.TypeOpkg,
  1063  					Metadata: &dpkgmeta.Metadata{
  1064  						PackageName:       "wantpurge_installed",
  1065  						PackageVersion:    "1.0",
  1066  						Status:            "purge ok installed",
  1067  						OSID:              "openwrt",
  1068  						OSVersionCodename: "openwrt-21.02.1",
  1069  						OSVersionID:       "21.02.1",
  1070  					},
  1071  					Locations: []string{"usr/lib/opkg/status"},
  1072  				},
  1073  				{
  1074  					Name:     "wantpurge_notinstalled",
  1075  					Version:  "1.0",
  1076  					PURLType: purl.TypeOpkg,
  1077  					Metadata: &dpkgmeta.Metadata{
  1078  						PackageName:       "wantpurge_notinstalled",
  1079  						PackageVersion:    "1.0",
  1080  						Status:            "purge ok not-installed",
  1081  						OSID:              "openwrt",
  1082  						OSVersionCodename: "openwrt-21.02.1",
  1083  						OSVersionID:       "21.02.1",
  1084  					},
  1085  					Locations: []string{"usr/lib/opkg/status"},
  1086  				},
  1087  				{
  1088  					Name:     "wantnostatus",
  1089  					Version:  "1.0",
  1090  					PURLType: purl.TypeOpkg,
  1091  					Metadata: &dpkgmeta.Metadata{
  1092  						PackageName:       "wantnostatus",
  1093  						PackageVersion:    "1.0",
  1094  						OSID:              "openwrt",
  1095  						OSVersionCodename: "openwrt-21.02.1",
  1096  						OSVersionID:       "21.02.1",
  1097  					},
  1098  					Locations: []string{"usr/lib/opkg/status"},
  1099  				},
  1100  			},
  1101  			wantResultMetric: stats.FileExtractedResultSuccess,
  1102  		},
  1103  		{
  1104  			name:             "empty",
  1105  			path:             "testdata/opkg/empty",
  1106  			osrelease:        OpkgRelease,
  1107  			isOPKG:           true,
  1108  			wantPackages:     []*extractor.Package{},
  1109  			wantResultMetric: stats.FileExtractedResultSuccess,
  1110  		},
  1111  		{
  1112  			name:             "invalid",
  1113  			path:             "testdata/opkg/invalid",
  1114  			osrelease:        OpkgRelease,
  1115  			isOPKG:           true,
  1116  			wantPackages:     []*extractor.Package{},
  1117  			wantErr:          cmpopts.AnyError,
  1118  			wantResultMetric: stats.FileExtractedResultErrorUnknown,
  1119  		},
  1120  		{
  1121  			name: "VERSION_CODENAME_not_set,_fallback_to_VERSION_ID",
  1122  			path: "testdata/opkg/single",
  1123  			osrelease: `VERSION_ID="21.02.1"
  1124  			ID=openwrt`,
  1125  			isOPKG: true,
  1126  			wantPackages: []*extractor.Package{
  1127  				{
  1128  					Name:     "ubus",
  1129  					Version:  "2024.10.20~252a9b0c-r1",
  1130  					PURLType: purl.TypeOpkg,
  1131  					Metadata: &dpkgmeta.Metadata{
  1132  						PackageName:    "ubus",
  1133  						PackageVersion: "2024.10.20~252a9b0c-r1",
  1134  						Status:         "install ok installed",
  1135  						Architecture:   "x86_64",
  1136  						OSID:           "openwrt",
  1137  						OSVersionID:    "21.02.1",
  1138  					},
  1139  					Locations: []string{"usr/lib/opkg/status"},
  1140  				},
  1141  			},
  1142  			wantResultMetric: stats.FileExtractedResultSuccess,
  1143  		},
  1144  		{
  1145  			name:      "no version",
  1146  			path:      "testdata/opkg/single",
  1147  			osrelease: `ID=openwrt`,
  1148  			isOPKG:    true,
  1149  			wantPackages: []*extractor.Package{
  1150  				{
  1151  					Name:     "ubus",
  1152  					Version:  "2024.10.20~252a9b0c-r1",
  1153  					PURLType: purl.TypeOpkg,
  1154  					Metadata: &dpkgmeta.Metadata{
  1155  						PackageName:    "ubus",
  1156  						PackageVersion: "2024.10.20~252a9b0c-r1",
  1157  						Status:         "install ok installed",
  1158  						Architecture:   "x86_64",
  1159  						OSID:           "openwrt",
  1160  					},
  1161  					Locations: []string{"usr/lib/opkg/status"},
  1162  				},
  1163  			},
  1164  			wantResultMetric: stats.FileExtractedResultSuccess,
  1165  		},
  1166  		{
  1167  			name:      "osrelease id not set",
  1168  			path:      "testdata/opkg/single",
  1169  			osrelease: `VERSION_CODENAME=openwrt-21.02.1`,
  1170  			isOPKG:    true,
  1171  			wantPackages: []*extractor.Package{
  1172  				{
  1173  					Name:     "ubus",
  1174  					Version:  "2024.10.20~252a9b0c-r1",
  1175  					PURLType: purl.TypeOpkg,
  1176  					Metadata: &dpkgmeta.Metadata{
  1177  						PackageName:       "ubus",
  1178  						PackageVersion:    "2024.10.20~252a9b0c-r1",
  1179  						Status:            "install ok installed",
  1180  						Architecture:      "x86_64",
  1181  						OSVersionCodename: "openwrt-21.02.1",
  1182  					},
  1183  					Locations: []string{"usr/lib/opkg/status"},
  1184  				},
  1185  			},
  1186  			wantResultMetric: stats.FileExtractedResultSuccess,
  1187  		},
  1188  		{
  1189  			name:      "newlines",
  1190  			path:      "testdata/opkg/trailingnewlines",
  1191  			osrelease: OpkgRelease,
  1192  			isOPKG:    true,
  1193  			wantPackages: []*extractor.Package{
  1194  				{
  1195  					Name:     "ubus",
  1196  					Version:  "2024.10.20~252a9b0c-r1",
  1197  					PURLType: purl.TypeOpkg,
  1198  					Metadata: &dpkgmeta.Metadata{
  1199  						PackageName:       "ubus",
  1200  						PackageVersion:    "2024.10.20~252a9b0c-r1",
  1201  						Status:            "install ok installed",
  1202  						Architecture:      "x86_64",
  1203  						OSID:              "openwrt",
  1204  						OSVersionCodename: "openwrt-21.02.1",
  1205  						OSVersionID:       "21.02.1",
  1206  					},
  1207  					Locations: []string{"usr/lib/opkg/status"},
  1208  				},
  1209  			},
  1210  			wantResultMetric: stats.FileExtractedResultSuccess,
  1211  			wantLogWarn:      0,
  1212  			wantLogErr:       0,
  1213  		},
  1214  		{
  1215  			name:      "transitional packages should be annotated",
  1216  			path:      "testdata/opkg/transitional",
  1217  			osrelease: OpkgRelease,
  1218  			isOPKG:    true,
  1219  			wantPackages: []*extractor.Package{
  1220  				{
  1221  					Name:     "ubus",
  1222  					Version:  "2024.10.20~252a9b0c-r1",
  1223  					PURLType: purl.TypeOpkg,
  1224  					Metadata: &dpkgmeta.Metadata{
  1225  						PackageName:       "ubus",
  1226  						PackageVersion:    "2024.10.20~252a9b0c-r1",
  1227  						Status:            "install ok installed",
  1228  						Architecture:      "x86_64",
  1229  						OSID:              "openwrt",
  1230  						OSVersionCodename: "openwrt-21.02.1",
  1231  						OSVersionID:       "21.02.1",
  1232  					},
  1233  					Locations: []string{"usr/lib/opkg/status"},
  1234  					ExploitabilitySignals: []*vex.PackageExploitabilitySignal{&vex.PackageExploitabilitySignal{
  1235  						Plugin:          dpkg.Name,
  1236  						Justification:   vex.ComponentNotPresent,
  1237  						MatchesAllVulns: true,
  1238  					}},
  1239  				},
  1240  			},
  1241  			wantResultMetric: stats.FileExtractedResultSuccess,
  1242  		},
  1243  	}
  1244  
  1245  	for _, tt := range tests {
  1246  		// Note the subtest here
  1247  		t.Run(tt.name, func(t *testing.T) {
  1248  			logger := &testLogger{}
  1249  			scalibrlog.SetLogger(logger)
  1250  
  1251  			collector := testcollector.New()
  1252  			tt.cfg.Stats = collector
  1253  
  1254  			d := t.TempDir()
  1255  			createOsRelease(t, d, tt.osrelease)
  1256  
  1257  			r, err := os.Open(tt.path)
  1258  			defer func() {
  1259  				if err = r.Close(); err != nil {
  1260  					t.Errorf("Close(): %v", err)
  1261  				}
  1262  			}()
  1263  			if err != nil {
  1264  				t.Fatal(err)
  1265  			}
  1266  
  1267  			info, err := os.Stat(tt.path)
  1268  			if err != nil {
  1269  				t.Fatalf("Failed to stat test file: %v", err)
  1270  			}
  1271  
  1272  			// Use valid os package descriptor paths instead of the testdata paths since the Extractor
  1273  			// uses the path to differentiate between things like PURL types.
  1274  			if tt.isOPKG {
  1275  				tt.path = "usr/lib/opkg/status"
  1276  			} else if strings.Contains(tt.path, "status.d") {
  1277  				tt.path = "var/lib/dpkg/status.d" + strings.Split(tt.path, "status.d")[1]
  1278  			} else {
  1279  				tt.path = "var/lib/dpkg/status"
  1280  			}
  1281  			input := &filesystem.ScanInput{
  1282  				FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info,
  1283  			}
  1284  
  1285  			e := dpkg.New(defaultConfigWith(tt.cfg))
  1286  			got, err := e.Extract(t.Context(), input)
  1287  			if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) {
  1288  				t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.path, err, tt.wantErr)
  1289  			}
  1290  
  1291  			ignoreOrder := cmpopts.SortSlices(func(a, b any) bool {
  1292  				return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b)
  1293  			})
  1294  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
  1295  			if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" {
  1296  				t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff)
  1297  			}
  1298  
  1299  			gotResultMetric := collector.FileExtractedResult(tt.path)
  1300  			if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric {
  1301  				t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric)
  1302  			}
  1303  
  1304  			gotFileSizeMetric := collector.FileExtractedFileSize(tt.path)
  1305  			if gotFileSizeMetric != info.Size() {
  1306  				t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size())
  1307  			}
  1308  
  1309  			if logger.warnings != tt.wantLogWarn {
  1310  				t.Errorf("Extract(%s) recorded %d warnings, want %d warnings", tt.path, logger.warnings, tt.wantLogWarn)
  1311  			}
  1312  			if logger.errors != tt.wantLogErr {
  1313  				t.Errorf("Extract(%s) recorded %d errors, want %d errors", tt.path, logger.errors, tt.wantLogErr)
  1314  			}
  1315  		})
  1316  	}
  1317  }
  1318  
  1319  var _ scalibrlog.Logger = &testLogger{}
  1320  
  1321  type testLogger struct {
  1322  	Verbose  bool
  1323  	warnings int
  1324  	errors   int
  1325  }
  1326  
  1327  // Errorf is the formatted error logging function.
  1328  func (l *testLogger) Errorf(format string, args ...any) {
  1329  	golog.Printf(format, args...)
  1330  	l.errors++
  1331  }
  1332  
  1333  // Warnf is the formatted warning logging function.
  1334  func (l *testLogger) Warnf(format string, args ...any) {
  1335  	golog.Printf(format, args...)
  1336  	l.warnings++
  1337  }
  1338  
  1339  // Infof is the formatted info logging function.
  1340  func (testLogger) Infof(format string, args ...any) {
  1341  	golog.Printf(format, args...)
  1342  }
  1343  
  1344  // Debugf is the formatted debug logging function.
  1345  func (l *testLogger) Debugf(format string, args ...any) {
  1346  	if l.Verbose {
  1347  		golog.Printf(format, args...)
  1348  	}
  1349  }
  1350  
  1351  // Error is the error logging function.
  1352  func (l *testLogger) Error(args ...any) {
  1353  	golog.Println(args...)
  1354  	l.errors++
  1355  }
  1356  
  1357  // Warn is the warning logging function.
  1358  func (l *testLogger) Warn(args ...any) {
  1359  	golog.Println(args...)
  1360  	l.warnings++
  1361  }
  1362  
  1363  // Info is the info logging function.
  1364  func (testLogger) Info(args ...any) {
  1365  	golog.Println(args...)
  1366  }
  1367  
  1368  // Debug is the debug logging function.
  1369  func (l *testLogger) Debug(args ...any) {
  1370  	if l.Verbose {
  1371  		golog.Println(args...)
  1372  	}
  1373  }
  1374  
  1375  func TestExtractNonexistentOSRelease(t *testing.T) {
  1376  	path := "testdata/dpkg/single"
  1377  
  1378  	want := inventory.Inventory{Packages: []*extractor.Package{
  1379  		{
  1380  			Name:     "acl",
  1381  			Version:  "2.3.1-3",
  1382  			PURLType: purl.TypeDebian,
  1383  			Metadata: &dpkgmeta.Metadata{
  1384  				PackageName:    "acl",
  1385  				PackageVersion: "2.3.1-3",
  1386  				Status:         "install ok installed",
  1387  				OSID:           "",
  1388  				OSVersionID:    "",
  1389  				Maintainer:     "Guillem Jover <guillem@debian.org>",
  1390  				Architecture:   "amd64",
  1391  			},
  1392  			Locations: []string{path},
  1393  		},
  1394  	}}
  1395  
  1396  	r, err := os.Open(path)
  1397  	defer func() {
  1398  		if err = r.Close(); err != nil {
  1399  			t.Errorf("Close(): %v", err)
  1400  		}
  1401  	}()
  1402  	if err != nil {
  1403  		t.Fatal(err)
  1404  	}
  1405  
  1406  	info, err := os.Stat(path)
  1407  	if err != nil {
  1408  		t.Fatal(err)
  1409  	}
  1410  	// Note that we didn't create any OS release file.
  1411  	input := &filesystem.ScanInput{FS: scalibrfs.DirFS("."), Path: path, Info: info, Reader: r}
  1412  
  1413  	e := dpkg.New(dpkg.DefaultConfig())
  1414  	got, err := e.Extract(t.Context(), input)
  1415  	if err != nil {
  1416  		t.Fatalf("Extract(%s) error: %v", path, err)
  1417  	}
  1418  	if diff := cmp.Diff(want, got); diff != "" {
  1419  		t.Errorf("Extract(%s) (-want +got):\n%s", path, diff)
  1420  	}
  1421  }
  1422  
  1423  func createOsRelease(t *testing.T, root string, content string) {
  1424  	t.Helper()
  1425  	_ = os.MkdirAll(filepath.Join(root, "etc"), 0755)
  1426  	err := os.WriteFile(filepath.Join(root, "etc/os-release"), []byte(content), 0644)
  1427  	if err != nil {
  1428  		t.Fatalf("write to %s: %v\n", filepath.Join(root, "etc/os-release"), err)
  1429  	}
  1430  }
  1431  
  1432  // defaultConfigWith combines any non-zero fields of cfg with packagejson.DefaultConfig().
  1433  func defaultConfigWith(cfg dpkg.Config) dpkg.Config {
  1434  	newCfg := dpkg.DefaultConfig()
  1435  
  1436  	if cfg.Stats != nil {
  1437  		newCfg.Stats = cfg.Stats
  1438  	}
  1439  
  1440  	if cfg.MaxFileSizeBytes > 0 {
  1441  		newCfg.MaxFileSizeBytes = cfg.MaxFileSizeBytes
  1442  	}
  1443  
  1444  	if cfg.IncludeNotInstalled {
  1445  		newCfg.IncludeNotInstalled = cfg.IncludeNotInstalled
  1446  	}
  1447  
  1448  	return newCfg
  1449  }