From b3dc36421f133ea6983574891720e974cf7974dd Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Mon, 31 Jan 2022 11:09:58 -0500 Subject: initial commit --- internal/feed/feed.go | 770 ++++++++++++++++++++++++++++++++++++++++++++++++++ internal/feed/meta.go | 104 +++++++ 2 files changed, 874 insertions(+) create mode 100644 internal/feed/feed.go create mode 100644 internal/feed/meta.go (limited to 'internal/feed') diff --git a/internal/feed/feed.go b/internal/feed/feed.go new file mode 100644 index 0000000..1c15a0a --- /dev/null +++ b/internal/feed/feed.go @@ -0,0 +1,770 @@ +package feed + +import ( + "encoding/json" + "fmt" + // "strconv" + "regexp" + "time" +) + +const ( + CveType = iota // CVE data type + MitreFormat // MITRE data format + DataVersion40 // Version 4.0 + + OrNodeOp // OR operator + AndNodeOp // And operator + + AdjacentNetwork // Adjacent Network attack vector. + Network // Network attack vector. + Local // Local attack vector. + Physical // Physical attack vector. + + None // no priv req/user interaction + Low // low complexity/priv req + Medium // medium complexity/priv req + High // high complexity/priv req + + Required // user interaction required + + Changed // scope changed + Unchanged // scope unchanged + + Complete // complete integrity impact + Partial // partial integrity impact + + Critical // critical severity + + CvssVersion31 // CVSS version 3.1 + CvssVersion20 // CVSS version 2.0 + + Single // Single authentication +) + +// TODO: parse cpe, cvss vectors (v3.x, v2) + +// Data type for NVD feeds and feed items. +type DataType int + +// 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 +} + +// Data format for NVD feeds and feed items. +type DataFormat int + +// 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 +} + +// Data version for NVD feeds and feed items. +type DataVersion int + +// Unmarshal DataVersion 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 = DataVersion40 + default: + // return error + return fmt.Errorf("unknown data version: %s", s) + } + + // return success + return nil +} + +// partial timestamp +type PartialTime time.Time + +var partialTimeRe = regexp.MustCompile("\\A\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}Z\\z") + +// Unmarshal partial timestamp from JSON. +func (me *PartialTime) 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 !partialTimeRe.MatchString(s) { + return fmt.Errorf("invalid partial time string: %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 + } + + // return success + return nil +} + +// Configuration node boolean operator. +type NodeOp int + +// 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 = AndNodeOp + case "OR": + *me = OrNodeOp + default: + // return error + return fmt.Errorf("unknown operator: %s", s) + } + + // return success + return nil +} + +// CVSS attack vector +type AttackVector int + +// Unmarshal CVSS attack vector from JSON. +func (me *AttackVector) 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 = AdjacentNetwork + case "LOCAL": + *me = Local + case "NETWORK": + *me = Network + case "PHYSICAL": + *me = Physical + default: + // return error + return fmt.Errorf("unknown attack vector: %s", s) + } + + // return success + return nil +} + +// CVSS attack complexity +type AttackComplexity int + +// Unmarshal CVSS attack complexity from JSON. +func (me *AttackComplexity) 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 = Low + case "MEDIUM": + *me = Medium + case "HIGH": + *me = High + default: + // return error + return fmt.Errorf("unknown attack complexity: %s", s) + } + + // return success + return nil +} + +// CVSS privileges required +type PrivilegesRequired int + +// Unmarshal CVSS privileges required from JSON. +func (me *PrivilegesRequired) 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 = None + case "LOW": + *me = Low + case "MEDIUM": + *me = Medium + case "HIGH": + *me = High + default: + // return error + return fmt.Errorf("unknown privileges required: %s", s) + } + + // return success + return nil +} + +// CVSS user interaction +type UserInteraction int + +// Unmarshal CVSS user interaction from JSON. +func (me *UserInteraction) 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 = None + case "REQUIRED": + *me = Required + default: + // return error + return fmt.Errorf("unknown user interaction: %s", s) + } + + // return success + return nil +} + +// CVSS scope +type Scope int + +// Unmarshal CVSS scope from JSON. +func (me *Scope) 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 = Changed + case "UNCHANGED": + *me = Unchanged + default: + // return error + return fmt.Errorf("unknown scope: %s", s) + } + + // return success + return nil +} + +// CVSS integrity/availability impact level +type ImpactLevel int + +// Unmarshal CVSS integrity/availability impact level from JSON. +func (me *ImpactLevel) 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 = None + case "LOW": + *me = Low + case "PARTIAL": + *me = Partial + case "HIGH": + *me = High + case "COMPLETE": + *me = Complete + default: + // return error + return fmt.Errorf("unknown impact level: %s", s) + } + + // return success + return nil +} + +// CVSS score +type Score float32 + +// Unmarshal CVSS score from JSON. +func (me *Score) UnmarshalJSON(b []byte) error { + // decode float, check for error + var v float32 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + // check score + if v < 0.0 || v > 10.0 { + return fmt.Errorf("score out of bounds: %f", v) + } + + // save result, return success + *me = Score(v) + return nil +} + +// CVSS severity +type Severity int + +// 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 "LOW": + *me = Low + case "MEDIUM": + *me = Medium + case "HIGH": + *me = High + case "CRITICAL": + *me = Critical + default: + // return error + return fmt.Errorf("unknown severity: %s", s) + } + + // return success + return nil +} + +type AccessVector int + +// Unmarshal CVSS V2 access vector from JSON. +func (me *AccessVector) 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 = AdjacentNetwork + case "LOCAL": + *me = Local + case "NETWORK": + *me = Network + default: + // return error + return fmt.Errorf("unknown CVSS access vector: %s", s) + } + + // return success + return nil +} + +// CVSS V2 attack complexity +type AccessComplexity int + +// Unmarshal CVSS V2 access complexity from JSON. +func (me *AccessComplexity) 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 = Low + case "MEDIUM": + *me = Medium + case "HIGH": + *me = High + default: + // return error + return fmt.Errorf("unknown access complexity: %s", s) + } + + // return success + return nil +} + + +// CVSS V2 authentication +type Authentication int + +// Unmarshal CVSS V2 authentication from JSON. +func (me *Authentication) 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 = None + case "SINGLE": + *me = Single + default: + // return error + return fmt.Errorf("unknown authentication: %s", s) + } + + // return success + return nil +} + +// CVE metadata +type CveMetadata struct { + // CVE ID + Id string `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 version. +type CvssV3Version int + +// Unmarshal CVSS version from JSON. +func (me *CvssV3Version) 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 = CvssVersion31 + default: + // return error + return fmt.Errorf("unknown CVSS V3 version: %s", s) + } + + // return success + return nil +} + +// CVSS V2 version. +type CvssV2Version int + +// Unmarshal CVSS version from JSON. +func (me *CvssV2Version) 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 = CvssVersion20 + default: + // return error + return fmt.Errorf("unknown CVSS V2 version: %s", s) + } + + // return success + return nil +} + +// CVSS V3 +type CvssV3 struct { + // CVSS V3 version + Version CvssV3Version `json:"version"` + + // CVSS V3 vector string (FIXME: add custom type) + VectorString string `json:"vectorString"` + + // attack vector + AttackVector AttackVector `json:"attackVector"` + + // attack complexity + AttackComplexity AttackComplexity `json:"attackComplexity"` + + // privileges required + PrivilegesRequired PrivilegesRequired `json:"privilegesRequired"` + + // user interaction + UserInteraction UserInteraction `json:"userInteraction"` + + // scope + Scope Scope `json:"scope"` + + // integrity impact + IntegrityImpact ImpactLevel `json:"integrityImpact"` + + // availability impact + AvailabilityImpact ImpactLevel `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 CvssV2Version `json:"version"` + + // CVSS V3 vector string (FIXME: add custom type) + VectorString string `json:"vectorString"` + + // attack vector + AccessVector AccessVector `json:"accessVector"` + + // attack complexity + AccessComplexity AccessComplexity `json:"accessComplexity"` + + // authentication + Authentication Authentication `json:"authentication"` + + ConfidentialityImpact ImpactLevel `json:"confidentialityImpact"` + IntegrityImpact ImpactLevel `json:"integrityImpact"` + AvailabilityImpact ImpactLevel `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:"impactScore"` + 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 PartialTime `json:"publishedDate"` + + // last modification date + LastModifiedDate PartialTime `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 PartialTime `json:"CVE_data_timestamp"` + + // CVE items + Items []Item `json:"CVE_Items"` +} diff --git a/internal/feed/meta.go b/internal/feed/meta.go new file mode 100644 index 0000000..da2849d --- /dev/null +++ b/internal/feed/meta.go @@ -0,0 +1,104 @@ +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 { + return nil, fmt.Errorf("invalid sha256 hash 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 +} -- cgit v1.2.3