Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cvss: bug fixes #1232

Merged
merged 10 commits into from
May 10, 2024
147 changes: 95 additions & 52 deletions toolkit/types/cvss/cvss.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"encoding"
"errors"
"fmt"
"math/bits"
"strings"
)

Expand Down Expand Up @@ -120,21 +121,38 @@
//
// 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

Check warning on line 141 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L139-L141

Added lines #L139 - L141 were not covered by tests
}

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

Check warning on line 155 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L155

Added line #L155 was not covered by tests
}
// v2 hack
if prefix == "" {
Expand All @@ -150,17 +168,25 @@
// 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 == "":

Check warning on line 189 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L189

Added line #L189 was not covered by tests
return fmt.Errorf("%w: invalid element: %q", ErrMalformedVector, e)
}

Expand All @@ -169,21 +195,23 @@
// 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)
}
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)

Check warning on line 212 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L212

Added line #L212 was not covered by tests
}
seen[m] = i
seen |= mark
v[m] = m.parse(val)
}
return nil
Expand All @@ -195,60 +223,72 @@
// 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:

Check warning on line 246 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L246

Added line #L246 was not covered by tests
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):

Check warning on line 273 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L273

Added line #L273 was not covered by tests
// 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

Check warning on line 291 in toolkit/types/cvss/cvss.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss.go#L291

Added line #L291 was not covered by tests
case numV2Metrics:
panic("programmer error: called with V2 vector")
default:
Expand Down Expand Up @@ -301,6 +341,9 @@
// 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 (
Expand Down
31 changes: 27 additions & 4 deletions toolkit/types/cvss/cvss_v2.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cvss

import (
"bytes"
"encoding"
"fmt"
"strings"
Expand Down Expand Up @@ -31,11 +32,18 @@
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)

Check warning on line 42 in toolkit/types/cvss/cvss_v2.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss_v2.go#L42

Added line #L42 was not covered by tests
}
if !bytes.Equal(chk, text) {
return fmt.Errorf("cvss v2: malformed input")
}
return nil
}

Expand Down Expand Up @@ -114,8 +122,11 @@
// 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:

Check warning on line 126 in toolkit/types/cvss/cvss_v2.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss_v2.go#L126

Added line #L126 was not covered by tests
return "", errValueUnset
case b == 0:
return "ND", errValueUnset
}
return v2Unparse(m, b), nil
}
Expand All @@ -134,12 +145,24 @@
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

Check warning on line 156 in toolkit/types/cvss/cvss_v2.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss_v2.go#L156

Added line #L156 was not covered by tests
}
b[0], b[1] = int(V2Exploitability), int(V2ReportConfidence)+1
if !yield(b) {
return

Check warning on line 160 in toolkit/types/cvss/cvss_v2.go

View check run for this annotation

Codecov / codecov/patch

toolkit/types/cvss/cvss_v2.go#L160

Added line #L160 was not covered by tests
}
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)]
Expand Down
7 changes: 5 additions & 2 deletions toolkit/types/cvss/cvss_v2_score.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading