github.com/quay/claircore@v1.5.28/enricher/cvss/cvss_test.go (about) 1 package cvss 2 3 import ( 4 "compress/gzip" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/http/httptest" 12 "os" 13 "path" 14 "path/filepath" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/google/go-cmp/cmp" 20 "github.com/quay/zlog" 21 22 "github.com/quay/claircore" 23 "github.com/quay/claircore/libvuln/driver" 24 ) 25 26 func TestConfigure(t *testing.T) { 27 t.Parallel() 28 ctx := zlog.Test(context.Background(), t) 29 tt := []configTestcase{ 30 { 31 Name: "None", 32 }, 33 { 34 Name: "OK", 35 Config: func(i interface{}) error { 36 cfg := i.(*Config) 37 s := "http://example.com/" 38 cfg.FeedRoot = &s 39 return nil 40 }, 41 }, 42 { 43 Name: "UnmarshalError", 44 Config: func(_ interface{}) error { return errors.New("expected error") }, 45 Check: func(t *testing.T, err error) { 46 if err == nil { 47 t.Error("expected unmarshal error") 48 } 49 }, 50 }, 51 { 52 Name: "TrailingSlash", 53 Config: func(i interface{}) error { 54 cfg := i.(*Config) 55 s := "http://example.com" 56 cfg.FeedRoot = &s 57 return nil 58 }, 59 Check: func(t *testing.T, err error) { 60 if err == nil { 61 t.Error("expected trailing slash error") 62 } 63 }, 64 }, 65 { 66 Name: "BadURL", 67 Config: func(i interface{}) error { 68 cfg := i.(*Config) 69 s := "http://[notaurl:/" 70 cfg.FeedRoot = &s 71 return nil 72 }, 73 Check: func(t *testing.T, err error) { 74 if err == nil { 75 t.Error("expected URL parse error") 76 } 77 }, 78 }, 79 } 80 for _, tc := range tt { 81 t.Run(tc.Name, tc.Run(ctx)) 82 } 83 } 84 85 type configTestcase struct { 86 Config func(interface{}) error 87 Check func(*testing.T, error) 88 Name string 89 } 90 91 func (tc configTestcase) Run(ctx context.Context) func(*testing.T) { 92 e := &Enricher{} 93 return func(t *testing.T) { 94 ctx := zlog.Test(ctx, t) 95 f := tc.Config 96 if f == nil { 97 f = noopConfig 98 } 99 err := e.Configure(ctx, f, nil) 100 if tc.Check == nil { 101 if err != nil { 102 t.Errorf("unexpected err: %v", err) 103 } 104 return 105 } 106 tc.Check(t, err) 107 } 108 } 109 110 func noopConfig(_ interface{}) error { return nil } 111 112 func TestFetch(t *testing.T) { 113 t.Parallel() 114 ctx := zlog.Test(context.Background(), t) 115 srv := mockServer(t) 116 tt := []fetchTestcase{ 117 { 118 Name: "Initial", 119 }, 120 { 121 Name: "InvalidHint", 122 Hint: `{bareword`, 123 Check: func(t *testing.T, rc io.ReadCloser, fp driver.Fingerprint, err error) { 124 if rc != nil { 125 t.Error("got non-nil ReadCloser") 126 } 127 if got, want := driver.Fingerprint(""), fp; got != want { 128 t.Errorf("bad fingerprint: got: %q, want: %q", got, want) 129 } 130 t.Logf("got error: %v", err) 131 if err == nil { 132 t.Error("wanted non-nil error") 133 } 134 }, 135 }, 136 { 137 Name: "Unchanged", 138 Hint: func() string { 139 // This is copied out of the metafile in testdata: 140 const h = `708083B92E47F0B25C7DD68B89ECD9EF3F2EF91403F511AE13195A596F02E02E` 141 var b strings.Builder 142 b.WriteByte('{') 143 for y, lim := firstYear, time.Now().Year(); y <= lim; y++ { 144 fmt.Fprintf(&b, `"%d":%q`, y, h) 145 if y != lim { 146 b.WriteByte(',') 147 } 148 } 149 b.WriteByte('}') 150 return b.String() 151 }(), 152 Check: func(t *testing.T, rc io.ReadCloser, _ driver.Fingerprint, err error) { 153 if rc != nil { 154 t.Error("got non-nil ReadCloser") 155 } 156 t.Logf("got error: %v", err) 157 if !errors.Is(err, driver.Unchanged) { 158 t.Errorf("wanted %v", driver.Unchanged) 159 } 160 }, 161 }, 162 } 163 164 for _, tc := range tt { 165 t.Run(tc.Name, tc.Run(ctx, srv)) 166 } 167 } 168 169 type fetchTestcase struct { 170 Check func(*testing.T, io.ReadCloser, driver.Fingerprint, error) 171 Name string 172 Hint string 173 } 174 175 func (tc fetchTestcase) Run(ctx context.Context, srv *httptest.Server) func(*testing.T) { 176 e := &Enricher{} 177 return func(t *testing.T) { 178 ctx := zlog.Test(ctx, t) 179 f := func(i interface{}) error { 180 cfg, ok := i.(*Config) 181 if !ok { 182 t.Fatal("assertion failed") 183 } 184 u := srv.URL + "/" 185 cfg.FeedRoot = &u 186 return nil 187 } 188 if err := e.Configure(ctx, f, srv.Client()); err != nil { 189 t.Errorf("unexpected error: %v", err) 190 } 191 rc, fp, err := e.FetchEnrichment(ctx, driver.Fingerprint(tc.Hint)) 192 if rc != nil { 193 defer rc.Close() 194 } 195 if tc.Check == nil { 196 if err != nil { 197 t.Errorf("unexpected error: %v", err) 198 } 199 return 200 } 201 tc.Check(t, rc, fp, err) 202 } 203 } 204 205 func mockServer(t *testing.T) *httptest.Server { 206 const root = `testdata/` 207 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 208 switch path.Ext(r.URL.Path) { 209 case ".gz": // return the gzipped feed 210 f, err := os.Open(filepath.Join(root, "feed.json")) 211 if err != nil { 212 t.Errorf("open failed: %v", err) 213 w.WriteHeader(http.StatusInternalServerError) 214 break 215 } 216 defer f.Close() 217 gz := gzip.NewWriter(w) 218 defer gz.Close() 219 if _, err := io.Copy(gz, f); err != nil { 220 t.Errorf("write error: %v", err) 221 w.WriteHeader(http.StatusInternalServerError) 222 break 223 } 224 case ".meta": // return the metafile 225 http.ServeFile(w, r, filepath.Join(root, "feed.meta")) 226 default: 227 t.Errorf("unknown request path: %q", r.URL.Path) 228 w.WriteHeader(http.StatusBadRequest) 229 } 230 })) 231 t.Cleanup(srv.Close) 232 return srv 233 } 234 235 func TestParse(t *testing.T) { 236 t.Parallel() 237 ctx := zlog.Test(context.Background(), t) 238 srv := mockServer(t) 239 tt := []parseTestcase{ 240 { 241 Name: "OK", 242 }, 243 } 244 for _, tc := range tt { 245 t.Run(tc.Name, tc.Run(ctx, srv)) 246 } 247 } 248 249 type parseTestcase struct { 250 Check func(*testing.T, []driver.EnrichmentRecord, error) 251 Name string 252 } 253 254 func (tc parseTestcase) Run(ctx context.Context, srv *httptest.Server) func(*testing.T) { 255 e := &Enricher{} 256 return func(t *testing.T) { 257 ctx := zlog.Test(ctx, t) 258 f := func(i interface{}) error { 259 cfg, ok := i.(*Config) 260 if !ok { 261 t.Fatal("assertion failed") 262 } 263 u := srv.URL + "/" 264 cfg.FeedRoot = &u 265 return nil 266 } 267 if err := e.Configure(ctx, f, srv.Client()); err != nil { 268 t.Errorf("unexpected error: %v", err) 269 } 270 rc, _, err := e.FetchEnrichment(ctx, "") 271 if err != nil { 272 t.Errorf("unexpected error: %v", err) 273 } 274 defer rc.Close() 275 rs, err := e.ParseEnrichment(ctx, rc) 276 if tc.Check == nil { 277 if err != nil { 278 t.Errorf("unexpected error: %v", err) 279 } 280 return 281 } 282 tc.Check(t, rs, err) 283 } 284 } 285 286 func TestEnrich(t *testing.T) { 287 t.Parallel() 288 ctx := zlog.Test(context.Background(), t) 289 feedIn, err := os.Open("testdata/feed.json") 290 if err != nil { 291 t.Fatal(err) 292 } 293 f, err := newItemFeed(2021, feedIn) 294 if err != nil { 295 t.Error(err) 296 } 297 g := &fakeGetter{itemFeed: f} 298 r := &claircore.VulnerabilityReport{ 299 Vulnerabilities: map[string]*claircore.Vulnerability{ 300 "-1": { 301 Description: "This is a fake vulnerability that doesn't have a CVE.", 302 }, 303 "1": { 304 Description: "This is a fake vulnerability that looks like CVE-2021-0498.", 305 }, 306 "6004": { 307 Description: "CVE-2020-6004 was unassigned", 308 }, 309 "6005": { 310 Description: "CVE-2021-0498 duplicate", 311 }, 312 }, 313 } 314 e := &Enricher{} 315 kind, es, err := e.Enrich(ctx, g, r) 316 if err != nil { 317 t.Error(err) 318 } 319 if got, want := kind, Type; got != want { 320 t.Errorf("got: %q, want: %q", got, want) 321 } 322 want := map[string][]map[string]interface{}{ 323 "1": {{ 324 "version": "3.1", 325 "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 326 "attackVector": "LOCAL", 327 "attackComplexity": "LOW", 328 "privilegesRequired": "LOW", 329 "userInteraction": "NONE", 330 "scope": "UNCHANGED", 331 "confidentialityImpact": "HIGH", 332 "integrityImpact": "HIGH", 333 "availabilityImpact": "HIGH", 334 "baseScore": 7.8, 335 "baseSeverity": "HIGH", 336 }}, 337 "6005": {{ 338 "version": "3.1", 339 "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 340 "attackVector": "LOCAL", 341 "attackComplexity": "LOW", 342 "privilegesRequired": "LOW", 343 "userInteraction": "NONE", 344 "scope": "UNCHANGED", 345 "confidentialityImpact": "HIGH", 346 "integrityImpact": "HIGH", 347 "availabilityImpact": "HIGH", 348 "baseScore": 7.8, 349 "baseSeverity": "HIGH", 350 }}, 351 } 352 got := map[string][]map[string]interface{}{} 353 if err := json.Unmarshal(es[0], &got); err != nil { 354 t.Error(err) 355 } 356 if !cmp.Equal(got, want) { 357 t.Error(cmp.Diff(got, want)) 358 } 359 } 360 361 type fakeGetter struct { 362 *itemFeed 363 res []driver.EnrichmentRecord 364 } 365 366 func (f *fakeGetter) GetEnrichment(ctx context.Context, tags []string) ([]driver.EnrichmentRecord, error) { 367 id := tags[0] 368 for _, cve := range f.items { 369 if cve.CVE.Meta.ID == id && cve.Impact.V3.CVSS != nil { 370 r := []driver.EnrichmentRecord{ 371 {Tags: tags, Enrichment: cve.Impact.V3.CVSS}, 372 } 373 f.res = r 374 return r, nil 375 } 376 } 377 return nil, nil 378 }