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  }