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 }