aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/feed/cveid.go112
-rw-r--r--internal/feed/cveid_test.go295
-rw-r--r--internal/feed/feed.go2
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"`