Skip to content

Commit

Permalink
Merge pull request #22 from goark/debug-and-refactoring
Browse files Browse the repository at this point in the history
Fixed validation of v2 metrics (issue #21)
  • Loading branch information
spiegel-im-spiegel authored Jan 30, 2023
2 parents 411bdf4 + cd1edf5 commit 78fad18
Show file tree
Hide file tree
Showing 8 changed files with 581 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ ref: [sample.go](https://github.com/goark/go-cvss/blob/master/sample/sample.go)

## Reference

- [CVSS v2 Complete Documentation](https://www.first.org/cvss/v2/guide)
- [CVSS v3.0 Specification Document](https://www.first.org/cvss/v3.0/specification-document)
- [CVSS v3.1 Specification Document](https://www.first.org/cvss/v3.1/specification-document)

Expand Down
97 changes: 87 additions & 10 deletions v2/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type Metrics struct {
E Exploitability
RL RemediationLevel
RC ReportConfidence
CDP CollateralDamagePotential
TD TargetDistribution
CR ConfidentialityRequirement
IR IntegrityRequirement
AR AvailabilityRequirement
names map[string]bool
}

Expand All @@ -35,6 +40,11 @@ func NewMetrics() *Metrics {
E: ExploitabilityNotDefined,
RL: RemediationLevelNotDefined,
RC: ReportConfidenceNotDefined,
CDP: CollateralDamagePotentialNotDefined,
TD: TargetDistributionNotDefined,
CR: ConfidentialityRequirementNotDefined,
IR: IntegrityRequirementNotDefined,
AR: AvailabilityRequirementNotDefined,
names: map[string]bool{},
}
}
Expand All @@ -56,7 +66,7 @@ func Decode(vector string) (*Metrics, error) {
if metrics.names[name] {
return nil, errs.Wrap(cvsserr.ErrSameMetric, errs.WithContext("metric", metric))
}
switch strings.ToUpper(metric[0]) {
switch name {
case "AV": // Access Vector
metrics.AV = GetAccessVector(metric[1])
if metrics.AV == AccessVectorUnknown {
Expand All @@ -67,7 +77,7 @@ func Decode(vector string) (*Metrics, error) {
if metrics.AC == AccessComplexityUnknown {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "AU": // Authentication
case "Au": // Authentication
metrics.Au = GetAuthentication(metric[1])
if metrics.Au == AuthenticationUnknown {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
Expand All @@ -89,12 +99,46 @@ func Decode(vector string) (*Metrics, error) {
}
case "E": // Exploitability
metrics.E = GetExploitability(metric[1])
if metrics.E == ExploitabilityInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "RL": // RemediationLevel
metrics.RL = GetRemediationLevel(metric[1])
if metrics.RL == RemediationLevelInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "RC": // RemediationLevel
metrics.RC = GetReportConfidence(metric[1])
if metrics.RC == ReportConfidenceInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "CDP": // CollateralDamagePotential
metrics.CDP = GetCollateralDamagePotential(metric[1])
if metrics.CDP == CollateralDamagePotentialInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "TD": // TargetDistribution
metrics.TD = GetTargetDistribution(metric[1])
if metrics.TD == TargetDistributionInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "CR": // ConfidentialityRequirement
metrics.CR = GetConfidentialityRequirement(metric[1])
if metrics.CR == ConfidentialityRequirementInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "IR": // IntegrityRequirement
metrics.IR = GetIntegrityRequirement(metric[1])
if metrics.IR == IntegrityRequirementInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
case "AR": // AvailabilityRequirement
metrics.AR = GetAvailabilityRequirement(metric[1])
if metrics.AR == AvailabilityRequirementInvalid {
return nil, errs.Wrap(cvsserr.ErrInvalidValue, errs.WithContext("metric", metric))
}
default:
return nil, errs.Wrap(cvsserr.ErrInvalidVector, errs.WithContext("vector", value))
return nil, errs.Wrap(cvsserr.ErrNotSupportMetric, errs.WithContext("vector", value))
}
metrics.names[name] = true
}
Expand All @@ -117,15 +161,29 @@ func (m *Metrics) Encode() (string, error) {
if m.E.IsDefined() {
r.WriteString(fmt.Sprintf("/E:%v", m.E)) // Exploitability
}

if m.RL.IsDefined() {
r.WriteString(fmt.Sprintf("/RL:%v", m.RL)) // Remediation Level
}

if m.RC.IsDefined() {
r.WriteString(fmt.Sprintf("/RC:%v", m.RC)) // Report Confidence
}

if m.CDP.IsDefined() {
r.WriteString(fmt.Sprintf("/CDP:%v", m.CDP)) // Collateral Damage Potential
}
if m.TD.IsDefined() {
r.WriteString(fmt.Sprintf("/TD:%v", m.TD)) // Target Distribution
}
if m.CR.IsDefined() {
r.WriteString(fmt.Sprintf("/CR:%v", m.CR)) // Confidentiality Requirement
}
if m.IR.IsDefined() {
r.WriteString(fmt.Sprintf("/IR:%v", m.IR)) // Integrity Requirement
}
if m.AR.IsDefined() {
r.WriteString(fmt.Sprintf("/AR:%v", m.AR)) // Availability Requirement
}

return r.String(), nil
}

Expand All @@ -141,7 +199,9 @@ func (m *Metrics) GetError() error {
return errs.Wrap(cvsserr.ErrUndefinedMetric)
}
switch true {
case !m.AV.IsDefined(), !m.AC.IsDefined(), !m.Au.IsDefined(), !m.C.IsDefined(), !m.I.IsDefined(), !m.A.IsDefined(), !m.E.IsValid(), !m.RL.IsValid(), !m.RC.IsValid():
case !m.AV.IsDefined(), !m.AC.IsDefined(), !m.Au.IsDefined(), !m.C.IsDefined(), !m.I.IsDefined(), !m.A.IsDefined(),
!m.E.IsValid(), !m.RL.IsValid(), !m.RC.IsValid(),
!m.CDP.IsValid(), !m.TD.IsValid(), !m.CR.IsValid(), !m.IR.IsValid(), !m.AR.IsValid():
return errs.Wrap(cvsserr.ErrUndefinedMetric)
default:
return nil
Expand All @@ -153,15 +213,19 @@ func (m *Metrics) Score() float64 {
if err := m.GetError(); err != nil {
return 0
}

impact := 10.41 * (1 - (1-m.C.Value())*(1-m.I.Value())*(1-m.A.Value()))
return m.baseScore(impact)
}

func (m *Metrics) baseScore(impact float64) float64 {
if err := m.GetError(); err != nil {
return 0
}
exploitability := 20 * m.AV.Value() * m.AC.Value() * m.Au.Value()
fimpact := 1.176

if impact == 0 {
fimpact = 0
}

return math.Round(((0.6*impact)+(0.4*exploitability)-1.5)*fimpact*10) / 10
}

Expand All @@ -180,8 +244,21 @@ func (m *Metrics) GetSeverity() Severity {
}
}

// TemporalScore returns score of Temporal metrics
func (m *Metrics) TemporalScore() float64 {
return math.Round(m.Score()*m.E.Value()*m.RL.Value()*m.RC.Value()*10) / 10
return m.temporalScore(m.Score())
}

func (m *Metrics) temporalScore(baseScore float64) float64 {
return math.Round(baseScore*m.E.Value()*m.RL.Value()*m.RC.Value()*10) / 10
}

// EnvironmentalScore returns score of Environmental metrics
func (m *Metrics) EnvironmentalScore() float64 {
adjustedImpact := math.Min(10.0, 10.41*(1-(1-m.C.Value()*m.CR.Value())*(1-m.I.Value()*m.IR.Value())*(1-m.A.Value()*m.AR.Value())))
baseScore := m.baseScore(adjustedImpact)
adjustedTemporal := m.temporalScore(baseScore)
return math.Round((adjustedTemporal+(10-adjustedTemporal)*m.CDP.Value()*m.TD.Value())*10) / 10
}

/* Copyright 2022 luxifer */
Expand Down
100 changes: 99 additions & 1 deletion v2/base/base_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
package base

import "testing"
import (
"errors"
"testing"

"github.com/goark/go-cvss/cvsserr"
)

func TestValidation(t *testing.T) {
testCases := []struct {
vec string
err error
}{
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:", err: cvsserr.ErrInvalidVector},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/:", err: cvsserr.ErrInvalidVector},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/:X", err: cvsserr.ErrInvalidVector},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:0", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:0/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:0/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:0/E:U/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:0/A:C/E:U/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:0/I:N/A:C/E:U/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:0/C:C/I:N/A:C/E:U/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:0/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:0/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND/CDP:H/TD:H/CR:M/IR:M/AR:0", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND/CDP:H/TD:H/CR:M/IR:0/AR:H", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND/CDP:H/TD:H/CR:0/IR:M/AR:H", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND/CDP:H/TD:0/CR:M/IR:M/AR:H", err: cvsserr.ErrInvalidValue},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND/CDP:0/TD:H/CR:M/IR:M/AR:H", err: cvsserr.ErrInvalidValue},
{vec: "av:n/ac:l/au:n/c:n/i:n/a:c/e:u/rl:nd/rc:nd/cdp:h/td:h/cr:m/ir:m/ar:h", err: cvsserr.ErrNotSupportMetric},
{vec: "AV:N/AC:L/AU:N/C:N/I:N/A:C/E:U/RL:ND/RC:ND/CDP:H/TD:H/CR:M/IR:M/AR:H", err: cvsserr.ErrNotSupportMetric},
{vec: "AV:N/AC:L/Au:N/C:N/I:N/A:C", err: nil},
{vec: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND", err: nil},
}

for _, tc := range testCases {
_, err := Decode(tc.vec)
if !errors.Is(err, tc.err) {
t.Errorf("Decode(%s) = \"%+v\", want \"%v\".", tc.vec, err, tc.err)
}
}
}

func TestScore(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -28,6 +69,11 @@ func TestScore(t *testing.T) {
vector: "AV:N/AC:H/Au:M/C:C/I:N/A:C/E:U/RL:ND/RC:ND",
want: 6.2,
},
{
name: "test2",
vector: "AV:N/AC:L/Au:N/C:N/I:N/A:C",
want: 7.8,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -44,6 +90,57 @@ func TestScore(t *testing.T) {
}
}

func TestEnvScore(t *testing.T) {
tests := []struct {
name string
vector string
base float64
temp float64
env float64
}{
{
name: "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",
base: 7.8,
temp: 6.4,
env: 9.2,
},
{
name: "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",
base: 10.0,
temp: 8.3,
env: 9.0,
},
{
name: "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",
base: 6.2,
temp: 4.9,
env: 7.5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m, err := Decode(tt.vector)
if err != nil {
t.Error(err)
} else {
if got := m.Score(); got != tt.base {
t.Errorf("Metrics.Score() = %v, want %v", got, tt.base)
}
if got := m.TemporalScore(); got != tt.temp {
t.Errorf("Metrics.TemporalScore() = %v, want %v", got, tt.env)
}
if got := m.EnvironmentalScore(); got != tt.env {
t.Errorf("Metrics.EnvironmentalScore() = %v, want %v", got, tt.temp)
}
}

})
}
}

func TestDecode(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -461,3 +558,4 @@ func TestEncode(t *testing.T) {
}

/* Copyright 2022 luxifer */
/* Contributed by Spiegel, 2023 */
77 changes: 77 additions & 0 deletions v2/base/metrics-ar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package base

// AvailabilityRequirement is metric type for Temporal Metrics
type AvailabilityRequirement int

// Constant of AvailabilityRequirement result
const (
AvailabilityRequirementInvalid AvailabilityRequirement = iota
AvailabilityRequirementNotDefined
AvailabilityRequirementLow
AvailabilityRequirementMedium
AvailabilityRequirementHigh
)

var availabilityRequirementMap = map[AvailabilityRequirement]string{
AvailabilityRequirementNotDefined: "ND",
AvailabilityRequirementLow: "L",
AvailabilityRequirementMedium: "M",
AvailabilityRequirementHigh: "H",
}

var availabilityRequirementValueMap = map[AvailabilityRequirement]float64{
AvailabilityRequirementNotDefined: 1.0,
AvailabilityRequirementLow: 0.5,
AvailabilityRequirementMedium: 1.0,
AvailabilityRequirementHigh: 1.51,
}

// GetAvailabilityRequirement returns result of AvailabilityRequirement metric
func GetAvailabilityRequirement(s string) AvailabilityRequirement {
for k, v := range availabilityRequirementMap {
if s == v {
return k
}
}
return AvailabilityRequirementInvalid
}

func (ar AvailabilityRequirement) String() string {
if s, ok := availabilityRequirementMap[ar]; ok {
return s
}
return ""
}

// Value returns value of AvailabilityRequirement metric
func (ar AvailabilityRequirement) Value() float64 {
if v, ok := availabilityRequirementValueMap[ar]; ok {
return v
}
return 0
}

// IsValid returns false if invalid result value of metric
func (ar AvailabilityRequirement) IsValid() bool {
return ar != AvailabilityRequirementInvalid
}

// IsDefined returns false if undefined result value of metric
func (ar AvailabilityRequirement) IsDefined() bool {
return ar.IsValid() && ar != AvailabilityRequirementNotDefined
}

/* Copyright 2023 Spiegel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Loading

0 comments on commit 78fad18

Please sign in to comment.