Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions toolkit/types/cvss/append_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package cvss

import (
"math/rand/v2"
"testing"
)

func BenchmarkAppend(b *testing.B) {
b.Run("V2", benchAppendV2)
b.Run("V3", benchAppendV3)
b.Run("V4", benchAppendV4)
}

func benchAppendV4(b *testing.B) {
buf := make([]byte, 0, 1024) // Is it cheating to oversize this?
benchOne := func(b *testing.B, vec []byte) {
b.Helper()
b.Attr("input", string(vec))
var v V4
if err := v.UnmarshalText(vec); err != nil {
b.Fatal(err)
}
var err error
var x []byte
b.ReportAllocs()

for b.Loop() {
x, err = v.AppendText(buf)
if err != nil {
b.Error(err)
}
_ = x
}
}

b.Run("List", func(b *testing.B) {
vecs := loadVectorList(b, `v4_roundtrip.list`)
todo := make([][]byte, 10)
for i := range todo {
todo[i] = vecs[rand.N(len(vecs))]
}
for _, vec := range todo {
b.Run("", func(b *testing.B) { benchOne(b, vec) })
}
})
// Each of the following test one fixture plucked from the Spec's examples.
b.Run("B", func(b *testing.B) {
benchOne(b, []byte("CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L"))
})
b.Run("BT", func(b *testing.B) {
benchOne(b, []byte("CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L/E:P"))
})
b.Run("BE", func(b *testing.B) {
benchOne(b, []byte("CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:A/VC:N/VI:N/VA:L/SC:H/SI:H/SA:H/CR:H/IR:H/AR:M/MAV:N/MAC:L/MAT:P/MPR:L/MUI:A/MVC:N/MVI:H/MVA:L/MSC:L/MSI:S/MSA:H"))
})
b.Run("BTES", func(b *testing.B) {
benchOne(b, []byte("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/E:U/CR:L/IR:X/AR:L/MAV:A/MAC:H/MAT:N/MPR:N/MUI:P/MVC:X/MVI:N/MVA:H/MSC:N/MSI:L/MSA:S/S:N/AU:N/R:I/V:C/RE:H/U:Green"))
})
}

func benchAppendV3(b *testing.B) {
buf := make([]byte, 0, 1024) // Is it cheating to oversize this?
benchOne := func(b *testing.B, vec []byte) {
b.Helper()
b.Attr("input", string(vec))
var v V3
if err := v.UnmarshalText(vec); err != nil {
b.Fatal(err)
}
var err error
var x []byte
b.ReportAllocs()

for b.Loop() {
x, err = v.AppendText(buf)
if err != nil {
b.Error(err)
}
_ = x
}
}

b.Run("List", func(b *testing.B) {
vecs := loadVectorList(b, `v31_score.list`)
todo := make([][]byte, 10)
for i := range todo {
todo[i] = vecs[rand.N(len(vecs))]
}
for _, vec := range todo {
b.Run("", func(b *testing.B) { benchOne(b, vec) })
}
})
b.Run("Heartbleed", func(b *testing.B) {
benchOne(b, []byte("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"))
})
}

func benchAppendV2(b *testing.B) {
buf := make([]byte, 0, 1024) // Is it cheating to oversize this?
benchOne := func(b *testing.B, vec []byte) {
b.Helper()
b.Attr("input", string(vec))
var v V2
if err := v.UnmarshalText(vec); err != nil {
b.Fatal(err)
}
var err error
var x []byte
b.ReportAllocs()

for b.Loop() {
x, err = v.AppendText(buf)
if err != nil {
b.Error(err)
}
_ = x
}
}

b.Run("List", func(b *testing.B) {
vecs := loadVectorList(b, `v2_score.list`)
todo := make([][]byte, 10)
for i := range todo {
todo[i] = vecs[rand.N(len(vecs))]
}
for _, vec := range todo {
b.Run("", func(b *testing.B) { benchOne(b, vec) })
}
})
b.Run("Heartbleed", func(b *testing.B) {
benchOne(b, []byte("AV:N/AC:L/Au:N/C:P/I:N/A:N"))
})
}
97 changes: 57 additions & 40 deletions toolkit/types/cvss/cvss.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,21 @@ var internalDoc = struct{}{}
// ErrMalformedVector is reported when a vector is invalid in some way.
var ErrMalformedVector = errors.New("malformed vector")

