diff --git a/toolkit/types/cvss/cvss.go b/toolkit/types/cvss/cvss.go index f0de1c138..7a6333242 100644 --- a/toolkit/types/cvss/cvss.go +++ b/toolkit/types/cvss/cvss.go @@ -38,6 +38,7 @@ import ( "encoding" "errors" "fmt" + "math/bits" "strings" ) @@ -120,21 +121,38 @@ func mkRevLookup[M Metric]() map[string]M { // // The [Vector.getString] method is used here. func marshalVector[M Metric, V Vector[M]](prefix string, v V) ([]byte, error) { - text := append(make([]byte, 0, 64), prefix...) - for i := 0; i < M(0).num(); i++ { - m := M(i) - val, err := v.getString(m) - switch { - case errors.Is(err, nil): - case errors.Is(err, errValueUnset): - continue - default: - return nil, errors.New("invalid cvss vector") + text := append(make([]byte, 0, 64), prefix...) // Guess at an initial capacity. + var err error + // This is a rangefunc-style iterator. + v.groups(func(b [2]int) bool { + var set bool + orig := len(text) + for i := b[0]; i < b[1]; i++ { + m := M(i) + val, err := v.getString(m) + switch { + case errors.Is(err, nil): + set = true + case errors.Is(err, errValueUnset) && val == "": + continue + case errors.Is(err, errValueUnset): + default: + err = errors.New("invalid cvss vector") + return false + } + + text = append(text, '/') + text = append(text, m.String()...) + text = append(text, ':') + text = append(text, val...) + } + if !set { + text = text[:orig] } - text = append(text, '/') - text = append(text, m.String()...) - text = append(text, ':') - text = append(text, val...) + return true + }) + if err != nil { + return nil, err } // v2 hack if prefix == "" { @@ -150,17 +168,25 @@ func marshalVector[M Metric, V Vector[M]](prefix string, v V) ([]byte, error) { // populated. The only validation this function provides is at-most-once // semantics. func parseStringLax[M Metric](v []byte, ver func(string) error, lookup map[string]M, s string) error { - elems := strings.Split(s, "/") - if len(elems) > len(v)+1 { // Extra for the prefix element + if ct := strings.Count(s, "/"); ct > len(v)+1 { // Extra for the prefix element return fmt.Errorf("%w: too many elements", ErrMalformedVector) } - seen := make(map[M]int, len(v)) - for i, e := range elems { + + var seen uint64 + var pre bool + for len(s) != 0 { + var e string + idx := strings.IndexByte(s, '/') + if idx > 0 { + e, s = s[:idx], s[idx+1:] + } else { + e, s = s, "" + } a, val, ok := strings.Cut(e, ":") - if !ok { + switch { + case !ok: return fmt.Errorf("%w: expected %q", ErrMalformedVector, ":") - } - if val == "" || a == "" { + case val == "", a == "": return fmt.Errorf("%w: invalid element: %q", ErrMalformedVector, e) } @@ -169,10 +195,11 @@ func parseStringLax[M Metric](v []byte, ver func(string) error, lookup map[strin // be there. This is needed for v2 vectors. m, ok := lookup[a] if !ok { - if i == 0 && a == "CVSS" { + if (!pre && seen == 0) && a == "CVSS" { if err := ver(val); err != nil { return fmt.Errorf("%w: %w", ErrMalformedVector, err) } + pre = true continue } return fmt.Errorf("%w: unknown abbreviation %q", ErrMalformedVector, a) @@ -180,10 +207,11 @@ func parseStringLax[M Metric](v []byte, ver func(string) error, lookup map[strin if strings.Index(m.validValues(), val) == -1 { return fmt.Errorf("%w: unknown value for %q: %q", ErrMalformedVector, a, val) } - if p, ok := seen[m]; ok { - return fmt.Errorf("%w: duplicate metric %q: %q and %q", ErrMalformedVector, a, elems[p], val) + mark := uint64(1) << int(m) + if seen&(mark) != 0 { + return fmt.Errorf("%w: duplicate metric: %q", ErrMalformedVector, a) } - seen[m] = i + seen |= mark v[m] = m.parse(val) } return nil @@ -195,60 +223,72 @@ func parseStringLax[M Metric](v []byte, ver func(string) error, lookup map[strin // function enforces the metrics appear in order (as dictated by the numeric // value of the [Metric]s), and that the vector is "complete". func parseString[M Metric](v []byte, ver func(string) error, lookup map[string]M, s string) error { - elems := strings.Split(s, "/") - if len(elems) > len(v)+1 { // Extra for the prefix element + switch ct := strings.Count(s, "/"); { + case ct > len(v)+1: // Extra for the prefix element return fmt.Errorf("%w: too many elements", ErrMalformedVector) - } - if len(elems) < minVectorLen(len(v)) { + case ct < minSepCount(len(v)): return fmt.Errorf("%w: too few elements", ErrMalformedVector) } - seen := make([]M, 0, len(v)) - for i, e := range elems { + + var seen uint64 + var pre bool + for len(s) != 0 { + var e string + idx := strings.IndexByte(s, '/') + if idx > 0 { + e, s = s[:idx], s[idx+1:] + } else { + e, s = s, "" + } a, val, ok := strings.Cut(e, ":") - if !ok { + + switch { + case !ok: return fmt.Errorf("%w: expected %q", ErrMalformedVector, ":") - } - if i == 0 { + case !pre && seen == 0: if a != "CVSS" { return fmt.Errorf(`%w: expected "CVSS" element`, ErrMalformedVector) } - // Append a bogus Metric to the seen list to keep everything - // organized. - seen = append(seen, -1) + if err := ver(val); err != nil { + return fmt.Errorf("%w: %w", ErrMalformedVector, err) + } + pre = true continue - } - if val == "" || a == "" { + case val == "", a == "": return fmt.Errorf("%w: invalid element: %q", ErrMalformedVector, e) } m, ok := lookup[a] - if !ok { + switch { + case !ok: return fmt.Errorf("%w: unknown abbreviation %q", ErrMalformedVector, a) - } - if strings.Index(m.validValues(), val) == -1 { + case strings.Index(m.validValues(), val) == -1: return fmt.Errorf("%w: unknown value for %q: %q", ErrMalformedVector, a, val) } - seen = append(seen, m) - switch p := seen[i-1]; { - case m == p: + + mark := uint64(1) << int(m) + switch { + case seen&(mark) != 0: return fmt.Errorf("%w: duplicate metric: %q", ErrMalformedVector, a) - case m < p: + case bits.LeadingZeros64(seen) < bits.LeadingZeros64(mark): + // This exploits the fact that metrics are strictly ordered; + // later metrics always have fewer leading zeros. return fmt.Errorf("%w: metric out of order: %q", ErrMalformedVector, a) - default: } + seen |= mark v[m] = m.parse(val) } return nil } -// MinVectorLen reports the minimum number of metrics present in a valid vector, -// including the "CVSS" prefix. -func minVectorLen(n int) (l int) { +// MinSepCount reports the minimum number of separators present in a valid +// vector. +func minSepCount(n int) (l int) { switch n { case numV4Metrics: - l = 12 + l = 11 case numV3Metrics: - l = 9 + l = 8 case numV2Metrics: panic("programmer error: called with V2 vector") default: @@ -301,6 +341,9 @@ type Vector[M Metric] interface { // GetScore returns the "packed" value representation after any default // rules are applied. getScore(M) byte + // Groups is a rangefunc-style iterator returning the bounds for groups of metrics. + // For a returned value "b", it represents the interval "[b[0], b[1])". + groups(func([2]int) bool) } var ( diff --git a/toolkit/types/cvss/cvss_v2.go b/toolkit/types/cvss/cvss_v2.go index da0a7299d..646dad6c6 100644 --- a/toolkit/types/cvss/cvss_v2.go +++ b/toolkit/types/cvss/cvss_v2.go @@ -1,6 +1,7 @@ package cvss import ( + "bytes" "encoding" "fmt" "strings" @@ -31,11 +32,18 @@ func (v *V2) UnmarshalText(text []byte) error { if err != nil { return fmt.Errorf("cvss v2: %w", err) } - for m, b := range v.mv[:V2Availability] { + for m, b := range v.mv[:V2Availability+1] { // range inclusive if b == 0 { return fmt.Errorf("cvss v2: %w: missing metric: %q", ErrMalformedVector, V3Metric(m).String()) } } + chk, err := v.MarshalText() + if err != nil { + return fmt.Errorf("cvss v2: %w", err) + } + if !bytes.Equal(chk, text) { + return fmt.Errorf("cvss v2: malformed input") + } return nil } @@ -114,8 +122,11 @@ func (v *V2) String() string { // GetString implements [Vector]. func (v *V2) getString(m V2Metric) (string, error) { b := v.mv[int(m)] - if b == 0 { + switch { + case b == 0 && m <= V2Availability: return "", errValueUnset + case b == 0: + return "ND", errValueUnset } return v2Unparse(m, b), nil } @@ -134,12 +145,24 @@ func (v *V2) getScore(m V2Metric) byte { case V2ConfidentialityRequirement, V2IntegrityRequirement, V2AvailabilityRequirement: b = 'N' } - default: - panic("invalid metric: " + m.String()) } return b } +func (v *V2) groups(yield func([2]int) bool) { + var b [2]int + b[0], b[1] = int(V2AccessVector), int(V2Availability)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V2Exploitability), int(V2ReportConfidence)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V2CollateralDamagePotential), int(V2AvailabilityRequirement)+1 + yield(b) +} + // Get implements [Vector]. func (v *V2) Get(m V2Metric) Value { b := v.mv[int(m)] diff --git a/toolkit/types/cvss/cvss_v2_score.go b/toolkit/types/cvss/cvss_v2_score.go index ec64fc451..e5121e813 100644 --- a/toolkit/types/cvss/cvss_v2_score.go +++ b/toolkit/types/cvss/cvss_v2_score.go @@ -7,6 +7,9 @@ import ( // The NaNs in here are to make the string index offsets line up, because V2 has // long metric values. +// +// Note the few that have values moved around because the metric values are +// subsets of other metric values. var v2Weights = [numV2Metrics][]float64{ {0.395, 0.646, 1.0}, // AV {0.35, 0.61, 0.71}, // AC @@ -17,9 +20,9 @@ var v2Weights = [numV2Metrics][]float64{ // Temporal: {0.85, 0.9, math.NaN(), math.NaN(), 0.95, 1.00, 1.00}, // E {0.87, math.NaN(), 0.90, math.NaN(), 0.95, 1.00, 1.00}, // RL - {0.90, math.NaN(), 0.95, math.NaN(), 1.00, 1.00}, // RC + {0.90, 1.00, 0.95, math.NaN(), math.NaN(), 1.00}, // RC -- "C" value packed earlier // Environmental: - {0, 0.1, 0.3, math.NaN(), 0.4, math.NaN(), 0.5, 0}, // CDP + {0, 0.1, 0.3, math.NaN(), 0.4, 0.5, math.NaN(), 0}, // CDP -- "H" value packed earlier {0, 0.25, 0.75, 1.00, 1.00}, // TD {0.5, 1.0, 1.51, 1.0}, // CR {0.5, 1.0, 1.51, 1.0}, // IR diff --git a/toolkit/types/cvss/cvss_v2_test.go b/toolkit/types/cvss/cvss_v2_test.go index 8a924c24b..97c3564a5 100644 --- a/toolkit/types/cvss/cvss_v2_test.go +++ b/toolkit/types/cvss/cvss_v2_test.go @@ -1,22 +1,84 @@ package cvss -import "testing" +import ( + "fmt" + "testing" +) func TestV2(t *testing.T) { + t.Run("Error", func(t *testing.T) { + tcs := []ErrorTestcase{ + {Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C", Error: false}, + {Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C", Error: false}, + {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C", Error: false}, + {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:C/CDP:H/TD:H/CR:M/IR:M/AR:H", Error: false}, + {Vector: "CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:N/A:C", Error: true}, + {Vector: "AV:N/AC:L/Au:N/C:N/I:N", Error: true}, + {Vector: "AV:A/AC:L/Au:N/C:C/I:C/A:C/CDP:H/TD:H/CR:H", Error: true}, + {Vector: "AV:A/AC:L/Au:N/C:C/I:C/A:C/E:F", Error: true}, + } + Error[V2, V2Metric, *V2](t, tcs) + }) t.Run("Roundtrip", func(t *testing.T) { vecs := []string{ - "AV:N/AC:L/Au:N/C:N/I:N/A:C", // CVE-2002-0392 - "AV:N/AC:L/Au:N/C:C/I:C/A:C", // CVE-2003-0818 - "AV:L/AC:H/Au:N/C:C/I:C/A:C", // CVE-2003-0062 + "AV:N/AC:L/Au:N/C:N/I:N/A:C", // CVE-2002-0392 + "AV:N/AC:L/Au:N/C:C/I:C/A:C", // CVE-2003-0818 + "AV:L/AC:H/Au:N/C:C/I:C/A:C", // CVE-2003-0062 + "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:C/CDP:H/TD:H/CR:M/IR:M/AR:H", // CVE-2002-0392 + "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:UR/CDP:ND/TD:ND/CR:ND/IR:ND/AR:ND", // made up + "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:UR/CDP:LM/TD:ND/CR:ND/IR:ND/AR:ND", // made up } Roundtrip[V2, V2Metric, *V2](t, vecs) }) t.Run("Score", func(t *testing.T) { tcs := []ScoreTestcase{ - {Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C", Score: 7.8}, // CVE-2002-0392 - {Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C", Score: 10.0}, // CVE-2003-0818 - {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C", Score: 6.2}, // CVE-2003-0062 + {Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C", Score: 7.8}, // CVE-2002-0392 + {Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C/E:F/RL:OF/RC:C", Score: 6.4}, // CVE-2002-0392 + {Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C/E:F/RL:OF/RC:C/CDP:N/TD:N/CR:M/IR:M/AR:H", Score: 0.0}, // CVE-2002-0392 + {Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C/E:F/RL:OF/RC:C/CDP:H/TD:H/CR:M/IR:M/AR:H", Score: 9.2}, // CVE-2002-0392 + {Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C", Score: 10.0}, // CVE-2003-0818 + {Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:C", Score: 8.3}, // CVE-2003-0818 + {Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:C/CDP:N/TD:N/CR:M/IR:M/AR:L", Score: 0.0}, // CVE-2003-0818 + {Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:C/CDP:H/TD:H/CR:M/IR:M/AR:L", Score: 9.0}, // CVE-2003-0818 + {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C", Score: 6.2}, // CVE-2003-0062 + {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:POC/RL:OF/RC:C", Score: 4.9}, // CVE-2003-0062 + {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:POC/RL:OF/RC:C/CDP:N/TD:N/CR:M/IR:M/AR:M", Score: 0.0}, // CVE-2003-0062 + {Vector: "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:POC/RL:OF/RC:C/CDP:H/TD:H/CR:M/IR:M/AR:M", Score: 7.5}, // CVE-2003-0062 } Score[V2, V2Metric, *V2](t, tcs) }) + + t.Run("Unparse", func(t *testing.T) { + tcs := []struct { + Metric V2Metric + Value Value + Want string + }{ + // Everything that does not have a direct single-byte mapping: + {V2Exploitability, 'P', "POC"}, + {V2Exploitability, 'N', "ND"}, + {V2RemediationLevel, 'O', "OF"}, + {V2RemediationLevel, 'T', "TF"}, + {V2RemediationLevel, 'N', "ND"}, + {V2ReportConfidence, 'U', "UC"}, + {V2ReportConfidence, 'u', "UR"}, + {V2ReportConfidence, 'N', "ND"}, + {V2CollateralDamagePotential, 'M', "MH"}, + {V2CollateralDamagePotential, 'l', "LM"}, + {V2CollateralDamagePotential, 'X', "ND"}, + {V2TargetDistribution, 'X', "ND"}, + {V2ConfidentialityRequirement, 'N', "ND"}, + {V2IntegrityRequirement, 'N', "ND"}, + {V2AvailabilityRequirement, 'N', "ND"}, + } + for _, tc := range tcs { + t.Run(fmt.Sprintf("%v:%c", tc.Metric, tc.Value), func(t *testing.T) { + got, want := UnparseV2Value(tc.Metric, tc.Value), tc.Want + t.Logf("got: %q, want: %q", got, want) + if got != want { + t.Fail() + } + }) + } + }) } diff --git a/toolkit/types/cvss/cvss_v3.go b/toolkit/types/cvss/cvss_v3.go index 9b1209e0f..f13750d24 100644 --- a/toolkit/types/cvss/cvss_v3.go +++ b/toolkit/types/cvss/cvss_v3.go @@ -1,6 +1,7 @@ package cvss import ( + "bytes" "encoding" "fmt" "strings" @@ -45,11 +46,21 @@ func (v *V3) UnmarshalText(text []byte) error { if err != nil { return fmt.Errorf("cvss v3: %w", err) } - for m, b := range v.mv[:V3Availability] { + if v.ver == -1 { // If the versionHook never set the version + return fmt.Errorf("cvss v3: %w", ErrMalformedVector) + } + for m, b := range v.mv[:V3Availability+1] { // range inclusive if b == 0 { return fmt.Errorf("cvss v3: %w: missing metric: %q", ErrMalformedVector, V3Metric(m).String()) } } + chk, err := v.MarshalText() + if err != nil { + return fmt.Errorf("cvss v3: %w", err) + } + if !bytes.Equal(chk, text) { + return fmt.Errorf("cvss v3: malformed input") + } return nil } @@ -81,12 +92,29 @@ func (v *V3) getString(m V3Metric) (string, error) { // GetScore implements [Vector]. func (v *V3) getScore(m V3Metric) byte { b := v.mv[int(m)] - if b == 0 { + switch { + case m >= V3ModifiedAttackVector && b == 0: + b = v.mv[int(m-V3ModifiedAttackVector)] + case b == 0: b = 'X' } return b } +func (v *V3) groups(yield func([2]int) bool) { + var b [2]int + b[0], b[1] = int(V3AttackVector), int(V3Availability)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V3ExploitMaturity), int(V3ReportConfidence)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V3ReportConfidence), int(V3ModifiedAvailability)+1 + yield(b) +} + // Get implements [Vector]. func (v *V3) Get(m V3Metric) Value { b := v.mv[int(m)] @@ -102,25 +130,23 @@ func (v *V3) Get(m V3Metric) Value { // Temporal reports if the vector has "Temporal" metrics. func (v *V3) Temporal() bool { m := v.mv[V3ExploitMaturity : V3ReportConfidence+1] - var ct int for _, v := range m { if v != 0 { - ct++ + return true } } - return ct == len(m) + return false } // Environmental reports if the vector has "Environmental" metrics. func (v *V3) Environmental() (ok bool) { - m := v.mv[V3ModifiedAttackVector:] - var ct int + m := v.mv[V3ConfidentialityRequirement:] for _, v := range m { if v != 0 { - ct++ + return true } } - return ct == len(m) + return false } //go:generate go run golang.org/x/tools/cmd/stringer@latest -type=V3Metric,v3Valid -linecomment diff --git a/toolkit/types/cvss/cvss_v3_score.go b/toolkit/types/cvss/cvss_v3_score.go index ae9a8bd57..2daab921e 100644 --- a/toolkit/types/cvss/cvss_v3_score.go +++ b/toolkit/types/cvss/cvss_v3_score.go @@ -100,7 +100,9 @@ func (v *V3) Score() float64 { issScale := 1.0 scope := v.getScore(V3Scope) if env { - scope = v.getScore(V3ModifiedScope) + if mod := v.getScore(V3ModifiedScope); mod != 'X' { + scope = mod + } if v.ver == 1 { issScale = 0.9731 } diff --git a/toolkit/types/cvss/cvss_v3_test.go b/toolkit/types/cvss/cvss_v3_test.go index 332f5aca8..ae7abad50 100644 --- a/toolkit/types/cvss/cvss_v3_test.go +++ b/toolkit/types/cvss/cvss_v3_test.go @@ -23,33 +23,36 @@ func TestV3(t *testing.T) { {Vector: "CVSS:3.1/AV:P/AC:H/PR:X/UI:R/S:U/C:N/I:N/A:N", Error: true}, {Vector: "CVSS:3.1/AV:P/AC:X/PR:H/UI:R/S:U/C:N/I:N/A:N", Error: true}, {Vector: "CVSS:3.1/AV:X/AC:H/PR:H/UI:R/S:U/C:N/I:N/A:N", Error: true}, + {Vector: "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", Error: true}, + {Vector: "CVSS:3.1/CVSS:3.0/AV:P/AC:H/PR:H/UI:R/S:U/C:N/I:N/A:N", Error: true}, } Error[V3, V3Metric, *V3](t, tcs) }) t.Run("Roundtrip", func(t *testing.T) { vecs := []string{ - "CVSS:3.1/AV:P/AC:H/PR:H/UI:R/S:U/C:N/I:N/A:N", // Zero metrics - "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", // CVE-2015-8252 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", // CVE-2013-1937 - "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", // CVE-2013-0375 - "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", // CVE-2014-3566 - "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H", // CVE-2012-1516 - "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", // CVE-2012-0384 - "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", // CVE-2015-1098 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", // CVE-2014-0160 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", // CVE-2014-6271 - "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:N", // CVE-2008-1447 - "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", // CVE-2014-2005 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N", // CVE-2010-0467 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N", // CVE-2012-1342 - "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N", // CVE-2014-9253 - "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", // CVE-2009-0658 - "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", // CVE-2011-1265 - "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", // CVE-2014-2019 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", // CVE-2015-0970 - "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", // CVE-2014-0224 - "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H", // CVE-2012-5376 + "CVSS:3.1/AV:P/AC:H/PR:H/UI:R/S:U/C:N/I:N/A:N", // Zero metrics + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", // CVE-2015-8252 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", // CVE-2013-1937 + "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", // CVE-2013-0375 + "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", // CVE-2014-3566 + "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H", // CVE-2012-1516 + "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", // CVE-2012-0384 + "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", // CVE-2015-1098 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", // CVE-2014-0160 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", // CVE-2014-6271 + "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:N", // CVE-2008-1447 + "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", // CVE-2014-2005 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N", // CVE-2010-0467 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N", // CVE-2012-1342 + "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N", // CVE-2014-9253 + "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", // CVE-2009-0658 + "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", // CVE-2011-1265 + "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", // CVE-2014-2019 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", // CVE-2015-0970 + "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", // CVE-2014-0224 + "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H", // CVE-2012-5376 + "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N/E:F/RL:X", // From Example } Roundtrip[V3, V3Metric, *V3](t, vecs) }) @@ -125,6 +128,9 @@ func TestV3(t *testing.T) { {Vector: "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", Score: 7.6}, // CVE-2018-3652 {Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H", Score: 7.5}, // CVE-2019-0884 (IE) {Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N", Score: 4.2}, // CVE-2019-0884 (Edge) + + {Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N/E:F/RL:X", Score: 3.7}, // From spec example + {Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N/CR:H/IR:H/AR:H", Score: 4.8}, // made up } Score[V3, V3Metric, *V3](t, tcs) }) diff --git a/toolkit/types/cvss/cvss_v4.go b/toolkit/types/cvss/cvss_v4.go index 00ea0fd41..9148c4ecd 100644 --- a/toolkit/types/cvss/cvss_v4.go +++ b/toolkit/types/cvss/cvss_v4.go @@ -1,6 +1,7 @@ package cvss import ( + "bytes" "encoding" "fmt" "strings" @@ -27,6 +28,18 @@ func (v *V4) UnmarshalText(text []byte) error { if err := parseString(v.mv[:], v4VerHook, v4Rev, string(text)); err != nil { return fmt.Errorf("cvss v4: %w", err) } + for m, b := range v.mv[:V4SubsequentSystemAvailability+1] { // range inclusive + if b == 0 { + return fmt.Errorf("cvss v4: %w: missing metric: %q", ErrMalformedVector, V4Metric(m).String()) + } + } + chk, err := v.MarshalText() + if err != nil { + return fmt.Errorf("cvss v4: %w", err) + } + if !bytes.Equal(chk, text) { + return fmt.Errorf("cvss v4: malformed input") + } return nil } @@ -86,9 +99,7 @@ func (v *V4) getScore(m V4Metric) byte { default: b = 'X' } - // Do this the easy way for the above once the errata clear up the - // ordering issue. - if m >= V4ModifiedAttackVector && m < V4ModifiedVulnerableSystemConfidentiality { + if m >= V4ModifiedAttackVector && m <= V4ModifiedSubsequentSystemAvailability { b = v.mv[m-V4ModifiedAttackVector] } } @@ -134,6 +145,24 @@ func (v *V4) Supplemental() (ok bool) { return ok } +func (v *V4) groups(yield func([2]int) bool) { + var b [2]int + b[0], b[1] = int(V4AttackVector), int(V4SubsequentSystemAvailability)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V4ExploitMaturity), int(V4ExploitMaturity)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V4ConfidentialityRequirement), int(V4ModifiedSubsequentSystemAvailability)+1 + if !yield(b) { + return + } + b[0], b[1] = int(V4Safety), int(V4ProviderUrgency)+1 + yield(b) +} + //go:generate go run golang.org/x/tools/cmd/stringer@latest -type=V4Metric,v4Valid -linecomment // V4Metric is a metric in a v4 vector. diff --git a/toolkit/types/cvss/cvss_v4_score.go b/toolkit/types/cvss/cvss_v4_score.go index 2f2176942..51ac43a74 100644 --- a/toolkit/types/cvss/cvss_v4_score.go +++ b/toolkit/types/cvss/cvss_v4_score.go @@ -118,7 +118,6 @@ func (v *V4) Score() float64 { // implementation's distance calculations using 0.1 as the unit instead of // 1. This shouldn't affect the output because the spec lays out how and // when to round the output. - // var calc scorecalc calc.Init() @@ -226,12 +225,8 @@ Done: }) score := value - calc.Mean() - switch { - case score < 0: - score = 0 - case score > 10: - score = 10 - } + score = math.Max(score, 0) + score = math.Min(score, 10) return math.Round(score*10) / 10 } @@ -403,7 +398,7 @@ var scoreData = struct { // MacrovectorScore is the macrovector → score mapping that's incorporated // by reference into the spec. // - // See Section 8.3 for the current fixture.. + // See Section 8.3 for the current fixture. macrovectorScore map[macrovector]float64 // MetricsInEQ returns a slice of the metrics in a given equivalence class. // diff --git a/toolkit/types/cvss/cvss_v4_test.go b/toolkit/types/cvss/cvss_v4_test.go index 8537f8499..2f9fc20bc 100644 --- a/toolkit/types/cvss/cvss_v4_test.go +++ b/toolkit/types/cvss/cvss_v4_test.go @@ -14,12 +14,13 @@ func TestV4(t *testing.T) { {Vector: "CVSS:3.1/AV:P/AC:H/PR:H/UI:R/S:U/C:N/I:N/A:N", Error: true}, {Vector: "CVSS:4.0/AV:Z/AC:L/AT:N/PR:H/UI:N/VC:L/SC:N/VI:L/SI:N/VA:N/SA:N", Error: true}, {Vector: "CVSS:4.0/AV:N/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/SC:N/VI:L/SI:N/VA:N/SA:N", Error: true}, - { - Vector: "CVSS:4.0///////////////////////////////////////", - Error: true, - }, + {Vector: "CVSS:4.0///////////////////////////////////////", Error: true}, {Vector: "CVSS:4.0/AV:/AC:L/AT:N/PR:H/UI:N/VC:L/SC:N/VI:L/SI:N/VA:N/SA:N", Error: true}, {Vector: "CVSS:4.0/:N/AC:L/AT:N/PR:H/UI:N/VC:L/SC:N/VI:L/SI:N/VA:N/SA:N", Error: true}, + {Vector: "CVSS:/AV:A/AC:L/AT:N/PR:H/UI:A/VC:L/VI:L/VA:N/SC:N/SA:N/S:X", Error: true}, + {Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/E:X", Error: true}, + {Vector: "CVSS:4.0/CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N", Error: true}, + {Vector: "CVSS:4.0/AV:A/AC:L/AT:N/PR:H/UI:A/VC:LN/VI:L/VA:N/SC:N/SI:N/SA:N", Error: true}, } Error[V4, V4Metric, *V4](t, tcs) })