diff options
-rw-r--r-- | cvss/v31vector.go | 295 | ||||
-rw-r--r-- | cvss/v31vector_test.go | 54 |
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) + } + }) + } +} |