diff --git a/toolkit/types/cvss/cvss.go b/toolkit/types/cvss/cvss.go index 88bd9d923..7a6333242 100644 --- a/toolkit/types/cvss/cvss.go +++ b/toolkit/types/cvss/cvss.go @@ -121,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 == "" { diff --git a/toolkit/types/cvss/cvss_v2.go b/toolkit/types/cvss/cvss_v2.go index 3db6840ee..0b4a32521 100644 --- a/toolkit/types/cvss/cvss_v2.go +++ b/toolkit/types/cvss/cvss_v2.go @@ -114,8 +114,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 } diff --git a/toolkit/types/cvss/cvss_v2_test.go b/toolkit/types/cvss/cvss_v2_test.go index 47ac73388..beb6ec01c 100644 --- a/toolkit/types/cvss/cvss_v2_test.go +++ b/toolkit/types/cvss/cvss_v2_test.go @@ -19,12 +19,12 @@ func TestV2(t *testing.T) { }) 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: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", // made up - "AV:L/AC:H/Au:N/C:C/I:C/A:C/E:F/RL:OF/RC:UR/CDP:LM/TD:ND", // made up + "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) })