From a8f1b4c33ee1e09010a5add6b2d7df25e1710692 Mon Sep 17 00:00:00 2001 From: miko37 Date: Tue, 21 May 2024 15:04:39 +0200 Subject: [PATCH] distinguish between version 3.0 and 4.0 --- lib/cvss.ts | 29 +- lib/score.ts | 694 ------------------------ lib/score_3_0.ts | 271 +++++++++ lib/score_4_0.ts | 512 +++++++++-------- lib/testFile.ts | 83 +-- lib/util.ts | 72 ++- test/{cvss.spec.js => cvss_3_0.spec.js} | 0 test/cvss_4_0.spec.js | 117 ++++ 8 files changed, 758 insertions(+), 1020 deletions(-) delete mode 100644 lib/score.ts create mode 100644 lib/score_3_0.ts rename test/{cvss.spec.js => cvss_3_0.spec.js} (100%) create mode 100644 test/cvss_4_0.spec.js diff --git a/lib/cvss.ts b/lib/cvss.ts index 7d3d723..be70e49 100644 --- a/lib/cvss.ts +++ b/lib/cvss.ts @@ -1,6 +1,7 @@ import { CvssVectorObject } from "./types"; import { util } from "./util"; -import { score } from "./score"; +import { score as score3_0 } from "./score_3_0"; +import { score as score4_0 } from "./score_4_0"; /** * Creates a new CVSS object @@ -9,7 +10,7 @@ import { score } from "./score"; */ export function CVSS(cvss: string | CvssVectorObject) { const vector = util.parseVectorObjectToString(cvss); - + const score = util.getVersion(vector) === "4.0" ? score4_0 : score3_0; /** * Retrieves an object of vector's metrics * Calls a function from util.js @@ -44,9 +45,13 @@ export function CVSS(cvss: string | CvssVectorObject) { * Calculates the Temporal Rating of the given vector * Calls a function from util.js * + * Only available for cvss 3.0 and 3.1 + * * @returns {string} returns one of the five possible ratings */ function getTemporalRating() { + if (!(util.getVersion(vector) === "3.0" || util.getVersion(vector) === "3.1")) + throw "This function is not supported for this cvss version"; return util.getRating(getTemporalScore()); } @@ -54,9 +59,13 @@ export function CVSS(cvss: string | CvssVectorObject) { * Calculates the Environmental Rating of the given vector * Calls a function from util.js * + * Only available for cvss 3.0 and 3.1 + * * @returns {string} returns one of the five possible ratings */ function getEnvironmentalRating() { + if (!(util.getVersion(vector) === "3.0" || util.getVersion(vector) === "3.1")) + throw "This function is not supported for this cvss version"; return util.getRating(getEnvironmentalScore()); } @@ -90,19 +99,25 @@ export function CVSS(cvss: string | CvssVectorObject) { /** * Parses the vector to the temporal score + * Only available for cvss 3.0 and 3.1 * * @returns {number} Temporal Score */ function getTemporalScore() { + if (!(util.getVersion(vector) === "3.0" || util.getVersion(vector) === "3.1")) + throw "This function is not supported for this cvss version"; return score.getTemporalScore(vector); } /** * Parses the vector to the environmental score + * Only available for cvss 3.0 and 3.1 * * @returns {number} Environmental Score */ function getEnvironmentalScore() { + if (!(util.getVersion(vector) === "3.0" || util.getVersion(vector) === "3.1")) + throw "This function is not supported for this cvss version"; return score.getEnvironmentalScore(vector); } @@ -134,10 +149,14 @@ export function CVSS(cvss: string | CvssVectorObject) { * * Scope Unchanged 6.42 × ISCBase * Scope Changed 7.52 × [ISCBase − 0.029] − 3.25 × [ISCBase - 0.02]15 - * * + * + * Only available for cvss 3.0 and 3.1 + * * @returns {number} Impact sub score */ function getImpactSubScore() { + if (!(util.getVersion(vector) === "3.0" || util.getVersion(vector) === "3.1")) + throw "This function is not supported for this cvss version"; return score.getImpactSubScore(vector); } @@ -146,9 +165,13 @@ export function CVSS(cvss: string | CvssVectorObject) { * * 8.22 x AttackVector x AttackComplexity x PrivilegeRequired x UserInteraction * + * Only available for cvss 3.0 and 3.1 + * * @returns {number} Exploitability sub score */ function getExploitabilitySubScore() { + if (!(util.getVersion(vector) === "3.0" || util.getVersion(vector) === "3.1")) + throw "This function is not supported for this cvss version"; return score.getExploitabilitySubScore(vector); } diff --git a/lib/score.ts b/lib/score.ts deleted file mode 100644 index b328ce2..0000000 --- a/lib/score.ts +++ /dev/null @@ -1,694 +0,0 @@ -import { - CvssVectorObject, - MetricPrivilegesRequired, - Metric, - MetricScope, - MetricUnion -} from "./types"; -import { util } from "./util"; - -/** - * Parses the vector to a number score - * - * @param {string} vector The vector string - * - * @returns {number} Calculated Score - */ -function getScore(vector: string) { - const vectorObject = util.getVectorObject(vector); - - const scopeChanged = vectorObject.S === "C"; - const ISCBase = calculateISCBase(vectorObject); - const ISC = calculateISC(ISCBase, scopeChanged, vector); - - if (ISC <= 0) return 0; - - const exploitability = calculateExploitability(vectorObject, scopeChanged); - - if (scopeChanged) { - return roundUp(Math.min(1.08 * (ISC + exploitability), 10), 1, vector); - } - - return roundUp(Math.min(ISC + exploitability, 10), 1, vector); -} - -/** - * Parses the vector to the temporal score - * - * @param {string} vector The vector string - * - * @returns {number} Temporal Score - */ -function getTemporalScore(vector: string) { - const vectorObject = util.getVectorObject(vector); - - const baseScore = getScore(vector); - - const eMetric = util.findMetricValue("E", vectorObject); - const exploitCodeMaturity = eMetric ? eMetric.numerical : 1; - const rMetric = util.findMetricValue("RL", vectorObject); - const remediationLevel = rMetric ? rMetric.numerical : 1; - const rcMetric = util.findMetricValue("RC", vectorObject); - const reportConfidence = rcMetric ? rcMetric.numerical : 1; - - return roundUp(baseScore * exploitCodeMaturity * remediationLevel * reportConfidence, 1, vector); -} - -/** - * Calculate the ISC base based on the cvss vector object - * - * @param {CvssVectorObject} vectorObject The cvss vector object - * - * @returns {number} ISC base - */ -const calculateISCBase = function (vectorObject: CvssVectorObject) { - const cValue = util.findMetricValue("C", vectorObject).numerical; - const iValue = util.findMetricValue("I", vectorObject).numerical; - const aValue = util.findMetricValue("A", vectorObject).numerical; - - return 1 - (1 - cValue) * (1 - iValue) * (1 - aValue); -}; - -/** - * Parses the vector to the environmental score - * - * @param {string} vector The vector string - * - * @returns {number} Environmental Score - */ -function getEnvironmentalScore(vector: string) { - const vectorObject = util.getVectorObject(vector); - const scopeChanged = vectorObject.MS === "X" ? vectorObject.S === "C" : vectorObject.MS === "C"; - const modifiedISCBase = calculateISCModifiedBase(vectorObject); - const modifiedExploitability = calculateModifiedExploitability(vectorObject, scopeChanged); - const modifiedISC = calculateModifiedISC(modifiedISCBase, scopeChanged, vector); - - if (modifiedISC <= 0) return 0; - - const e = util.findMetricValue("E", vectorObject); - const rl = util.findMetricValue("RL", vectorObject); - const rc = util.findMetricValue("RC", vectorObject); - const eValue = e ? e.numerical : 1; - const rlValue = rl ? rl.numerical : 1; - const rcValue = rc ? rc.numerical : 1; - - if (!scopeChanged) { - return roundUp( - roundUp(Math.min(modifiedISC + modifiedExploitability, 10), 1, vector) * - eValue * - rlValue * - rcValue, - 1, - vector - ); - } - return roundUp( - roundUp(Math.min(1.08 * (modifiedISC + modifiedExploitability), 10), 1, vector) * - eValue * - rlValue * - rcValue, - 1, - vector - ); -} - -/** - * Calculates the ISC value based on the ISC base, whether the scope has changed and the vector string - * - * @param {number} iscBase Value of the ISC base - * @param {boolean} scopeChanged Boolean value whether the scope has changed - * @param {string} vector The vector string - * - * @returns {number} ISC value - */ -const calculateISC = function (iscBase: number, scopeChanged: boolean, vector: string) { - if (!scopeChanged) return 6.42 * iscBase; - if (util.getVersion(vector) === "3.0") { - return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15); - } else if (util.getVersion(vector) === "3.1") { - return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15); - } -}; - -/** - * Calculates the modified ISC value based on the ISC base, whether the scope has changed and the vector string - * - * @param {number} iscBase Value of the ISC base - * @param {boolean} scopeChanged Boolean value whether the scope has changed - * @param {string} vector The vector string - * - * @returns {number} Modified ISC value - */ -const calculateModifiedISC = function (iscBase: number, scopeChanged: boolean, vector: string) { - if (!scopeChanged) return 6.42 * iscBase; - if (util.getVersion(vector) === "3.0") { - return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15); - } else if (util.getVersion(vector) === "3.1") { - return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase * 0.9731 - 0.02, 13); - } -}; - -/** - * Calculates the exploitability value based on the cvss vector object and whether the scope has changed - * - * @param {CvssVectorObject} vectorObject Cvss vector object - * @param {boolean} scopeChanged Boolean value whether the scope has changed - * - * @returns {number} Exploitability value - */ -const calculateExploitability = function (vectorObject: CvssVectorObject, scopeChanged: boolean) { - const avValue = util.findMetricValue("AV", vectorObject).numerical; - const acValue = util.findMetricValue("AC", vectorObject).numerical; - const prMetrics = util.findMetricValue("PR", vectorObject).numerical; - const uiValue = util.findMetricValue("UI", vectorObject).numerical; - - const prValue = scopeChanged ? prMetrics.changed : prMetrics.unchanged; - - return 8.22 * avValue * acValue * prValue * uiValue; -}; - -/** - * Calculates the ISC modified base value based on the cvss vector object - * - * @param {CvssVectorObject} vectorObject Cvss vector object - * - * @returns {number} ISC modified base value - */ -const calculateISCModifiedBase = function (vectorObject: CvssVectorObject) { - let mcValue = util.findMetricValue("MC", vectorObject); - let miValue = util.findMetricValue("MI", vectorObject); - let maValue = util.findMetricValue("MA", vectorObject); - const crValue = util.findMetricValue("CR", vectorObject).numerical; - const irValue = util.findMetricValue("IR", vectorObject).numerical; - const arValue = util.findMetricValue("AR", vectorObject).numerical; - - if (!mcValue || mcValue.abbr === "X") mcValue = util.findMetricValue("C", vectorObject); - if (!miValue || miValue.abbr === "X") miValue = util.findMetricValue("I", vectorObject); - if (!maValue || maValue.abbr === "X") maValue = util.findMetricValue("A", vectorObject); - - return Math.min( - 1 - - (1 - mcValue.numerical * crValue) * - (1 - miValue.numerical * irValue) * - (1 - maValue.numerical * arValue), - 0.915 - ); -}; - -const calculateModifiedExploitability = function ( - vectorObject: CvssVectorObject, - scopeChanged: boolean -) { - let mavValue = util.findMetricValue("MAV", vectorObject); - let macValue = util.findMetricValue("MAC", vectorObject); - let mprMetrics = util.findMetricValue("MPR", vectorObject); - let muiValue = util.findMetricValue("MUI", vectorObject); - - if (!mavValue || mavValue.abbr === "X") mavValue = util.findMetricValue("AV", vectorObject); - if (!macValue || macValue.abbr === "X") macValue = util.findMetricValue("AC", vectorObject); - if (!mprMetrics || mprMetrics.abbr === "X") mprMetrics = util.findMetricValue("PR", vectorObject); - if (!muiValue || muiValue.abbr === "X") muiValue = util.findMetricValue("UI", vectorObject); - - const mprValue = scopeChanged ? mprMetrics.numerical.changed : mprMetrics.numerical.unchanged; - - return 8.22 * mavValue.numerical * macValue.numerical * mprValue * muiValue.numerical; -}; - -/** - * Chooses the correct way to round numbers depending on the CVSS version number - * - * @param {number} num The number to round - * @param {number} precision The number of decimal places to preserve (only affects CVSS 3.0) - * @param {string} vector The vector string currently being parsed - * - * @returns {number} The rounded number - */ -function roundUp(num: number, precision: number, vector: string) { - if (util.getVersion(vector) === "3.0") { - return util.roundUpApprox(num, precision); - } else if (util.getVersion(vector) === "3.1") { - return util.roundUpExact(num); - } -} - -/** - * Returns an Impact sub score - * - * ISCBase = 1 − [(1 − ImpactConf) × (1 − ImpactInteg) × (1 − ImpactAvail)] - * - * Scope Unchanged 6.42 × ISCBase - * Scope Changed 7.52 × [ISCBase − 0.029] − 3.25 × [ISCBase - 0.02]15 - * - * @param {string} vector The vector string currently being parsed - * - * @returns {number} Impact sub score - */ -function getImpactSubScore(vector: string) { - const vectorObject = util.getVectorObject(vector); - const { S } = vectorObject; - - const ISCBase = calculateISCBase(vectorObject); - - return Number(calculateISC(ISCBase, S === "C", vector).toFixed(1)); -} - -/** - * Returns an Exploitability sub score - * - * 8.22 x AttackVector x AttackComplexity x PrivilegeRequired x UserInteraction - * - * @param {string} vector The vector string currently being parsed - * - * @returns {number} Exploitability sub score - */ -function getExploitabilitySubScore(vector: string) { - const vectorObject = util.getVectorObject(vector); - const { S } = vectorObject; - - return Number(calculateExploitability(vectorObject, S === "C").toFixed(1)); -} - -// testing 4.0 score calc -// -// -// -// -// -// -// -// - -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 = { - getScore, - getTemporalScore, - getEnvironmentalScore, - getImpactSubScore, - getExploitabilitySubScore, - cvss4_0scoring -}; diff --git a/lib/score_3_0.ts b/lib/score_3_0.ts new file mode 100644 index 0000000..d0aa6b6 --- /dev/null +++ b/lib/score_3_0.ts @@ -0,0 +1,271 @@ +import { CvssVectorObject, MetricPrivilegesRequired, Metric } from "./types"; +import { util } from "./util"; + +/** + * Parses the vector to a number score + * + * @param {string} vector The vector string + * + * @returns {number} Calculated Score + */ +function getScore(vector: string) { + const vectorObject = util.getVectorObject(vector); + + const scopeChanged = vectorObject.S === "C"; + const ISCBase = calculateISCBase(vectorObject); + const ISC = calculateISC(ISCBase, scopeChanged, vector); + + if (ISC <= 0) return 0; + + const exploitability = calculateExploitability(vectorObject, scopeChanged); + + if (scopeChanged) { + return roundUp(Math.min(1.08 * (ISC + exploitability), 10), 1, vector); + } + + return roundUp(Math.min(ISC + exploitability, 10), 1, vector); +} + +/** + * Parses the vector to the temporal score + * + * @param {string} vector The vector string + * + * @returns {number} Temporal Score + */ +function getTemporalScore(vector: string) { + const vectorObject = util.getVectorObject(vector); + + const baseScore = getScore(vector); + + const eMetric = util.findMetricValue("E", vectorObject); + const exploitCodeMaturity = eMetric ? eMetric.numerical : 1; + const rMetric = util.findMetricValue("RL", vectorObject); + const remediationLevel = rMetric ? rMetric.numerical : 1; + const rcMetric = util.findMetricValue("RC", vectorObject); + const reportConfidence = rcMetric ? rcMetric.numerical : 1; + + return roundUp(baseScore * exploitCodeMaturity * remediationLevel * reportConfidence, 1, vector); +} + +/** + * Calculate the ISC base based on the cvss vector object + * + * @param {CvssVectorObject} vectorObject The cvss vector object + * + * @returns {number} ISC base + */ +const calculateISCBase = function (vectorObject: CvssVectorObject) { + const cValue = util.findMetricValue("C", vectorObject).numerical; + const iValue = util.findMetricValue("I", vectorObject).numerical; + const aValue = util.findMetricValue("A", vectorObject).numerical; + + return 1 - (1 - cValue) * (1 - iValue) * (1 - aValue); +}; + +/** + * Parses the vector to the environmental score + * + * @param {string} vector The vector string + * + * @returns {number} Environmental Score + */ +function getEnvironmentalScore(vector: string) { + const vectorObject = util.getVectorObject(vector); + const scopeChanged = vectorObject.MS === "X" ? vectorObject.S === "C" : vectorObject.MS === "C"; + const modifiedISCBase = calculateISCModifiedBase(vectorObject); + const modifiedExploitability = calculateModifiedExploitability(vectorObject, scopeChanged); + const modifiedISC = calculateModifiedISC(modifiedISCBase, scopeChanged, vector); + + if (modifiedISC <= 0) return 0; + + const e = util.findMetricValue("E", vectorObject); + const rl = util.findMetricValue("RL", vectorObject); + const rc = util.findMetricValue("RC", vectorObject); + const eValue = e ? e.numerical : 1; + const rlValue = rl ? rl.numerical : 1; + const rcValue = rc ? rc.numerical : 1; + + if (!scopeChanged) { + return roundUp( + roundUp(Math.min(modifiedISC + modifiedExploitability, 10), 1, vector) * + eValue * + rlValue * + rcValue, + 1, + vector + ); + } + return roundUp( + roundUp(Math.min(1.08 * (modifiedISC + modifiedExploitability), 10), 1, vector) * + eValue * + rlValue * + rcValue, + 1, + vector + ); +} + +/** + * Calculates the ISC value based on the ISC base, whether the scope has changed and the vector string + * + * @param {number} iscBase Value of the ISC base + * @param {boolean} scopeChanged Boolean value whether the scope has changed + * @param {string} vector The vector string + * + * @returns {number} ISC value + */ +const calculateISC = function (iscBase: number, scopeChanged: boolean, vector: string) { + if (!scopeChanged) return 6.42 * iscBase; + if (util.getVersion(vector) === "3.0") { + return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15); + } else if (util.getVersion(vector) === "3.1") { + return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15); + } +}; + +/** + * Calculates the modified ISC value based on the ISC base, whether the scope has changed and the vector string + * + * @param {number} iscBase Value of the ISC base + * @param {boolean} scopeChanged Boolean value whether the scope has changed + * @param {string} vector The vector string + * + * @returns {number} Modified ISC value + */ +const calculateModifiedISC = function (iscBase: number, scopeChanged: boolean, vector: string) { + if (!scopeChanged) return 6.42 * iscBase; + if (util.getVersion(vector) === "3.0") { + return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15); + } else if (util.getVersion(vector) === "3.1") { + return 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase * 0.9731 - 0.02, 13); + } +}; + +/** + * Calculates the exploitability value based on the cvss vector object and whether the scope has changed + * + * @param {CvssVectorObject} vectorObject Cvss vector object + * @param {boolean} scopeChanged Boolean value whether the scope has changed + * + * @returns {number} Exploitability value + */ +const calculateExploitability = function (vectorObject: CvssVectorObject, scopeChanged: boolean) { + const avValue = util.findMetricValue("AV", vectorObject).numerical; + const acValue = util.findMetricValue("AC", vectorObject).numerical; + const prMetrics = util.findMetricValue("PR", vectorObject).numerical; + const uiValue = util.findMetricValue("UI", vectorObject).numerical; + + const prValue = scopeChanged ? prMetrics.changed : prMetrics.unchanged; + + return 8.22 * avValue * acValue * prValue * uiValue; +}; + +/** + * Calculates the ISC modified base value based on the cvss vector object + * + * @param {CvssVectorObject} vectorObject Cvss vector object + * + * @returns {number} ISC modified base value + */ +const calculateISCModifiedBase = function (vectorObject: CvssVectorObject) { + let mcValue = util.findMetricValue("MC", vectorObject); + let miValue = util.findMetricValue("MI", vectorObject); + let maValue = util.findMetricValue("MA", vectorObject); + const crValue = util.findMetricValue("CR", vectorObject).numerical; + const irValue = util.findMetricValue("IR", vectorObject).numerical; + const arValue = util.findMetricValue("AR", vectorObject).numerical; + + if (!mcValue || mcValue.abbr === "X") mcValue = util.findMetricValue("C", vectorObject); + if (!miValue || miValue.abbr === "X") miValue = util.findMetricValue("I", vectorObject); + if (!maValue || maValue.abbr === "X") maValue = util.findMetricValue("A", vectorObject); + + return Math.min( + 1 - + (1 - mcValue.numerical * crValue) * + (1 - miValue.numerical * irValue) * + (1 - maValue.numerical * arValue), + 0.915 + ); +}; + +const calculateModifiedExploitability = function ( + vectorObject: CvssVectorObject, + scopeChanged: boolean +) { + let mavValue = util.findMetricValue("MAV", vectorObject); + let macValue = util.findMetricValue("MAC", vectorObject); + let mprMetrics = util.findMetricValue("MPR", vectorObject); + let muiValue = util.findMetricValue("MUI", vectorObject); + + if (!mavValue || mavValue.abbr === "X") mavValue = util.findMetricValue("AV", vectorObject); + if (!macValue || macValue.abbr === "X") macValue = util.findMetricValue("AC", vectorObject); + if (!mprMetrics || mprMetrics.abbr === "X") mprMetrics = util.findMetricValue("PR", vectorObject); + if (!muiValue || muiValue.abbr === "X") muiValue = util.findMetricValue("UI", vectorObject); + + const mprValue = scopeChanged ? mprMetrics.numerical.changed : mprMetrics.numerical.unchanged; + + return 8.22 * mavValue.numerical * macValue.numerical * mprValue * muiValue.numerical; +}; + +/** + * Chooses the correct way to round numbers depending on the CVSS version number + * + * @param {number} num The number to round + * @param {number} precision The number of decimal places to preserve (only affects CVSS 3.0) + * @param {string} vector The vector string currently being parsed + * + * @returns {number} The rounded number + */ +function roundUp(num: number, precision: number, vector: string) { + if (util.getVersion(vector) === "3.0") { + return util.roundUpApprox(num, precision); + } else if (util.getVersion(vector) === "3.1") { + return util.roundUpExact(num); + } +} + +/** + * Returns an Impact sub score + * + * ISCBase = 1 − [(1 − ImpactConf) × (1 − ImpactInteg) × (1 − ImpactAvail)] + * + * Scope Unchanged 6.42 × ISCBase + * Scope Changed 7.52 × [ISCBase − 0.029] − 3.25 × [ISCBase - 0.02]15 + * + * @param {string} vector The vector string currently being parsed + * + * @returns {number} Impact sub score + */ +function getImpactSubScore(vector: string) { + const vectorObject = util.getVectorObject(vector); + const { S } = vectorObject; + + const ISCBase = calculateISCBase(vectorObject); + + return Number(calculateISC(ISCBase, S === "C", vector).toFixed(1)); +} + +/** + * Returns an Exploitability sub score + * + * 8.22 x AttackVector x AttackComplexity x PrivilegeRequired x UserInteraction + * + * @param {string} vector The vector string currently being parsed + * + * @returns {number} Exploitability sub score + */ +function getExploitabilitySubScore(vector: string) { + const vectorObject = util.getVectorObject(vector); + const { S } = vectorObject; + + return Number(calculateExploitability(vectorObject, S === "C").toFixed(1)); +} + +export const score = { + getScore, + getTemporalScore, + getEnvironmentalScore, + getImpactSubScore, + getExploitabilitySubScore +}; diff --git a/lib/score_4_0.ts b/lib/score_4_0.ts index caf826f..4013739 100644 --- a/lib/score_4_0.ts +++ b/lib/score_4_0.ts @@ -1,17 +1,14 @@ -import { CvssVectorObject, Metric, MetricScope, MetricUnion } from "./types"; +import { CvssVectorObject, Metric, MetricUnion } from "./types"; import { definitions, cvssLookup_global, maxComposed, maxSeverity } from "./cvss_4_0"; +import { util } from "./util"; -const findMetric4_0 = function (abbr: string) { - return definitions.definitions.find((def) => def.abbr === abbr); -}; - -const findMetricValue = function ( +const parseMetric = function ( + value: T, abbr: string, vectorObject: CvssVectorObject ) { - const definition = findMetric4_0(abbr); - let value = definition.metrics.find((metric) => metric.abbr === vectorObject[definition.abbr]); + const definition = util.findMetric(abbr, vectorObject.CVSS); if (vectorObject.CVSS === "4.0") { // If E=X it will default to the worst case i.e. E=A @@ -33,106 +30,85 @@ const findMetricValue = function ( // 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); + const modifiedDefinition = util.findMetric("M" + abbr, vectorObject.CVSS); value = definition.metrics.find( (metric) => metric.abbr === vectorObject[modifiedDefinition.abbr] - ); + ) as T; } } - - return value as T; + return value; }; -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]; +function getScore(vector: string) { + const vectorObj = util.getVectorObject(vector); + + const metrics: { [key: string]: Metric } = { + AV: {} as Metric, // EQ1 + PR: {} as Metric, // EQ1 + UI: {} as Metric, // EQ1 + AC: {} as Metric, // EQ2 + AT: {} as Metric, // EQ2 + VC: {} as Metric, // EQ3 + EQ6 + VI: {} as Metric, // EQ3 + EQ6 + VA: {} as Metric, // EQ3 + EQ6 + SC: {} as Metric, // EQ4 + SI: {} as Metric, // EQ4 + SA: {} as Metric, // EQ4 + MSI: {} as Metric, // EQ4 + MSA: {} as Metric, // EQ4 + E: {} as Metric, // EQ5 + CR: {} as Metric, // EQ6 + IR: {} as Metric, // EQ6 + AR: {} as Metric // EQ6 + }; + + for (let [key] of Object.entries(metrics)) { + metrics[key] = parseMetric( + util.findMetricValue(key, vectorObj), + key, + vectorObj + ); } - 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"; + const eqLevels = { eq1: "0", eq2: "0", eq3: "0", eq4: "0", eq5: "0", 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"; + if (metrics.AV.abbr === "N" && metrics.PR.abbr === "N" && metrics.UI.abbr === "N") + eqLevels.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") + (metrics.AV.abbr === "N" || metrics.PR.abbr === "N" || metrics.UI.abbr === "N") && + !(metrics.AV.abbr === "N" && metrics.PR.abbr === "N" && metrics.UI.abbr === "N") && + !(metrics.AV.abbr === "P") ) - eq1 = "1"; + eqLevels.eq1 = "1"; else if ( - AVmetric.abbr === "P" || - !(AVmetric.abbr === "N" || PRmetric.abbr === "N" || UImetric.abbr === "N") + metrics.AV.abbr === "P" || + !(metrics.AV.abbr === "N" || metrics.PR.abbr === "N" || metrics.UI.abbr === "N") ) - eq1 = "2"; + eqLevels.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"; + if (metrics.AC.abbr === "L" && metrics.AT.abbr === "N") eqLevels.eq2 = "0"; + else if (!(metrics.AC.abbr === "L" && metrics.AT.abbr === "N")) eqLevels.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"; + if (metrics.VC.abbr === "H" && metrics.VI.abbr === "H") eqLevels.eq3 = "0"; else if ( - !(VCmetric.abbr === "H" && VImetric.abbr === "H") && - (VCmetric.abbr === "H" || VImetric.abbr === "H" || VAmetric.abbr === "H") + !(metrics.VC.abbr === "H" && metrics.VI.abbr === "H") && + (metrics.VC.abbr === "H" || metrics.VI.abbr === "H" || metrics.VA.abbr === "H") ) - eq3 = "1"; - else if (!(VCmetric.abbr === "H" || VImetric.abbr === "H" || VAmetric.abbr === "H")) eq3 = "2"; + eqLevels.eq3 = "1"; + else if (!(metrics.VC.abbr === "H" || metrics.VI.abbr === "H" || metrics.VA.abbr === "H")) + eqLevels.eq3 = "2"; // EQ4 // 0 MSI:S or MSA:S @@ -140,45 +116,46 @@ function cvss4_0scoring(vector: string) { // 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"; + if (metrics.MSI.abbr === "S" || metrics.MSA.abbr === "S") eqLevels.eq4 = "0"; else if ( - !(MSImetric.abbr === "S" || MSAmetric.abbr === "S") && - (SCmetric.abbr === "H" || SImetric.abbr === "H" || SAmetric.abbr === "H") + !(metrics.MSI.abbr === "S" || metrics.MSA.abbr === "S") && + (metrics.SC.abbr === "H" || metrics.SI.abbr === "H" || metrics.SA.abbr === "H") ) - eq4 = "1"; + eqLevels.eq4 = "1"; else if ( - !(MSImetric.abbr === "S" || MSAmetric.abbr === "S") && - !(SCmetric.abbr === "H" || SImetric.abbr === "H" || SAmetric.abbr === "H") + !(metrics.MSI.abbr === "S" || metrics.MSA.abbr === "S") && + !(metrics.SC.abbr === "H" || metrics.SI.abbr === "H" || metrics.SA.abbr === "H") ) - eq4 = "2"; + eqLevels.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"; + if (metrics.E.abbr === "A") eqLevels.eq5 = "0"; + else if (metrics.E.abbr === "P") eqLevels.eq5 = "1"; + else if (metrics.E.abbr === "U") eqLevels.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") + ((metrics.CR.abbr === "H" || metrics.CR.abbr === "X") && metrics.VC.abbr === "H") || + ((metrics.IR.abbr === "H" || metrics.IR.abbr === "X") && metrics.VI.abbr === "H") || + ((metrics.AR.abbr === "H" || metrics.AR.abbr === "X") && metrics.VA.abbr === "H") ) - eq6 = "0"; + eqLevels.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") + !((metrics.CR.abbr === "H" || metrics.CR.abbr === "X") && metrics.VC.abbr === "H") && + !((metrics.IR.abbr === "H" || metrics.IR.abbr === "X") && metrics.VI.abbr === "H") && + !((metrics.AR.abbr === "H" || metrics.AR.abbr === "X") && metrics.VA.abbr === "H") ) - eq6 = "1"; + eqLevels.eq6 = "1"; - const macroVector = eq1 + eq2 + eq3 + eq4 + eq5 + eq6; + const macroVector = + eqLevels.eq1 + eqLevels.eq2 + eqLevels.eq3 + eqLevels.eq4 + eqLevels.eq5 + eqLevels.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 @@ -186,32 +163,113 @@ function cvss4_0scoring(vector: string) { // 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)]; + cvssLookup_global[ + "".concat( + "" + (parseInt(eqLevels.eq1) + 1), + eqLevels.eq2, + eqLevels.eq3, + eqLevels.eq4, + eqLevels.eq5, + eqLevels.eq6 + ) + ]; const eq2NextLowerMarcoVectorScore = - cvssLookup_global["".concat(eq1, "" + (parseInt(eq2) + 1), eq3, eq4, eq5, eq6)]; + cvssLookup_global[ + "".concat( + eqLevels.eq1, + "" + (parseInt(eqLevels.eq2) + 1), + eqLevels.eq3, + eqLevels.eq4, + eqLevels.eq5, + eqLevels.eq6 + ) + ]; const eq4NextLowerMarcoVectorScore = - cvssLookup_global["".concat(eq1, eq2, eq3, "" + (parseInt(eq4) + 1), eq5, eq6)]; + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + eqLevels.eq3, + "" + (parseInt(eqLevels.eq4) + 1), + eqLevels.eq5, + eqLevels.eq6 + ) + ]; const eq5NextLowerMarcoVectorScore = - cvssLookup_global["".concat(eq1, eq2, eq3, eq4, "" + (parseInt(eq5) + 1), eq6)]; + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + eqLevels.eq3, + eqLevels.eq4, + "" + (parseInt(eqLevels.eq5) + 1), + eqLevels.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") { + if (eqLevels.eq3 === "1" && eqLevels.eq6 === "1") { eq3eq6NextLowerMarcoVector = - cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; - } else if (eq3 === "1" && eq6 === "0") { + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + "" + (parseInt(eqLevels.eq3) + 1), + eqLevels.eq4, + eqLevels.eq5, + eqLevels.eq6 + ) + ]; + } else if (eqLevels.eq3 === "1" && eqLevels.eq6 === "0") { eq3eq6NextLowerMarcoVector = - cvssLookup_global["".concat(eq1, eq2, eq3, eq4, eq5, "" + (parseInt(eq6) + 1))]; - } else if (eq3 === "0" && eq6 === "1") { + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + eqLevels.eq3, + eqLevels.eq4, + eqLevels.eq5, + "" + (parseInt(eqLevels.eq6) + 1) + ) + ]; + } else if (eqLevels.eq3 === "0" && eqLevels.eq6 === "1") { eq3eq6NextLowerMarcoVector = - cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; - } else if (eq3 === "0" && eq6 === "0") { + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + "" + (parseInt(eqLevels.eq3) + 1), + eqLevels.eq4, + eqLevels.eq5, + eqLevels.eq6 + ) + ]; + } else if (eqLevels.eq3 === "0" && eqLevels.eq6 === "0") { eq3eq6NextLowerLeftMarcoVector = - cvssLookup_global["".concat(eq1, eq2, eq3, eq4, eq5, "" + (parseInt(eq6) + 1))]; + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + eqLevels.eq3, + eqLevels.eq4, + eqLevels.eq5, + "" + (parseInt(eqLevels.eq6) + 1) + ) + ]; eq3eq6NextLowerRightMarcoVector = - cvssLookup_global["".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, eq6)]; + cvssLookup_global[ + "".concat( + eqLevels.eq1, + eqLevels.eq2, + "" + (parseInt(eqLevels.eq3) + 1), + eqLevels.eq4, + eqLevels.eq5, + eqLevels.eq6 + ) + ]; eq3eq6NextLowerMarcoVector = eq3eq6NextLowerLeftMarcoVector > eq3eq6NextLowerRightMarcoVector ? eq3eq6NextLowerLeftMarcoVector @@ -220,180 +278,158 @@ function cvss4_0scoring(vector: string) { else eq3eq6NextLowerMarcoVector = cvssLookup_global[ - "".concat(eq1, eq2, "" + (parseInt(eq3) + 1), eq4, eq5, "" + (parseInt(eq6) + 1)) + "".concat( + eqLevels.eq1, + eqLevels.eq2, + "" + (parseInt(eqLevels.eq3) + 1), + eqLevels.eq4, + eqLevels.eq5, + "" + (parseInt(eqLevels.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)]; + const maxima = { + eq1: maxComposed["eq1"][parseInt(eqLevels.eq1)], + eq2: maxComposed["eq2"][parseInt(eqLevels.eq2)], + eq3eq6: maxComposed["eq3"][parseInt(eqLevels.eq3)][parseInt(eqLevels.eq6)], + eq4: maxComposed["eq4"][parseInt(eqLevels.eq4)], + eq5: maxComposed["eq5"][parseInt(eqLevels.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); + const possibleMaximumVectorStrings = []; + for (const eq1Max of maxima.eq1) { + for (const eq2Max of maxima.eq2) { + for (const eq3eq6Max of maxima.eq3eq6) { + for (const eq4Max of maxima.eq4) { + for (const eq5Max of maxima.eq5) { + possibleMaximumVectorStrings.push( + "CVSS:4.0/" + 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); + const eqDistance = { eq1: 0, eq2: 0, eq3eq6: 0, eq4: 0, eq5: 0 }; + outerLoop: for (let i = 0; i < possibleMaximumVectorStrings.length; i++) { + const max = possibleMaximumVectorStrings[i]; + const maxVectorObj = util.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; + const severityDistance = { + AV: 0, + PR: 0, + UI: 0, + AC: 0, + AT: 0, + VC: 0, + VI: 0, + VA: 0, + SC: 0, + SI: 0, + SA: 0, + CR: 0, + IR: 0, + AR: 0 + }; + + innerLoop: for (let [key] of Object.entries(severityDistance)) { + severityDistance[key] = + metrics[key].numerical - + parseMetric(util.findMetricValue(key, maxVectorObj), key, maxVectorObj) + .numerical; + + // if any of the values is negative, a greater max vector can be found + if (severityDistance[key] < 0) { + continue outerLoop; + } } // 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; + eqDistance.eq1 = severityDistance.AV + severityDistance.PR + severityDistance.UI; + eqDistance.eq2 = severityDistance.AC + severityDistance.AT; + eqDistance.eq3eq6 = + severityDistance.VC + + severityDistance.VI + + severityDistance.VA + + severityDistance.CR + + severityDistance.IR + + severityDistance.AR; + eqDistance.eq4 = severityDistance.SC + severityDistance.SI + severityDistance.SA; + eqDistance.eq5 = 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 msd = { + eq1: currentMacroVectorValue - eq1NextLowerMarcoVectorScore, + eq2: currentMacroVectorValue - eq2NextLowerMarcoVectorScore, + eq3eq6: currentMacroVectorValue - eq3eq6NextLowerMarcoVector, + eq4: currentMacroVectorValue - eq4NextLowerMarcoVectorScore, + eq5: 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; + const maxSeverityNormalized = { + eq1: maxSeverity["eq1"][parseInt(eqLevels.eq1)] * step, + eq2: maxSeverity["eq2"][parseInt(eqLevels.eq2)] * step, + eq3eq6: maxSeverity["eq3eq6"][parseInt(eqLevels.eq3)][parseInt(eqLevels.eq6)] * step, + eq4: maxSeverity["eq4"][parseInt(eqLevels.eq4)] * step, + eq5: maxSeverity["eq5"][parseInt(eqLevels.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)) { + if (!isNaN(msd.eq1)) { count++; - eq1MSD = eq1MSD * (eq1Distance / eq1MaxSevertity); + msd.eq1 = msd.eq1 * (eqDistance.eq1 / maxSeverityNormalized.eq1); } else { - eq1MSD = 0; + msd.eq1 = 0; } - if (!isNaN(eq2MSD)) { + if (!isNaN(msd.eq2)) { count++; - eq2MSD = eq2MSD * (eq2Distance / eq2MaxSevertity); + msd.eq2 = msd.eq2 * (eqDistance.eq2 / maxSeverityNormalized.eq2); } else { - eq2MSD = 0; + msd.eq2 = 0; } - if (!isNaN(eq3eq6MSD)) { + if (!isNaN(msd.eq3eq6)) { count++; - eq3eq6MSD = eq3eq6MSD * (eq3eq6Distance / eq3eq6MaxSevertity); + msd.eq3eq6 = msd.eq3eq6 * (eqDistance.eq3eq6 / maxSeverityNormalized.eq3eq6); } else { - eq3eq6MSD = 0; + msd.eq3eq6 = 0; } - if (!isNaN(eq4MSD)) { + if (!isNaN(msd.eq4)) { count++; - eq4MSD = eq4MSD * (eq4Distance / eq4MaxSevertity); + msd.eq4 = msd.eq4 * (eqDistance.eq4 / maxSeverityNormalized.eq4); } else { - eq4MSD = 0; + msd.eq4 = 0; } - if (!isNaN(eq5MSD)) { + if (!isNaN(msd.eq5)) { count++; - eq5MSD = 0; + msd.eq5 = 0; } else { - eq5MSD = 0; + msd.eq5 = 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; + if ( + !isNaN(msd.eq1) || + !isNaN(msd.eq2) || + !isNaN(msd.eq3eq6) || + !isNaN(msd.eq4) || + !isNaN(msd.eq5) + ) { + mean = (msd.eq1 + msd.eq2 + msd.eq3eq6 + msd.eq4 + msd.eq5) / 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; @@ -403,9 +439,9 @@ function cvss4_0scoring(vector: string) { if (vectorScore > 10) { vectorScore = 10.0; } - return vectorScore.toFixed(1); + return parseFloat(vectorScore.toFixed(1)); } export const score = { - cvss4_0scoring + getScore }; diff --git a/lib/testFile.ts b/lib/testFile.ts index 5b5bc93..d517a96 100644 --- a/lib/testFile.ts +++ b/lib/testFile.ts @@ -1,80 +1,9 @@ import { score } from "./score_4_0"; - +import { CVSS } from "./cvss"; // TODO: delete this file -// 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); -} +console.log( + CVSS("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").getVectorObject() +); +const vector = CVSS("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"); +vector.getTemporalScore(); diff --git a/lib/util.ts b/lib/util.ts index f10266b..7a9a775 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -1,5 +1,6 @@ import { CvssVectorObject, DetailedVectorObject, MetricUnion } from "./types"; -import { definitions } from "./cvss_3_0"; +import { definitions as definitions3_0 } from "./cvss_3_0"; +import { definitions as definitions4_0 } from "./cvss_4_0"; /** * Finds the vector's metric by it's abbreviation @@ -8,7 +9,9 @@ import { definitions } from "./cvss_3_0"; * * @returns {Definition} Definition of the vector metric matching the abbreviation */ -const findMetric = function (abbr: string) { +const findMetric = function (abbr: string, cvssVersion: string) { + const definitions = cvssVersion === "4.0" ? definitions4_0 : definitions3_0; + return definitions.definitions.find((def) => def.abbr === abbr); }; @@ -24,8 +27,36 @@ const findMetricValue = function ( abbr: string, vectorObject: CvssVectorObject ) { - const definition = findMetric(abbr); - const value = definition.metrics.find((metric) => metric.abbr === vectorObject[definition.abbr]); + const definition = findMetric(abbr, vectorObject.CVSS); + let value = definition.metrics.find((metric) => metric.abbr === vectorObject[definition.abbr]); + + // important for cvss 4.0 scoring + 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 = findMetric("M" + abbr, vectorObject.CVSS); + value = definition.metrics.find( + (metric) => metric.abbr === vectorObject[modifiedDefinition.abbr] + ); + } + } return value as T; }; @@ -65,6 +96,7 @@ function roundUpExact(num: number) { */ function getVectorObject(vector: string) { const vectorArray = vector.split("/"); + const definitions = vector.includes("4.0") ? definitions4_0 : definitions3_0; const vectorObject = definitions.definitions .map((definition) => definition.abbr) .reduce((acc, curr) => { @@ -111,7 +143,7 @@ function getDetailedVectorObject(vector: string) { const values = vectorItem.split(":"); const metrics = { ...vectorObjectAccumulator.metrics }; if (index) { - const vectorDef = findMetric(values[0]); + const vectorDef = findMetric(values[0], vectorArray[0].split(":")[1]); const detailedVectorObject = { name: vectorDef.name, abbr: vectorDef.abbr, @@ -167,6 +199,7 @@ function getRating(score: number) { * @returns {boolean} Result with whether the vector is valid or not */ const isVectorValid = function (vector: string) { + const definitions = vector.includes("4.0") ? definitions4_0 : definitions3_0; /** * This function is used to scan the definitions file and join all * abbreviations in a format that RegExp understands. @@ -188,7 +221,9 @@ const isVectorValid = function (vector: string) { } }, ""); - const totalExpressionVector = new RegExp("^CVSS:3.(0|1)(/" + expression + ")+$"); + const totalExpressionVector = new RegExp("^CVSS:(3.(0|1)|4.0)(/" + expression + ")+$"); + + console.log(totalExpressionVector); //Checks if the vector is in valid format if (!totalExpressionVector.test(vector)) { @@ -213,13 +248,15 @@ const isVectorValid = function (vector: string) { ); }); + console.log(allExpressions); + for (const regex of allExpressions) { if ((vector.match(regex) || []).length > 1) { return false; } } - const mandatoryParams = [ + const mandatoryParamsVersion3_0 = [ /\/AV:[NALP]/g, /\/AC:[LH]/g, /\/PR:[NLH]/g, @@ -230,8 +267,24 @@ const isVectorValid = function (vector: string) { /\/A:[NLH]/g ]; + const mandatoryParamsVersion4_0 = [ + /\/AV:[NALP]/g, + /\/AC:[LH]/g, + /\/AT:[NP]/g, + /\/PR:[NLH]/g, + /\/UI:[NPA]/g, + /\/VC:[NLH]/g, + /\/VI:[NLH]/g, + /\/VA:[NLH]/g, + /\/SC:[NLH]/g, + /\/SI:[NLHS]/g, + /\/SA:[NLHS]/g + ]; + //Checks whether all mandatory parameters are present in the vector - for (const regex of mandatoryParams) { + for (const regex of getVersion(vector) === "4.0" + ? mandatoryParamsVersion4_0 + : mandatoryParamsVersion3_0) { if ((vector.match(regex) || []).length < 1) { return false; } @@ -255,6 +308,7 @@ function parseVectorObjectToString(cvssInput: string | CvssVectorObject) { let vectorString = `CVSS:${cvssInput["CVSS"]}/`; + const definitions = cvssInput.CVSS === "4.0" ? definitions4_0 : definitions3_0; for (const entry of definitions["definitions"]) { const metric = entry["abbr"]; if (Object.prototype.hasOwnProperty.call(cvssInput, metric)) { @@ -298,6 +352,8 @@ function getVersion(vector: string) { return "3.0"; } else if (version[0] === "CVSS:3.1") { return "3.1"; + } else if (version[0] === "CVSS:4.0") { + return "4.0"; } else { return "Error"; } diff --git a/test/cvss.spec.js b/test/cvss_3_0.spec.js similarity index 100% rename from test/cvss.spec.js rename to test/cvss_3_0.spec.js diff --git a/test/cvss_4_0.spec.js b/test/cvss_4_0.spec.js new file mode 100644 index 0000000..99314fe --- /dev/null +++ b/test/cvss_4_0.spec.js @@ -0,0 +1,117 @@ +import { CVSS } from "../lib/cvss"; + +// 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" // this breaks + }, + { + 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" } +]; + +describe("Score Tests", () => { + it("Should return the score", () => { + // These are just some example test cases. + + const vector = CVSS( + "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" + ); + console.log(vector.getVersion()); + expect(vector.getScore()).toBe(9.4); + + // for (const ex of examples) { + // try { + // const vector = CVSS(ex.vector); + // console.log(vector.getVersion()); + // expect(vector.getScore()).toBe(ex.score); + // } catch { + // console.log(ex.vector, ex.score); + // } + // } + }); +}); + +// describe("Version Tests", () => { +// it("Should throw error when calling getVersion", () => { +// const vector = CVSS("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"); +// expect(vector.getVersion()).toBe("4.0"); +// }); +// }); + +describe("Temporal Tests", () => { + it("Should throw error when calling getTemporalScore", () => { + const vector = CVSS("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"); + vector.getTemporalScore(); + expect(function () { + parser.parse(raw); + }).toThrow("This function is not supported for this cvss version"); + }); +}); + +// describe("Environmental score tests", () => { +// it("Should throw error when calling getEnvironmentalScore", () => { +// const vector = CVSS("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"); +// expect(vector.getEnvironmentalScore()).toThrow( +// "This function is not supported for this cvss version" +// ); +// }); +// });