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  }