aboutsummaryrefslogtreecommitdiff
path: root/feed
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2022-02-04 00:35:31 -0500
committerPaul Duncan <pabs@pablotron.org>2022-02-04 00:35:31 -0500
commit9c17b97cd0f83be3fff9fa4e87fd1d29052ea616 (patch)
tree0d97030a0d0c3ad983be281ce89f80571338887f /feed
parent92400d731546557d110c9c3cc3906d700f83dda8 (diff)
downloadcvez-9c17b97cd0f83be3fff9fa4e87fd1d29052ea616.tar.bz2
cvez-9c17b97cd0f83be3fff9fa4e87fd1d29052ea616.zip
rename to github.com/pablotron/cvez, remove internal libs
Diffstat (limited to 'feed')
-rw-r--r--feed/cveid.go112
-rw-r--r--feed/cveid_test.go295
-rw-r--r--feed/dataformat.go36
-rw-r--r--feed/dataformat_string.go23
-rw-r--r--feed/dataformat_test.go65
-rw-r--r--feed/datatype.go36
-rw-r--r--feed/datatype_string.go23
-rw-r--r--feed/datatype_test.go65
-rw-r--r--feed/dataversion.go36
-rw-r--r--feed/dataversion_string.go23
-rw-r--r--feed/dataversion_test.go65
-rw-r--r--feed/feed.go255
-rw-r--r--feed/feed_test.go56
-rw-r--r--feed/meta.go106
-rw-r--r--feed/meta_test.go149
-rw-r--r--feed/nodeop.go39
-rw-r--r--feed/nodeop_string.go24
-rw-r--r--feed/nodeop_test.go77
-rw-r--r--feed/score.go34
-rw-r--r--feed/score_test.go92
-rw-r--r--feed/severity.go47
-rw-r--r--feed/severity_string.go27
-rw-r--r--feed/severity_test.go83
-rw-r--r--feed/testdata/nvdcve-1.1-2002.json.gzbin0 -> 1453835 bytes
-rw-r--r--feed/testdata/nvdcve-1.1-2003.json.gzbin0 -> 434020 bytes
-rw-r--r--feed/testdata/nvdcve-1.1-2021.json.gzbin0 -> 4852632 bytes
-rw-r--r--feed/testdata/nvdcve-1.1-modified.json.gzbin0 -> 288574 bytes
-rw-r--r--feed/time.go44
-rw-r--r--feed/time_test.go66
-rw-r--r--feed/v2accesscomplexity.go42
-rw-r--r--feed/v2accesscomplexity_string.go25
-rw-r--r--feed/v2accesscomplexity_test.go79
-rw-r--r--feed/v2accessvector.go42
-rw-r--r--feed/v2accessvector_string.go25
-rw-r--r--feed/v2accessvector_test.go79
-rw-r--r--feed/v2authentication.go39
-rw-r--r--feed/v2authentication_string.go24
-rw-r--r--feed/v2authentication_test.go77
-rw-r--r--feed/v2impact.go42
-rw-r--r--feed/v2impact_string.go25
-rw-r--r--feed/v2impact_test.go79
-rw-r--r--feed/v2version.go36
-rw-r--r--feed/v2version_string.go23
-rw-r--r--feed/v2version_test.go75
-rw-r--r--feed/v3attackcomplexity.go42
-rw-r--r--feed/v3attackcomplexity_string.go25
-rw-r--r--feed/v3attackcomplexity_test.go79
-rw-r--r--feed/v3attackvector.go46
-rw-r--r--feed/v3attackvector_string.go26
-rw-r--r--feed/v3attackvector_test.go81
-rw-r--r--feed/v3impact.go42
-rw-r--r--feed/v3impact_string.go25
-rw-r--r--feed/v3impact_test.go79
-rw-r--r--feed/v3privilegesrequired.go45
-rw-r--r--feed/v3privilegesrequired_string.go26
-rw-r--r--feed/v3privilegesrequired_test.go81
-rw-r--r--feed/v3scope.go39
-rw-r--r--feed/v3scope_string.go24
-rw-r--r--feed/v3scope_test.go77
-rw-r--r--feed/v3userinteraction.go40
-rw-r--r--feed/v3userinteraction_string.go24
-rw-r--r--feed/v3userinteraction_test.go77
-rw-r--r--feed/v3version.go36
-rw-r--r--feed/v3version_string.go23
-rw-r--r--feed/v3version_test.go75
-rw-r--r--feed/vector.go39
-rw-r--r--feed/vector_test.go99
67 files changed, 3740 insertions, 0 deletions
diff --git a/feed/cveid.go b/feed/cveid.go
new file mode 100644
index 0000000..8796029
--- /dev/null
+++ b/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/feed/cveid_test.go b/feed/cveid_test.go
new file mode 100644
index 0000000..8df3642
--- /dev/null
+++ b/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/feed/dataformat.go b/feed/dataformat.go
new file mode 100644
index 0000000..bb3f8f8
--- /dev/null
+++ b/feed/dataformat.go
@@ -0,0 +1,36 @@
+package feed
+
+//go:generate stringer -linecomment -type=DataFormat
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// Data format for NVD feeds and feed items.
+type DataFormat byte
+
+const (
+ MitreFormat DataFormat = iota // MITRE
+)
+
+// Unmarshal DataFormat from JSON.
+func (me *DataFormat) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "MITRE":
+ *me = MitreFormat
+ default:
+ // return error
+ return fmt.Errorf("unknown data format: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/dataformat_string.go b/feed/dataformat_string.go
new file mode 100644
index 0000000..4b755f4
--- /dev/null
+++ b/feed/dataformat_string.go
@@ -0,0 +1,23 @@
+// Code generated by "stringer -linecomment -type=DataFormat"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[MitreFormat-0]
+}
+
+const _DataFormat_name = "MITRE"
+
+var _DataFormat_index = [...]uint8{0, 5}
+
+func (i DataFormat) String() string {
+ if i >= DataFormat(len(_DataFormat_index)-1) {
+ return "DataFormat(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _DataFormat_name[_DataFormat_index[i]:_DataFormat_index[i+1]]
+}
diff --git a/feed/dataformat_test.go b/feed/dataformat_test.go
new file mode 100644
index 0000000..efb4986
--- /dev/null
+++ b/feed/dataformat_test.go
@@ -0,0 +1,65 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestDataFormatUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val DataFormat
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestDataFormatUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown data format: foo"
+ var val DataFormat
+
+ 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 TestDataFormatUnmarshalValid(t *testing.T) {
+ test := []byte(`"MITRE"`)
+ exp := MitreFormat
+ var got DataFormat
+
+ if err := json.Unmarshal(test, &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, exp)
+ }
+}
+
+func TestDataFormatString(t *testing.T) {
+ tests := []struct {
+ val DataFormat
+ exp string
+ } {
+ { MitreFormat, "MITRE" },
+ { DataFormat(255), "DataFormat(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val.String(), func(t *testing.T) {
+ got := test.val.String()
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/datatype.go b/feed/datatype.go
new file mode 100644
index 0000000..6eaa145
--- /dev/null
+++ b/feed/datatype.go
@@ -0,0 +1,36 @@
+package feed
+
+//go:generate stringer -linecomment -type=DataType
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// Data type for NVD feeds and feed items.
+type DataType byte
+
+const (
+ CveType DataType = iota // CVE
+)
+
+// Unmarshal DataType from JSON.
+func (me *DataType) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "CVE":
+ *me = CveType
+ default:
+ // return error
+ return fmt.Errorf("unknown data type: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/datatype_string.go b/feed/datatype_string.go
new file mode 100644
index 0000000..f126add
--- /dev/null
+++ b/feed/datatype_string.go
@@ -0,0 +1,23 @@
+// Code generated by "stringer -linecomment -type=DataType"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[CveType-0]
+}
+
+const _DataType_name = "CVE"
+
+var _DataType_index = [...]uint8{0, 3}
+
+func (i DataType) String() string {
+ if i >= DataType(len(_DataType_index)-1) {
+ return "DataType(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _DataType_name[_DataType_index[i]:_DataType_index[i+1]]
+}
diff --git a/feed/datatype_test.go b/feed/datatype_test.go
new file mode 100644
index 0000000..05f6a74
--- /dev/null
+++ b/feed/datatype_test.go
@@ -0,0 +1,65 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestDataTypeUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val DataType
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestDataTypeUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown data type: foo"
+ var val DataType
+
+ 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 TestDataTypeUnmarshalValid(t *testing.T) {
+ test := []byte(`"CVE"`)
+ exp := CveType
+ var got DataType
+
+ if err := json.Unmarshal(test, &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, exp)
+ }
+}
+
+func TestDataTypeString(t *testing.T) {
+ tests := []struct {
+ val DataType
+ exp string
+ } {
+ { CveType, "CVE" },
+ { DataType(255), "DataType(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val.String(), func(t *testing.T) {
+ got := test.val.String()
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/dataversion.go b/feed/dataversion.go
new file mode 100644
index 0000000..c6f1b8d
--- /dev/null
+++ b/feed/dataversion.go
@@ -0,0 +1,36 @@
+package feed
+
+//go:generate stringer -linecomment -type=DataVersion
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// Data version for NVD feeds and feed items.
+type DataVersion byte
+
+const (
+ V40 DataVersion = iota // 4.0
+)
+
+// Unmarshal data version from JSON.
+func (me *DataVersion) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "4.0":
+ *me = V40
+ default:
+ // return error
+ return fmt.Errorf("unknown data version: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/dataversion_string.go b/feed/dataversion_string.go
new file mode 100644
index 0000000..26a0fdb
--- /dev/null
+++ b/feed/dataversion_string.go
@@ -0,0 +1,23 @@
+// Code generated by "stringer -linecomment -type=DataVersion"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V40-0]
+}
+
+const _DataVersion_name = "4.0"
+
+var _DataVersion_index = [...]uint8{0, 3}
+
+func (i DataVersion) String() string {
+ if i >= DataVersion(len(_DataVersion_index)-1) {
+ return "DataVersion(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _DataVersion_name[_DataVersion_index[i]:_DataVersion_index[i+1]]
+}
diff --git a/feed/dataversion_test.go b/feed/dataversion_test.go
new file mode 100644
index 0000000..6bf683c
--- /dev/null
+++ b/feed/dataversion_test.go
@@ -0,0 +1,65 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestDataVersionUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val DataVersion
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestDataVersionUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown data version: foo"
+ var val DataVersion
+
+ 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 TestDataVersionUnmarshalValid(t *testing.T) {
+ test := []byte(`"4.0"`)
+ exp := V40
+ var got DataVersion
+
+ if err := json.Unmarshal(test, &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, exp)
+ }
+}
+
+func TestDataVersionString(t *testing.T) {
+ tests := []struct {
+ val DataVersion
+ exp string
+ } {
+ { V40, "4.0" },
+ { DataVersion(255), "DataVersion(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val.String(), func(t *testing.T) {
+ got := test.val.String()
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/feed.go b/feed/feed.go
new file mode 100644
index 0000000..bdf260c
--- /dev/null
+++ b/feed/feed.go
@@ -0,0 +1,255 @@
+// NVD JSON feed parser.
+package feed
+
+// import "nvd/internal/cvss"
+
+// TODO: parse cpe
+
+// CVE metadata
+type CveMetadata struct {
+ // CVE ID
+ Id CveId `json:"ID"`
+
+ // CVE assigner email address
+ Assigner string `json:"ASSIGNER"`
+}
+
+// CVE description string.
+type Description struct {
+ // Language code
+ Lang string `json:"lang"`
+
+ // String value
+ Value string `json:"value"`
+}
+
+// CVE problem type
+type CveProblemType struct {
+ // problem type descriptions
+ Descriptions []Description `json:"description"`
+}
+
+// Slice of CVE problem types.
+type CveProblemTypes struct {
+ // problem types
+ ProblemTypes []CveProblemType `json:"problemtype_data"`
+}
+
+// CVE reference
+type CveReference struct {
+ // reference URL
+ Url string `json:"url"`
+
+ // reference name
+ Name string `json:"name"`
+
+ // reference source
+ RefSource string `json:"refsource"`
+
+ // tags
+ Tags []string `json:"tags"`
+}
+
+// Slice of CVE references
+type CveReferences struct {
+ References []CveReference `json:"reference_data"`
+}
+
+// CVE item descriptions
+type CveDescription struct {
+ // slice of descriptions
+ Descriptions []Description `json:"description_data"`
+}
+
+// CVE data
+type Cve struct {
+ // feed data type
+ DataType DataType `json:"CVE_data_type"`
+
+ // feed data format
+ DataFormat DataFormat `json:"CVE_data_format"`
+
+ // feed data format version
+ DataVersion DataVersion `json:"CVE_data_version"`
+
+ // CVE metadata
+ Metadata CveMetadata `json:"CVE_data_meta"`
+
+ // CVE problem types
+ ProblemTypes CveProblemTypes `json:"problemtype"`
+
+ // CVE references
+ References CveReferences `json:"references"`
+
+ // CVE description
+ Description CveDescription `json:"description"`
+}
+
+// CPE match
+type CpeMatch struct {
+ // Vulnerable?
+ Vulnerable bool `json:"vulnerable"`
+
+ VersionEndExcluding string `json:"versionEndExcluding"`
+
+ // CPE URI (FIXME: decode this)
+ Cpe23Uri string `json:"cpe23Uri"`
+
+ // CPE names (not sure if this is correct)
+ Names []string `json:"cpe_name"`
+}
+
+// CVE item configuration node
+type ConfigurationNode struct {
+ // node operator
+ Operator NodeOp `json:"operator"`
+
+ // node children
+ Children []ConfigurationNode `json:"children"`
+
+ CpeMatches []CpeMatch `json:"cpe_match"`
+}
+
+// CVE item configurations
+type ItemConfigurations struct {
+ // data version
+ DataVersion DataVersion `json:"CVE_data_version"`
+
+ // slice of configuration nodes
+ Nodes []ConfigurationNode `json:"nodes"`
+}
+
+// CVSS V3
+type CvssV3 struct {
+ // CVSS V3 version
+ Version V3Version `json:"version"`
+
+ // CVSS V3 vector string
+ // VectorString string `json:"vectorString"`
+
+ // CVSS vector
+ Vector Vector `json:"vectorString"`
+
+ // attack vector
+ AttackVector V3AttackVector `json:"attackVector"`
+
+ // attack complexity
+ AttackComplexity V3AttackComplexity `json:"attackComplexity"`
+
+ // privileges required
+ PrivilegesRequired V3PrivilegesRequired `json:"privilegesRequired"`
+
+ // user interaction
+ UserInteraction V3UserInteraction `json:"userInteraction"`
+
+ // scope
+ Scope V3Scope `json:"scope"`
+
+ // integrity impact
+ IntegrityImpact V3Impact `json:"integrityImpact"`
+
+ // availability impact
+ AvailabilityImpact V3Impact `json:"availabilityImpact"`
+
+ // base score
+ BaseScore Score `json:"baseScore"`
+
+ // base severity
+ BaseSeverity Severity `json:"baseSeverity"`
+}
+
+// CVSS V3 base metrics
+type BaseMetricV3 struct {
+ CvssV3 CvssV3 `json:"cvssV3"`
+ ExploitabilityScore Score `json:"exploitabilityScore"`
+ ImpactScore Score `json:"impactScore"`
+}
+
+// CVSS V2
+type CvssV2 struct {
+ // CVSS V2 version
+ Version V2Version `json:"version"`
+
+ // CVSS vector string
+ // VectorString string `json:"vectorString"`
+
+ // CVSS vector
+ Vector Vector `json:"vectorString"`
+
+ // attack vector
+ AccessVector V2AccessVector `json:"accessVector"`
+
+ // attack complexity
+ AccessComplexity V2AccessComplexity `json:"accessComplexity"`
+
+ // authentication
+ Authentication V2Authentication `json:"authentication"`
+
+ ConfidentialityImpact V2Impact `json:"confidentialityImpact"`
+ IntegrityImpact V2Impact `json:"integrityImpact"`
+ AvailabilityImpact V2Impact `json:"availabilityImpact"`
+
+ // base score
+ BaseScore Score `json:"baseScore"`
+}
+
+// CVSS V2 base metrics
+type BaseMetricV2 struct {
+ CvssV2 CvssV2 `json:"cvssV2"`
+ Severity Severity `json:"severity"`
+ ExploitabilityScore Score `json:"exploitabilityScore"`
+ ImpactScore Score `json:"impactScore"`
+ InsufficientInfo bool `json:"acInsufInfo"`
+ ObtainAllPrivilege bool `json:"obtainAllPrivilege"`
+ ObtainUserPrivilege bool `json:"obtainUserPrivilege"`
+ ObtainOtherPrivilege bool `json:"obtainOtherPrivilege"`
+ UserInteractionRequired bool `json:"userInteractionRequired"`
+}
+
+// Item impact
+type Impact struct {
+ // CVSS V3 base metrics
+ BaseMetricV3 BaseMetricV3 `json:"baseMetricV3"`
+
+ // CVSS V2 base metrics
+ BaseMetricV2 BaseMetricV2 `json:"baseMetricV2"`
+}
+
+// CVE feed item
+type Item struct {
+ // item CVE data
+ Cve Cve `json:"cve"`
+
+ // item configuration
+ Configurations ItemConfigurations `json:"configurations"`
+
+ // item impact
+ Impact Impact `json:"impact"`
+
+ // item published date
+ PublishedDate Time `json:"publishedDate"`
+
+ // last modification date
+ LastModifiedDate Time `json:"lastModifiedDate"`
+}
+
+// NVD feed
+type Feed struct {
+ // feed data type
+ DataType DataType `json:"CVE_data_type"`
+
+ // feed data format
+ DataFormat DataFormat `json:"CVE_data_format"`
+
+ // feed data format version
+ DataVersion DataVersion `json:"CVE_data_version"`
+
+ // number of CVEs in feed
+ NumCVEs uint64 `json:"CVE_data_numberOfCVEs,string"`
+
+ // data timestamp
+ Timestamp Time `json:"CVE_data_timestamp"`
+
+ // CVE items
+ Items []Item `json:"CVE_Items"`
+}
diff --git a/feed/feed_test.go b/feed/feed_test.go
new file mode 100644
index 0000000..f31a3ae
--- /dev/null
+++ b/feed/feed_test.go
@@ -0,0 +1,56 @@
+package feed
+
+import (
+ "compress/gzip"
+ "encoding/json"
+ "io"
+ // "fmt"
+ "os"
+ "testing"
+)
+
+func openTest(path string) (io.Reader, error) {
+ // open file for reading
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ // wrap in reader, return success
+ return gzip.NewReader(file)
+}
+
+// Test feed parser
+func TestFeedParser(t *testing.T) {
+ t.Run("TestUnmarshalJSON", func(t *testing.T) {
+ var f Feed
+
+ // read test data, check for error
+ src, err := openTest("testdata/nvdcve-1.1-2021.json.gz")
+ if err != nil {
+ t.Error(err)
+ }
+
+ // decode cve feed, check for error
+ d := json.NewDecoder(src)
+ if err := d.Decode(&f); err != nil {
+ t.Error(err)
+ }
+ })
+// var f Feed
+//
+// // decode cve feed
+// d := json.NewDecoder(os.Stdin)
+// if err := d.Decode(&f); err != nil {
+// t.Error(err)
+// }
+//
+// var dst bytes.Buffer
+//
+// // create json encoder
+// e := json.NewEncoder(&dst)
+// if err := e.Encode(f); err != nil {
+// t.Error(err)
+// }
+}
+
diff --git a/feed/meta.go b/feed/meta.go
new file mode 100644
index 0000000..fd46025
--- /dev/null
+++ b/feed/meta.go
@@ -0,0 +1,106 @@
+package feed
+
+import (
+ "bufio"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// NVD metadata.
+type Meta struct {
+ LastModifiedDate time.Time // last modified time
+ Size uint64 // uncompressed size, in bytes
+ ZipSize uint64 // zip file size, in bytes
+ GzSize uint64 // gz file size, in bytes
+ Sha256 [32]byte // sha256 hash of uncompressed data
+}
+
+func parseMetaSize(name, val string) (uint64, error) {
+ // parse value, check for error
+ v, err := strconv.ParseUint(val, 10, 64)
+ if err == nil {
+ // return size
+ return v, nil
+ } else {
+ // return error
+ return 0, fmt.Errorf("invalid %s: \"%s\"", name, val)
+ }
+}
+
+// Unmarshal new Metadata from reader.
+func NewMeta(r io.Reader) (*Meta, error) {
+ // declare result
+ var m Meta
+
+ // create scanner
+ scanner := bufio.NewScanner(r)
+
+ // read lines
+ for scanner.Scan() {
+ // split into key/value pair, check for error
+ pair := strings.SplitN(scanner.Text(), ":", 2)
+ if len(pair) != 2 {
+ return nil, fmt.Errorf("bad meta line: \"%s\"", scanner.Text())
+ }
+
+ switch pair[0] {
+ case "lastModifiedDate":
+ // parse time, check for error
+ if err := m.LastModifiedDate.UnmarshalText([]byte(pair[1])); err != nil {
+ return nil, err
+ }
+ case "size":
+ if v, err := parseMetaSize("size", pair[1]); err == nil {
+ m.Size = v
+ } else {
+ return nil, err
+ }
+ case "zipSize":
+ if v, err := parseMetaSize("zipSize", pair[1]); err == nil {
+ m.ZipSize = v
+ } else {
+ return nil, err
+ }
+ case "gzSize":
+ if v, err := parseMetaSize("gzSize", pair[1]); err == nil {
+ m.GzSize = v
+ } else {
+ return nil, err
+ }
+ case "sha256":
+ // check hash length
+ if len(pair[1]) != 64 {
+ return nil, fmt.Errorf("invalid sha256 hash length: %d", len(pair[1]))
+ }
+
+ // decode hex, check for error
+ buf, err := hex.DecodeString(pair[1])
+ if err != nil {
+ return nil, fmt.Errorf("invalid sha256 hash: %v", err)
+ }
+
+ // save to buffer, check for error
+ len := copy(m.Sha256[:], buf[0:32])
+ if len != 32 {
+ // difficult to test, but this basically doesn't happen, see here:
+ // https://github.com/golang/go/blob/2ebe77a2fda1ee9ff6fd9a3e08933ad1ebaea039/src/runtime/slice.go#L247
+ return nil, fmt.Errorf("invalid copy length: %d", len)
+ }
+ default:
+ // return error
+ return nil, fmt.Errorf("unknown key: \"%s\"", pair[0])
+ }
+ }
+
+ // check for scanner error
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ // return success
+ return &m, nil
+}
diff --git a/feed/meta_test.go b/feed/meta_test.go
new file mode 100644
index 0000000..3ea5acb
--- /dev/null
+++ b/feed/meta_test.go
@@ -0,0 +1,149 @@
+package feed
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "testing"
+)
+
+func TestParseMetaSize(t *testing.T) {
+ passTests := []struct {
+ val string
+ exp uint64
+ } {
+ { "0", 0 },
+ { "1024", 1024 },
+ { "18446744073709551615", 18446744073709551615 },
+ }
+
+ for _, test := range(passTests) {
+ t.Run(test.val, func(t *testing.T) {
+ got, err := parseMetaSize("foo", test.val)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got %d, exp %d", got, test.exp)
+ return
+ }
+ })
+ }
+
+ failTests := []struct {
+ val string
+ exp string
+ } {
+ { "-1", "invalid foo: \"-1\"" },
+ { "a", "invalid foo: \"a\"" },
+ { "18446744073709551616", "invalid foo: \"18446744073709551616\"" },
+ }
+
+ for _, test := range(failTests) {
+ t.Run(test.val, func(t *testing.T) {
+ got, err := parseMetaSize("foo", test.val)
+ if 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)
+ }
+ })
+ }
+}
+
+// test data
+const testMeta = `lastModifiedDate:2022-01-29T03:01:16-05:00
+size:73202582
+zipSize:3753799
+gzSize:3753663
+sha256:B86258D5D9861507A1894A7B92011764803D7267787B1487539E240EA2405440
+`
+
+// Test meta parser
+func TestNewMeta(t *testing.T) {
+ passTests := []string {
+ `lastModifiedDate:2022-01-29T03:01:16-05:00
+size:73202582
+zipSize:3753799
+gzSize:3753663
+sha256:B86258D5D9861507A1894A7B92011764803D7267787B1487539E240EA2405440
+`,
+ }
+
+ for i, val := range(passTests) {
+ // build test name
+ name := fmt.Sprintf("passTests[%d]", i)
+
+ t.Run(name, func(t *testing.T) {
+ // create buffer
+ buf := bytes.NewBufferString(val)
+
+ // decode meta, check for error
+ _, err := NewMeta(buf)
+ if err != nil {
+ t.Error(err)
+ }
+ })
+ }
+
+ // build 65k token to make scanner fail
+ longVal := make([]byte, 65536)
+ for i := 0; i < cap(longVal); i++ {
+ longVal[i] = 'a'
+ }
+
+ failTests := []struct {
+ val string
+ exp string
+ } {
+ { "asdf", "bad meta line: \"asdf\"" },
+ { "lastModifiedDate:asdf", "parsing time \"asdf\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"asdf\" as \"2006\"" },
+ { "size:a", "invalid size: \"a\"" },
+ { "zipSize:a", "invalid zipSize: \"a\"" },
+ { "gzSize:a", "invalid gzSize: \"a\"" },
+ { "sha256:a", "invalid sha256 hash length: 1" },
+ {
+ val: "sha256:0z00000000000000000000000000000000000000000000000000000000000000",
+ exp: "invalid sha256 hash: encoding/hex: invalid byte: U+007A 'z'",
+ },
+ { string(longVal), "bufio.Scanner: token too long" },
+ { "foo:bar", "unknown key: \"foo\"" },
+ }
+
+ for _, test := range(failTests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create buffer
+ buf := bytes.NewBufferString(test.val)
+
+ // decode meta, check for error
+ got, err := NewMeta(buf)
+ if err == nil {
+ t.Errorf("got %v, exp error", got)
+ } else if err.Error() != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp)
+ }
+ })
+ }
+
+ t.Run("JsonEncode", func(t *testing.T) {
+ // create buffer
+ buf := bytes.NewBufferString(passTests[0])
+
+ // decode meta, check for error
+ meta, err := NewMeta(buf)
+ if err != nil {
+ t.Error(err)
+ }
+
+ // create destination buffer
+ var dst bytes.Buffer
+
+ // create json encoder
+ e := json.NewEncoder(&dst)
+ if err := e.Encode(meta); err != nil {
+ t.Error(err)
+ }
+ })
+}
diff --git a/feed/nodeop.go b/feed/nodeop.go
new file mode 100644
index 0000000..8bfa0a0
--- /dev/null
+++ b/feed/nodeop.go
@@ -0,0 +1,39 @@
+package feed
+
+//go:generate stringer -linecomment -type=NodeOp
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// Node boolean operator.
+type NodeOp byte
+
+const (
+ OrOp NodeOp = iota // OR
+ AndOp // AND
+)
+
+// Unmarshal DataVersion from JSON.
+func (me *NodeOp) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "AND":
+ *me = AndOp
+ case "OR":
+ *me = OrOp
+ default:
+ // return error
+ return fmt.Errorf("unknown operator: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/nodeop_string.go b/feed/nodeop_string.go
new file mode 100644
index 0000000..2c120d4
--- /dev/null
+++ b/feed/nodeop_string.go
@@ -0,0 +1,24 @@
+// Code generated by "stringer -linecomment -type=NodeOp"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[OrOp-0]
+ _ = x[AndOp-1]
+}
+
+const _NodeOp_name = "ORAND"
+
+var _NodeOp_index = [...]uint8{0, 2, 5}
+
+func (i NodeOp) String() string {
+ if i >= NodeOp(len(_NodeOp_index)-1) {
+ return "NodeOp(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _NodeOp_name[_NodeOp_index[i]:_NodeOp_index[i+1]]
+}
diff --git a/feed/nodeop_test.go b/feed/nodeop_test.go
new file mode 100644
index 0000000..dd538e5
--- /dev/null
+++ b/feed/nodeop_test.go
@@ -0,0 +1,77 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestNodeOpUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val NodeOp
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestNodeOpUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown operator: foo"
+ var val NodeOp
+
+ 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 TestNodeOpUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp NodeOp
+ } {
+ { "\"AND\"", AndOp },
+ { "\"OR\"", OrOp },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got NodeOp
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestNodeOpString(t *testing.T) {
+ tests := []struct {
+ val NodeOp
+ exp string
+ } {
+ { AndOp, "AND" },
+ { OrOp, "OR" },
+
+ { NodeOp(255), "NodeOp(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/score.go b/feed/score.go
new file mode 100644
index 0000000..051522f
--- /dev/null
+++ b/feed/score.go
@@ -0,0 +1,34 @@
+package feed
+
+import (
+ "encoding/json"
+ "fmt"
+ "math"
+ "strconv"
+)
+
+// CVSS score
+type Score uint8
+
+// Unmarshal CVSS score from JSON.
+func (me *Score) UnmarshalJSON(b []byte) error {
+ // decode float, check for error
+ var v float64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+
+ // check score
+ if v < 0.0 || v > 10.0 {
+ return fmt.Errorf("CVSS score out of bounds: %2.1f", v)
+ }
+
+ // save result, return success
+ *me = Score(uint8(math.Trunc(10.0 * v)))
+ return nil
+}
+
+func (me Score) String() string {
+ val := float64(me) / 10.0
+ return strconv.FormatFloat(val, 'f', 1, 64)
+}
diff --git a/feed/score_test.go b/feed/score_test.go
new file mode 100644
index 0000000..2baa7ab
--- /dev/null
+++ b/feed/score_test.go
@@ -0,0 +1,92 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestScoreUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val Score
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestScoreUnmarshalInvalidValues(t *testing.T) {
+ tests := []struct {
+ val string
+ exp string
+ } {
+ { `-100.0`, "CVSS score out of bounds: -100.0" },
+ { `-90.0`, "CVSS score out of bounds: -90.0" },
+ { `-9.3`, "CVSS score out of bounds: -9.3" },
+ { `-1`, "CVSS score out of bounds: -1.0" },
+ { `10.1`, "CVSS score out of bounds: 10.1" },
+ { `100.0`, "CVSS score out of bounds: 100.0" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got Score
+
+ if err := json.Unmarshal([]byte(test.val), &got); 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)
+ }
+ })
+ }
+}
+
+func TestScoreUnmarshalValidValues(t *testing.T) {
+ tests := []struct {
+ val string
+ exp uint8
+ } {
+ { `0.0`, 0 },
+ { `0.1`, 1 },
+ { `1.2`, 12 },
+ { `5.9`, 59 },
+ { `9.9`, 99 },
+ { `10.0`, 100 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got Score
+
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ } else if uint8(got) != test.exp {
+ t.Errorf("got \"%d\", exp \"%d\"", uint8(got), test.exp)
+ }
+ })
+ }
+}
+
+func TestScoreString(t *testing.T) {
+ tests := []struct {
+ val uint8
+ exp string
+ } {
+ { 0, "0.0" },
+ { 1, "0.1" },
+ { 9, "0.9" },
+ { 12, "1.2" },
+ { 59, "5.9" },
+ { 99, "9.9" },
+ { 100, "10.0" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := Score(test.val).String()
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/severity.go b/feed/severity.go
new file mode 100644
index 0000000..50969ed
--- /dev/null
+++ b/feed/severity.go
@@ -0,0 +1,47 @@
+package feed
+
+//go:generate stringer -linecomment -type=Severity
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+type Severity byte
+
+const (
+ SeverityNone Severity = iota // NONE
+ SeverityLow // LOW
+ SeverityMedium // MEDIUM
+ SeverityHigh // HIGH
+ SeverityCritical // CRITICAL
+)
+
+// Unmarshal CVSS severity from JSON.
+func (me *Severity) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "NONE":
+ *me = SeverityNone
+ case "LOW":
+ *me = SeverityLow
+ case "MEDIUM":
+ *me = SeverityMedium
+ case "HIGH":
+ *me = SeverityHigh
+ case "CRITICAL":
+ *me = SeverityCritical
+ default:
+ // return error
+ return fmt.Errorf("unknown severity: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/severity_string.go b/feed/severity_string.go
new file mode 100644
index 0000000..28e1e91
--- /dev/null
+++ b/feed/severity_string.go
@@ -0,0 +1,27 @@
+// Code generated by "stringer -linecomment -type=Severity"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[SeverityNone-0]
+ _ = x[SeverityLow-1]
+ _ = x[SeverityMedium-2]
+ _ = x[SeverityHigh-3]
+ _ = x[SeverityCritical-4]
+}
+
+const _Severity_name = "NONELOWMEDIUMHIGHCRITICAL"
+
+var _Severity_index = [...]uint8{0, 4, 7, 13, 17, 25}
+
+func (i Severity) String() string {
+ if i >= Severity(len(_Severity_index)-1) {
+ return "Severity(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _Severity_name[_Severity_index[i]:_Severity_index[i+1]]
+}
diff --git a/feed/severity_test.go b/feed/severity_test.go
new file mode 100644
index 0000000..75aec72
--- /dev/null
+++ b/feed/severity_test.go
@@ -0,0 +1,83 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestSeverityUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val Severity
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestSeverityUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown severity: foo"
+ var val Severity
+
+ 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 TestSeverityUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp Severity
+ } {
+ { "\"NONE\"", SeverityNone },
+ { "\"LOW\"", SeverityLow },
+ { "\"MEDIUM\"", SeverityMedium },
+ { "\"HIGH\"", SeverityHigh },
+ { "\"CRITICAL\"", SeverityCritical },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got Severity
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestSeverityString(t *testing.T) {
+ tests := []struct {
+ val Severity
+ exp string
+ } {
+ { SeverityNone, "NONE" },
+ { SeverityLow, "LOW" },
+ { SeverityMedium, "MEDIUM" },
+ { SeverityHigh, "HIGH" },
+ { SeverityCritical, "CRITICAL" },
+
+ { Severity(255), "Severity(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/testdata/nvdcve-1.1-2002.json.gz b/feed/testdata/nvdcve-1.1-2002.json.gz
new file mode 100644
index 0000000..45e714d
--- /dev/null
+++ b/feed/testdata/nvdcve-1.1-2002.json.gz
Binary files differ
diff --git a/feed/testdata/nvdcve-1.1-2003.json.gz b/feed/testdata/nvdcve-1.1-2003.json.gz
new file mode 100644
index 0000000..c7796a6
--- /dev/null
+++ b/feed/testdata/nvdcve-1.1-2003.json.gz
Binary files differ
diff --git a/feed/testdata/nvdcve-1.1-2021.json.gz b/feed/testdata/nvdcve-1.1-2021.json.gz
new file mode 100644
index 0000000..83ca5e6
--- /dev/null
+++ b/feed/testdata/nvdcve-1.1-2021.json.gz
Binary files differ
diff --git a/feed/testdata/nvdcve-1.1-modified.json.gz b/feed/testdata/nvdcve-1.1-modified.json.gz
new file mode 100644
index 0000000..c675fb6
--- /dev/null
+++ b/feed/testdata/nvdcve-1.1-modified.json.gz
Binary files differ
diff --git a/feed/time.go b/feed/time.go
new file mode 100644
index 0000000..6eb5d37
--- /dev/null
+++ b/feed/time.go
@@ -0,0 +1,44 @@
+package feed
+
+import (
+ "encoding/json"
+ "fmt"
+ // "strconv"
+ "regexp"
+ "time"
+)
+
+// partial timestamp
+type Time time.Time
+
+var timeRe = regexp.MustCompile("\\A\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}Z\\z")
+
+// Unmarshal timestamp from JSON.
+func (me *Time) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // match partial string regex
+ if !timeRe.MatchString(s) {
+ return fmt.Errorf("invalid time: \"%s\"", s)
+ }
+
+ // correct string suffix
+ s = s[0:16] + ":00Z"
+
+ // unmarshal time
+ var t time.Time
+ if err := t.UnmarshalText([]byte(s)); err != nil {
+ return err
+ }
+
+ // save time
+ *me = Time(t)
+
+ // return success
+ return nil
+}
+
diff --git a/feed/time_test.go b/feed/time_test.go
new file mode 100644
index 0000000..cc490c5
--- /dev/null
+++ b/feed/time_test.go
@@ -0,0 +1,66 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+)
+
+func TestTimeUnmarshallInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val Time
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%v\", exp error", val)
+ }
+}
+
+func TestTimeUnmarshallInvalidString(t *testing.T) {
+ test := []byte(`"2020-"`)
+ exp := "invalid time: \"2020-\""
+ var val Time
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%v\", exp error", val)
+ } else if err.Error() != exp {
+ t.Errorf("got \"%s\", exp \"%s\"", err.Error(), exp)
+ }
+}
+
+func TestTimeUnmarshallInvalidTime(t *testing.T) {
+ test := []byte(`"2020-99-99T99:99Z"`)
+ var val Time
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%v\", exp error", val)
+ }
+}
+
+func TestTimeString(t *testing.T) {
+ tests := []struct {
+ val string
+ exp string
+ } {
+ { "\"2021-06-09T20:15Z\"", "2021-06-09T20:15:00Z" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var gotTime Time
+ if err := json.Unmarshal([]byte(test.val), &gotTime); err != nil {
+ t.Error(err)
+ return
+ }
+
+ got, err := time.Time(gotTime).MarshalText()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if string(got) != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", string(got), test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v2accesscomplexity.go b/feed/v2accesscomplexity.go
new file mode 100644
index 0000000..5885e0d
--- /dev/null
+++ b/feed/v2accesscomplexity.go
@@ -0,0 +1,42 @@
+package feed
+
+//go:generate stringer -linecomment -type=V2AccessComplexity
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v2 access complexity
+type V2AccessComplexity byte
+
+const (
+ V2ACLow V2AccessComplexity = iota // LOW
+ V2ACMedium // MEDIUM
+ V2ACHigh // HIGH
+)
+
+// Unmarshal CVSS V2 access complexity from JSON.
+func (me *V2AccessComplexity) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "LOW":
+ *me = V2ACLow
+ case "MEDIUM":
+ *me = V2ACMedium
+ case "HIGH":
+ *me = V2ACHigh
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v2 access complexity: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v2accesscomplexity_string.go b/feed/v2accesscomplexity_string.go
new file mode 100644
index 0000000..8638b3d
--- /dev/null
+++ b/feed/v2accesscomplexity_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -linecomment -type=V2AccessComplexity"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V2ACLow-0]
+ _ = x[V2ACMedium-1]
+ _ = x[V2ACHigh-2]
+}
+
+const _V2AccessComplexity_name = "LOWMEDIUMHIGH"
+
+var _V2AccessComplexity_index = [...]uint8{0, 3, 9, 13}
+
+func (i V2AccessComplexity) String() string {
+ if i >= V2AccessComplexity(len(_V2AccessComplexity_index)-1) {
+ return "V2AccessComplexity(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V2AccessComplexity_name[_V2AccessComplexity_index[i]:_V2AccessComplexity_index[i+1]]
+}
diff --git a/feed/v2accesscomplexity_test.go b/feed/v2accesscomplexity_test.go
new file mode 100644
index 0000000..2dd173d
--- /dev/null
+++ b/feed/v2accesscomplexity_test.go
@@ -0,0 +1,79 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV2AccessComplexityUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V2AccessComplexity
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV2AccessComplexityUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v2 access complexity: foo"
+ var val V2AccessComplexity
+
+ 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 TestV2AccessComplexityUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V2AccessComplexity
+ } {
+ { "\"LOW\"", V2ACLow },
+ { "\"MEDIUM\"", V2ACMedium },
+ { "\"HIGH\"", V2ACHigh },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V2AccessComplexity
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV2AccessComplexityString(t *testing.T) {
+ tests := []struct {
+ val V2AccessComplexity
+ exp string
+ } {
+ { V2ACLow, "LOW" },
+ { V2ACMedium, "MEDIUM" },
+ { V2ACHigh, "HIGH" },
+
+ { V2AccessComplexity(255), "V2AccessComplexity(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v2accessvector.go b/feed/v2accessvector.go
new file mode 100644
index 0000000..80490c2
--- /dev/null
+++ b/feed/v2accessvector.go
@@ -0,0 +1,42 @@
+package feed
+
+//go:generate stringer -linecomment -type=V2AccessVector
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+type V2AccessVector byte
+
+const (
+ V2AVAdjacentNetwork V2AccessVector = iota // ADJACENT_NETWORK
+ V2AVLocal // LOCAL
+ V2AVNetwork // NETWORK
+)
+
+// Unmarshal CVSS V2 access vector from JSON.
+func (me *V2AccessVector) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "ADJACENT_NETWORK":
+ *me = V2AVAdjacentNetwork
+ case "LOCAL":
+ *me = V2AVLocal
+ case "NETWORK":
+ *me = V2AVNetwork
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v2 access vector: %s", s)
+ }
+
+ // return success
+ return nil
+}
+
diff --git a/feed/v2accessvector_string.go b/feed/v2accessvector_string.go
new file mode 100644
index 0000000..bf354fc
--- /dev/null
+++ b/feed/v2accessvector_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -linecomment -type=V2AccessVector"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V2AVAdjacentNetwork-0]
+ _ = x[V2AVLocal-1]
+ _ = x[V2AVNetwork-2]
+}
+
+const _V2AccessVector_name = "ADJACENT_NETWORKLOCALNETWORK"
+
+var _V2AccessVector_index = [...]uint8{0, 16, 21, 28}
+
+func (i V2AccessVector) String() string {
+ if i >= V2AccessVector(len(_V2AccessVector_index)-1) {
+ return "V2AccessVector(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V2AccessVector_name[_V2AccessVector_index[i]:_V2AccessVector_index[i+1]]
+}
diff --git a/feed/v2accessvector_test.go b/feed/v2accessvector_test.go
new file mode 100644
index 0000000..6e0df24
--- /dev/null
+++ b/feed/v2accessvector_test.go
@@ -0,0 +1,79 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV2AccessVectorUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V2AccessVector
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV2AccessVectorUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v2 access vector: foo"
+ var val V2AccessVector
+
+ 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 TestV2AccessVectorUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V2AccessVector
+ } {
+ { "\"ADJACENT_NETWORK\"", V2AVAdjacentNetwork },
+ { "\"LOCAL\"", V2AVLocal },
+ { "\"NETWORK\"", V2AVNetwork },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V2AccessVector
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV2AccessVectorString(t *testing.T) {
+ tests := []struct {
+ val V2AccessVector
+ exp string
+ } {
+ { V2AVAdjacentNetwork, "ADJACENT_NETWORK" },
+ { V2AVLocal, "LOCAL" },
+ { V2AVNetwork, "NETWORK" },
+
+ { V2AccessVector(255), "V2AccessVector(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v2authentication.go b/feed/v2authentication.go
new file mode 100644
index 0000000..853954f
--- /dev/null
+++ b/feed/v2authentication.go
@@ -0,0 +1,39 @@
+package feed
+
+//go:generate stringer -linecomment -type=V2Authentication
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v2 authentication
+type V2Authentication byte
+
+const (
+ V2AuthNone V2Authentication = iota // NONE
+ V2AuthSingle // SINGLE
+)
+
+// Unmarshal CVSS V2 authentication from JSON.
+func (me *V2Authentication) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "NONE":
+ *me = V2AuthNone
+ case "SINGLE":
+ *me = V2AuthSingle
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v2 authentication: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v2authentication_string.go b/feed/v2authentication_string.go
new file mode 100644
index 0000000..856c808
--- /dev/null
+++ b/feed/v2authentication_string.go
@@ -0,0 +1,24 @@
+// Code generated by "stringer -linecomment -type=V2Authentication"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V2AuthNone-0]
+ _ = x[V2AuthSingle-1]
+}
+
+const _V2Authentication_name = "NONESINGLE"
+
+var _V2Authentication_index = [...]uint8{0, 4, 10}
+
+func (i V2Authentication) String() string {
+ if i >= V2Authentication(len(_V2Authentication_index)-1) {
+ return "V2Authentication(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V2Authentication_name[_V2Authentication_index[i]:_V2Authentication_index[i+1]]
+}
diff --git a/feed/v2authentication_test.go b/feed/v2authentication_test.go
new file mode 100644
index 0000000..4f23764
--- /dev/null
+++ b/feed/v2authentication_test.go
@@ -0,0 +1,77 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV2AuthenticationUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V2Authentication
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV2AuthenticationUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v2 authentication: foo"
+ var val V2Authentication
+
+ 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 TestV2AuthenticationUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V2Authentication
+ } {
+ { "\"NONE\"", V2AuthNone },
+ { "\"SINGLE\"", V2AuthSingle },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V2Authentication
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV2AuthenticationString(t *testing.T) {
+ tests := []struct {
+ val V2Authentication
+ exp string
+ } {
+ { V2AuthNone, "NONE" },
+ { V2AuthSingle, "SINGLE" },
+
+ { V2Authentication(255), "V2Authentication(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v2impact.go b/feed/v2impact.go
new file mode 100644
index 0000000..1585e18
--- /dev/null
+++ b/feed/v2impact.go
@@ -0,0 +1,42 @@
+package feed
+
+//go:generate stringer -linecomment -type=V2Impact
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v2 impact level.
+type V2Impact byte
+
+const (
+ V2ImpactNone V2Impact = iota // NONE
+ V2ImpactPartial // PARTIAL
+ V2ImpactComplete // COMPLETE
+)
+
+// Unmarshal CVSS v2 impact level from JSON.
+func (me *V2Impact) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "NONE":
+ *me = V2ImpactNone
+ case "PARTIAL":
+ *me = V2ImpactPartial
+ case "COMPLETE":
+ *me = V2ImpactComplete
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v2 impact: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v2impact_string.go b/feed/v2impact_string.go
new file mode 100644
index 0000000..1dcf21b
--- /dev/null
+++ b/feed/v2impact_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -linecomment -type=V2Impact"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V2ImpactNone-0]
+ _ = x[V2ImpactPartial-1]
+ _ = x[V2ImpactComplete-2]
+}
+
+const _V2Impact_name = "NONEPARTIALCOMPLETE"
+
+var _V2Impact_index = [...]uint8{0, 4, 11, 19}
+
+func (i V2Impact) String() string {
+ if i >= V2Impact(len(_V2Impact_index)-1) {
+ return "V2Impact(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V2Impact_name[_V2Impact_index[i]:_V2Impact_index[i+1]]
+}
diff --git a/feed/v2impact_test.go b/feed/v2impact_test.go
new file mode 100644
index 0000000..54dc566
--- /dev/null
+++ b/feed/v2impact_test.go
@@ -0,0 +1,79 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV2ImpactUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V2Impact
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV2ImpactUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v2 impact: foo"
+ var val V2Impact
+
+ 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 TestV2ImpactUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V2Impact
+ } {
+ { "\"NONE\"", V2ImpactNone },
+ { "\"PARTIAL\"", V2ImpactPartial },
+ { "\"COMPLETE\"", V2ImpactComplete },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V2Impact
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV2ImpactString(t *testing.T) {
+ tests := []struct {
+ val V2Impact
+ exp string
+ } {
+ { V2ImpactNone, "NONE" },
+ { V2ImpactPartial, "PARTIAL" },
+ { V2ImpactComplete, "COMPLETE" },
+
+ { V2Impact(255), "V2Impact(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v2version.go b/feed/v2version.go
new file mode 100644
index 0000000..76e6134
--- /dev/null
+++ b/feed/v2version.go
@@ -0,0 +1,36 @@
+package feed
+
+//go:generate stringer -linecomment -type=V2Version
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v2 version
+type V2Version byte
+
+const (
+ V20 V2Version = iota // 2.0
+)
+
+// Unmarshal CVSS V2 version from JSON.
+func (me *V2Version) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "2.0":
+ *me = V20
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS version: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v2version_string.go b/feed/v2version_string.go
new file mode 100644
index 0000000..6b13870
--- /dev/null
+++ b/feed/v2version_string.go
@@ -0,0 +1,23 @@
+// Code generated by "stringer -linecomment -type=V2Version"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V20-0]
+}
+
+const _V2Version_name = "2.0"
+
+var _V2Version_index = [...]uint8{0, 3}
+
+func (i V2Version) String() string {
+ if i >= V2Version(len(_V2Version_index)-1) {
+ return "V2Version(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V2Version_name[_V2Version_index[i]:_V2Version_index[i+1]]
+}
diff --git a/feed/v2version_test.go b/feed/v2version_test.go
new file mode 100644
index 0000000..3b9b029
--- /dev/null
+++ b/feed/v2version_test.go
@@ -0,0 +1,75 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV2VersionUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V2Version
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV2VersionUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS version: foo"
+ var val V2Version
+
+ 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 TestV2VersionUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V2Version
+ } {
+ { "\"2.0\"", V20 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V2Version
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV2VersionString(t *testing.T) {
+ tests := []struct {
+ val V2Version
+ exp string
+ } {
+ { V20, "2.0" },
+
+ { V2Version(255), "V2Version(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3attackcomplexity.go b/feed/v3attackcomplexity.go
new file mode 100644
index 0000000..6e7481c
--- /dev/null
+++ b/feed/v3attackcomplexity.go
@@ -0,0 +1,42 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3AttackComplexity
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 attack complexity
+type V3AttackComplexity byte
+
+const (
+ V3ACLow V3AttackComplexity = iota // LOW
+ V3ACMedium // MEDIUM
+ V3ACHigh // HIGH
+)
+
+// Unmarshal CVSS v3 attack complexity from JSON.
+func (me *V3AttackComplexity) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "LOW":
+ *me = V3ACLow
+ case "MEDIUM":
+ *me = V3ACMedium
+ case "HIGH":
+ *me = V3ACHigh
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v3 attack complexity: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v3attackcomplexity_string.go b/feed/v3attackcomplexity_string.go
new file mode 100644
index 0000000..12110c8
--- /dev/null
+++ b/feed/v3attackcomplexity_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -linecomment -type=V3AttackComplexity"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V3ACLow-0]
+ _ = x[V3ACMedium-1]
+ _ = x[V3ACHigh-2]
+}
+
+const _V3AttackComplexity_name = "LOWMEDIUMHIGH"
+
+var _V3AttackComplexity_index = [...]uint8{0, 3, 9, 13}
+
+func (i V3AttackComplexity) String() string {
+ if i >= V3AttackComplexity(len(_V3AttackComplexity_index)-1) {
+ return "V3AttackComplexity(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3AttackComplexity_name[_V3AttackComplexity_index[i]:_V3AttackComplexity_index[i+1]]
+}
diff --git a/feed/v3attackcomplexity_test.go b/feed/v3attackcomplexity_test.go
new file mode 100644
index 0000000..a76efe3
--- /dev/null
+++ b/feed/v3attackcomplexity_test.go
@@ -0,0 +1,79 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3AttackComplexityUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3AttackComplexity
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3AttackComplexityUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v3 attack complexity: foo"
+ var val V3AttackComplexity
+
+ 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 TestV3AttackComplexityUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3AttackComplexity
+ } {
+ { "\"LOW\"", V3ACLow },
+ { "\"MEDIUM\"", V3ACMedium },
+ { "\"HIGH\"", V3ACHigh },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3AttackComplexity
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3AttackComplexityString(t *testing.T) {
+ tests := []struct {
+ val V3AttackComplexity
+ exp string
+ } {
+ { V3ACLow, "LOW" },
+ { V3ACMedium, "MEDIUM" },
+ { V3ACHigh, "HIGH" },
+
+ { V3AttackComplexity(255), "V3AttackComplexity(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3attackvector.go b/feed/v3attackvector.go
new file mode 100644
index 0000000..ecc309a
--- /dev/null
+++ b/feed/v3attackvector.go
@@ -0,0 +1,46 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3AttackVector
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 attack vector.
+type V3AttackVector byte
+
+const (
+ V3AVAdjacentNetwork V3AttackVector = iota // ADJACENT_NETWORK
+ V3AVNetwork // NETWORK
+ V3AVLocal // LOCAL
+ V3AVPhysical // PHYSICAL
+)
+
+// Unmarshal CVSS v3 attack vector from JSON.
+func (me *V3AttackVector) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "ADJACENT_NETWORK":
+ *me = V3AVAdjacentNetwork
+ case "LOCAL":
+ *me = V3AVLocal
+ case "NETWORK":
+ *me = V3AVNetwork
+ case "PHYSICAL":
+ *me = V3AVPhysical
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v3 attack vector: %s", s)
+ }
+
+ // return success
+ return nil
+}
+
diff --git a/feed/v3attackvector_string.go b/feed/v3attackvector_string.go
new file mode 100644
index 0000000..277520f
--- /dev/null
+++ b/feed/v3attackvector_string.go
@@ -0,0 +1,26 @@
+// Code generated by "stringer -linecomment -type=V3AttackVector"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V3AVAdjacentNetwork-0]
+ _ = x[V3AVNetwork-1]
+ _ = x[V3AVLocal-2]
+ _ = x[V3AVPhysical-3]
+}
+
+const _V3AttackVector_name = "ADJACENT_NETWORKNETWORKLOCALPHYSICAL"
+
+var _V3AttackVector_index = [...]uint8{0, 16, 23, 28, 36}
+
+func (i V3AttackVector) String() string {
+ if i >= V3AttackVector(len(_V3AttackVector_index)-1) {
+ return "V3AttackVector(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3AttackVector_name[_V3AttackVector_index[i]:_V3AttackVector_index[i+1]]
+}
diff --git a/feed/v3attackvector_test.go b/feed/v3attackvector_test.go
new file mode 100644
index 0000000..251cfd4
--- /dev/null
+++ b/feed/v3attackvector_test.go
@@ -0,0 +1,81 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3AttackVectorUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3AttackVector
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3AttackVectorUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v3 attack vector: foo"
+ var val V3AttackVector
+
+ 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 TestV3AttackVectorUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3AttackVector
+ } {
+ { "\"ADJACENT_NETWORK\"", V3AVAdjacentNetwork },
+ { "\"LOCAL\"", V3AVLocal },
+ { "\"NETWORK\"", V3AVNetwork },
+ { "\"PHYSICAL\"", V3AVPhysical },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3AttackVector
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3AttackVectorString(t *testing.T) {
+ tests := []struct {
+ val V3AttackVector
+ exp string
+ } {
+ { V3AVAdjacentNetwork, "ADJACENT_NETWORK" },
+ { V3AVLocal, "LOCAL" },
+ { V3AVNetwork, "NETWORK" },
+ { V3AVPhysical, "PHYSICAL" },
+
+ { V3AttackVector(255), "V3AttackVector(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3impact.go b/feed/v3impact.go
new file mode 100644
index 0000000..d6c450e
--- /dev/null
+++ b/feed/v3impact.go
@@ -0,0 +1,42 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3Impact
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 impact level.
+type V3Impact byte
+
+const (
+ V3ImpactNone V3Impact = iota // NONE
+ V3ImpactLow // LOW
+ V3ImpactHigh // HIGH
+)
+
+// Unmarshal CVSS v3 impact level from JSON.
+func (me *V3Impact) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "NONE":
+ *me = V3ImpactNone
+ case "LOW":
+ *me = V3ImpactLow
+ case "HIGH":
+ *me = V3ImpactHigh
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v3 impact: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v3impact_string.go b/feed/v3impact_string.go
new file mode 100644
index 0000000..13c7ee3
--- /dev/null
+++ b/feed/v3impact_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -linecomment -type=V3Impact"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V3ImpactNone-0]
+ _ = x[V3ImpactLow-1]
+ _ = x[V3ImpactHigh-2]
+}
+
+const _V3Impact_name = "NONELOWHIGH"
+
+var _V3Impact_index = [...]uint8{0, 4, 7, 11}
+
+func (i V3Impact) String() string {
+ if i >= V3Impact(len(_V3Impact_index)-1) {
+ return "V3Impact(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3Impact_name[_V3Impact_index[i]:_V3Impact_index[i+1]]
+}
diff --git a/feed/v3impact_test.go b/feed/v3impact_test.go
new file mode 100644
index 0000000..a369f44
--- /dev/null
+++ b/feed/v3impact_test.go
@@ -0,0 +1,79 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3ImpactUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3Impact
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3ImpactUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v3 impact: foo"
+ var val V3Impact
+
+ 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 TestV3ImpactUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3Impact
+ } {
+ { "\"NONE\"", V3ImpactNone },
+ { "\"LOW\"", V3ImpactLow },
+ { "\"HIGH\"", V3ImpactHigh },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3Impact
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3ImpactString(t *testing.T) {
+ tests := []struct {
+ val V3Impact
+ exp string
+ } {
+ { V3ImpactNone, "NONE" },
+ { V3ImpactLow, "LOW" },
+ { V3ImpactHigh, "HIGH" },
+
+ { V3Impact(255), "V3Impact(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3privilegesrequired.go b/feed/v3privilegesrequired.go
new file mode 100644
index 0000000..3e69334
--- /dev/null
+++ b/feed/v3privilegesrequired.go
@@ -0,0 +1,45 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3PrivilegesRequired
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 privileges required.
+type V3PrivilegesRequired byte
+
+const (
+ V3PRNone V3PrivilegesRequired = iota // NONE
+ V3PRLow // LOW
+ V3PRMedium // MEDIUM
+ V3PRHigh // HIGH
+)
+
+// Unmarshal CVSS privileges required from JSON.
+func (me *V3PrivilegesRequired) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "NONE":
+ *me = V3PRNone
+ case "LOW":
+ *me = V3PRLow
+ case "MEDIUM":
+ *me = V3PRMedium
+ case "HIGH":
+ *me = V3PRHigh
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v3 privileges required: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v3privilegesrequired_string.go b/feed/v3privilegesrequired_string.go
new file mode 100644
index 0000000..2951a64
--- /dev/null
+++ b/feed/v3privilegesrequired_string.go
@@ -0,0 +1,26 @@
+// Code generated by "stringer -linecomment -type=V3PrivilegesRequired"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V3PRNone-0]
+ _ = x[V3PRLow-1]
+ _ = x[V3PRMedium-2]
+ _ = x[V3PRHigh-3]
+}
+
+const _V3PrivilegesRequired_name = "NONELOWMEDIUMHIGH"
+
+var _V3PrivilegesRequired_index = [...]uint8{0, 4, 7, 13, 17}
+
+func (i V3PrivilegesRequired) String() string {
+ if i >= V3PrivilegesRequired(len(_V3PrivilegesRequired_index)-1) {
+ return "V3PrivilegesRequired(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3PrivilegesRequired_name[_V3PrivilegesRequired_index[i]:_V3PrivilegesRequired_index[i+1]]
+}
diff --git a/feed/v3privilegesrequired_test.go b/feed/v3privilegesrequired_test.go
new file mode 100644
index 0000000..f200ed1
--- /dev/null
+++ b/feed/v3privilegesrequired_test.go
@@ -0,0 +1,81 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3PrivilegesRequiredUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3PrivilegesRequired
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3PrivilegesRequiredUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v3 privileges required: foo"
+ var val V3PrivilegesRequired
+
+ 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 TestV3PrivilegesRequiredUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3PrivilegesRequired
+ } {
+ { "\"NONE\"", V3PRNone },
+ { "\"LOW\"", V3PRLow },
+ { "\"MEDIUM\"", V3PRMedium },
+ { "\"HIGH\"", V3PRHigh },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3PrivilegesRequired
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3PrivilegesRequiredString(t *testing.T) {
+ tests := []struct {
+ val V3PrivilegesRequired
+ exp string
+ } {
+ { V3PRNone, "NONE" },
+ { V3PRLow, "LOW" },
+ { V3PRMedium, "MEDIUM" },
+ { V3PRHigh, "HIGH" },
+
+ { V3PrivilegesRequired(255), "V3PrivilegesRequired(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3scope.go b/feed/v3scope.go
new file mode 100644
index 0000000..20fe0a5
--- /dev/null
+++ b/feed/v3scope.go
@@ -0,0 +1,39 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3Scope
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 scope.
+type V3Scope byte
+
+const (
+ V3ScopeChanged V3Scope = iota // CHANGED
+ V3ScopeUnchanged // UNCHANGED
+)
+
+// Unmarshal CVSS scope from JSON.
+func (me *V3Scope) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "CHANGED":
+ *me = V3ScopeChanged
+ case "UNCHANGED":
+ *me = V3ScopeUnchanged
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v3 scope: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v3scope_string.go b/feed/v3scope_string.go
new file mode 100644
index 0000000..982cead
--- /dev/null
+++ b/feed/v3scope_string.go
@@ -0,0 +1,24 @@
+// Code generated by "stringer -linecomment -type=V3Scope"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V3ScopeChanged-0]
+ _ = x[V3ScopeUnchanged-1]
+}
+
+const _V3Scope_name = "CHANGEDUNCHANGED"
+
+var _V3Scope_index = [...]uint8{0, 7, 16}
+
+func (i V3Scope) String() string {
+ if i >= V3Scope(len(_V3Scope_index)-1) {
+ return "V3Scope(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3Scope_name[_V3Scope_index[i]:_V3Scope_index[i+1]]
+}
diff --git a/feed/v3scope_test.go b/feed/v3scope_test.go
new file mode 100644
index 0000000..54170b0
--- /dev/null
+++ b/feed/v3scope_test.go
@@ -0,0 +1,77 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3ScopeUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3Scope
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3ScopeUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v3 scope: foo"
+ var val V3Scope
+
+ 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 TestV3ScopeUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3Scope
+ } {
+ { "\"CHANGED\"", V3ScopeChanged },
+ { "\"UNCHANGED\"", V3ScopeUnchanged },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3Scope
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3ScopeString(t *testing.T) {
+ tests := []struct {
+ val V3Scope
+ exp string
+ } {
+ { V3ScopeChanged, "CHANGED" },
+ { V3ScopeUnchanged, "UNCHANGED" },
+
+ { V3Scope(255), "V3Scope(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3userinteraction.go b/feed/v3userinteraction.go
new file mode 100644
index 0000000..a6a53ca
--- /dev/null
+++ b/feed/v3userinteraction.go
@@ -0,0 +1,40 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3UserInteraction
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 user interaction
+type V3UserInteraction byte
+
+const (
+ V3UINone V3UserInteraction = iota // NONE
+ V3UIRequired // REQUIRED
+)
+
+// Unmarshal CVSS user interaction from JSON.
+func (me *V3UserInteraction) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "NONE":
+ *me = V3UINone
+ case "REQUIRED":
+ *me = V3UIRequired
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS v3 user interaction: %s", s)
+ }
+
+ // return success
+ return nil
+}
+
diff --git a/feed/v3userinteraction_string.go b/feed/v3userinteraction_string.go
new file mode 100644
index 0000000..be78920
--- /dev/null
+++ b/feed/v3userinteraction_string.go
@@ -0,0 +1,24 @@
+// Code generated by "stringer -linecomment -type=V3UserInteraction"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V3UINone-0]
+ _ = x[V3UIRequired-1]
+}
+
+const _V3UserInteraction_name = "NONEREQUIRED"
+
+var _V3UserInteraction_index = [...]uint8{0, 4, 12}
+
+func (i V3UserInteraction) String() string {
+ if i >= V3UserInteraction(len(_V3UserInteraction_index)-1) {
+ return "V3UserInteraction(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3UserInteraction_name[_V3UserInteraction_index[i]:_V3UserInteraction_index[i+1]]
+}
diff --git a/feed/v3userinteraction_test.go b/feed/v3userinteraction_test.go
new file mode 100644
index 0000000..c5949c2
--- /dev/null
+++ b/feed/v3userinteraction_test.go
@@ -0,0 +1,77 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3UserInteractionUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3UserInteraction
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3UserInteractionUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS v3 user interaction: foo"
+ var val V3UserInteraction
+
+ 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 TestV3UserInteractionUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3UserInteraction
+ } {
+ { "\"NONE\"", V3UINone },
+ { "\"REQUIRED\"", V3UIRequired },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3UserInteraction
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3UserInteractionString(t *testing.T) {
+ tests := []struct {
+ val V3UserInteraction
+ exp string
+ } {
+ { V3UINone, "NONE" },
+ { V3UIRequired, "REQUIRED" },
+
+ { V3UserInteraction(255), "V3UserInteraction(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/v3version.go b/feed/v3version.go
new file mode 100644
index 0000000..537fecc
--- /dev/null
+++ b/feed/v3version.go
@@ -0,0 +1,36 @@
+package feed
+
+//go:generate stringer -linecomment -type=V3Version
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CVSS v3 version
+type V3Version byte
+
+const (
+ V31 V3Version = iota // 3.1
+)
+
+// Unmarshal CVSS V3 version from JSON.
+func (me *V3Version) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // check value
+ switch s {
+ case "3.1":
+ *me = V31
+ default:
+ // return error
+ return fmt.Errorf("unknown CVSS version: %s", s)
+ }
+
+ // return success
+ return nil
+}
diff --git a/feed/v3version_string.go b/feed/v3version_string.go
new file mode 100644
index 0000000..9de58a7
--- /dev/null
+++ b/feed/v3version_string.go
@@ -0,0 +1,23 @@
+// Code generated by "stringer -linecomment -type=V3Version"; DO NOT EDIT.
+
+package feed
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[V31-0]
+}
+
+const _V3Version_name = "3.1"
+
+var _V3Version_index = [...]uint8{0, 3}
+
+func (i V3Version) String() string {
+ if i >= V3Version(len(_V3Version_index)-1) {
+ return "V3Version(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _V3Version_name[_V3Version_index[i]:_V3Version_index[i+1]]
+}
diff --git a/feed/v3version_test.go b/feed/v3version_test.go
new file mode 100644
index 0000000..89cc6ed
--- /dev/null
+++ b/feed/v3version_test.go
@@ -0,0 +1,75 @@
+package feed
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestV3VersionUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val V3Version
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestV3VersionUnmarshalUnknown(t *testing.T) {
+ test := []byte(`"foo"`)
+ exp := "unknown CVSS version: foo"
+ var val V3Version
+
+ 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 TestV3VersionUnmarshalValid(t *testing.T) {
+ tests := []struct {
+ val string
+ exp V3Version
+ } {
+ { "\"3.1\"", V31 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got V3Version
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestV3VersionString(t *testing.T) {
+ tests := []struct {
+ val V3Version
+ exp string
+ } {
+ { V31, "3.1" },
+
+ { V3Version(255), "V3Version(255)" },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.exp, func(t *testing.T) {
+ got := test.val.String()
+
+ if got != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}
diff --git a/feed/vector.go b/feed/vector.go
new file mode 100644
index 0000000..9f20dc6
--- /dev/null
+++ b/feed/vector.go
@@ -0,0 +1,39 @@
+// NVD JSON feed parser.
+package feed
+
+import (
+ "encoding/json"
+ "github.com/pablotron/cvez/cvss"
+)
+
+// CVSS vector
+type Vector struct {
+ // CVSS vector
+ Vector cvss.Vector
+}
+
+// Unmarshal CVSS vector from JSON.
+func (me *Vector) UnmarshalJSON(b []byte) error {
+ // decode string, check for error
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // parse vector
+ vec, err := cvss.NewVector(s)
+ if err != nil {
+ return err
+ }
+
+ // save result
+ me.Vector = vec
+
+ // return success
+ return nil
+}
+
+// Marshal CVSS vector to JSON.
+func (me Vector) MarshalJSON() ([]byte, error) {
+ return json.Marshal(me.Vector.String())
+}
diff --git a/feed/vector_test.go b/feed/vector_test.go
new file mode 100644
index 0000000..16b1d64
--- /dev/null
+++ b/feed/vector_test.go
@@ -0,0 +1,99 @@
+package feed
+
+import (
+ "encoding/json"
+ "github.com/pablotron/cvez/cvss"
+ "testing"
+)
+
+func TestVectorUnmarshalInvalidData(t *testing.T) {
+ test := []byte(`{}`)
+ var val Vector
+
+ if err := json.Unmarshal(test, &val); err == nil {
+ t.Errorf("got \"%s\", exp error", val)
+ }
+}
+
+func TestVectorUnmarshalJSON(t *testing.T) {
+ failTests := []struct {
+ val string
+ exp string
+ } {
+ {
+ val: "\"AV:N/junk/Au:S/C:P/I:P/A:P\"",
+ exp: "invalid CVSS vector: AV:N/junk/Au:S/C:P/I:P/A:P",
+ }, {
+ val: "\"CVSS:3.0/junk/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H\"",
+ exp: "invalid CVSS vector: CVSS:3.0/junk/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H",
+ }, {
+ val: "\"CVSS:3.1/AV:A/junk/PR:N/UI:N/S:U/C:H/I:H/A:H\"",
+ exp: "invalid CVSS vector: CVSS:3.1/AV:A/junk/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ },
+ }
+
+ for _, test := range(failTests) {
+ t.Run(test.val, func(t *testing.T) {
+ var got Vector
+
+ if err := json.Unmarshal([]byte(test.val), &got); err == nil {
+ t.Errorf("got \"%v\", exp error", got)
+ } else if err.Error() != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", err.Error(), test.exp)
+ }
+ })
+ }
+
+ passTests := []string {
+ "AV:N/AC:M/Au:S/C:P/I:P/A:P",
+ "CVSS:3.0/AV:A/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H",
+ "CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ }
+
+ for _, val := range(passTests) {
+ t.Run(val, func(t *testing.T) {
+ var got Vector
+
+ if err := json.Unmarshal([]byte("\"" + val + "\""), &got); err != nil {
+ t.Error(err)
+ } else if got.Vector.String() != val {
+ t.Errorf("got \"%s\", exp \"%s\"", got.Vector.String(), val)
+ }
+ })
+ }
+}
+
+func TestVectorMarshalJSON(t *testing.T) {
+ tests := []string {
+ "AV:N/AC:M/Au:S/C:P/I:P/A:P",
+ "CVSS:3.0/AV:A/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H",
+ "CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ }
+
+ for _, val := range(tests) {
+ t.Run(val, func(t *testing.T) {
+ // get expected string
+ exp := "\"" + val + "\""
+
+ // create inner vector
+ vec, err := cvss.NewVector(val)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // serialize as json
+ buf, err := json.Marshal(Vector { vec })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // check result
+ got := string(buf)
+ if got != exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, exp)
+ }
+ })
+ }
+}