aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cvss/v31vector.go295
-rw-r--r--cvss/v31vector_test.go54
2 files changed, 347 insertions, 2 deletions
diff --git a/cvss/v31vector.go b/cvss/v31vector.go
index f286ea0..a9d5205 100644
--- a/cvss/v31vector.go
+++ b/cvss/v31vector.go
@@ -1,6 +1,7 @@
package cvss
import (
+ "math"
"regexp"
"strings"
)
@@ -72,13 +73,13 @@ func newV31Vector(s string) (v31Vector, error) {
// if err := json.Unmarshal(b, &s); err != nil {
// return err
// }
-//
+//
// // parse vector, check for error
// r, err := newV31Vector(s)
// if err != nil {
// return err
// }
-//
+//
// // save result, return success
// *me = r
// return nil
@@ -117,3 +118,293 @@ func isV31VectorString(s string) bool {
(s[:len(v31Prefix)] == v31Prefix) &&
v31VecRe.MatchString(s)
}
+
+// roundup implemention (from CVSS v3.1 spec, appendix A)
+func roundup(val float64) float64 {
+ return math.Ceil(10.0 * val) / 10.0
+}
+
+// Return numerical scores for this vector.
+func (v v31Vector) Scores() (Scores, error) {
+ attackVector := 0.0
+ attackComplexity := 0.0
+ privsRequired := 0.0
+ userInteraction := 0.0
+ scopeChanged := false
+ conf := 0.0
+ integ := 0.0
+ avail := 0.0
+ ecm := 1.0
+ remediationLevel := 1.0
+ reportConfidence := 1.0
+ confReq := 1.0
+ availReq := 1.0
+ integReq := 1.0
+
+ modAttackVector := 0.0
+ modAttackComplexity := 0.0
+ modPrivsRequired := 0.0
+ modUserInteraction := 0.0
+ modScopeChanged := false
+ modConf := 0.0
+ modInteg := 0.0
+ modAvail := 0.0
+
+ keys := make(map[Key]v3Metric)
+
+ for _, m := range([]v3Metric(v)) {
+ keys[m.Key()] = m
+
+ switch m {
+ case v3AVNetwork: // AV:N
+ attackVector = 0.85
+ case v3AVAdjacentNetwork: // AV:A
+ attackVector = 0.62
+ case v3AVLocal: // AV:L
+ attackVector = 0.55
+ case v3AVPhysical: // AV:P
+ attackVector = 0.2
+
+ case v3ACLow: // AC:L
+ attackComplexity = 0.77
+ case v3ACHigh: // AC:H
+ attackComplexity = 0.44
+
+ case v3PRNone: // PR:N
+ privsRequired = 0.85
+ case v3PRLow: // PR:L
+ privsRequired = 0.62
+ case v3PRHigh: // PR:H
+ privsRequired = 0.27
+
+ case v3UINone: // UI:N
+ userInteraction = 0.85
+ case v3UIRequired: // UI:R
+ userInteraction = 0.62
+
+ case v3SUnchanged: // S:U
+ scopeChanged = false
+ case v3SChanged: // S:C
+ scopeChanged = true
+
+ case v3CHigh: // C:H
+ conf = 0.56
+ case v3CLow: // C:L
+ conf = 0.22
+ case v3CNone: // C:N
+ conf = 0.0
+
+ case v3IHigh: // I:H
+ integ = 0.56
+ case v3ILow: // I:L
+ integ = 0.22
+ case v3INone: // I:N
+ integ = 0.0
+
+ case v3AHigh: // A:H
+ avail = 0.56
+ case v3ALow: // A:L
+ avail = 0.22
+ case v3ANone: // A:N
+ avail = 0.0
+
+ case v3ENotDefined: // E:X
+ ecm = 1.0
+ case v3EHigh: // E:H
+ ecm = 1.0
+ case v3EFunctional: // E:F
+ ecm = 0.97
+ case v3EProofOfConcept: // E:P
+ ecm = 0.94
+ case v3EUnproven: // E:U
+ ecm = 0.91
+
+ case v3RLNotDefined: // RL:X
+ remediationLevel = 1.0
+ case v3RLUnavailable: // RL:U
+ remediationLevel = 1.0
+ case v3RLWorkaround: // RL:W
+ remediationLevel = 0.97
+ case v3RLTemporaryFix: // RL:T
+ remediationLevel = 0.96
+ case v3RLOfficialFix: // RL:O
+ remediationLevel = 0.95
+
+ case v3RCNotDefined: // RC:X
+ reportConfidence = 1.0
+ case v3RCConfirmed: // RC:C
+ reportConfidence = 1.0
+ case v3RCReasonable: // RC:R
+ reportConfidence = 0.96
+ case v3RCUnknown: // RC:U
+ reportConfidence = 0.92
+
+ case v3CRNotDefined: // CR:X
+ confReq = 1.0
+ case v3CRHigh: // CR:H
+ confReq = 1.5
+ case v3CRMedium: // CR:M
+ confReq = 1.0
+ case v3CRLow: // CR:L
+ confReq = 0.5
+
+ case v3IRNotDefined: // IR:X
+ integReq = 1.0
+ case v3IRHigh: // IR:H
+ integReq = 1.5
+ case v3IRMedium: // IR:M
+ integReq = 1.0
+ case v3IRLow: // IR:L
+ integReq = 0.5
+
+ case v3ARNotDefined: // AR:X
+ availReq = 1.0
+ case v3ARHigh: // AR:H
+ availReq = 1.5
+ case v3ARMedium: // AR:M
+ availReq = 1.0
+ case v3ARLow: // AR:L
+ availReq = 0.5
+
+ case v3MAVNotDefined: // MAV:X
+ modAttackVector = 0.0
+ case v3MAVNetwork: // MAV:N
+ modAttackVector = 0.85
+ case v3MAVAdjacentNetwork: // MAV:A
+ modAttackVector = 0.62
+ case v3MAVLocal: // MAV:L
+ modAttackVector = 0.55
+ case v3MAVPhysical: // MAV:P
+ modAttackVector = 0.2
+
+ case v3MACNotDefined: // MAC:X
+ modAttackComplexity = 0.0
+ case v3MACLow: // MAC:L
+ modAttackComplexity = 0.77
+ case v3MACHigh: // MAC:H
+ modAttackComplexity = 0.44
+
+ case v3MPRNotDefined: // MPR:X
+ modPrivsRequired = 0.0
+ case v3MPRLow: // MPR:L
+ modPrivsRequired = 0.62
+ case v3MPRHigh: // MPR:H
+ modPrivsRequired = 0.27
+
+ case v3MUINotDefined: // MUI:X
+ modUserInteraction = 0.85
+ case v3MUINone: // MUI:N
+ modUserInteraction = 0.85
+ case v3MUIRequired: // MUI:R
+ modUserInteraction = 0.62
+
+ case v3MSNotDefined: // MS:X
+ modScopeChanged = false
+ case v3MSUnchanged: // MS:U
+ modScopeChanged = false
+ case v3MSChanged: // MS:C
+ modScopeChanged = true
+
+ case v3MCNotDefined: // MC:X
+ modConf = 0.0
+ case v3MCHigh: // MC:H
+ modConf = 0.56
+ case v3MCLow: // MC:L
+ modConf = 0.22
+ case v3MCNone: // MC:N
+ modConf = 0.0
+
+ case v3MINotDefined: // MI:X
+ modInteg = 0.0
+ case v3MIHigh: // MI:H
+ modInteg = 0.56
+ case v3MILow: // MI:L
+ modInteg = 0.22
+ case v3MINone: // MI:N
+ modInteg = 0.0
+
+ case v3MANotDefined: // MA:X
+ modAvail = 0.0
+ case v3MAHigh: // MA:H
+ modAvail = 0.56
+ case v3MALow: // MA:L
+ modAvail = 0.22
+ case v3MANone: // MA:N
+ modAvail = 0.0
+ }
+ }
+
+ // calculate base score (CVSS v3.1 spec, section 7.1)
+ baseScore := 0.0
+ {
+ // calculate impact sub-score (cvss v3.1 spec, section 7.1)
+ iss := 1.0 - ((1.0 - conf) * (1.0 - integ) * (1.0 - avail))
+
+ // calculate impact
+ impact := 0.0
+ if scopeChanged {
+ // impact = 7.52 * (iss - 0.029) - 3.25 * math.Pow(iss - 0.02, 15)
+ impact = 6.42 * iss
+ } else {
+ impact = 6.42 * iss
+ }
+
+ // exploitability
+ expl := 8.22 * attackVector * attackComplexity * privsRequired * userInteraction
+
+ if impact <= 0.0 {
+ baseScore = 0
+ } else if scopeChanged {
+ baseScore = roundup(math.Min(1.08 * (impact + expl), 10.0))
+ } else {
+ baseScore = roundup(math.Min(impact + expl, 10.0))
+ }
+ }
+
+ // temporal score (CVSS v3.1 spec, section 7.2)
+ tempScore := roundup(baseScore * ecm * remediationLevel * reportConfidence)
+
+ // environmental score (CVSS v3.1 spec, section 7.3)
+ //
+ // MISS = Minimum(
+ // 1 - [(1 - ConfidentialityRequirement × ModifiedConfidentiality) × (1 - IntegrityRequirement × ModifiedIntegrity) × (1 - AvailabilityRequirement × ModifiedAvailability)],
+ // 0.915
+ // )
+ //
+
+ // environmental score (CVSS v3.1 spec, section 7.3)
+ envScore := 0.0
+ {
+ // modified impact sub score
+ miss := math.Min(
+ 1 - (1 - confReq * modConf) * (1 - integReq * modInteg) * (1 - availReq * modAvail),
+ 0.915,
+ )
+
+ // modified impact
+ // NOTE: exponent of 13 differs for CVSS v3.0 and CVSS v3.1
+ impact := 0.0
+ if modScopeChanged {
+ impact = 7.52 * (miss - 0.029) - 3.25 * math.Pow(miss * 0.9731 - 0.02, 13)
+ } else {
+ impact = 6.42 * miss
+ }
+
+ // modified exploitability
+ expl := 8.22 * modAttackVector * modAttackComplexity * modPrivsRequired * modUserInteraction
+
+ // calculate env score
+ if impact < 0.0 {
+ envScore = 0.0
+ } else if modScopeChanged {
+ // Roundup ( Roundup [Minimum (1.08 × [ModifiedImpact + ModifiedExploitability], 10) ] × ExploitCodeMaturity × RemediationLevel × ReportConfidence)
+ envScore = roundup(roundup(math.Min(1.08 * (impact + expl), 10.0)) * ecm * remediationLevel * reportConfidence)
+ } else {
+ // Roundup ( Roundup [Minimum ([ModifiedImpact + ModifiedExploitability], 10) ] × ExploitCodeMaturity × RemediationLevel × ReportConfidence)
+ envScore = roundup(roundup(math.Min((impact * expl), 10.0)) * ecm * remediationLevel * reportConfidence)
+ }
+ }
+
+ // build and return new scores
+ return NewScores(baseScore, tempScore, envScore)
+}
diff --git a/cvss/v31vector_test.go b/cvss/v31vector_test.go
index d05edca..86fb8eb 100644
--- a/cvss/v31vector_test.go
+++ b/cvss/v31vector_test.go
@@ -1,6 +1,7 @@
package cvss
import (
+ "reflect"
"testing"
)
@@ -559,3 +560,56 @@ func TestIsV31VectorString(t *testing.T) {
})
}
}
+
+func TestV31VectorScores(t *testing.T) {
+ tests := []struct {
+ name string // test name
+ val string // v3.1 vector string
+ exp []float64 // expected scores
+ } {{
+ name: "initial",
+ val: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
+ exp: []float64 { 0.0, 0.0, 0.0 },
+ }, {
+ name: "initial I:H",
+ val: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+ exp: []float64 { 9.8, 9.8, 0.0 },
+ }, {
+ name: "initial A:L",
+ val: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
+ exp: []float64 { 5.3, 5.3, 0.0 },
+ }, {
+ name: "AV:A",
+ val: "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
+ exp: []float64 { 6.3, 6.3, 0.0 },
+ }}
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create expected value
+ exp, err := NewScores(test.exp[0], test.exp[1], test.exp[2])
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // create vector
+ vec, err := newV31Vector(test.val)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get scores
+ got, err := vec.Scores()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if !reflect.DeepEqual(got, exp) {
+ t.Errorf("got %v, exp %v", got, exp)
+ }
+ })
+ }
+}