// ErrValueUnset is used by [Vector.getString] implementations to signal a
// metric's value is unset.
// ErrValueUnset is used by [Vector] implementations to signal a metric's value
// is unset.
var errValueUnset = errors.New("unset")

// ErrValueDefault is used by [Vector] implementations to signal a metric's value
// is unset, but a default value was used for the requested operation.
var errValueDefault = fmt.Errorf("default: %w", errValueUnset)

// MarshalSize is the initial size of the backing slice for
// [encoding.TextMarshaler] implementations.
//
// This was arrived at by trying sizes until [BenchmarkMarshal] reported a
// single allocation for all but the longest V4 vectors.
const marshalSize = 128

// Value is a "packed" representation of the value of a metric.
//
// When possible, this is the first byte of the abbreviated form in the relevant
Expand Down Expand Up @@ -107,55 +118,56 @@ func Version(vec string) (v int) {
return v
}

// MarshalVector is a generic function to marshal vectors.
//
// 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...) // Guess at an initial capacity.
// AppendVector is a generic function to marshal vectors via appending to the
// provided byte slice.
func appendVector[M Metric, V Vector[M]](b []byte, prefix string, v V) ([]byte, error) {
b = append(b, prefix...)
var err error
// This is a rangefunc-style iterator.
v.groups(func(b [2]int) bool {
meta := v.meta()
g := meta.Groups
for s, e := 0, 1; e < len(g); s, e = s+1, e+1 {
var set bool
orig := len(text)
for i := b[0]; i < b[1]; i++ {
i, lim := g[s], g[e]
skipGroup := len(b)
for ; i < lim; i++ {
skipMetric := len(b)
m := M(i)
val, err := v.getString(m)

b = append(b, '/')
b, err = m.AppendText(b)
if err != nil {
return nil, fmt.Errorf("invalid cvss vector: %w", err)
}
b = append(b, ':')

b, err = v.appendValue(b, m)
switch {
case errors.Is(err, nil):
set = true
case errors.Is(err, errValueUnset) && val == "":
continue
case errors.Is(err, errValueDefault):
case errors.Is(err, errValueUnset):
b = b[:skipMetric]
default:
err = errors.New("invalid cvss vector")
return false
return nil, fmt.Errorf("invalid cvss vector: %w", err)
}

text = append(text, '/')
text = append(text, m.String()...)
text = append(text, ':')
text = append(text, val...)
}
if !set {
text = text[:orig]
b = b[:skipGroup]
}
return true
})
if err != nil {
return nil, err
}
// v2 hack
// v2 hack: remove the leading slash.
if prefix == "" {
text = text[1:]
b = b[1:]
}
return text, nil
return b, nil
}

// Metric is a CVSS metric.
//
// The set of types this describes is namespaced per-version.
type Metric interface {
~int
encoding.TextAppender
fmt.Stringer

// Valid returns the concatenation of valid values for the metric.
Expand All @@ -166,6 +178,7 @@ type Metric interface {

// Vector is a CVSS vector of any version.
type Vector[M Metric] interface {
encoding.TextAppender
encoding.TextUnmarshaler
encoding.TextMarshaler
fmt.Stringer
Expand All @@ -181,20 +194,16 @@ type Vector[M Metric] interface {
// Environmental reports if the vector contains environmental metrics.
Environmental() bool

// GetString is a hook for returning the stringified version of the metric
// value. If the value is unset, implementations should return err
// [errValueUnset] rather than a specified default, as defaults are omitted
// from the string representation.
//
// CVSSv2 notably does not use names that are identifiable by a single byte,
// so they need to be packed and unpacked.
getString(M) (string, error)
// AppendValue is a hook for appending the stringified version of the metric
// value. If the value is unset, implementations should return the input
// slice and err == [errValueUnset] rather than a specified default, as
// defaults are omitted from the string representation.
appendValue([]byte, M) ([]byte, error)
// 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)
// Meta returns the static metadata for this vector.
meta() *vectorMetadata
}

var (
Expand All @@ -203,6 +212,14 @@ var (
_ Vector[V2Metric] = (*V2)(nil)
)

// VectorMetadata is static metadata about a vector.
type vectorMetadata struct {
// Groups is a slice of boundaries for the groups of the vector.
//
// The pairs of ints are [lower, upper).
Groups []int
}

// Qualitative is the "Qualitative Severity" of a Vector.
type Qualitative int

Expand Down
Loading
Loading