github.com/quay/claircore@v1.5.28/updater/osv/cvss.go (about) 1 package osv 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "strconv" 8 "strings" 9 10 "github.com/quay/zlog" 11 12 "github.com/quay/claircore" 13 ) 14 15 // FromCVSS3 is an attempt at an implementation of the scale and formulas 16 // described here: https://www.first.org/cvss/v3.1/specification-document#Qualitative-Severity-Rating-Scale 17 func fromCVSS3(ctx context.Context, s string) (sev claircore.Severity, err error) { 18 ms := strings.Split(strings.TrimRight(s, "/"), "/") // "m" as in "metric" 19 if !strings.HasPrefix(ms[0], "CVSS:3") { 20 return 0, fmt.Errorf("unknown label: %q", ms[0]) 21 } 22 ver, err := strconv.ParseInt(ms[0][7:], 10, 32) 23 if err != nil { 24 return 0, fmt.Errorf("unknown label: %q", ms[0]) 25 } 26 switch ver { 27 case 0, 1: 28 // As far as I can tell, the equations for calculating v3.0 and v3.1 29 // "base" scores are the same. 30 default: 31 zlog.Warn(ctx). 32 Str("version", ms[0]). 33 Msg("unknown version, interpreting as CVSSv3.1") 34 } 35 if len(ms) < 9 { 36 return 0, fmt.Errorf("bad vector: %q", s) 37 } 38 // Giant switch ahoy 39 var ns [8]float64 40 for _, m := range ms[1:] { 41 n, v, ok := strings.Cut(m, ":") 42 if !ok { 43 return 0, fmt.Errorf("bad metric: %q", m) 44 } 45 switch n { 46 // Base metrics: 47 case `AV`: 48 const i = 0 49 switch v { 50 case `N`: 51 ns[i] = 0.85 52 case `A`: 53 ns[i] = 0.62 54 case `L`: 55 ns[i] = 0.55 56 case `P`: 57 ns[i] = 0.2 58 default: 59 return 0, fmt.Errorf("bad metric value: %q", m) 60 } 61 case `AC`: 62 const i = 1 63 switch v { 64 case `L`: 65 ns[i] = 0.77 66 case `H`: 67 ns[i] = 0.44 68 default: 69 return 0, fmt.Errorf("bad metric value: %q", m) 70 } 71 case `PR`: 72 const i = 2 73 switch v { 74 case `N`: 75 ns[i] = 0.85 76 case `L`: 77 ns[i] = 0.62 // Fixup later 78 case `H`: 79 ns[i] = 0.27 // Fixup later 80 default: 81 return 0, fmt.Errorf("bad metric value: %q", m) 82 } 83 case `UI`: 84 const i = 3 85 switch v { 86 case `N`: 87 ns[i] = 0.85 88 case `R`: 89 ns[i] = 0.62 90 default: 91 return 0, fmt.Errorf("bad metric value: %q", m) 92 } 93 case `S`: 94 const i = 4 95 // This is a cheat. Encode "changed" as 1. Not actually used in 96 // calculations, just changes the values used for other metrics. 97 switch v { 98 case `U`: 99 ns[i] = 0 100 case `C`: 101 ns[i] = 1 102 default: 103 return 0, fmt.Errorf("bad metric value: %q", m) 104 } 105 case `C`: 106 const i = 5 107 switch v { 108 case `H`: 109 ns[i] = 0.56 110 case `L`: 111 ns[i] = 0.22 112 case `N`: 113 ns[i] = 0 114 default: 115 return 0, fmt.Errorf("bad metric value: %q", m) 116 } 117 case `I`: 118 const i = 6 119 switch v { 120 case `H`: 121 ns[i] = 0.56 122 case `L`: 123 ns[i] = 0.22 124 case `N`: 125 ns[i] = 0 126 default: 127 return 0, fmt.Errorf("bad metric value: %q", m) 128 } 129 case `A`: 130 const i = 7 131 switch v { 132 case `H`: 133 ns[i] = 0.56 134 case `L`: 135 ns[i] = 0.22 136 case `N`: 137 ns[i] = 0 138 default: 139 return 0, fmt.Errorf("bad metric value: %q", m) 140 } 141 case `E`, `RL`, `RC`, `CR`, `IR`, `AR`, `MAV`, `MAC`, `MPR`, `MUI`, `MS`, `MC`, `MI`, `MA`: 142 // Ignore temporal and environmental metrics. 143 default: 144 return 0, fmt.Errorf("bad metric: %q", m) 145 } 146 } 147 changed := ns[4] != 0 // if Scope == Changed 148 if changed { 149 switch ns[ /*Privileges Required*/ 2] { 150 case 0.62: 151 ns[2] = 0.68 152 case 0.27: 153 ns[2] = 0.5 154 } 155 } 156 157 var score float64 158 iss := 1 - ((1 - ns[ /*C*/ 5]) * (1 - ns[ /*I*/ 6]) * (1 - ns[ /*A*/ 7])) 159 var imp float64 160 if changed { 161 imp = 7.52*(iss-0.029) - 3.25*math.Pow((iss-0.02), 15) 162 } else { 163 imp = iss * 6.42 164 } 165 if imp > 0 { // Score is 0 when impact is 0 or below. 166 exp := 8.22 * ns[ /*AV*/ 0] * ns[ /*AC*/ 1] * ns[ /*PR*/ 2] * ns[ /*UI*/ 3] 167 s := exp + imp 168 if changed { 169 s *= 1.08 170 } 171 s = math.Min(s, 10) 172 // Roundup function, as spec'd. 173 i := int(s * 100_000) 174 if (i % 10_000) == 0 { 175 score = float64(i) / 100_000.0 176 } else { 177 score = ((float64(i) / 10_000) + 1) / 10.0 178 } 179 } 180 181 // See https://nvd.nist.gov/vuln-metrics/cvss 182 switch { 183 case score == 0: 184 sev = claircore.Negligible // aka None 185 case score < 4: 186 sev = claircore.Low 187 case score < 7: 188 sev = claircore.Medium 189 case score < 9: 190 sev = claircore.High 191 case score <= 10: 192 sev = claircore.Critical 193 default: 194 return sev, fmt.Errorf("bogus score: %02f", score) 195 } 196 return sev, nil 197 } 198 199 // FromCVSS2 is an attempt at an implementation of the formulas 200 // described here: https://www.first.org/cvss/v2/guide 201 func fromCVSS2(s string) (sev claircore.Severity, err error) { 202 ms := strings.Split(s, "/") // "m" as in "metric" 203 if len(ms) < 6 { 204 return 0, fmt.Errorf("bad vector: %q", s) 205 } 206 // Giant switch ahoy 207 var ns [6]float64 208 for _, m := range ms { 209 n, v, ok := strings.Cut(m, ":") 210 if !ok { 211 return 0, fmt.Errorf("bad metric: %q", m) 212 } 213 switch n { 214 // Base metrics: 215 case `AV`: 216 const i = 0 217 switch v { 218 case `N`: 219 ns[i] = 1 220 case `A`: 221 ns[i] = 0.646 222 case `L`: 223 ns[i] = 0.395 224 default: 225 return 0, fmt.Errorf("bad metric value: %q", m) 226 } 227 case `AC`: 228 const i = 1 229 switch v { 230 case `L`: 231 ns[i] = 0.71 232 case `M`: 233 ns[i] = 0.61 234 case `H`: 235 ns[i] = 0.35 236 default: 237 return 0, fmt.Errorf("bad metric value: %q", m) 238 } 239 case `Au`: 240 const i = 2 241 switch v { 242 case `M`: 243 ns[i] = 0.45 244 case `S`: 245 ns[i] = 0.56 246 case `N`: 247 ns[i] = 0.704 248 default: 249 return 0, fmt.Errorf("bad metric value: %q", m) 250 } 251 case `C`: 252 const i = 3 253 switch v { 254 case `C`: 255 ns[i] = 0.660 256 case `P`: 257 ns[i] = 0.275 258 case `N`: 259 ns[i] = 0 260 default: 261 return 0, fmt.Errorf("bad metric value: %q", m) 262 } 263 case `I`: 264 const i = 4 265 switch v { 266 case `C`: 267 ns[i] = 0.660 268 case `P`: 269 ns[i] = 0.275 270 case `N`: 271 ns[i] = 0 272 default: 273 return 0, fmt.Errorf("bad metric value: %q", m) 274 } 275 case `A`: 276 const i = 5 277 switch v { 278 case `C`: 279 ns[i] = 0.660 280 case `P`: 281 ns[i] = 0.275 282 case `N`: 283 ns[i] = 0 284 default: 285 return 0, fmt.Errorf("bad metric value: %q", m) 286 } 287 case `E`, `RL`, `RC`, `CDP`, `TD`, `CR`, `IR`, `AR`: 288 // Ignore temporal and environmental metrics. 289 default: 290 return 0, fmt.Errorf("bad metric: %q", m) 291 } 292 } 293 294 var score float64 295 exploitability := 20 * ns[ /*AV*/ 0] * ns[ /*AC*/ 1] * ns[ /*Au*/ 2] 296 impact := 10.41 * (1 - (1-ns[ /*C*/ 3])*(1-ns[ /*I*/ 4])*(1-ns[ /*A*/ 5])) 297 var fImpact float64 298 if impact != 0 { 299 fImpact = 1.176 300 } 301 score = ((0.6 * impact) + (0.4 * exploitability) - 1.5) * fImpact 302 // An attempt to "round_to_1_decimal," per spec. 303 score = math.Round(score*10) / 10.0 304 score = math.Min(score, 10) 305 306 // See https://nvd.nist.gov/vuln-metrics/cvss 307 switch { 308 case score < 4: 309 sev = claircore.Low 310 case score < 7: 311 sev = claircore.Medium 312 case score <= 10: 313 sev = claircore.High 314 default: 315 return sev, fmt.Errorf("bogus score: %02f", score) 316 } 317 return sev, nil 318 }