diff options
author | Paul Duncan <pabs@pablotron.org> | 2022-02-04 00:35:31 -0500 |
---|---|---|
committer | Paul Duncan <pabs@pablotron.org> | 2022-02-04 00:35:31 -0500 |
commit | 9c17b97cd0f83be3fff9fa4e87fd1d29052ea616 (patch) | |
tree | 0d97030a0d0c3ad983be281ce89f80571338887f /feed | |
parent | 92400d731546557d110c9c3cc3906d700f83dda8 (diff) | |
download | cvez-9c17b97cd0f83be3fff9fa4e87fd1d29052ea616.tar.bz2 cvez-9c17b97cd0f83be3fff9fa4e87fd1d29052ea616.zip |
rename to github.com/pablotron/cvez, remove internal libs
Diffstat (limited to 'feed')
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 Binary files differnew file mode 100644 index 0000000..45e714d --- /dev/null +++ b/feed/testdata/nvdcve-1.1-2002.json.gz diff --git a/feed/testdata/nvdcve-1.1-2003.json.gz b/feed/testdata/nvdcve-1.1-2003.json.gz Binary files differnew file mode 100644 index 0000000..c7796a6 --- /dev/null +++ b/feed/testdata/nvdcve-1.1-2003.json.gz diff --git a/feed/testdata/nvdcve-1.1-2021.json.gz b/feed/testdata/nvdcve-1.1-2021.json.gz Binary files differnew file mode 100644 index 0000000..83ca5e6 --- /dev/null +++ b/feed/testdata/nvdcve-1.1-2021.json.gz diff --git a/feed/testdata/nvdcve-1.1-modified.json.gz b/feed/testdata/nvdcve-1.1-modified.json.gz Binary files differnew file mode 100644 index 0000000..c675fb6 --- /dev/null +++ b/feed/testdata/nvdcve-1.1-modified.json.gz 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) + } + }) + } +} |