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 }