github.com/google/osv-scalibr@v0.4.1/annotator/misc/dpkgsource/dpkgsource_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 dpkgsource_test
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/google/go-cpy/cpy"
    23  	"github.com/google/osv-scalibr/annotator"
    24  	"github.com/google/osv-scalibr/annotator/misc/dpkgsource"
    25  	"github.com/google/osv-scalibr/extractor"
    26  	dpkgmetadata "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg/metadata"
    27  	"github.com/google/osv-scalibr/inventory"
    28  	"github.com/google/osv-scalibr/purl"
    29  	"google.golang.org/protobuf/proto"
    30  )
    31  
    32  func TestAnnotate_DPKGSource(t *testing.T) {
    33  	copier := cpy.New(
    34  		cpy.Func(proto.Clone),
    35  		cpy.IgnoreAllUnexported(),
    36  	)
    37  
    38  	origFetchAptCachePolicy := dpkgsource.FetchAptCachePolicy
    39  	defer func() { dpkgsource.FetchAptCachePolicy = origFetchAptCachePolicy }()
    40  
    41  	mockPolicyResults := map[string]string{
    42  		"libfoo": "http://deb.debian.org/debian",
    43  		"libbar": "/var/lib/dpkg/status",
    44  	}
    45  	dpkgsource.FetchAptCachePolicy = mockGetAptCachePolicy(mockPolicyResults)
    46  
    47  	annotatorInstance := dpkgsource.New()
    48  
    49  	testCases := []struct {
    50  		name  string
    51  		input *inventory.Inventory
    52  		want  *inventory.Inventory
    53  	}{
    54  		{
    55  			name: "debian_package_remote_source",
    56  			input: &inventory.Inventory{
    57  				Packages: []*extractor.Package{
    58  					{
    59  						Name:     "libfoo",
    60  						Version:  "1.0",
    61  						PURLType: purl.TypeDebian,
    62  						Metadata: &dpkgmetadata.Metadata{},
    63  					},
    64  				},
    65  			},
    66  			want: &inventory.Inventory{
    67  				Packages: []*extractor.Package{
    68  					{
    69  						Name:     "libfoo",
    70  						Version:  "1.0",
    71  						PURLType: purl.TypeDebian,
    72  						Metadata: &dpkgmetadata.Metadata{
    73  							PackageSource: "http://deb.debian.org/debian",
    74  						},
    75  					},
    76  				},
    77  			},
    78  		},
    79  		{
    80  			name: "debian_package_local_source",
    81  			input: &inventory.Inventory{
    82  				Packages: []*extractor.Package{
    83  					{
    84  						Name:     "libbar",
    85  						Version:  "1.0",
    86  						PURLType: purl.TypeDebian,
    87  						Metadata: &dpkgmetadata.Metadata{},
    88  					},
    89  				},
    90  			},
    91  			want: &inventory.Inventory{
    92  				Packages: []*extractor.Package{
    93  					{
    94  						Name:     "libbar",
    95  						Version:  "1.0",
    96  						PURLType: purl.TypeDebian,
    97  						Metadata: &dpkgmetadata.Metadata{
    98  							PackageSource: "/var/lib/dpkg/status",
    99  						},
   100  					},
   101  				},
   102  			},
   103  		},
   104  		{
   105  			name: "not_debian_package",
   106  			input: &inventory.Inventory{
   107  				Packages: []*extractor.Package{
   108  					{
   109  						Name:     "not-deb",
   110  						Version:  "1.0",
   111  						PURLType: purl.TypeNPM,
   112  						Metadata: &dpkgmetadata.Metadata{},
   113  					},
   114  				},
   115  			},
   116  			want: &inventory.Inventory{
   117  				Packages: []*extractor.Package{
   118  					{
   119  						Name:     "not-deb",
   120  						Version:  "1.0",
   121  						PURLType: purl.TypeNPM,
   122  						Metadata: &dpkgmetadata.Metadata{},
   123  					},
   124  				},
   125  			},
   126  		},
   127  		{
   128  			name: "debian_package_missing_source",
   129  			input: &inventory.Inventory{
   130  				Packages: []*extractor.Package{
   131  					{
   132  						Name:     "missing-source",
   133  						Version:  "1.0",
   134  						PURLType: purl.TypeDebian,
   135  						Metadata: &dpkgmetadata.Metadata{},
   136  					},
   137  				},
   138  			},
   139  			want: &inventory.Inventory{
   140  				Packages: []*extractor.Package{
   141  					{
   142  						Name:     "missing-source",
   143  						Version:  "1.0",
   144  						PURLType: purl.TypeDebian,
   145  						Metadata: &dpkgmetadata.Metadata{
   146  							PackageSource: "unknown",
   147  						},
   148  					},
   149  				},
   150  			},
   151  		},
   152  	}
   153  
   154  	for _, tt := range testCases {
   155  		t.Run(tt.name, func(t *testing.T) {
   156  			inputPackagesCopy := make([]*extractor.Package, len(tt.input.Packages))
   157  			for i, pkg := range tt.input.Packages {
   158  				inputPackagesCopy[i] = copier.Copy(pkg).(*extractor.Package)
   159  			}
   160  			inv := &inventory.Inventory{Packages: inputPackagesCopy}
   161  
   162  			err := annotatorInstance.Annotate(t.Context(), &annotator.ScanInput{}, inv)
   163  			if err != nil {
   164  				t.Fatalf("Annotate() unexpected error: %v", err)
   165  			}
   166  
   167  			if diff := cmp.Diff(tt.want, inv); diff != "" {
   168  				t.Errorf("Annotate() unexpected diff (-want +got):\n%s", diff)
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func TestMapPackageToSource(t *testing.T) {
   175  	tests := []struct {
   176  		name           string
   177  		aptCacheOutput string
   178  		want           map[string]string
   179  	}{
   180  		{
   181  			name:           "empty_input",
   182  			aptCacheOutput: "",
   183  			want:           map[string]string{},
   184  		},
   185  		{
   186  			name: "single_package",
   187  			aptCacheOutput: `mypackage:
   188    Installed: 20.1.1
   189    Candidate: 20.1.1
   190    Version table:
   191   *** 20.1.1
   192         500 http://deb.debian.org/debian stable/main amd64 Packages
   193  `,
   194  			want: map[string]string{"mypackage": "http://deb.debian.org/debian"},
   195  		},
   196  		{
   197  			name: "multiple_packages",
   198  			aptCacheOutput: `pkg1:
   199    Installed: 1.0
   200   *** 1.0 500
   201        500 http://deb.debian.org/debian stable/main amd64 Packages
   202  pkg2:
   203    Installed: 2.1
   204   *** 2.1 900
   205        100 http://security.debian.org/debian-security stretch/updates/main amd64 Packages
   206  `,
   207  			want: map[string]string{
   208  				"pkg1": "http://deb.debian.org/debian",
   209  				"pkg2": "http://security.debian.org/debian-security",
   210  			},
   211  		},
   212  		{
   213  			name: "multiple_repositories",
   214  			aptCacheOutput: `mypackage:
   215    Installed: 1.5
   216   *** 1.5 500
   217        500 http://deb.debian.org/debian stable/main amd64 Packages
   218        100 http://archive.debian.org/debian oldstable/main amd64 Packages
   219  `,
   220  			want: map[string]string{"mypackage": "http://deb.debian.org/debian"},
   221  		},
   222  		{
   223  			name: "missing_installed_version",
   224  			aptCacheOutput: `mypackage:
   225    Candidate: 1.0
   226    Version table:
   227       1.0 500
   228         500 http://deb.debian.org/debian stable/main amd64 Packages
   229  `,
   230  			want: map[string]string{},
   231  		},
   232  		{
   233  			name: "whitespace_variation",
   234  			aptCacheOutput: `mypackage:
   235    Installed: 20.1.1
   236    Candidate: 20.1.1
   237    Version table:
   238     *** 20.1.1
   239         500 http://deb.debian.org/debian stable/main amd64 Packages
   240  `,
   241  			want: map[string]string{"mypackage": "http://deb.debian.org/debian"},
   242  		},
   243  		{
   244  			name: "package_name_special_chars",
   245  			aptCacheOutput: `libfoo-dev:
   246    Installed: 1.0-1
   247        *** 1.0-1 500
   248        500 http://deb.debian.org/debian stable/main amd64 Packages
   249  `,
   250  			want: map[string]string{"libfoo-dev": "http://deb.debian.org/debian"},
   251  		},
   252  		{
   253  			name: "input_ends_after_installed_version",
   254  			aptCacheOutput: `mypackage:
   255    Installed: 1.0
   256        *** 1.0 500`,
   257  			want: map[string]string{"mypackage": "unknown"},
   258  		},
   259  		{
   260  			name: "repository_source_single_field",
   261  			aptCacheOutput: `mypackage:
   262    Installed: 1.0
   263        *** 1.0 500
   264        onlyone`,
   265  			want: map[string]string{"mypackage": "unknown"},
   266  		},
   267  		{
   268  			name: "single_malformed_source",
   269  			aptCacheOutput: `pkg1:
   270    Installed: 1.0
   271        *** 1.0 900
   272        malformedline
   273        
   274  pkg2:
   275    Installed: 2.1
   276        *** 2.1 900
   277        500 http://deb.debian.org/debian stable/main amd64 Packages
   278        `,
   279  			want: map[string]string{
   280  				"pkg1": "unknown",
   281  				"pkg2": "http://deb.debian.org/debian",
   282  			},
   283  		},
   284  	}
   285  
   286  	for _, tt := range tests {
   287  		t.Run(tt.name, func(t *testing.T) {
   288  			got, err := dpkgsource.MapPackageToSource(t.Context(), tt.aptCacheOutput)
   289  			if err != nil {
   290  				t.Fatalf("MapPackageToSource(%q) returned an unexpected error: %v", tt.aptCacheOutput, err)
   291  			}
   292  
   293  			if diff := cmp.Diff(tt.want, got); diff != "" {
   294  				t.Errorf("MapPackageToSource(%q) returned an unexpected diff (-want +got):\n%s", tt.aptCacheOutput, diff)
   295  			}
   296  		})
   297  	}
   298  }
   299  
   300  // Mock implementation for fetchAptCachePolicy
   301  func mockGetAptCachePolicy(mockResults map[string]string) func(context.Context, []*extractor.Package) (map[string]string, error) {
   302  	return func(ctx context.Context, pkgs []*extractor.Package) (map[string]string, error) {
   303  		return mockResults, nil
   304  	}
   305  }