github.com/google/osv-scalibr@v0.4.1/enricher/enricher_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 enricher_test
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  	"github.com/google/go-cmp/cmp/cmpopts"
    24  	"github.com/google/go-cpy/cpy"
    25  	"github.com/google/osv-scalibr/enricher"
    26  	"github.com/google/osv-scalibr/extractor"
    27  	"github.com/google/osv-scalibr/inventory"
    28  	"github.com/google/osv-scalibr/plugin"
    29  	"github.com/google/osv-scalibr/testing/fakeenricher"
    30  	"google.golang.org/protobuf/proto"
    31  )
    32  
    33  func TestRun(t *testing.T) {
    34  	inventory1 := &inventory.Inventory{
    35  		Packages: []*extractor.Package{
    36  			{Name: "package1", Version: "1.0"},
    37  		},
    38  		GenericFindings: []*inventory.GenericFinding{
    39  			{Adv: &inventory.GenericFindingAdvisory{ID: &inventory.AdvisoryID{Publisher: "CVE", Reference: "CVE-2024-12345"}}},
    40  		},
    41  	}
    42  	inventory2 := &inventory.Inventory{
    43  		Packages: []*extractor.Package{
    44  			{Name: "package2", Version: "2.0"},
    45  			{Name: "package3", Version: "3.0"},
    46  		},
    47  		GenericFindings: []*inventory.GenericFinding{
    48  			{Adv: &inventory.GenericFindingAdvisory{ID: &inventory.AdvisoryID{Publisher: "CVE", Reference: "CVE-2024-12345"}}},
    49  			{
    50  				Adv: &inventory.GenericFindingAdvisory{
    51  					ID:             &inventory.AdvisoryID{Publisher: "CVE", Reference: "CVE-2024-67890"},
    52  					Recommendation: "do something",
    53  				},
    54  				Target: &inventory.GenericFindingTargetDetails{Extra: "extra info"},
    55  			},
    56  		},
    57  	}
    58  	inventory3 := &inventory.Inventory{
    59  		Packages: []*extractor.Package{
    60  			{Name: "package2", Version: "2.0"},
    61  			{Name: "package3", Version: "3.0"},
    62  			{Name: "package4", Version: "4.0"},
    63  		},
    64  		GenericFindings: []*inventory.GenericFinding{
    65  			{
    66  				Adv:    &inventory.GenericFindingAdvisory{ID: &inventory.AdvisoryID{Publisher: "CVE", Reference: "CVE-2024-12345"}, Recommendation: "do something"},
    67  				Target: &inventory.GenericFindingTargetDetails{Extra: "extra info"},
    68  			},
    69  			{
    70  				Adv:    &inventory.GenericFindingAdvisory{ID: &inventory.AdvisoryID{Publisher: "CVE", Reference: "CVE-2024-67890"}, Recommendation: "do something else"},
    71  				Target: &inventory.GenericFindingTargetDetails{Extra: "extra info"},
    72  			},
    73  			{Adv: &inventory.GenericFindingAdvisory{ID: &inventory.AdvisoryID{Publisher: "GHSA", Reference: "GHSA-2024-45678"}, Recommendation: "none"}},
    74  		},
    75  	}
    76  
    77  	copier := cpy.New(
    78  		cpy.Func(proto.Clone),
    79  		cpy.IgnoreAllUnexported(),
    80  	)
    81  
    82  	tests := []struct {
    83  		name    string
    84  		cfg     *enricher.Config
    85  		inv     *inventory.Inventory
    86  		want    []*plugin.Status
    87  		wantErr error
    88  		wantInv *inventory.Inventory // Inventory after enrichment.
    89  	}{
    90  		{
    91  			name: "no_enrichers",
    92  			cfg:  &enricher.Config{},
    93  			want: nil,
    94  		},
    95  		{
    96  			name: "enricher_requires_FS_access_but_no_scan_root_is_provided",
    97  			cfg: &enricher.Config{
    98  				Enrichers: []enricher.Enricher{
    99  					fakeenricher.MustNew(t, &fakeenricher.Config{
   100  						Name: "enricher1", Version: 1,
   101  						Capabilities: &plugin.Capabilities{DirectFS: true},
   102  					}),
   103  				},
   104  			},
   105  			inv:     inventory1,
   106  			want:    nil,
   107  			wantErr: enricher.ErrNoDirectFS,
   108  			wantInv: inventory1, // Inventory is not modified.
   109  		},
   110  		{
   111  			name: "some_enrichers_run_successfully",
   112  			cfg: &enricher.Config{
   113  				Enrichers: []enricher.Enricher{
   114  					fakeenricher.MustNew(t, &fakeenricher.Config{
   115  						Name: "enricher1", Version: 1,
   116  						WantEnrich: map[uint64]fakeenricher.InventoryAndErr{
   117  							fakeenricher.MustHash(t, &enricher.ScanInput{}, inventory1): fakeenricher.InventoryAndErr{Inventory: inventory2},
   118  						},
   119  					}),
   120  					fakeenricher.MustNew(t, &fakeenricher.Config{
   121  						Name: "enricher2", Version: 2,
   122  						WantEnrich: map[uint64]fakeenricher.InventoryAndErr{
   123  							fakeenricher.MustHash(t, &enricher.ScanInput{}, inventory2): fakeenricher.InventoryAndErr{Inventory: inventory3},
   124  						},
   125  					}),
   126  				},
   127  			},
   128  			inv: inventory1,
   129  			want: []*plugin.Status{
   130  				{Name: "enricher1", Version: 1, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
   131  				{Name: "enricher2", Version: 2, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
   132  			},
   133  			wantInv: inventory3,
   134  		},
   135  		{
   136  			name: "some_fail_and_some_succeed",
   137  			cfg: &enricher.Config{
   138  				Enrichers: []enricher.Enricher{
   139  					fakeenricher.MustNew(t, &fakeenricher.Config{
   140  						Name: "enricher1", Version: 1,
   141  						WantEnrich: map[uint64]fakeenricher.InventoryAndErr{
   142  							fakeenricher.MustHash(t, &enricher.ScanInput{}, inventory1): fakeenricher.InventoryAndErr{Inventory: inventory2, Err: errors.New("some error")},
   143  						},
   144  					}),
   145  					fakeenricher.MustNew(t, &fakeenricher.Config{
   146  						Name: "enricher2", Version: 2,
   147  						WantEnrich: map[uint64]fakeenricher.InventoryAndErr{
   148  							fakeenricher.MustHash(t, &enricher.ScanInput{}, inventory2): fakeenricher.InventoryAndErr{Inventory: inventory3},
   149  						},
   150  					}),
   151  				},
   152  			},
   153  			inv: inventory1,
   154  			want: []*plugin.Status{
   155  				{Name: "enricher1", Version: 1, Status: &plugin.ScanStatus{Status: plugin.ScanStatusFailed, FailureReason: "some error"}},
   156  				{Name: "enricher2", Version: 2, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
   157  			},
   158  			wantInv: inventory3,
   159  		},
   160  	}
   161  
   162  	for _, tc := range tests {
   163  		t.Run(tc.name, func(t *testing.T) {
   164  			// Deep copy the inventory to avoid modifying the original inventory that is used in other tests.
   165  			inv := copier.Copy(tc.inv).(*inventory.Inventory)
   166  			got, err := enricher.Run(t.Context(), tc.cfg, inv)
   167  			if !cmp.Equal(err, tc.wantErr, cmpopts.EquateErrors()) {
   168  				t.Errorf("Run(%+v) error: got %v, want %v\n", tc.cfg, err, tc.wantErr)
   169  			}
   170  			if diff := cmp.Diff(tc.want, got); diff != "" {
   171  				t.Errorf("Run(%+v) returned an unexpected diff of statuses (-want +got): %v", tc.cfg, diff)
   172  			}
   173  			if diff := cmp.Diff(tc.wantInv, inv); diff != "" {
   174  				t.Errorf("Run(%+v) returned an unexpected diff of mutated inventory (-want +got): %v", tc.cfg, diff)
   175  			}
   176  		})
   177  	}
   178  }
   179  
   180  type fakeVulnMatcher struct{}
   181  
   182  func (fakeVulnMatcher) Name() string                       { return "vulnmatch/osvdev" }
   183  func (fakeVulnMatcher) Version() int                       { return 0 }
   184  func (fakeVulnMatcher) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
   185  func (fakeVulnMatcher) RequiredPlugins() []string          { return nil }
   186  func (fakeVulnMatcher) Enrich(_ context.Context, _ *enricher.ScanInput, inv *inventory.Inventory) error {
   187  	inv.PackageVulns = append(inv.PackageVulns, &inventory.PackageVuln{})
   188  	return nil
   189  }
   190  
   191  // Expects the fakeVulnMatcher plugin to run first.
   192  type fakeVEXFilterer struct{}
   193  
   194  func (fakeVEXFilterer) Name() string                       { return "vex/filter" }
   195  func (fakeVEXFilterer) Version() int                       { return 0 }
   196  func (fakeVEXFilterer) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
   197  func (fakeVEXFilterer) RequiredPlugins() []string          { return nil }
   198  func (fakeVEXFilterer) Enrich(_ context.Context, _ *enricher.ScanInput, inv *inventory.Inventory) error {
   199  	if len(inv.PackageVulns) == 0 {
   200  		return errors.New("vuln matcher didn't run before vex filterer")
   201  	}
   202  	inv.PackageVulns = nil
   203  	return nil
   204  }
   205  
   206  // A third enricher that can run in any order.
   207  type fakePackageAdder struct{}
   208  
   209  func (fakePackageAdder) Name() string                       { return "fake-package-adder" }
   210  func (fakePackageAdder) Version() int                       { return 0 }
   211  func (fakePackageAdder) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
   212  func (fakePackageAdder) RequiredPlugins() []string          { return nil }
   213  func (fakePackageAdder) Enrich(_ context.Context, _ *enricher.ScanInput, inv *inventory.Inventory) error {
   214  	inv.Packages = append(inv.Packages, &extractor.Package{})
   215  	return nil
   216  }
   217  
   218  func TestRunEnricherOrdering(t *testing.T) {
   219  	cfg := &enricher.Config{
   220  		Enrichers: []enricher.Enricher{
   221  			&fakePackageAdder{},
   222  			&fakeVEXFilterer{},
   223  			&fakeVulnMatcher{},
   224  		},
   225  	}
   226  	inv := &inventory.Inventory{}
   227  
   228  	wantInv := &inventory.Inventory{
   229  		// One package (added by fakePackageAdder)
   230  		Packages: []*extractor.Package{{}},
   231  		// No vulns (removed by fakeVEXFilterer)
   232  		PackageVulns: nil,
   233  	}
   234  	wantStatus := []*plugin.Status{
   235  		{Name: "vulnmatch/osvdev", Version: 0, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
   236  		{Name: "vex/filter", Version: 0, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
   237  		{Name: "fake-package-adder", Version: 0, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
   238  	}
   239  
   240  	gotStatus, err := enricher.Run(t.Context(), cfg, inv)
   241  	if err != nil {
   242  		t.Errorf("Run(%+v) error: %v", cfg, err)
   243  	}
   244  	if diff := cmp.Diff(wantStatus, gotStatus); diff != "" {
   245  		t.Errorf("Run(%+v) returned an unexpected diff of statuses (-want +got): %v", cfg, diff)
   246  	}
   247  	if diff := cmp.Diff(wantInv, inv); diff != "" {
   248  		t.Errorf("Run(%+v) returned an unexpected diff of mutated inventory (-want +got): %v", cfg, diff)
   249  	}
   250  }