208 lines
6.0 KiB
Go
208 lines
6.0 KiB
Go
// ©AngelaMos | 2026
|
|
// cve_test.go
|
|
|
|
package cve
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
func fileServer(t *testing.T, path string) *httptest.Server {
|
|
t.Helper()
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read fixture %s: %v", path, err)
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write(body)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func jsonServer(t *testing.T, body string) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func TestExtract(t *testing.T) {
|
|
cases := []struct {
|
|
in []string
|
|
want []string
|
|
}{
|
|
{[]string{"See CVE-2021-44228 for details"}, []string{"CVE-2021-44228"}},
|
|
{[]string{"cve-2021-44228 lowercase"}, []string{"CVE-2021-44228"}},
|
|
{[]string{"CVE-2021-44228 and CVE-2021-44228 again"}, []string{"CVE-2021-44228"}},
|
|
{[]string{"CVE-2026-1 too short", "CVE-2026-12345 ok"}, []string{"CVE-2026-12345"}},
|
|
{[]string{"CVE-2021-45046, CVE-2021-44228"}, []string{"CVE-2021-44228", "CVE-2021-45046"}},
|
|
{[]string{"no identifiers here"}, nil},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := Extract(tc.in...); !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("Extract(%v) = %v, want %v", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNVDParsesLog4Shell(t *testing.T) {
|
|
srv := fileServer(t, "../../testdata/nvd/CVE-2021-44228.json")
|
|
c := NewNVDClient(srv.Client(), srv.URL, "")
|
|
|
|
res, err := c.Fetch(context.Background(), "CVE-2021-44228")
|
|
if err != nil {
|
|
t.Fatalf("Fetch: %v", err)
|
|
}
|
|
if !res.Found {
|
|
t.Fatal("expected Found")
|
|
}
|
|
if res.CVSSScore == nil || *res.CVSSScore != 10.0 {
|
|
t.Errorf("cvss score = %v, want 10.0", res.CVSSScore)
|
|
}
|
|
if res.CVSSSeverity != "CRITICAL" {
|
|
t.Errorf("severity = %q, want CRITICAL", res.CVSSSeverity)
|
|
}
|
|
if res.CVSSVersion != "3.1" {
|
|
t.Errorf("version = %q, want 3.1", res.CVSSVersion)
|
|
}
|
|
if res.CWE != "CWE-20" {
|
|
t.Errorf("cwe = %q, want CWE-20", res.CWE)
|
|
}
|
|
if res.Description == "" {
|
|
t.Error("description empty")
|
|
}
|
|
}
|
|
|
|
func TestNVDNotFound(t *testing.T) {
|
|
srv := jsonServer(t, `{"totalResults":0,"vulnerabilities":[]}`)
|
|
c := NewNVDClient(srv.Client(), srv.URL, "")
|
|
res, err := c.Fetch(context.Background(), "CVE-2099-99999")
|
|
if err != nil {
|
|
t.Fatalf("Fetch: %v", err)
|
|
}
|
|
if res.Found {
|
|
t.Error("expected not found for totalResults 0")
|
|
}
|
|
}
|
|
|
|
func TestNVDPrecedencePrefersV40(t *testing.T) {
|
|
body := `{"totalResults":1,"vulnerabilities":[{"cve":{
|
|
"id":"CVE-2026-1","descriptions":[{"lang":"en","value":"d"}],
|
|
"metrics":{
|
|
"cvssMetricV40":[{"cvssData":{"version":"4.0","baseScore":9.3,"baseSeverity":"CRITICAL","vectorString":"CVSS:4.0/x"}}],
|
|
"cvssMetricV31":[{"cvssData":{"version":"3.1","baseScore":7.5,"baseSeverity":"HIGH","vectorString":"CVSS:3.1/y"}}]
|
|
}}}]}`
|
|
srv := jsonServer(t, body)
|
|
c := NewNVDClient(srv.Client(), srv.URL, "")
|
|
res, err := c.Fetch(context.Background(), "CVE-2026-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.CVSSVersion != "4.0" || res.CVSSScore == nil || *res.CVSSScore != 9.3 {
|
|
t.Errorf("expected v4.0/9.3, got %s/%v", res.CVSSVersion, res.CVSSScore)
|
|
}
|
|
}
|
|
|
|
func TestNVDV2SeverityFromMetricLevel(t *testing.T) {
|
|
body := `{"totalResults":1,"vulnerabilities":[{"cve":{
|
|
"id":"CVE-2005-1","descriptions":[{"lang":"en","value":"d"}],
|
|
"metrics":{"cvssMetricV2":[{"baseSeverity":"HIGH","cvssData":{"version":"2.0","baseScore":7.5,"vectorString":"AV:N"}}]}
|
|
}}]}`
|
|
srv := jsonServer(t, body)
|
|
c := NewNVDClient(srv.Client(), srv.URL, "")
|
|
res, err := c.Fetch(context.Background(), "CVE-2005-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.CVSSVersion != "2.0" || res.CVSSSeverity != "HIGH" {
|
|
t.Errorf("v2 severity should fall back to metric level, got %s/%q", res.CVSSVersion, res.CVSSSeverity)
|
|
}
|
|
}
|
|
|
|
func TestKEVParsesSample(t *testing.T) {
|
|
srv := fileServer(t, "../../testdata/kev/kev-sample.json")
|
|
c := NewKEVClient(srv.Client(), srv.URL)
|
|
catalog, err := c.Fetch(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Fetch: %v", err)
|
|
}
|
|
log4, ok := catalog.Lookup("CVE-2021-44228")
|
|
if !ok {
|
|
t.Fatal("Log4Shell should be in KEV")
|
|
}
|
|
if !log4.Ransomware {
|
|
t.Error("Log4Shell knownRansomwareCampaignUse=Known should map to true")
|
|
}
|
|
unknown, ok := catalog.Lookup("CVE-2026-45659")
|
|
if !ok || unknown.Ransomware {
|
|
t.Error("Unknown ransomware entry should map to false")
|
|
}
|
|
if _, ok := catalog.Lookup("CVE-1999-0001"); ok {
|
|
t.Error("absent CVE should not be a KEV member")
|
|
}
|
|
}
|
|
|
|
func TestEPSSParsesQuotedStrings(t *testing.T) {
|
|
srv := fileServer(t, "../../testdata/epss/CVE-2021-44228.json")
|
|
c := NewEPSSClient(srv.Client(), srv.URL)
|
|
scores, err := c.Fetch(context.Background(), []string{"CVE-2021-44228"})
|
|
if err != nil {
|
|
t.Fatalf("Fetch: %v", err)
|
|
}
|
|
s, ok := scores["CVE-2021-44228"]
|
|
if !ok {
|
|
t.Fatal("expected a score for Log4Shell")
|
|
}
|
|
if math.Abs(s.EPSS-0.99999) > 1e-5 {
|
|
t.Errorf("epss = %v, want ~0.99999 (string must be ParseFloat'd, not zeroed)", s.EPSS)
|
|
}
|
|
if math.Abs(s.Percentile-1.0) > 1e-9 {
|
|
t.Errorf("percentile = %v, want 1.0", s.Percentile)
|
|
}
|
|
}
|
|
|
|
func TestChunk(t *testing.T) {
|
|
got := chunk([]string{"a", "b", "c", "d", "e"}, 2)
|
|
want := [][]string{{"a", "b"}, {"c", "d"}, {"e"}}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("chunk = %v, want %v", got, want)
|
|
}
|
|
if chunk(nil, 2) != nil {
|
|
t.Error("chunk(nil) should be nil")
|
|
}
|
|
}
|
|
|
|
func TestNVDRetriesOn503(t *testing.T) {
|
|
var calls int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls++
|
|
if calls == 1 {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"totalResults":0,"vulnerabilities":[]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
c := NewNVDClient(srv.Client(), srv.URL, "")
|
|
c.limiter = rate.NewLimiter(rate.Inf, 1)
|
|
c.backoffBase = time.Millisecond
|
|
if _, err := c.Fetch(context.Background(), "CVE-2026-1"); err != nil {
|
|
t.Fatalf("Fetch after retry: %v", err)
|
|
}
|
|
if calls != 2 {
|
|
t.Errorf("calls = %d, want 2 (one 503, one 200)", calls)
|
|
}
|
|
}
|