go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers-sdk/v1/upstream/mvd/cvss/cvss.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package cvss
     5  
     6  import (
     7  	"errors"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/rs/zerolog/log"
    13  )
    14  
    15  //go:generate protoc --proto_path=. --go_out=. --go_opt=paths=source_relative cvss.proto
    16  //go:generate go run golang.org/x/tools/cmd/stringer -type=Severity
    17  
    18  var Metrics map[string][]string
    19  
    20  // 5.8/AV:N/AC:M/Au:N/C:P/I:P/A:N
    21  func init() {
    22  	// defines the valid metrics
    23  	// CVSS v3 https://www.first.org/cvss/specification-document#2-Base-Metrics
    24  	// CVSS v2 https://www.first.org/cvss/v2/guide
    25  	Metrics = map[string][]string{
    26  		// Version
    27  		"CVSS": {"3.0", "3.1"},
    28  		// Attack Vector
    29  		"AV": {"N", "A", "L", "P"},
    30  		// Attack Complexity
    31  		"AC": {
    32  			// CVSS 3.0
    33  			"L", "H",
    34  			// CVSS 2.0
    35  			"M",
    36  		},
    37  		// Privileges Required
    38  		"PR": {"N", "L", "H"},
    39  		// User Interaction
    40  		"UI": {"N", "R"},
    41  		// Scope
    42  		"S": {"U", "C"},
    43  		// Confidentiality Impact
    44  		"C": {
    45  			// CVSS 3.0
    46  			"H", "L", "N",
    47  			// CVSS 2.0
    48  			"P", "C",
    49  		},
    50  		//  Integrity Impact
    51  		"I": {
    52  			// CVSS 3.0
    53  			"H", "L", "N",
    54  			// CVSS 2.0
    55  			"P", "C",
    56  		},
    57  		// Availability Impact
    58  		"A": {
    59  			// CVSS 3.0
    60  			"H", "L", "N",
    61  			// CVSS 2.0
    62  			"P", "C",
    63  		},
    64  		// Exploit Code Maturity
    65  		"E": {
    66  			// CVSS 3.0
    67  			"X", "H", "F", "P", "U",
    68  			// CVSS 2.0
    69  			"POC", "ND",
    70  		},
    71  		// Remediation Level
    72  		"RL": {
    73  			// CVSS 3.0
    74  			"X", "U", "W", "T", "O",
    75  			// CVSS 2.0
    76  			"OF", "TF", "ND",
    77  		},
    78  		// Report Confidence
    79  		"RC": {
    80  			// CVSS 3.0
    81  			"X", "C", "R", "U",
    82  			// CVSS 2.0
    83  			"UC", "UR", "ND",
    84  		},
    85  		// Confidentiality Requirement
    86  		"CR": {
    87  			// CVSS 3.0
    88  			"X", "H", "M", "L",
    89  			// CVSS 2.0
    90  			"ND",
    91  		},
    92  		// Integrity Req
    93  		"IR": {
    94  			// CVSS 3.0
    95  			"X", "H", "M", "L",
    96  			// CVSS 2.0
    97  			"ND",
    98  		},
    99  		// Availability Req
   100  		"AR": {
   101  			// CVSS 3.0
   102  			"X", "H", "M", "L",
   103  			// CVSS 2.0
   104  			"ND",
   105  		},
   106  
   107  		// Authentication, CVSS 2.0 only
   108  		// https://www.first.org/cvss/v2/guide#2-1-3-Authentication-Au
   109  		"AU": {
   110  			"M", "S", "N",
   111  		},
   112  		// https://www.first.org/cvss/v2/guide#2-3-1-Collateral-Damage-Potential-CDP
   113  		"CDP": {
   114  			"N", "L", "LM", "MH", "H", "ND",
   115  		},
   116  		// https://www.first.org/cvss/v2/guide#2-3-2-Target-Distribution-TD
   117  		"TD": {
   118  			"M", "L", "M", "H", "ND",
   119  		},
   120  	}
   121  }
   122  
   123  const NoneVector = "0.0/CVSS:3.0"
   124  
   125  var CVSS_VERSION = regexp.MustCompile(`^.*\/CVSS:([\d.]+)(?:\/.*)*$`)
   126  
   127  func New(vector string) (*Cvss, error) {
   128  	if len(vector) == 0 {
   129  		return nil, errors.New("vector cannot be empty")
   130  	}
   131  
   132  	// trim whitespace
   133  	vector = strings.TrimSpace(vector)
   134  
   135  	c := &Cvss{Vector: vector}
   136  
   137  	// ensure score field is set
   138  	c.Score = c.DetermineScore()
   139  
   140  	// check that the vector is parsable and the metrics are correct
   141  	if !c.Verify() {
   142  		return nil, errors.New("cvss vector is not parsable or valid: " + vector)
   143  	}
   144  
   145  	return c, nil
   146  }
   147  
   148  func (c *Cvss) Version() string {
   149  	m := CVSS_VERSION.FindStringSubmatch(c.Vector)
   150  
   151  	if len(m) == 2 {
   152  		return m[1]
   153  	} else {
   154  		return "2.0"
   155  	}
   156  }
   157  
   158  func (c *Cvss) DetermineScore() float32 {
   159  	var err error
   160  	vector := c.Vector
   161  	pairs := strings.Split(vector, "/")
   162  
   163  	if len(pairs) < 1 {
   164  		c.Score = float32(0.0)
   165  	}
   166  
   167  	// first entry includes the score
   168  	var score float64
   169  	if score, err = strconv.ParseFloat(pairs[0], 32); err != nil {
   170  		// error handling, fallback to default value
   171  		return float32(0.0)
   172  	}
   173  
   174  	c.Score = float32(score)
   175  	return c.Score
   176  }
   177  
   178  func (c *Cvss) Metrics() (map[string]string, error) {
   179  	values := make(map[string]string)
   180  
   181  	vector := c.Vector
   182  	pairs := strings.Split(vector, "/")
   183  
   184  	if len(pairs) < 1 {
   185  		return nil, errors.New("invalid cvss string: " + vector)
   186  	}
   187  
   188  	// parse the key values
   189  	for i, entry := range pairs {
   190  		// ignore first entry which is a score, do not save it here to avoid side-effects
   191  		// functionality has moved ParseScore
   192  		if i == 0 {
   193  			continue
   194  		}
   195  
   196  		// likely an entry  with trailing slash (6.5/AV:N/AC:L/Au:S/C:P/I:P/A:P/), that is okay
   197  		if len(entry) == 0 {
   198  			continue
   199  		}
   200  
   201  		// split key value
   202  		kv := strings.Split(entry, ":")
   203  		if len(kv) < 2 {
   204  			log.Debug().Str("vector", vector).Msg("could not parse vector properly")
   205  		} else {
   206  			values[strings.ToUpper(kv[0])] = strings.ToUpper(kv[1])
   207  		}
   208  	}
   209  
   210  	return values, nil
   211  }
   212  
   213  // Severity converts the CVSS Score (0.0 - 10.0) as specified in CVSS v3.0
   214  // specification (https://www.first.org/cvss/specification-document) table 14
   215  // to qualitative severity rating scale
   216  func (c *Cvss) Severity() Severity {
   217  	return Rating(c.Score)
   218  }
   219  
   220  func (c *Cvss) Verify() bool {
   221  	values, err := c.Metrics()
   222  	if err != nil {
   223  		return false
   224  	}
   225  
   226  	for k, v := range values {
   227  		values, ok := Metrics[k]
   228  		if !ok {
   229  			return false
   230  		}
   231  		if !contains(values, v) {
   232  			return false
   233  		}
   234  	}
   235  	return true
   236  }
   237  
   238  func contains(slice []string, search string) bool {
   239  	for _, value := range slice {
   240  		if value == search {
   241  			return true
   242  		}
   243  	}
   244  	return false
   245  }
   246  
   247  // Compare returns an integer comparing two cvss scores
   248  // The result will be 0 if a==b, -1 if a < b, and +1 if a > b
   249  func (c *Cvss) Compare(d *Cvss) int {
   250  	if c.Score == d.Score {
   251  		return 0
   252  	} else if c.Score < d.Score {
   253  		return -1
   254  	} else {
   255  		return 1
   256  	}
   257  }
   258  
   259  func Rating(score float32) Severity {
   260  	switch {
   261  	case score == 0.0:
   262  		return None
   263  	case score > 0 && score < 4.0:
   264  		return Low
   265  	case score >= 4.0 && score < 7.0:
   266  		return Medium
   267  	case score >= 7.0 && score < 9.0:
   268  		return High
   269  	case score >= 9.0:
   270  		return Critical
   271  	}
   272  	// negative numbers may be used for no-parsable cvss vectors
   273  	return Unknown
   274  }
   275  
   276  // Severity defines the cvss v3 range
   277  // in addition is defines an additional state unknown
   278  // iota is sorted by criticality to ease easy int comparison to detect the severity with the
   279  // highest criticality
   280  type Severity int
   281  
   282  const (
   283  	Unknown  Severity = iota // could not be determined
   284  	None                     // 0.0, e.g. mapped ubuntu negligible is mapped to none
   285  	Low                      // 0.1 - 3.9
   286  	Medium                   // 4.0 - 6.9
   287  	High                     // 7.0 - 8.9
   288  	Critical                 // 9.0 - 10.0
   289  )
   290  
   291  func MaxScore(cvsslist []*Cvss) (*Cvss, error) {
   292  	none, _ := New(NoneVector)
   293  
   294  	// no entry, no return :-)
   295  	if len(cvsslist) == 0 {
   296  		return none, nil
   297  	}
   298  
   299  	res := cvsslist[0]
   300  
   301  	// easy, we just have one entry
   302  	if len(cvsslist) == 1 {
   303  		return res, nil
   304  	}
   305  
   306  	// fun starts, we need to compare cvss scores now
   307  	max := res
   308  	maxScore, err := New(max.Vector)
   309  	if err != nil {
   310  		return none, err
   311  	}
   312  
   313  	for i := 1; i < len(cvsslist); i++ {
   314  		entry := cvsslist[i]
   315  		vector := entry.Vector
   316  		score, err := New(vector)
   317  		if err != nil {
   318  			return none, err
   319  		}
   320  
   321  		if maxScore.Compare(score) < 0 {
   322  			max = entry
   323  			maxScore = score
   324  		}
   325  	}
   326  
   327  	return max, nil
   328  }