From 4a683a8ca01f579e54f446b2829c2eef479e013b Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Mon, 29 Jan 2024 15:34:51 -0600 Subject: [PATCH 01/10] cvss: enforce v4 vector prefix See-also: #1230 Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss.go | 3 +++ toolkit/types/cvss/cvss_v4_test.go | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/toolkit/types/cvss/cvss.go b/toolkit/types/cvss/cvss.go index f0de1c138..5fec05652 100644 --- a/toolkit/types/cvss/cvss.go +++ b/toolkit/types/cvss/cvss.go @@ -212,6 +212,9 @@ func parseString[M Metric](v []byte, ver func(string) error, lookup map[string]M if a != "CVSS" { return fmt.Errorf(`%w: expected "CVSS" element`, ErrMalformedVector) } + if err := ver(val); err != nil { + return fmt.Errorf("%w: %w", ErrMalformedVector, err) + } // Append a bogus Metric to the seen list to keep everything // organized. seen = append(seen, -1) diff --git a/toolkit/types/cvss/cvss_v4_test.go b/toolkit/types/cvss/cvss_v4_test.go index 8537f8499..4ac8074b4 100644 --- a/toolkit/types/cvss/cvss_v4_test.go +++ b/toolkit/types/cvss/cvss_v4_test.go @@ -14,12 +14,10 @@ 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}, } Error[V4, V4Metric, *V4](t, tcs) }) From a64d79465871729e49a28e7d3c2583ba3d2f27b1 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Mon, 29 Jan 2024 15:39:14 -0600 Subject: [PATCH 02/10] cvss: enforce v3 vector prefix See-also: #1230 Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss_v3.go | 3 +++ toolkit/types/cvss/cvss_v3_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/toolkit/types/cvss/cvss_v3.go b/toolkit/types/cvss/cvss_v3.go index 9b1209e0f..da5ef1f10 100644 --- a/toolkit/types/cvss/cvss_v3.go +++ b/toolkit/types/cvss/cvss_v3.go @@ -45,6 +45,9 @@ func (v *V3) UnmarshalText(text []byte) error { if err != nil { return fmt.Errorf("cvss v3: %w", err) } + 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] { if b == 0 { return fmt.Errorf("cvss v3: %w: missing metric: %q", ErrMalformedVector, V3Metric(m).String()) diff --git a/toolkit/types/cvss/cvss_v3_test.go b/toolkit/types/cvss/cvss_v3_test.go index 332f5aca8..a643bbdd3 100644 --- a/toolkit/types/cvss/cvss_v3_test.go +++ b/toolkit/types/cvss/cvss_v3_test.go @@ -23,6 +23,7 @@ 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}, } Error[V3, V3Metric, *V3](t, tcs) }) From 21a61dc5951f17d4a6bc42de75a4f4fead31d7d0 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Mon, 29 Jan 2024 18:03:08 -0600 Subject: [PATCH 03/10] cvss: fix v2 not recognizing non-base metrics Got too zealous with switch and didn't have any testcases to exercise the metrics. Closes: #1230 Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss_v2.go | 4 +- toolkit/types/cvss/cvss_v2_score.go | 7 ++- toolkit/types/cvss/cvss_v2_test.go | 74 ++++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/toolkit/types/cvss/cvss_v2.go b/toolkit/types/cvss/cvss_v2.go index da0a7299d..d477ff6fc 100644 --- a/toolkit/types/cvss/cvss_v2.go +++ b/toolkit/types/cvss/cvss_v2.go @@ -31,7 +31,7 @@ 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()) } @@ -134,8 +134,6 @@ func (v *V2) getScore(m V2Metric) byte { case V2ConfidentialityRequirement, V2IntegrityRequirement, V2AvailabilityRequirement: b = 'N' } - default: - panic("invalid metric: " + m.String()) } return b } 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..47ac73388 100644 --- a/toolkit/types/cvss/cvss_v2_test.go +++ b/toolkit/types/cvss/cvss_v2_test.go @@ -1,22 +1,82 @@ 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}, + } + 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", // 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 } 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() + } + }) + } + }) } From 273f703c6049d57882cfcdd14312b4246066647e Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Mon, 29 Jan 2024 18:06:58 -0600 Subject: [PATCH 04/10] cvss: coverage increases and bugfixes This adds additional tests driven by coverage numbers, and fixes the bugs that cropped up while exercising those paths. Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss_v3.go | 19 ++++++------ toolkit/types/cvss/cvss_v3_score.go | 4 ++- toolkit/types/cvss/cvss_v3_test.go | 46 ++++++++++++++++------------- toolkit/types/cvss/cvss_v4.go | 4 +-- toolkit/types/cvss/cvss_v4_score.go | 11 ++----- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/toolkit/types/cvss/cvss_v3.go b/toolkit/types/cvss/cvss_v3.go index da5ef1f10..3eafaca27 100644 --- a/toolkit/types/cvss/cvss_v3.go +++ b/toolkit/types/cvss/cvss_v3.go @@ -48,7 +48,7 @@ func (v *V3) UnmarshalText(text []byte) error { 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] { + 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()) } @@ -84,7 +84,10 @@ 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 @@ -105,25 +108,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 a643bbdd3..fc5579b6f 100644 --- a/toolkit/types/cvss/cvss_v3_test.go +++ b/toolkit/types/cvss/cvss_v3_test.go @@ -30,27 +30,28 @@ func TestV3(t *testing.T) { 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) }) @@ -126,6 +127,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..bc7426ba9 100644 --- a/toolkit/types/cvss/cvss_v4.go +++ b/toolkit/types/cvss/cvss_v4.go @@ -86,9 +86,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] } } 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. // From 2bdff8b76b5ee5c38c708ad9bfa6ab50f0a2ed2c Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Tue, 30 Jan 2024 14:04:46 -0600 Subject: [PATCH 05/10] cvss: verify required metrics for v4 vectors See-also: #1232 Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss_v4.go | 5 +++++ toolkit/types/cvss/cvss_v4_test.go | 1 + 2 files changed, 6 insertions(+) diff --git a/toolkit/types/cvss/cvss_v4.go b/toolkit/types/cvss/cvss_v4.go index bc7426ba9..d9af385a9 100644 --- a/toolkit/types/cvss/cvss_v4.go +++ b/toolkit/types/cvss/cvss_v4.go @@ -27,6 +27,11 @@ 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()) + } + } return nil } diff --git a/toolkit/types/cvss/cvss_v4_test.go b/toolkit/types/cvss/cvss_v4_test.go index 4ac8074b4..ee5e4b0ef 100644 --- a/toolkit/types/cvss/cvss_v4_test.go +++ b/toolkit/types/cvss/cvss_v4_test.go @@ -18,6 +18,7 @@ func TestV4(t *testing.T) { {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}, } Error[V4, V4Metric, *V4](t, tcs) }) From 7b31028ba3e9fd2fb09f8a0c06cd3407433149e2 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Wed, 31 Jan 2024 10:22:49 -0600 Subject: [PATCH 06/10] cvss: add duplicate prefix tests Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss_v3_test.go | 1 + toolkit/types/cvss/cvss_v4_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/toolkit/types/cvss/cvss_v3_test.go b/toolkit/types/cvss/cvss_v3_test.go index fc5579b6f..ae7abad50 100644 --- a/toolkit/types/cvss/cvss_v3_test.go +++ b/toolkit/types/cvss/cvss_v3_test.go @@ -24,6 +24,7 @@ func TestV3(t *testing.T) { {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) }) diff --git a/toolkit/types/cvss/cvss_v4_test.go b/toolkit/types/cvss/cvss_v4_test.go index ee5e4b0ef..652eb642a 100644 --- a/toolkit/types/cvss/cvss_v4_test.go +++ b/toolkit/types/cvss/cvss_v4_test.go @@ -19,6 +19,7 @@ func TestV4(t *testing.T) { {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}, } Error[V4, V4Metric, *V4](t, tcs) }) From 18c97927625329359a4ae274b98e8ce61129cb71 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Wed, 31 Jan 2024 10:24:00 -0600 Subject: [PATCH 07/10] cvss: reduce allocations during parsing Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss.go | 96 +++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git a/toolkit/types/cvss/cvss.go b/toolkit/types/cvss/cvss.go index 5fec05652..8cf8867de 100644 --- a/toolkit/types/cvss/cvss.go +++ b/toolkit/types/cvss/cvss.go @@ -38,6 +38,7 @@ import ( "encoding" "errors" "fmt" + "math/bits" "strings" ) @@ -150,17 +151,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 +178,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 +190,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,63 +206,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) } if err := ver(val); err != nil { return fmt.Errorf("%w: %w", ErrMalformedVector, err) } - // Append a bogus Metric to the seen list to keep everything - // organized. - seen = append(seen, -1) + 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: From 8a8722cc00d5be886df7c6c68815f2d3e4936346 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Wed, 31 Jan 2024 13:00:51 -0600 Subject: [PATCH 08/10] cvss: add `groups` method to `Vector` This allows the package to be able to reason about metrics group-wise. Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss.go | 3 +++ toolkit/types/cvss/cvss_v2.go | 14 ++++++++++++++ toolkit/types/cvss/cvss_v3.go | 14 ++++++++++++++ toolkit/types/cvss/cvss_v4.go | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/toolkit/types/cvss/cvss.go b/toolkit/types/cvss/cvss.go index 8cf8867de..88bd9d923 100644 --- a/toolkit/types/cvss/cvss.go +++ b/toolkit/types/cvss/cvss.go @@ -324,6 +324,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 d477ff6fc..3db6840ee 100644 --- a/toolkit/types/cvss/cvss_v2.go +++ b/toolkit/types/cvss/cvss_v2.go @@ -138,6 +138,20 @@ func (v *V2) getScore(m V2Metric) byte { 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_v3.go b/toolkit/types/cvss/cvss_v3.go index 3eafaca27..8c2a56779 100644 --- a/toolkit/types/cvss/cvss_v3.go +++ b/toolkit/types/cvss/cvss_v3.go @@ -93,6 +93,20 @@ func (v *V3) getScore(m V3Metric) byte { 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)] diff --git a/toolkit/types/cvss/cvss_v4.go b/toolkit/types/cvss/cvss_v4.go index d9af385a9..20b165e99 100644 --- a/toolkit/types/cvss/cvss_v4.go +++ b/toolkit/types/cvss/cvss_v4.go @@ -137,6 +137,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. From 068edb66d4f4fcde36f6c6ca407615d061daf4ec Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Wed, 31 Jan 2024 13:02:25 -0600 Subject: [PATCH 09/10] cvss: fix v2 group-wise marshaling behavior Section 2.4 of the standard implies that a vector should have metrics with not defined values included if a metric in the group has a defined value. One might also say that the environmental group implies the temporal group, but that's quite annoying and the standard should say that if it wants to say that. Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss.go | 45 ++++++++++++++++++++---------- toolkit/types/cvss/cvss_v2.go | 5 +++- toolkit/types/cvss/cvss_v2_test.go | 12 ++++---- 3 files changed, 41 insertions(+), 21 deletions(-) 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) }) From 34c3319dc49eca8084acb8ef8d35de972580d26a Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Thu, 9 May 2024 14:10:17 -0500 Subject: [PATCH 10/10] cvss: add additional check for malformed input Signed-off-by: Hank Donnay --- toolkit/types/cvss/cvss_v2.go | 8 ++++++++ toolkit/types/cvss/cvss_v2_test.go | 2 ++ toolkit/types/cvss/cvss_v3.go | 8 ++++++++ toolkit/types/cvss/cvss_v4.go | 8 ++++++++ toolkit/types/cvss/cvss_v4_test.go | 1 + 5 files changed, 27 insertions(+) diff --git a/toolkit/types/cvss/cvss_v2.go b/toolkit/types/cvss/cvss_v2.go index 0b4a32521..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" @@ -36,6 +37,13 @@ func (v *V2) UnmarshalText(text []byte) error { 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 } diff --git a/toolkit/types/cvss/cvss_v2_test.go b/toolkit/types/cvss/cvss_v2_test.go index beb6ec01c..97c3564a5 100644 --- a/toolkit/types/cvss/cvss_v2_test.go +++ b/toolkit/types/cvss/cvss_v2_test.go @@ -14,6 +14,8 @@ func TestV2(t *testing.T) { {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) }) diff --git a/toolkit/types/cvss/cvss_v3.go b/toolkit/types/cvss/cvss_v3.go index 8c2a56779..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" @@ -53,6 +54,13 @@ func (v *V3) UnmarshalText(text []byte) error { 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 } diff --git a/toolkit/types/cvss/cvss_v4.go b/toolkit/types/cvss/cvss_v4.go index 20b165e99..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" @@ -32,6 +33,13 @@ func (v *V4) UnmarshalText(text []byte) error { 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 } diff --git a/toolkit/types/cvss/cvss_v4_test.go b/toolkit/types/cvss/cvss_v4_test.go index 652eb642a..2f9fc20bc 100644 --- a/toolkit/types/cvss/cvss_v4_test.go +++ b/toolkit/types/cvss/cvss_v4_test.go @@ -20,6 +20,7 @@ func TestV4(t *testing.T) { {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) })