diff options
-rw-r--r-- | internal/feed/cveid.go | 112 | ||||
-rw-r--r-- | internal/feed/cveid_test.go | 295 | ||||
-rw-r--r-- | internal/feed/feed.go | 2 |
3 files changed, 408 insertions, 1 deletions
diff --git a/internal/feed/cveid.go b/internal/feed/cveid.go new file mode 100644 index 0000000..8796029 --- /dev/null +++ b/internal/feed/cveid.go @@ -0,0 +1,112 @@ +package feed + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" +) + +// CVE ID +type CveId uint32 + +var cveIdRe = regexp.MustCompile("\\ACVE-(\\d{4})-(\\d{1,8})\\z") + +// parse year component of CVE ID +func parseCveIdYear(s string) (uint16, error) { + // parse year, check for error + year, err := strconv.ParseUint(s, 10, 16) + if err != nil { + return 0, err + } + + // check bounds + if year < 2000 || year > 2127 { + return 0, fmt.Errorf("year out of bounds: %s", s) + } + + // return value + return uint16(year), nil +} + +// parse number component of CVE ID +func parseCveIdNum(s string) (uint32, error) { + // parse number, check for error + num, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return 0, err + } + + // check bounds + if num > 0x01ffffff { + return 0, fmt.Errorf("number out of bounds: %d", num) + } + + // return value + return uint32(num), nil +} + +// Encode CVE ID as uint32. +func encodeCveId(year uint16, num uint32) uint32 { + return uint32((uint32((year - 2000) & 0x7f) << 25) | (num & 0x01ffffff)) +} + +// Create CVE ID from string. +func NewCveId(s string) (CveId, error) { + // match components, check for error + md := cveIdRe.FindStringSubmatch(s) + if len(md) != 3 { + return CveId(0), fmt.Errorf("invalid CVE ID: %s", s) + } + + // parse year, check for error + year, err := parseCveIdYear(md[1]) + if err != nil { + return CveId(0), err + } + + // parse number, check for error + num, err := parseCveIdNum(md[2]) + if err != nil { + return CveId(0), err + } + + // encode and return result + return CveId(encodeCveId(year, num)), nil +} + +// Unmarshal CVE ID from JSON. +func (me *CveId) UnmarshalJSON(b []byte) error { + // decode string, check for error + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + // parse year, check for error + r, err := NewCveId(s) + if err != nil { + return err + } + + // serialize ID + *me = r + + // return success + return nil +} + +// Get year component. +func (me CveId) Year() uint16 { + return uint16((uint32(me) >> 25) & 0x7f) + 2000 +} + +// Get number component. +func (me CveId) Number() uint32 { + return (uint32(me) & 0x01ffffff) +} + +// Return string representation of CVE ID. +func (me CveId) String() string { + return fmt.Sprintf("CVE-%04d-%04d", me.Year(), me.Number()) +} diff --git a/internal/feed/cveid_test.go b/internal/feed/cveid_test.go new file mode 100644 index 0000000..8df3642 --- /dev/null +++ b/internal/feed/cveid_test.go @@ -0,0 +1,295 @@ +package feed + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" +) + +func TestParseCveIdYear(t *testing.T) { + if got, err := parseCveIdYear("asdf"); err == nil { + t.Errorf("got %d, exp error", got) + return + } + + goodTests := []struct { + val string + exp uint16 + } { + { "2000", 2000 }, + { "2001", 2001 }, + { "2100", 2100 }, + } + + for _, test := range(goodTests) { + t.Run(test.val, func(t *testing.T) { + got, err := parseCveIdYear(test.val) + if err != nil { + t.Error(err) + return + } + + if got != test.exp { + t.Errorf("got %d, exp %d", got, test.exp) + return + } + }) + } + + badTests := []struct { + val string + exp string + } { + { "0000", "year out of bounds: 0000" }, + { "0001", "year out of bounds: 0001" }, + { "1999", "year out of bounds: 1999" }, + { "2128", "year out of bounds: 2128" }, + { "9999", "year out of bounds: 9999" }, + } + + for _, test := range(badTests) { + t.Run(test.val, func(t *testing.T) { + if got, err := parseCveIdYear(test.val); err == nil { + t.Errorf("got %d, exp error", got) + return + } else if err.Error() != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp) + } + }) + } +} + +func TestParseCveIdNum(t *testing.T) { + if got, err := parseCveIdNum("asdf"); err == nil { + t.Errorf("got %d, exp error", got) + return + } + + goodTests := []struct { + val string + exp uint32 + } { + { "0", 0 }, + { "0001", 1 }, + { "2100", 2100 }, + { "999999", 999999 }, + { "33554431", 33554431 }, + } + + for _, test := range(goodTests) { + t.Run(test.val, func(t *testing.T) { + got, err := parseCveIdNum(test.val) + if err != nil { + t.Error(err) + return + } + + if got != test.exp { + t.Errorf("got %d, exp %d", got, test.exp) + return + } + }) + } + + badTests := []struct { + val string + exp string + } { + { "33554432", "number out of bounds: 33554432" }, + { "99999999", "number out of bounds: 99999999" }, + } + + for _, test := range(badTests) { + t.Run(test.val, func(t *testing.T) { + if got, err := parseCveIdNum(test.val); err == nil { + t.Errorf("got %d, exp error", got) + } else if err.Error() != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp) + } + }) + } +} + +func TestNewCveId(t *testing.T) { + badMatchTests := []string { + "", + "\nCVE-2002-1234", + "CVE-2002-1234\n", + "CVE20021234\n", + "asdf", + } + + for _, test := range(badMatchTests) { + t.Run(test, func(t *testing.T) { + exp := fmt.Sprintf("invalid CVE ID: %s", test) + if got, err := NewCveId(test); err == nil { + t.Errorf("got %s, exp error", got) + } else if err.Error() != exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), exp) + } + }) + } + + badYearTests := []struct { + val string + exp string + } { + { "CVE-0000-1234", "year out of bounds: 0000" }, + { "CVE-1999-1234", "year out of bounds: 1999" }, + { "CVE-2128-1234", "year out of bounds: 2128" }, + { "CVE-9999-1234", "year out of bounds: 9999" }, + } + + for _, test := range(badYearTests) { + t.Run(test.val, func(t *testing.T) { + if got, err := NewCveId(test.val); err == nil { + t.Errorf("got %s, exp error", got) + } else if err.Error() != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp) + } + }) + } + + badNumTests := []struct { + val string + exp string + } { + { "CVE-2000-33554432", "number out of bounds: 33554432" }, + { "CVE-2000-99999999", "number out of bounds: 99999999" }, + } + + for _, test := range(badNumTests) { + t.Run(test.val, func(t *testing.T) { + if got, err := NewCveId(test.val); err == nil { + t.Errorf("got %s, exp error", got) + } else if err.Error() != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp) + } + }) + } + + goodTests := []string { + "CVE-2000-0", + "CVE-2127-0", + "CVE-2000-33554431", + "CVE-2127-33554431", + } + + for _, val := range(goodTests) { + t.Run(val, func(t *testing.T) { + if _, err := NewCveId(val); err != nil { + t.Error(err) + } + }) + } +} +func TestCveIdYear(t *testing.T) { + for year := 2000; year < 2127; year++ { + t.Run(strconv.FormatInt(int64(year), 10), func(t *testing.T) { + // expected value + exp := uint16(year) + + // build cve id, check for error + id, err := NewCveId(fmt.Sprintf("CVE-%04d-0000", year)) + if err != nil { + t.Error(err) + return + } + + // check year + got := id.Year() + if got != exp { + t.Errorf("got %d, exp %d", got, exp) + } + }) + } +} + +func TestCveIdNumber(t *testing.T) { + for num := 0; num < 99999; num++ { + t.Run(strconv.FormatInt(int64(num), 10), func(t *testing.T) { + // expected value + exp := uint32(num) + + // build cve id, check for error + id, err := NewCveId(fmt.Sprintf("CVE-2000-%04d", num)) + if err != nil { + t.Error(err) + return + } + + // check number + got := id.Number() + if got != exp { + t.Errorf("got %d, exp %d", got, exp) + } + }) + } +} + +func TestCveIdUnmarshalInvalidData(t *testing.T) { + test := []byte(`{}`) + var val CveId + + if err := json.Unmarshal(test, &val); err == nil { + t.Errorf("got \"%s\", exp error", val) + } +} + +func TestCveIdUnmarshalUnknown(t *testing.T) { + test := []byte(`"foo"`) + exp := "invalid CVE ID: foo" + var val CveId + + err := json.Unmarshal(test, &val) + if err == nil { + t.Errorf("got \"%s\", exp error", val) + return + } + + if err.Error() != exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), exp) + } +} + +func TestCveIdUnmarshalValid(t *testing.T) { + tests := []struct { + val string + expYear uint16 + expNum uint32 + exp string + } { + { "\"CVE-2000-0\"", 2000, 0, "CVE-2000-0000" }, + { "\"CVE-2000-1234\"", 2000, 1234, "CVE-2000-1234" }, + { "\"CVE-2000-33554431\"", 2000, 33554431, "CVE-2000-33554431" }, + { "\"CVE-2127-0\"", 2127, 0, "CVE-2127-0000" }, + { "\"CVE-2127-1234\"", 2127, 1234, "CVE-2127-1234" }, + { "\"CVE-2127-33554431\"", 2127, 33554431, "CVE-2127-33554431" }, + } + + for _, test := range(tests) { + t.Run(test.val, func(t *testing.T) { + var got CveId + if err := json.Unmarshal([]byte(test.val), &got); err != nil { + t.Error(err) + return + } + + // check year + if got.Year() != test.expYear { + t.Errorf("got \"%d\", exp \"%d\"", got.Year(), test.expYear) + } + + // check year + if got.Number() != test.expNum { + t.Errorf("got \"%d\", exp \"%d\"", got.Number(), test.expNum) + } + + // check string + if got.String() != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", got.String(), test.exp) + } + }) + } +} diff --git a/internal/feed/feed.go b/internal/feed/feed.go index 3a6e695..bd66ed3 100644 --- a/internal/feed/feed.go +++ b/internal/feed/feed.go @@ -5,7 +5,7 @@ package feed // CVE metadata type CveMetadata struct { // CVE ID - Id string `json:"ID"` + Id CveId `json:"ID"` // CVE assigner email address Assigner string `json:"ASSIGNER"` |