@neuralegion/cvss
Version:
The Common Vulnerability Scoring System ([CVSS](https://www.first.org/cvss/)) [score](https://www.first.org/cvss/specification-document#1-2-Scoring) calculator and validator library written in [TypeScript](https://www.typescriptlang.org/).
268 lines (267 loc) • 13.3 kB
JavaScript
import { BaseMetric, EnvironmentalMetric, TemporalMetric, environmentalMetrics, temporalMetrics } from './models';
import { parseMetricsAsMap, parseVersion } from '../../parser';
// https://www.first.org/cvss/v3.1/specification-document#7-4-Metric-Values
const baseMetricValueScores = {
[BaseMetric.ATTACK_VECTOR]: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
[BaseMetric.ATTACK_COMPLEXITY]: { L: 0.77, H: 0.44 },
[BaseMetric.PRIVILEGES_REQUIRED]: null,
[BaseMetric.USER_INTERACTION]: { N: 0.85, R: 0.62 },
[BaseMetric.SCOPE]: { U: 0, C: 0 },
[BaseMetric.CONFIDENTIALITY]: { N: 0, L: 0.22, H: 0.56 },
[BaseMetric.INTEGRITY]: { N: 0, L: 0.22, H: 0.56 },
[BaseMetric.AVAILABILITY]: { N: 0, L: 0.22, H: 0.56 }
};
const temporalMetricValueScores = {
[TemporalMetric.EXPLOIT_CODE_MATURITY]: {
X: 1,
U: 0.91,
F: 0.97,
P: 0.94,
H: 1
},
[TemporalMetric.REMEDIATION_LEVEL]: { X: 1, O: 0.95, T: 0.96, W: 0.97, U: 1 },
[TemporalMetric.REPORT_CONFIDENCE]: { X: 1, U: 0.92, R: 0.96, C: 1 }
};
const environmentalMetricValueScores = {
[EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT]: {
M: 1,
L: 0.5,
H: 1.5,
X: 1
},
[EnvironmentalMetric.INTEGRITY_REQUIREMENT]: { M: 1, L: 0.5, H: 1.5, X: 1 },
[EnvironmentalMetric.AVAILABILITY_REQUIREMENT]: {
M: 1,
L: 0.5,
H: 1.5,
X: 1
},
[EnvironmentalMetric.MODIFIED_ATTACK_VECTOR]: baseMetricValueScores[BaseMetric.ATTACK_VECTOR],
[EnvironmentalMetric.MODIFIED_ATTACK_COMPLEXITY]: baseMetricValueScores[BaseMetric.ATTACK_COMPLEXITY],
[EnvironmentalMetric.MODIFIED_PRIVILEGES_REQUIRED]: null,
[EnvironmentalMetric.MODIFIED_USER_INTERACTION]: baseMetricValueScores[BaseMetric.USER_INTERACTION],
[EnvironmentalMetric.MODIFIED_SCOPE]: baseMetricValueScores[BaseMetric.SCOPE],
[EnvironmentalMetric.MODIFIED_CONFIDENTIALITY]: baseMetricValueScores[BaseMetric.CONFIDENTIALITY],
[EnvironmentalMetric.MODIFIED_INTEGRITY]: baseMetricValueScores[BaseMetric.INTEGRITY],
[EnvironmentalMetric.MODIFIED_AVAILABILITY]: baseMetricValueScores[BaseMetric.AVAILABILITY]
};
const getPrivilegesRequiredNumericValue = (value, scopeValue) => {
if (scopeValue !== 'U' && scopeValue !== 'C') {
throw new Error(`Unknown Scope value: ${scopeValue}`);
}
switch (value) {
case 'N':
return 0.85;
case 'L':
return scopeValue === 'U' ? 0.62 : 0.68;
case 'H':
return scopeValue === 'U' ? 0.27 : 0.5;
default:
throw new Error(`Unknown PrivilegesRequired value: ${value}`);
}
};
const getMetricValue = (metric, metricsMap) => {
if (!metricsMap.has(metric)) {
throw new Error(`Missing metric: ${metric}`);
}
return metricsMap.get(metric);
};
const getMetricNumericValue = (metric, metricsMap) => {
const value = getMetricValue(metric || TemporalMetric || EnvironmentalMetric, metricsMap);
if (metric === BaseMetric.PRIVILEGES_REQUIRED) {
return getPrivilegesRequiredNumericValue(value, getMetricValue(BaseMetric.SCOPE, metricsMap));
}
if (metric === EnvironmentalMetric.MODIFIED_PRIVILEGES_REQUIRED) {
return getPrivilegesRequiredNumericValue(value, getMetricValue(EnvironmentalMetric.MODIFIED_SCOPE, metricsMap));
}
const score = {
...baseMetricValueScores,
...temporalMetricValueScores,
...environmentalMetricValueScores
}[metric];
if (!score) {
throw new Error(`Internal error. Missing metric score: ${metric}`);
}
return score[value];
};
// ISS = 1 - [ (1 - Confidentiality) × (1 - Integrity) × (1 - Availability) ]
export const calculateIss = (metricsMap) => {
const confidentiality = getMetricNumericValue(BaseMetric.CONFIDENTIALITY, metricsMap);
const integrity = getMetricNumericValue(BaseMetric.INTEGRITY, metricsMap);
const availability = getMetricNumericValue(BaseMetric.AVAILABILITY, metricsMap);
return 1 - (1 - confidentiality) * (1 - integrity) * (1 - availability);
};
// https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations
// MISS = Minimum ( 1 - [ (1 - ConfidentialityRequirement × ModifiedConfidentiality) × (1 - IntegrityRequirement × ModifiedIntegrity) × (1 - AvailabilityRequirement × ModifiedAvailability) ], 0.915)
export const calculateMiss = (metricsMap) => {
const rConfidentiality = getMetricNumericValue(EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT, metricsMap);
const mConfidentiality = getMetricNumericValue(EnvironmentalMetric.MODIFIED_CONFIDENTIALITY, metricsMap);
const rIntegrity = getMetricNumericValue(EnvironmentalMetric.INTEGRITY_REQUIREMENT, metricsMap);
const mIntegrity = getMetricNumericValue(EnvironmentalMetric.MODIFIED_INTEGRITY, metricsMap);
const rAvailability = getMetricNumericValue(EnvironmentalMetric.AVAILABILITY_REQUIREMENT, metricsMap);
const mAvailability = getMetricNumericValue(EnvironmentalMetric.MODIFIED_AVAILABILITY, metricsMap);
return Math.min(1 -
(1 - rConfidentiality * mConfidentiality) *
(1 - rIntegrity * mIntegrity) *
(1 - rAvailability * mAvailability), 0.915);
};
// https://www.first.org/cvss/v3.1/specification-document#7-1-Base-Metrics-Equations
// Impact =
// If Scope is Unchanged 6.42 × ISS
// If Scope is Changed 7.52 × (ISS - 0.029) - 3.25 × (ISS - 0.02)^15
export const calculateImpact = (metricsMap, iss) => metricsMap.get(BaseMetric.SCOPE) === 'U'
? 6.42 * iss
: 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15);
// https://www.first.org/cvss/v3-0/specification-document#8-1-Base
// ModifiedImpact =
// If ModifiedScope is Unchanged 6.42 × MISS
// If ModifiedScope is Changed 7.52 × (MISS - 0.029) - 3.25 × (MISS - 0.02)^15
// ModifiedExploitability = 8.22 × ModifiedAttackVector × ModifiedAttackComplexity × ModifiedPrivilegesRequired × ModifiedUserInteraction
const calculateModifiedImpactV3 = (metricsMap, miss) => metricsMap.get(EnvironmentalMetric.MODIFIED_SCOPE) === 'U'
? 6.42 * miss
: 7.52 * (miss - 0.029) - 3.25 * Math.pow(miss * 1 - 0.02, 15);
// https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations
// ModifiedImpact =
// If ModifiedScope is Unchanged 6.42 × MISS
// If ModifiedScope is Changed 7.52 × (MISS - 0.029) - 3.25 × (MISS × 0.9731 - 0.02)^13
// ModifiedExploitability = 8.22 × ModifiedAttackVector × ModifiedAttackComplexity × ModifiedPrivilegesRequired × ModifiedUserInteraction
const calculateModifiedImpactV31 = (metricsMap, miss) => metricsMap.get(EnvironmentalMetric.MODIFIED_SCOPE) === 'U'
? 6.42 * miss
: 7.52 * (miss - 0.029) - 3.25 * Math.pow(miss * 0.9731 - 0.02, 13);
export const calculateModifiedImpact = (metricsMap, miss, versionStr) => versionStr === '3.0'
? calculateModifiedImpactV3(metricsMap, miss)
: calculateModifiedImpactV31(metricsMap, miss);
// https://www.first.org/cvss/v3.1/specification-document#7-1-Base-Metrics-Equations
// Exploitability = 8.22 × AttackVector × AttackComplexity × PrivilegesRequired × UserInteraction
export const calculateExploitability = (metricsMap) => 8.22 *
getMetricNumericValue(BaseMetric.ATTACK_VECTOR, metricsMap) *
getMetricNumericValue(BaseMetric.ATTACK_COMPLEXITY, metricsMap) *
getMetricNumericValue(BaseMetric.PRIVILEGES_REQUIRED, metricsMap) *
getMetricNumericValue(BaseMetric.USER_INTERACTION, metricsMap);
// https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations
// Exploitability = 8.22 × ModifiedAttackVector × ModifiedAttackComplexity × ModifiedPrivilegesRequired × ModifiedUserInteraction
export const calculateModifiedExploitability = (metricsMap) => 8.22 *
getMetricNumericValue(EnvironmentalMetric.MODIFIED_ATTACK_VECTOR, metricsMap) *
getMetricNumericValue(EnvironmentalMetric.MODIFIED_ATTACK_COMPLEXITY, metricsMap) *
getMetricNumericValue(EnvironmentalMetric.MODIFIED_PRIVILEGES_REQUIRED, metricsMap) *
getMetricNumericValue(EnvironmentalMetric.MODIFIED_USER_INTERACTION, metricsMap);
// https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding
export const roundUp = (input) => {
const intInput = Math.round(input * 100000);
return intInput % 10000 === 0
? intInput / 100000
: (Math.floor(intInput / 10000) + 1) / 10;
};
const round = (input) => {
const intInput = Math.round(input * 100000);
return Math.round(intInput / 10000) / 10;
};
export const modifiedMetricsMap = {
MAV: BaseMetric.ATTACK_VECTOR,
MAC: BaseMetric.ATTACK_COMPLEXITY,
MPR: BaseMetric.PRIVILEGES_REQUIRED,
MUI: BaseMetric.USER_INTERACTION,
MS: BaseMetric.SCOPE,
MC: BaseMetric.CONFIDENTIALITY,
MI: BaseMetric.INTEGRITY,
MA: BaseMetric.AVAILABILITY
};
// When Modified Temporal metric value is 'Not Defined' ('X'), which is the default value,
// then Base metric value should be used.
export const populateTemporalMetricDefaults = (metricsMap) => {
[...temporalMetrics].forEach((metric) => {
if (!metricsMap.has(metric)) {
metricsMap.set(metric, 'X');
}
});
return metricsMap;
};
export const populateEnvironmentalMetricDefaults = (metricsMap) => {
[...environmentalMetrics].forEach((metric) => {
if (!metricsMap.has(metric)) {
metricsMap.set(metric, 'X');
}
if (metricsMap.get(metric) === 'X') {
metricsMap.set(metric, metricsMap.has(modifiedMetricsMap[metric])
? metricsMap.get(modifiedMetricsMap[metric])
: 'X');
}
});
return metricsMap;
};
export class CvssV3Calculator {
calculate(cvssString) {
const metricsMap = parseMetricsAsMap(cvssString);
const versionStr = parseVersion(cvssString);
const baseResult = this.calculateBaseScore(metricsMap, versionStr);
const temporalResult = this.calculateTemporalScore(baseResult);
const environmentalResult = this.calculateEnvironmentalScore(metricsMap, versionStr);
return {
...baseResult,
...temporalResult,
...environmentalResult
};
}
/**
* Calculate the base score for a CVSS v3.x string
*/
calculateBaseScore(metricsMap, versionStr) {
const iss = calculateIss(metricsMap);
const impact = calculateImpact(metricsMap, iss);
const exploitability = calculateExploitability(metricsMap);
const scopeUnchanged = metricsMap.get(BaseMetric.SCOPE) === 'U';
const baseScore = impact <= 0
? 0
: scopeUnchanged
? roundUp(Math.min(impact + exploitability, 10))
: roundUp(Math.min(1.08 * (impact + exploitability), 10));
return {
version: versionStr,
baseScore,
baseImpact: impact <= 0 ? 0 : round(impact),
baseExploitability: exploitability <= 0 ? 0 : round(exploitability),
metrics: metricsMap
};
}
/**
* Calculate the temporal score for a CVSS v3.x string
*/
calculateTemporalScore(baseResult) {
// populate temp metrics
const metricsMap = populateTemporalMetricDefaults(baseResult.metrics);
const temporalScore = roundUp(baseResult.baseScore *
getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) *
getMetricNumericValue(TemporalMetric.EXPLOIT_CODE_MATURITY, metricsMap) *
getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap));
return {
temporalScore
};
}
/**
* Calculate the environmental score for a CVSS v3.x string
*/
calculateEnvironmentalScore(metricsMap, versionStr) {
metricsMap = populateTemporalMetricDefaults(metricsMap);
metricsMap = populateEnvironmentalMetricDefaults(metricsMap);
const miss = calculateMiss(metricsMap);
const impact = calculateModifiedImpact(metricsMap, miss, versionStr);
const exploitability = calculateModifiedExploitability(metricsMap);
const scopeUnchanged = metricsMap.get(EnvironmentalMetric.MODIFIED_SCOPE) === 'U';
const environmentalScore = impact <= 0
? 0
: scopeUnchanged
? roundUp(roundUp(Math.min(impact + exploitability, 10)) *
getMetricNumericValue(TemporalMetric.EXPLOIT_CODE_MATURITY, metricsMap) *
getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap) *
getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap))
: roundUp(roundUp(Math.min(1.08 * (impact + exploitability), 10)) *
getMetricNumericValue(TemporalMetric.EXPLOIT_CODE_MATURITY, metricsMap) *
getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap) *
getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap));
return {
environmentalScore,
modifiedImpact: impact <= 0 ? 0 : round(impact),
modifiedExploitability: exploitability <= 0 ? 0 : round(exploitability)
};
}
}