diff --git a/lib/cvss_4_0.ts b/lib/cvss_4_0.ts index 8062c0c..5dbb4df 100644 --- a/lib/cvss_4_0.ts +++ b/lib/cvss_4_0.ts @@ -377,9 +377,6 @@ export const definitions = { ] }; -// Copyright FIRST, Red Hat, and contributors -// SPDX-License-Identifier: BSD-2-Clause - export const cvssLookup_global = { "000000": 10, "000001": 9.9, @@ -471,191 +468,188 @@ export const cvssLookup_global = { "012201": 6.3, "012211": 2.9, "012221": 1.7, - 100000: 9.8, - 100001: 9.5, - 100010: 9.4, - 100011: 8.7, - 100020: 9.1, - 100021: 8.1, - 100100: 9.4, - 100101: 8.9, - 100110: 8.6, - 100111: 7.4, - 100120: 7.7, - 100121: 6.4, - 100200: 8.7, - 100201: 7.5, - 100210: 7.4, - 100211: 6.3, - 100220: 6.3, - 100221: 4.9, - 101000: 9.4, - 101001: 8.9, - 101010: 8.8, - 101011: 7.7, - 101020: 7.6, - 101021: 6.7, - 101100: 8.6, - 101101: 7.6, - 101110: 7.4, - 101111: 5.8, - 101120: 5.9, - 101121: 5, - 101200: 7.2, - 101201: 5.7, - 101210: 5.7, - 101211: 5.2, - 101220: 5.2, - 101221: 2.5, - 102001: 8.3, - 102011: 7, - 102021: 5.4, - 102101: 6.5, - 102111: 5.8, - 102121: 2.6, - 102201: 5.3, - 102211: 2.1, - 102221: 1.3, - 110000: 9.5, - 110001: 9, - 110010: 8.8, - 110011: 7.6, - 110020: 7.6, - 110021: 7, - 110100: 9, - 110101: 7.7, - 110110: 7.5, - 110111: 6.2, - 110120: 6.1, - 110121: 5.3, - 110200: 7.7, - 110201: 6.6, - 110210: 6.8, - 110211: 5.9, - 110220: 5.2, - 110221: 3, - 111000: 8.9, - 111001: 7.8, - 111010: 7.6, - 111011: 6.7, - 111020: 6.2, - 111021: 5.8, - 111100: 7.4, - 111101: 5.9, - 111110: 5.7, - 111111: 5.7, - 111120: 4.7, - 111121: 2.3, - 111200: 6.1, - 111201: 5.2, - 111210: 5.7, - 111211: 2.9, - 111220: 2.4, - 111221: 1.6, - 112001: 7.1, - 112011: 5.9, - 112021: 3, - 112101: 5.8, - 112111: 2.6, - 112121: 1.5, - 112201: 2.3, - 112211: 1.3, - 112221: 0.6, - 200000: 9.3, - 200001: 8.7, - 200010: 8.6, - 200011: 7.2, - 200020: 7.5, - 200021: 5.8, - 200100: 8.6, - 200101: 7.4, - 200110: 7.4, - 200111: 6.1, - 200120: 5.6, - 200121: 3.4, - 200200: 7, - 200201: 5.4, - 200210: 5.2, - 200211: 4, - 200220: 4, - 200221: 2.2, - 201000: 8.5, - 201001: 7.5, - 201010: 7.4, - 201011: 5.5, - 201020: 6.2, - 201021: 5.1, - 201100: 7.2, - 201101: 5.7, - 201110: 5.5, - 201111: 4.1, - 201120: 4.6, - 201121: 1.9, - 201200: 5.3, - 201201: 3.6, - 201210: 3.4, - 201211: 1.9, - 201220: 1.9, - 201221: 0.8, - 202001: 6.4, - 202011: 5.1, - 202021: 2, - 202101: 4.7, - 202111: 2.1, - 202121: 1.1, - 202201: 2.4, - 202211: 0.9, - 202221: 0.4, - 210000: 8.8, - 210001: 7.5, - 210010: 7.3, - 210011: 5.3, - 210020: 6, - 210021: 5, - 210100: 7.3, - 210101: 5.5, - 210110: 5.9, - 210111: 4, - 210120: 4.1, - 210121: 2, - 210200: 5.4, - 210201: 4.3, - 210210: 4.5, - 210211: 2.2, - 210220: 2, - 210221: 1.1, - 211000: 7.5, - 211001: 5.5, - 211010: 5.8, - 211011: 4.5, - 211020: 4, - 211021: 2.1, - 211100: 6.1, - 211101: 5.1, - 211110: 4.8, - 211111: 1.8, - 211120: 2, - 211121: 0.9, - 211200: 4.6, - 211201: 1.8, - 211210: 1.7, - 211211: 0.7, - 211220: 0.8, - 211221: 0.2, - 212001: 5.3, - 212011: 2.4, - 212021: 1.4, - 212101: 2.4, - 212111: 1.2, - 212121: 0.5, - 212201: 1, - 212211: 0.3, - 212221: 0.1 + "100000": 9.8, + "100001": 9.5, + "100010": 9.4, + "100011": 8.7, + "100020": 9.1, + "100021": 8.1, + "100100": 9.4, + "100101": 8.9, + "100110": 8.6, + "100111": 7.4, + "100120": 7.7, + "100121": 6.4, + "100200": 8.7, + "100201": 7.5, + "100210": 7.4, + "100211": 6.3, + "100220": 6.3, + "100221": 4.9, + "101000": 9.4, + "101001": 8.9, + "101010": 8.8, + "101011": 7.7, + "101020": 7.6, + "101021": 6.7, + "101100": 8.6, + "101101": 7.6, + "101110": 7.4, + "101111": 5.8, + "101120": 5.9, + "101121": 5, + "101200": 7.2, + "101201": 5.7, + "101210": 5.7, + "101211": 5.2, + "101220": 5.2, + "101221": 2.5, + "102001": 8.3, + "102011": 7, + "102021": 5.4, + "102101": 6.5, + "102111": 5.8, + "102121": 2.6, + "102201": 5.3, + "102211": 2.1, + "102221": 1.3, + "110000": 9.5, + "110001": 9, + "110010": 8.8, + "110011": 7.6, + "110020": 7.6, + "110021": 7, + "110100": 9, + "110101": 7.7, + "110110": 7.5, + "110111": 6.2, + "110120": 6.1, + "110121": 5.3, + "110200": 7.7, + "110201": 6.6, + "110210": 6.8, + "110211": 5.9, + "110220": 5.2, + "110221": 3, + "111000": 8.9, + "111001": 7.8, + "111010": 7.6, + "111011": 6.7, + "111020": 6.2, + "111021": 5.8, + "111100": 7.4, + "111101": 5.9, + "111110": 5.7, + "111111": 5.7, + "111120": 4.7, + "111121": 2.3, + "111200": 6.1, + "111201": 5.2, + "111210": 5.7, + "111211": 2.9, + "111220": 2.4, + "111221": 1.6, + "112001": 7.1, + "112011": 5.9, + "112021": 3, + "112101": 5.8, + "112111": 2.6, + "112121": 1.5, + "112201": 2.3, + "112211": 1.3, + "112221": 0.6, + "200000": 9.3, + "200001": 8.7, + "200010": 8.6, + "200011": 7.2, + "200020": 7.5, + "200021": 5.8, + "200100": 8.6, + "200101": 7.4, + "200110": 7.4, + "200111": 6.1, + "200120": 5.6, + "200121": 3.4, + "200200": 7, + "200201": 5.4, + "200210": 5.2, + "200211": 4, + "200220": 4, + "200221": 2.2, + "201000": 8.5, + "201001": 7.5, + "201010": 7.4, + "201011": 5.5, + "201020": 6.2, + "201021": 5.1, + "201100": 7.2, + "201101": 5.7, + "201110": 5.5, + "201111": 4.1, + "201120": 4.6, + "201121": 1.9, + "201200": 5.3, + "201201": 3.6, + "201210": 3.4, + "201211": 1.9, + "201220": 1.9, + "201221": 0.8, + "202001": 6.4, + "202011": 5.1, + "202021": 2, + "202101": 4.7, + "202111": 2.1, + "202121": 1.1, + "202201": 2.4, + "202211": 0.9, + "202221": 0.4, + "210000": 8.8, + "210001": 7.5, + "210010": 7.3, + "210011": 5.3, + "210020": 6, + "210021": 5, + "210100": 7.3, + "210101": 5.5, + "210110": 5.9, + "210111": 4, + "210120": 4.1, + "210121": 2, + "210200": 5.4, + "210201": 4.3, + "210210": 4.5, + "210211": 2.2, + "210220": 2, + "210221": 1.1, + "211000": 7.5, + "211001": 5.5, + "211010": 5.8, + "211011": 4.5, + "211020": 4, + "211021": 2.1, + "211100": 6.1, + "211101": 5.1, + "211110": 4.8, + "211111": 1.8, + "211120": 2, + "211121": 0.9, + "211200": 4.6, + "211201": 1.8, + "211210": 1.7, + "211211": 0.7, + "211220": 0.8, + "211221": 0.2, + "212001": 5.3, + "212011": 2.4, + "212021": 1.4, + "212101": 2.4, + "212111": 1.2, + "212121": 0.5, + "212201": 1, + "212211": 0.3, + "212221": 0.1 }; -// Copyright FIRST, Red Hat, and contributors -// SPDX-License-Identifier: BSD-2-Clause - export const maxComposed = { // EQ1 eq1: { @@ -700,10 +694,6 @@ export const maxComposed = { } }; -// Copyright FIRST, Red Hat, and contributors -// SPDX-License-Identifier: BSD-2-Clause - -// max severity distances in EQs MacroVectors (+1) export const maxSeverity = { eq1: { 0: 1, diff --git a/lib/score.ts b/lib/score.ts index 92c6177..b328ce2 100644 --- a/lib/score.ts +++ b/lib/score.ts @@ -289,7 +289,34 @@ const findMetricValue = function ( vectorObject: CvssVectorObject ) { const definition = findMetric4_0(abbr); - const value = definition.metrics.find((metric) => metric.abbr === vectorObject[definition.abbr]); + let value = definition.metrics.find((metric) => metric.abbr === vectorObject[definition.abbr]); + + if (vectorObject.CVSS === "4.0") { + // If E=X it will default to the worst case i.e. E=A + if (abbr == "E" && vectorObject["E"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "A") as T; + } + // If CR=X, IR=X or AR=X they will default to the worst case i.e. CR=H, IR=H and AR=H + if (abbr == "CR" && vectorObject["CR"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "H") as T; + } + // IR:X is the same as IR:H + if (abbr == "IR" && vectorObject["IR"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "H") as T; + } + // AR:X is the same as AR:H + if (abbr == "AR" && vectorObject["AR"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "H") as T; + } + // All other environmental metrics just overwrite base score values, + // so if they’re not defined just use the base score value. + if (vectorObject["M" + abbr] !== undefined && vectorObject["M" + abbr] !== "X") { + const modifiedDefinition = findMetric4_0("M" + abbr); + value = definition.metrics.find( + (metric) => metric.abbr === vectorObject[modifiedDefinition.abbr] + ); + } + } return value as T; }; @@ -313,7 +340,7 @@ function getVectorObject(vector: string) { function cvss4_0scoring(vector: string) { const vectorObj = getVectorObject(vector); - // EQ1 + // EQ1 (equivalent classes) const AVmetric = findMetricValue("AV", vectorObj); const PRmetric = findMetricValue("PR", vectorObj); const UImetric = findMetricValue("UI", vectorObj); @@ -358,7 +385,7 @@ function cvss4_0scoring(vector: string) { else if ( (AVmetric.abbr === "N" || PRmetric.abbr === "N" || UImetric.abbr === "N") && !(AVmetric.abbr === "N" && PRmetric.abbr === "N" && UImetric.abbr === "N") && - !(AVmetric.abbr = "P") + !(AVmetric.abbr === "P") ) eq1 = "1"; else if ( @@ -398,12 +425,7 @@ function cvss4_0scoring(vector: string) { ) eq4 = "1"; else if ( - !( - MSImetric.abbr === "S" || - MSImetric.abbr === "X" || - MSAmetric.abbr === "S" || - MSAmetric.abbr === "X" - ) && + !(MSImetric.abbr === "S" || MSAmetric.abbr === "S") && !(SCmetric.abbr === "H" || SImetric.abbr === "H" || SAmetric.abbr === "H") ) eq4 = "2"; @@ -413,7 +435,7 @@ function cvss4_0scoring(vector: string) { // 1 E:P // 2 E:U // If E=X it will default to the worst case (i.e., E=A). - if (Emetric.abbr === "A" || Emetric.abbr === "X") eq5 = "0"; + if (Emetric.abbr === "A") eq5 = "0"; else if (Emetric.abbr === "P") eq5 = "1"; else if (Emetric.abbr === "U") eq5 = "2"; @@ -434,9 +456,11 @@ function cvss4_0scoring(vector: string) { ) eq6 = "1"; - // For each of the EQs - // The maximal scoring difference is determined as the difference between the current MacroVector and the lower MacroVector - // there is no lower MacroVector the available distance is set to NaN and then ignored in the further calculations + const macroVector = eq1 + eq2 + eq3 + eq4 + eq5 + eq6; + + // 1. For each of the EQs + // 1.1 The maximal scoring difference is determined as the difference between the current MacroVector and the lower MacroVector + // 1.1.1 there is no lower MacroVector the available distance is set to NaN and then ignored in the further calculations // The scores of each MacroVector can be found in the cvssLookup table const eq1NextLowerMarcoVectorScore = @@ -448,7 +472,7 @@ function cvss4_0scoring(vector: string) { const eq5NextLowerMarcoVectorScore = cvssLookup_global["".concat(eq1, eq2, eq3, eq4, "" + (parseInt(eq5) + 1), eq6)]; - // // EQ3 and EQ6 are joint see Table 30, an if case represents an change in level constraint f.e 11 -> 21 + // EQ3 and EQ6 are joint see Table 30, an if case represents an change in level constraint f.e 11 -> 21 let eq3eq6NextLowerMarcoVector = 0; let eq3eq6NextLowerLeftMarcoVector = 0; let eq3eq6NextLowerRightMarcoVector = 0; @@ -466,6 +490,10 @@ function cvss4_0scoring(vector: string) { cvssLookup_global["".concat(eq1, eq2, eq3, eq4, eq5, "" + (parseInt(eq6) + 1))]; eq3eq6NextLowerRightMarcoVector = cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; + eq3eq6NextLowerMarcoVector = + eq3eq6NextLowerLeftMarcoVector > eq3eq6NextLowerRightMarcoVector + ? eq3eq6NextLowerLeftMarcoVector + : eq3eq6NextLowerRightMarcoVector; } // cannot exist path else eq3eq6NextLowerMarcoVector = @@ -473,12 +501,12 @@ function cvss4_0scoring(vector: string) { "".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, "" + (parseInt(eq6) + 1)) ]; - // The severity distance of the to-be scored vector from a highest severity vector in the same MacroVector is determined - const eq1Maxima = maxComposed["eq1"][eq1]; - const eq2Maxima = maxComposed["eq2"][eq2]; - const eq3eq6Maxima = maxComposed["eq3"][eq3][eq6]; - const eq4Maxima = maxComposed["eq4"][eq4]; - const eq5Maxima = maxComposed["eq5"][eq5]; + // 1.2. The severity distance of the to-be scored vector from a highest severity vector in the same MacroVector is determined + const eq1Maxima = maxComposed["eq1"][parseInt(eq1)]; + const eq2Maxima = maxComposed["eq2"][parseInt(eq2)]; + const eq3eq6Maxima = maxComposed["eq3"][parseInt(eq3)][parseInt(eq6)]; + const eq4Maxima = maxComposed["eq4"][parseInt(eq4)]; + const eq5Maxima = maxComposed["eq5"][parseInt(eq5)]; // combine all vector maximas to create all possible maximums const possibleMaximumVectorString = []; @@ -495,11 +523,17 @@ function cvss4_0scoring(vector: string) { } let max = ""; + let eq1Distance = 0; + let eq2Distance = 0; + let eq3eq6Distance = 0; + let eq4Distance = 0; + let eq5Distance = 0; + for (let i = 0; i < possibleMaximumVectorString.length; i++) { max = possibleMaximumVectorString[i]; - console.log(max); const maxVectorObj = getVectorObject(max); + // distance of the to-be scored vector from a highest severity vector const severity_distance_AV = AVmetric.numerical - findMetricValue("AV", maxVectorObj).numerical; const severity_distance_PR = @@ -533,6 +567,7 @@ function cvss4_0scoring(vector: string) { const severity_distance_AR = ARmetric.numerical - findMetricValue("AR", maxVectorObj).numerical; + // if any of the values is negative, a greater max vector can be found if ( [ severity_distance_AV, @@ -553,11 +588,100 @@ function cvss4_0scoring(vector: string) { ) { continue; } + + // add the severity distance of the metric groups to calculate the serverity distance of the equivalent class + eq1Distance = severity_distance_AV + severity_distance_PR + severity_distance_UI; + eq2Distance = severity_distance_AC + severity_distance_AT; + eq3eq6Distance = + severity_distance_VC + + severity_distance_VI + + severity_distance_VA + + severity_distance_CR + + severity_distance_IR + + severity_distance_AR; + eq4Distance = severity_distance_SC + severity_distance_SI + severity_distance_SA; + eq5Distance = 0; + break; } - console.log(max); - // TODO : https://www.first.org/cvss/v4.0/specification-document#New-Scoring-System-Development step 1.3 + // calculate maximal scoring difference + const currentMacroVectorValue = cvssLookup_global[macroVector]; + + let eq1MSD = currentMacroVectorValue - eq1NextLowerMarcoVectorScore; + let eq2MSD = currentMacroVectorValue - eq2NextLowerMarcoVectorScore; + let eq3eq6MSD = currentMacroVectorValue - eq3eq6NextLowerMarcoVector; + let eq4MSD = currentMacroVectorValue - eq4NextLowerMarcoVectorScore; + let eq5MSD = currentMacroVectorValue - eq5NextLowerMarcoVectorScore; + + const step = 0.1; + + const eq1MaxSevertity = maxSeverity["eq1"][parseInt(eq1)] * step; + const eq2MaxSevertity = maxSeverity["eq2"][parseInt(eq2)] * step; + const eq3eq6MaxSevertity = maxSeverity["eq3eq6"][parseInt(eq3)][parseInt(eq6)] * step; + const eq4MaxSevertity = maxSeverity["eq4"][parseInt(eq4)] * step; + const eq5MaxSevertity = maxSeverity["eq5"][parseInt(eq5)] * step; + + // 1.1.1 if there is no lower MacroVector the available distance is set to NaN and then ignored in the further calculations + // 1.3 The proportion of the distance is determined by dividing the severity distance of the to-be-scored vector by the depth of the MacroVector + // 1.4 The maximal scoring difference is multiplied by the proportion of distance + let count = 0; + + if (!isNaN(eq1MSD)) { + count++; + eq1MSD = eq1MSD * (eq1Distance / eq1MaxSevertity); + } else { + eq1MSD = 0; + } + if (!isNaN(eq2MSD)) { + count++; + eq2MSD = eq2MSD * (eq2Distance / eq2MaxSevertity); + } else { + eq2MSD = 0; + } + if (!isNaN(eq3eq6MSD)) { + count++; + eq3eq6MSD = eq3eq6MSD * (eq3eq6Distance / eq3eq6MaxSevertity); + } else { + eq3eq6MSD = 0; + } + if (!isNaN(eq4MSD)) { + count++; + eq4MSD = eq4MSD * (eq4Distance / eq4MaxSevertity); + } else { + eq4MSD = 0; + } + if (!isNaN(eq5MSD)) { + count++; + eq5MSD = 0; + } else { + eq5MSD = 0; + } + + // 2. The mean of the above computed proportional distances is computed + let mean = 0; + if (!isNaN(eq1MSD) || !isNaN(eq1MSD) || !isNaN(eq1MSD) || !isNaN(eq1MSD) || !isNaN(eq1MSD)) { + mean = (eq1MSD + eq2MSD + eq3eq6MSD + eq4MSD + eq5MSD) / count; + } + + // normalized_severity_eq1 0.44999999999999996 + // normalized_severity_eq2 0 + // normalized_severity_eq3eq6 0.6749999999999997 + // normalized_severity_eq4 1.2666666666666662 + // normalized_severity_eq5 0 + // value 7.6 + // mean_distance 0.5979166666666664 + + // 3. The score of the vector is the score of the MacroVector (i.e. the score of the highest severity vector) minus the mean distance so computed. + // This score is rounded to one decimal place. + let vectorScore = currentMacroVectorValue - mean; + if (vectorScore < 0) { + vectorScore = 0.0; + } + if (vectorScore > 10) { + vectorScore = 10.0; + } + return vectorScore.toFixed(1); } export const score = { diff --git a/lib/score_4_0.ts b/lib/score_4_0.ts new file mode 100644 index 0000000..caf826f --- /dev/null +++ b/lib/score_4_0.ts @@ -0,0 +1,411 @@ +import { CvssVectorObject, Metric, MetricScope, MetricUnion } from "./types"; + +import { definitions, cvssLookup_global, maxComposed, maxSeverity } from "./cvss_4_0"; + +const findMetric4_0 = function (abbr: string) { + return definitions.definitions.find((def) => def.abbr === abbr); +}; + +const findMetricValue = function ( + abbr: string, + vectorObject: CvssVectorObject +) { + const definition = findMetric4_0(abbr); + let value = definition.metrics.find((metric) => metric.abbr === vectorObject[definition.abbr]); + + if (vectorObject.CVSS === "4.0") { + // If E=X it will default to the worst case i.e. E=A + if (abbr == "E" && vectorObject["E"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "A") as T; + } + // If CR=X, IR=X or AR=X they will default to the worst case i.e. CR=H, IR=H and AR=H + if (abbr == "CR" && vectorObject["CR"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "H") as T; + } + // IR:X is the same as IR:H + if (abbr == "IR" && vectorObject["IR"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "H") as T; + } + // AR:X is the same as AR:H + if (abbr == "AR" && vectorObject["AR"] == "X") { + return definition.metrics.find((metric) => metric.abbr === "H") as T; + } + // All other environmental metrics just overwrite base score values, + // so if they’re not defined just use the base score value. + if (vectorObject["M" + abbr] !== undefined && vectorObject["M" + abbr] !== "X") { + const modifiedDefinition = findMetric4_0("M" + abbr); + value = definition.metrics.find( + (metric) => metric.abbr === vectorObject[modifiedDefinition.abbr] + ); + } + } + + return value as T; +}; + +function getVectorObject(vector: string) { + const vectorArray = vector.split("/"); + const vectorObject = definitions.definitions + .map((definition) => definition.abbr) + .reduce((acc, curr) => { + acc[curr] = "X"; + return acc; + }, {} as CvssVectorObject); + + for (const entry of vectorArray) { + const values = entry.split(":"); + vectorObject[values[0]] = values[1]; + } + return vectorObject; +} + +function cvss4_0scoring(vector: string) { + const vectorObj = getVectorObject(vector); + + // EQ1 (equivalent classes) + const AVmetric = findMetricValue("AV", vectorObj); + const PRmetric = findMetricValue("PR", vectorObj); + const UImetric = findMetricValue("UI", vectorObj); + + // EQ2 + const ACmetric = findMetricValue("AC", vectorObj); + const ATmetric = findMetricValue("AT", vectorObj); + + // EQ3 + half of EQ6 + const VCmetric = findMetricValue("VC", vectorObj); + const VImetric = findMetricValue("VI", vectorObj); + const VAmetric = findMetricValue("VA", vectorObj); + + // EQ4 + const SCmetric = findMetricValue("SC", vectorObj); + const SImetric = findMetricValue("SI", vectorObj); + const SAmetric = findMetricValue("SA", vectorObj); + const MSImetric = findMetricValue("MSI", vectorObj); + const MSAmetric = findMetricValue("MSA", vectorObj); + + // EQ5 + const Emetric = findMetricValue("E", vectorObj); + + // half of EQ6 + const CRmetric = findMetricValue("CR", vectorObj); + const IRmetric = findMetricValue("IR", vectorObj); + const ARmetric = findMetricValue("AR", vectorObj); + + // calculate EQ levels + let eq1 = "0"; + let eq2 = "0"; + let eq3 = "0"; + let eq4 = "0"; + let eq5 = "0"; + let eq6 = "0"; + + // EQ1 + // 0 AV:N and PR:N and UI:N + // 1 (AV:N or PR:N or UI:N) and not (AV:N and PR:N and UI:N) and not AV:P + // 2 AV:P or not(AV:N or PR:N or UI:N) + if (AVmetric.abbr === "N" && PRmetric.abbr === "N" && UImetric.abbr === "N") eq1 = "0"; + else if ( + (AVmetric.abbr === "N" || PRmetric.abbr === "N" || UImetric.abbr === "N") && + !(AVmetric.abbr === "N" && PRmetric.abbr === "N" && UImetric.abbr === "N") && + !(AVmetric.abbr === "P") + ) + eq1 = "1"; + else if ( + AVmetric.abbr === "P" || + !(AVmetric.abbr === "N" || PRmetric.abbr === "N" || UImetric.abbr === "N") + ) + eq1 = "2"; + + // EQ2 + // 0 AC:L and AT:N + // 1 not (AC:L and AT:N) + if (ACmetric.abbr === "L" && ATmetric.abbr === "N") eq2 = "0"; + else if (!(ACmetric.abbr === "L" && ATmetric.abbr === "N")) eq2 = "1"; + + // EQ3 + // 0 VC:H and VI:H + // 1 not (VC:H and VI:H) and (VC:H or VI:H or VA:H) + // 2 not (VC:H or VI:H or VA:H) + if (VCmetric.abbr === "H" && VImetric.abbr === "H") eq3 = "0"; + else if ( + !(VCmetric.abbr === "H" && VImetric.abbr === "H") && + (VCmetric.abbr === "H" || VImetric.abbr === "H" || VAmetric.abbr === "H") + ) + eq3 = "1"; + else if (!(VCmetric.abbr === "H" || VImetric.abbr === "H" || VAmetric.abbr === "H")) eq3 = "2"; + + // EQ4 + // 0 MSI:S or MSA:S + // 1 not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H) + // 2 not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H) + // If MSI=X or MSA=X they will default to the corresponding value of SI and SA according to the rules of Modified Base Metrics in section 4.2 (See Table 15). + // So if there are no modified base metrics, the highest value that EQ4 can reach is 1. + if (MSImetric.abbr === "S" || MSAmetric.abbr === "S") eq4 = "0"; + else if ( + !(MSImetric.abbr === "S" || MSAmetric.abbr === "S") && + (SCmetric.abbr === "H" || SImetric.abbr === "H" || SAmetric.abbr === "H") + ) + eq4 = "1"; + else if ( + !(MSImetric.abbr === "S" || MSAmetric.abbr === "S") && + !(SCmetric.abbr === "H" || SImetric.abbr === "H" || SAmetric.abbr === "H") + ) + eq4 = "2"; + + // EQ5 + // 0 E:A + // 1 E:P + // 2 E:U + // If E=X it will default to the worst case (i.e., E=A). + if (Emetric.abbr === "A") eq5 = "0"; + else if (Emetric.abbr === "P") eq5 = "1"; + else if (Emetric.abbr === "U") eq5 = "2"; + + // EQ6 + // 0 (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H) + // 1 not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H) + // If CR=X, IR=X or AR=X they will default to the worst case (i.e., CR=H, IR=H and AR=H). + if ( + ((CRmetric.abbr === "H" || CRmetric.abbr === "X") && VCmetric.abbr === "H") || + ((IRmetric.abbr === "H" || IRmetric.abbr === "X") && VImetric.abbr === "H") || + ((ARmetric.abbr === "H" || ARmetric.abbr === "X") && VAmetric.abbr === "H") + ) + eq6 = "0"; + else if ( + !((CRmetric.abbr === "H" || CRmetric.abbr === "X") && VCmetric.abbr === "H") && + !((IRmetric.abbr === "H" || IRmetric.abbr === "X") && VImetric.abbr === "H") && + !((ARmetric.abbr === "H" || ARmetric.abbr === "X") && VAmetric.abbr === "H") + ) + eq6 = "1"; + + const macroVector = eq1 + eq2 + eq3 + eq4 + eq5 + eq6; + + // 1. For each of the EQs + // 1.1 The maximal scoring difference is determined as the difference between the current MacroVector and the lower MacroVector + // 1.1.1 there is no lower MacroVector the available distance is set to NaN and then ignored in the further calculations + // The scores of each MacroVector can be found in the cvssLookup table + + const eq1NextLowerMarcoVectorScore = + cvssLookup_global["".concat("" + (parseInt(eq1) + 1), eq2, eq3, eq4, eq5, eq6)]; + const eq2NextLowerMarcoVectorScore = + cvssLookup_global["".concat(eq1, "" + (parseInt(eq2) + 1), eq3, eq4, eq5, eq6)]; + const eq4NextLowerMarcoVectorScore = + cvssLookup_global["".concat(eq1, eq2, eq3, "" + (parseInt(eq4) + 1), eq5, eq6)]; + const eq5NextLowerMarcoVectorScore = + cvssLookup_global["".concat(eq1, eq2, eq3, eq4, "" + (parseInt(eq5) + 1), eq6)]; + + // EQ3 and EQ6 are joint see Table 30, an if case represents an change in level constraint f.e 11 -> 21 + let eq3eq6NextLowerMarcoVector = 0; + let eq3eq6NextLowerLeftMarcoVector = 0; + let eq3eq6NextLowerRightMarcoVector = 0; + if (eq3 === "1" && eq6 === "1") { + eq3eq6NextLowerMarcoVector = + cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; + } else if (eq3 === "1" && eq6 === "0") { + eq3eq6NextLowerMarcoVector = + cvssLookup_global["".concat(eq1, eq2, eq3, eq4, eq5, "" + (parseInt(eq6) + 1))]; + } else if (eq3 === "0" && eq6 === "1") { + eq3eq6NextLowerMarcoVector = + cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; + } else if (eq3 === "0" && eq6 === "0") { + eq3eq6NextLowerLeftMarcoVector = + cvssLookup_global["".concat(eq1, eq2, eq3, eq4, eq5, "" + (parseInt(eq6) + 1))]; + eq3eq6NextLowerRightMarcoVector = + cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; + eq3eq6NextLowerMarcoVector = + eq3eq6NextLowerLeftMarcoVector > eq3eq6NextLowerRightMarcoVector + ? eq3eq6NextLowerLeftMarcoVector + : eq3eq6NextLowerRightMarcoVector; + } // cannot exist path + else + eq3eq6NextLowerMarcoVector = + cvssLookup_global[ + "".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, "" + (parseInt(eq6) + 1)) + ]; + + // 1.2. The severity distance of the to-be scored vector from a highest severity vector in the same MacroVector is determined + const eq1Maxima = maxComposed["eq1"][parseInt(eq1)]; + const eq2Maxima = maxComposed["eq2"][parseInt(eq2)]; + const eq3eq6Maxima = maxComposed["eq3"][parseInt(eq3)][parseInt(eq6)]; + const eq4Maxima = maxComposed["eq4"][parseInt(eq4)]; + const eq5Maxima = maxComposed["eq5"][parseInt(eq5)]; + + // combine all vector maximas to create all possible maximums + const possibleMaximumVectorString = []; + for (const eq1Max of eq1Maxima) { + for (const eq2Max of eq2Maxima) { + for (const eq3eq6Max of eq3eq6Maxima) { + for (const eq4Max of eq4Maxima) { + for (const eq5Max of eq5Maxima) { + possibleMaximumVectorString.push(eq1Max + eq2Max + eq3eq6Max + eq4Max + eq5Max); + } + } + } + } + } + + let max = ""; + let eq1Distance = 0; + let eq2Distance = 0; + let eq3eq6Distance = 0; + let eq4Distance = 0; + let eq5Distance = 0; + + for (let i = 0; i < possibleMaximumVectorString.length; i++) { + max = possibleMaximumVectorString[i]; + const maxVectorObj = getVectorObject(max); + + // distance of the to-be scored vector from a highest severity vector + const severity_distance_AV = + AVmetric.numerical - findMetricValue("AV", maxVectorObj).numerical; + const severity_distance_PR = + PRmetric.numerical - findMetricValue("PR", maxVectorObj).numerical; + const severity_distance_UI = + UImetric.numerical - findMetricValue("UI", maxVectorObj).numerical; + + const severity_distance_AC = + ACmetric.numerical - findMetricValue("AC", maxVectorObj).numerical; + const severity_distance_AT = + ATmetric.numerical - findMetricValue("AT", maxVectorObj).numerical; + + const severity_distance_VC = + VCmetric.numerical - findMetricValue("VC", maxVectorObj).numerical; + const severity_distance_VI = + VImetric.numerical - findMetricValue("VI", maxVectorObj).numerical; + const severity_distance_VA = + VAmetric.numerical - findMetricValue("VA", maxVectorObj).numerical; + + const severity_distance_SC = + SCmetric.numerical - findMetricValue("SC", maxVectorObj).numerical; + const severity_distance_SI = + SImetric.numerical - findMetricValue("SI", maxVectorObj).numerical; + const severity_distance_SA = + SAmetric.numerical - findMetricValue("SA", maxVectorObj).numerical; + + const severity_distance_CR = + CRmetric.numerical - findMetricValue("CR", maxVectorObj).numerical; + const severity_distance_IR = + IRmetric.numerical - findMetricValue("IR", maxVectorObj).numerical; + const severity_distance_AR = + ARmetric.numerical - findMetricValue("AR", maxVectorObj).numerical; + + // if any of the values is negative, a greater max vector can be found + if ( + [ + severity_distance_AV, + severity_distance_PR, + severity_distance_UI, + severity_distance_AC, + severity_distance_AT, + severity_distance_VC, + severity_distance_VI, + severity_distance_VA, + severity_distance_SC, + severity_distance_SI, + severity_distance_SA, + severity_distance_CR, + severity_distance_IR, + severity_distance_AR + ].some((met) => met < 0) + ) { + continue; + } + + // add the severity distance of the metric groups to calculate the serverity distance of the equivalent class + eq1Distance = severity_distance_AV + severity_distance_PR + severity_distance_UI; + eq2Distance = severity_distance_AC + severity_distance_AT; + eq3eq6Distance = + severity_distance_VC + + severity_distance_VI + + severity_distance_VA + + severity_distance_CR + + severity_distance_IR + + severity_distance_AR; + eq4Distance = severity_distance_SC + severity_distance_SI + severity_distance_SA; + eq5Distance = 0; + + break; + } + + // calculate maximal scoring difference + const currentMacroVectorValue = cvssLookup_global[macroVector]; + + let eq1MSD = currentMacroVectorValue - eq1NextLowerMarcoVectorScore; + let eq2MSD = currentMacroVectorValue - eq2NextLowerMarcoVectorScore; + let eq3eq6MSD = currentMacroVectorValue - eq3eq6NextLowerMarcoVector; + let eq4MSD = currentMacroVectorValue - eq4NextLowerMarcoVectorScore; + let eq5MSD = currentMacroVectorValue - eq5NextLowerMarcoVectorScore; + + const step = 0.1; + + const eq1MaxSevertity = maxSeverity["eq1"][parseInt(eq1)] * step; + const eq2MaxSevertity = maxSeverity["eq2"][parseInt(eq2)] * step; + const eq3eq6MaxSevertity = maxSeverity["eq3eq6"][parseInt(eq3)][parseInt(eq6)] * step; + const eq4MaxSevertity = maxSeverity["eq4"][parseInt(eq4)] * step; + const eq5MaxSevertity = maxSeverity["eq5"][parseInt(eq5)] * step; + + // 1.1.1 if there is no lower MacroVector the available distance is set to NaN and then ignored in the further calculations + // 1.3 The proportion of the distance is determined by dividing the severity distance of the to-be-scored vector by the depth of the MacroVector + // 1.4 The maximal scoring difference is multiplied by the proportion of distance + let count = 0; + + if (!isNaN(eq1MSD)) { + count++; + eq1MSD = eq1MSD * (eq1Distance / eq1MaxSevertity); + } else { + eq1MSD = 0; + } + if (!isNaN(eq2MSD)) { + count++; + eq2MSD = eq2MSD * (eq2Distance / eq2MaxSevertity); + } else { + eq2MSD = 0; + } + if (!isNaN(eq3eq6MSD)) { + count++; + eq3eq6MSD = eq3eq6MSD * (eq3eq6Distance / eq3eq6MaxSevertity); + } else { + eq3eq6MSD = 0; + } + if (!isNaN(eq4MSD)) { + count++; + eq4MSD = eq4MSD * (eq4Distance / eq4MaxSevertity); + } else { + eq4MSD = 0; + } + if (!isNaN(eq5MSD)) { + count++; + eq5MSD = 0; + } else { + eq5MSD = 0; + } + + // 2. The mean of the above computed proportional distances is computed + let mean = 0; + if (!isNaN(eq1MSD) || !isNaN(eq1MSD) || !isNaN(eq1MSD) || !isNaN(eq1MSD) || !isNaN(eq1MSD)) { + mean = (eq1MSD + eq2MSD + eq3eq6MSD + eq4MSD + eq5MSD) / count; + } + + // normalized_severity_eq1 0.44999999999999996 + // normalized_severity_eq2 0 + // normalized_severity_eq3eq6 0.6749999999999997 + // normalized_severity_eq4 1.2666666666666662 + // normalized_severity_eq5 0 + // value 7.6 + // mean_distance 0.5979166666666664 + + // 3. The score of the vector is the score of the MacroVector (i.e. the score of the highest severity vector) minus the mean distance so computed. + // This score is rounded to one decimal place. + let vectorScore = currentMacroVectorValue - mean; + if (vectorScore < 0) { + vectorScore = 0.0; + } + if (vectorScore > 10) { + vectorScore = 10.0; + } + return vectorScore.toFixed(1); +} + +export const score = { + cvss4_0scoring +}; diff --git a/lib/testFile.ts b/lib/testFile.ts index 497a3f1..5b5bc93 100644 --- a/lib/testFile.ts +++ b/lib/testFile.ts @@ -1,5 +1,80 @@ -import { score } from "./score"; +import { score } from "./score_4_0"; // TODO: delete this file -score.cvss4_0scoring("CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N"); +// examples taken from: https://www.first.org/cvss/v4.0/examples +const examples = [ + { score: 7.3, vector: "CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, + { score: 7.7, vector: "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, + { score: 5.2, vector: "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:U" }, + { score: 8.3, vector: "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N" }, + { + score: 8.1, + vector: + "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N/CR:H/IR:L/AR:L/MAV:N/MAC:H/MVC:H/MVI:L/MVA:L" + }, + { score: 4.6, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N" }, + { score: 5.1, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N" }, + { score: 6.9, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N" }, + { score: 5.9, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N" }, + { score: 9.4, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H" }, + { score: 8.3, vector: "CVSS:4.0/AV:P/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:H/SA:N/S:P/V:D" }, + { score: 8.7, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:A" }, + { score: 10, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:A" }, + { score: 9.3, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:A" }, + { score: 6.4, vector: "CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:H/SI:N/SA:H" }, + { score: 9.3, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/R:I" }, + { score: 8.7, vector: "CVSS:4.0/AV:L/AC:L/AT:P/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/R:I" }, + { score: 8.6, vector: "CVSS:4.0/AV:P/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H" }, + { score: 7.1, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" }, + { score: 8.2, vector: "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" }, + { score: 8.7, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:L" }, + { score: 6.6, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:L/E:U" }, + { score: 5.1, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N" }, + { score: 5.1, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N" }, + { score: 7.7, vector: "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, + { score: 8.3, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N" }, + { score: 5.6, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:U" }, + { score: 8.5, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, + { score: 9.2, vector: "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:A" }, + { score: 5.4, vector: "CVSS:4.0/AV:P/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, + { score: 8.7, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" }, + { score: 6.9, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" }, + { score: 9.3, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, + { score: 6.9, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N" }, + { score: 8.5, vector: "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/S:P" }, + { + score: 9.4, + vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/S:P/AU:Y/V:C/RE:L" + }, + { + score: 7.0, + vector: + "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:P/CR:L/IR:H/AR:L/MAV:L/MAC:H/MAT:N/MPR:N/MUI:N/MVC:N/MVI:H/MVA:L/MSC:N/MSI:S/MSA:L" + }, + { + score: 7.4, + vector: + "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/MAV:A/MAC:H/MAT:N/MPR:L/MUI:N/MVC:L/MVI:H/MVA:H/MSC:L/MSI:S/MSA:S/CR:L/IR:H/AR:H/E:P" + }, + { + score: 8.7, + vector: + "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/MAV:N/MAC:H/MAT:N/MPR:L/MUI:N/MVC:H/MVI:H/MVA:H/MSC:H/MSI:S/MSA:H/CR:M/IR:H/AR:M/E:P" + }, + { score: 8.6, vector: "CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/S:P" }, + { + score: 9.7, + vector: "CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/MSI:S/S:P" + }, + { score: 8.7, vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/V:C" } +]; + +let i = 0; +for (const ex of examples) { + // if (i == 37) break; + console.log("------------------------------------"); + console.log("Example nr.", ++i); + const calcScore = score.cvss4_0scoring(ex.vector); + console.log("Should be value of:", ex.score, "Calculation is at:", calcScore); +}