From db4e861c8d3e4a4ccfaf9ae97e7e5ffaf71a2a3d Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Tue, 23 Feb 2021 14:41:43 +0100 Subject: [PATCH 01/12] feat(metrics): update models with temporal and environmental models --- src/models.ts | 109 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/src/models.ts b/src/models.ts index 47880b0..1235a9a 100644 --- a/src/models.ts +++ b/src/models.ts @@ -9,6 +9,26 @@ export enum BaseMetric { AVAILABILITY = 'A' } +export enum TemporalMetric { + EXPLOITABILITY = 'E', + REMEDIATION_LEVEL = 'RL', + REPORT_CONFIDENCE = 'RC' +} + +export enum EnvironmentalMetric { + ATTACK_VECTOR = 'MAV', + ATTACK_COMPLEXITY = 'MAC', + PRIVILEGES_REQUIRED = 'MPR', + USER_INTERACTION = 'MUI', + SCOPE = 'MS', + CONFIDENTIALITY = 'MC', + INTEGRITY = 'MI', + AVAILABILITY = 'MA', + CONFIDENTIALITY_REQUIREMENT = 'CR', + INTEGRITY_REQUIREMENT = 'IR', + AVAILABILITY_REQUIREMENT = 'AR' +} + export const baseMetrics: ReadonlyArray = [ BaseMetric.ATTACK_VECTOR, BaseMetric.ATTACK_COMPLEXITY, @@ -20,9 +40,27 @@ export const baseMetrics: ReadonlyArray = [ BaseMetric.AVAILABILITY ]; -export type BaseMetricValue = 'A' | 'C' | 'H' | 'L' | 'N' | 'P' | 'R' | 'U'; +export const temporalMetrics: Metrics = [ + TemporalMetric.EXPLOITABILITY, + TemporalMetric.REMEDIATION_LEVEL, + TemporalMetric.REPORT_CONFIDENCE +]; + +export const environmentalMetrics: Metrics = [ + EnvironmentalMetric.ATTACK_VECTOR, + EnvironmentalMetric.ATTACK_COMPLEXITY, + EnvironmentalMetric.PRIVILEGES_REQUIRED, + EnvironmentalMetric.USER_INTERACTION, + EnvironmentalMetric.SCOPE, + EnvironmentalMetric.CONFIDENTIALITY, + EnvironmentalMetric.INTEGRITY, + EnvironmentalMetric.AVAILABILITY, + EnvironmentalMetric.AVAILABILITY_REQUIREMENT, + EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT, + EnvironmentalMetric.INTEGRITY_REQUIREMENT +]; -export const baseMetricValues: Record = { +export const baseMetricValues: MetricValues = { [BaseMetric.ATTACK_VECTOR]: ['N', 'A', 'L', 'P'], [BaseMetric.ATTACK_COMPLEXITY]: ['L', 'H'], [BaseMetric.PRIVILEGES_REQUIRED]: ['N', 'L', 'H'], @@ -32,3 +70,70 @@ export const baseMetricValues: Record = { [BaseMetric.INTEGRITY]: ['N', 'L', 'H'], [BaseMetric.AVAILABILITY]: ['N', 'L', 'H'] }; + +export const environmentalMetricValues: MetricValues< + EnvironmentalMetric, + EnvironmentalMetricValue +> = { + [EnvironmentalMetric.ATTACK_VECTOR]: ['N', 'A', 'L', 'P', 'X'], + [EnvironmentalMetric.ATTACK_COMPLEXITY]: ['L', 'H', 'X'], + [EnvironmentalMetric.PRIVILEGES_REQUIRED]: ['N', 'L', 'H', 'X'], + [EnvironmentalMetric.USER_INTERACTION]: ['N', 'R', 'X'], + [EnvironmentalMetric.SCOPE]: ['U', 'C', 'X'], + [EnvironmentalMetric.CONFIDENTIALITY]: ['N', 'L', 'H', 'X'], + [EnvironmentalMetric.INTEGRITY]: ['N', 'L', 'H', 'X'], + [EnvironmentalMetric.AVAILABILITY]: ['N', 'L', 'H', 'X'], + [EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT]: ['M', 'L', 'H', 'X'], + [EnvironmentalMetric.INTEGRITY_REQUIREMENT]: ['M', 'L', 'H', 'X'], + [EnvironmentalMetric.AVAILABILITY_REQUIREMENT]: ['M', 'L', 'H', 'X'] +}; + +export const temporalMetricValues: MetricValues< + TemporalMetric, + TemporalMetricValue +> = { + [TemporalMetric.EXPLOITABILITY]: ['X', 'U', 'P', 'F', 'H'], + [TemporalMetric.REMEDIATION_LEVEL]: ['X', 'O', 'T', 'W', 'U'], + [TemporalMetric.REPORT_CONFIDENCE]: ['X', 'U', 'R', 'C'] +}; + +export const metricsIndex: { [key: string]: BaseMetric } = { + 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 +}; + +export type Metric = BaseMetric | TemporalMetric | EnvironmentalMetric; +export type AnyMetric = BaseMetric & TemporalMetric & EnvironmentalMetric; +export type BaseMetricValue = 'A' | 'C' | 'H' | 'L' | 'N' | 'P' | 'R' | 'U'; +export type TemporalMetricValue = + | 'X' + | 'F' + | 'H' + | 'O' + | 'T' + | 'W' + | 'U' + | 'P' + | 'C' + | 'R'; +export type EnvironmentalMetricValue = BaseMetricValue | 'M' | 'X'; +export type MetricValue = + | BaseMetricValue + | TemporalMetricValue + | EnvironmentalMetricValue + | any; +export type MetricValues< + M extends Metric = Metric, + V extends MetricValue = MetricValue +> = Record; +export type Metrics = ReadonlyArray; +export type AllMetricValues = + | typeof baseMetricValues + | typeof temporalMetricValues + | typeof environmentalMetricValues; From 250dfae16be984563de3d87a8a3687e8ffebe205 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Tue, 23 Feb 2021 15:56:07 +0100 Subject: [PATCH 02/12] feat(metrics): refacto humanizer and parser with temporal and environemental models --- src/humanizer.ts | 10 +++++----- src/parser.ts | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/humanizer.ts b/src/humanizer.ts index b36510c..f9e46ce 100644 --- a/src/humanizer.ts +++ b/src/humanizer.ts @@ -1,7 +1,7 @@ -import { BaseMetric, BaseMetricValue } from './models'; +import { BaseMetric, Metric, MetricValue } from './models'; -export const humanizeBaseMetric = (baseMetric: BaseMetric): string => { - switch (baseMetric) { +export const humanizeBaseMetric = (metric: Metric): string => { + switch (metric) { case BaseMetric.ATTACK_VECTOR: return 'Attack Vector'; case BaseMetric.ATTACK_COMPLEXITY: @@ -25,8 +25,8 @@ export const humanizeBaseMetric = (baseMetric: BaseMetric): string => { // eslint-disable-next-line complexity export const humanizeBaseMetricValue = ( - value: BaseMetricValue, - metric: BaseMetric + value: MetricValue, + metric: Metric ): string => { switch (value) { case 'A': diff --git a/src/parser.ts b/src/parser.ts index 4b558c9..1ce71ed 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,4 @@ -import { BaseMetric, BaseMetricValue } from './models'; +import { BaseMetric, BaseMetricValue, Metric, MetricValue } from './models'; export interface KeyValue { key: K; @@ -30,9 +30,7 @@ export const parseMetrics = (vectorStr: string): KeyValue[] => return { key: parts[0], value: parts[1] }; }); -export const parseMetricsAsMap = ( - cvssStr: string -): Map => +export const parseMetricsAsMap = (cvssStr: string): Map => parseMetrics(parseVector(cvssStr) || '').reduce( ( res: Map, From fbb11eacbfe0c7c651218142f8f546408893653a Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Tue, 23 Feb 2021 17:40:22 +0100 Subject: [PATCH 03/12] feat(metrics): add temporal and environemental functions for calcultor and validator --- src/models.ts | 9 +- src/score-calculator.ts | 283 +++++++++++++++++++++++++++++---- src/validator.ts | 113 ++++++++++--- tests/score-calculator.spec.ts | 177 ++++++++++++++------- tests/validator.spec.ts | 14 ++ 5 files changed, 487 insertions(+), 109 deletions(-) diff --git a/src/models.ts b/src/models.ts index 1235a9a..00a665d 100644 --- a/src/models.ts +++ b/src/models.ts @@ -60,7 +60,7 @@ export const environmentalMetrics: Metrics = [ EnvironmentalMetric.INTEGRITY_REQUIREMENT ]; -export const baseMetricValues: MetricValues = { +export const baseMetricValues: MetricValues = { [BaseMetric.ATTACK_VECTOR]: ['N', 'A', 'L', 'P'], [BaseMetric.ATTACK_COMPLEXITY]: ['L', 'H'], [BaseMetric.PRIVILEGES_REQUIRED]: ['N', 'L', 'H'], @@ -137,3 +137,10 @@ export type AllMetricValues = | typeof baseMetricValues | typeof temporalMetricValues | typeof environmentalMetricValues; + +export type ScoreResult = { + score: number; + impact: number; + exploitability: number; + metricsMap: Map; +}; diff --git a/src/score-calculator.ts b/src/score-calculator.ts index a3dafb4..809cb36 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -1,5 +1,18 @@ -import { BaseMetric, BaseMetricValue } from './models'; -import { parseMetricsAsMap } from './parser'; +import { + AnyMetric, + BaseMetric, + BaseMetricValue, + EnvironmentalMetric, + environmentalMetrics, + EnvironmentalMetricValue, + Metric, + MetricValue, + metricsIndex, + ScoreResult, + TemporalMetric, + temporalMetrics, + TemporalMetricValue +} from './models'; import { validate } from './validator'; // https://www.first.org/cvss/v3.1/specification-document#7-4-Metric-Values @@ -17,9 +30,45 @@ const baseMetricValueScores: Record< [BaseMetric.AVAILABILITY]: { N: 0, L: 0.22, H: 0.56 } }; +const temporalMetricValueScores: Record< + TemporalMetric, + Partial> | null +> = { + [TemporalMetric.EXPLOITABILITY]: { 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: Record< + EnvironmentalMetric, + Partial> | null +> = { + [EnvironmentalMetric.ATTACK_VECTOR]: + baseMetricValueScores[BaseMetric.ATTACK_VECTOR], + [EnvironmentalMetric.ATTACK_COMPLEXITY]: + baseMetricValueScores[BaseMetric.ATTACK_COMPLEXITY], + [EnvironmentalMetric.PRIVILEGES_REQUIRED]: null, // scope-dependent: see getPrivilegesRequiredNumericValue() + [EnvironmentalMetric.USER_INTERACTION]: + baseMetricValueScores[BaseMetric.USER_INTERACTION], + [EnvironmentalMetric.SCOPE]: baseMetricValueScores[BaseMetric.SCOPE], + [EnvironmentalMetric.CONFIDENTIALITY]: + baseMetricValueScores[BaseMetric.CONFIDENTIALITY], + [EnvironmentalMetric.INTEGRITY]: baseMetricValueScores[BaseMetric.INTEGRITY], + [EnvironmentalMetric.AVAILABILITY]: + baseMetricValueScores[BaseMetric.AVAILABILITY], + [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 } +}; + const getPrivilegesRequiredNumericValue = ( - value: BaseMetricValue, - scopeValue: BaseMetricValue + value: MetricValue, + scopeValue: MetricValue ): number => { if (scopeValue !== 'U' && scopeValue !== 'C') { throw new Error(`Unknown Scope value: ${scopeValue}`); @@ -38,9 +87,9 @@ const getPrivilegesRequiredNumericValue = ( }; const getMetricValue = ( - metric: BaseMetric, - metricsMap: Map -): BaseMetricValue => { + metric: Metric, + metricsMap: Map +): MetricValue => { if (!metricsMap.has(metric)) { throw new Error(`Missing metric: ${metric}`); } @@ -49,20 +98,30 @@ const getMetricValue = ( }; const getMetricNumericValue = ( - metric: BaseMetric, - metricsMap: Map + metric: Metric, + metricsMap: Map ): number => { - const value = getMetricValue(metric, metricsMap); + const value = getMetricValue(metric as AnyMetric, metricsMap); if (metric === BaseMetric.PRIVILEGES_REQUIRED) { return getPrivilegesRequiredNumericValue( value, - getMetricValue(BaseMetric.SCOPE, metricsMap) + getMetricValue(BaseMetric.SCOPE as AnyMetric, metricsMap) ); } + if (metric === EnvironmentalMetric.PRIVILEGES_REQUIRED) { + return getPrivilegesRequiredNumericValue( + value, + getMetricValue(EnvironmentalMetric.SCOPE as AnyMetric, metricsMap) + ); + } + + const score: Partial> | null = { + ...baseMetricValueScores, + ...temporalMetricValueScores, + ...environmentalMetricValueScores + }[metric]; - const score: Partial> | null = - baseMetricValueScores[metric]; if (!score) { throw new Error(`Internal error. Missing metric score: ${metric}`); } @@ -71,9 +130,7 @@ const getMetricNumericValue = ( }; // ISS = 1 - [ (1 - Confidentiality) × (1 - Integrity) × (1 - Availability) ] -export const calculateIss = ( - metricsMap: Map -): number => { +export const calculateIss = (metricsMap: Map): number => { const confidentiality = getMetricNumericValue( BaseMetric.CONFIDENTIALITY, metricsMap @@ -87,22 +144,77 @@ export const calculateIss = ( 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: Map): number => { + const rConfidentiality = getMetricNumericValue( + EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT, + metricsMap + ); + const mConfidentiality = getMetricNumericValue( + EnvironmentalMetric.CONFIDENTIALITY, + metricsMap + ); + + const rIntegrity = getMetricNumericValue( + EnvironmentalMetric.INTEGRITY_REQUIREMENT, + metricsMap + ); + const mIntegrity = getMetricNumericValue( + EnvironmentalMetric.INTEGRITY, + metricsMap + ); + + const rAvailability = getMetricNumericValue( + EnvironmentalMetric.AVAILABILITY_REQUIREMENT, + metricsMap + ); + const mAvailability = getMetricNumericValue( + EnvironmentalMetric.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) export const calculateImpact = ( - metricsMap: Map, + metricsMap: Map, iss: number ): number => 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.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 +// Note : Math.pow is 15 in 3.0 but 13 int 3.1 +export const calculateMImpact = ( + metricsMap: Map, + miss: number, + versionStr: string | null +): number => + metricsMap.get(EnvironmentalMetric.SCOPE) === 'U' + ? 6.42 * miss + : 7.52 * (miss - 0.029) - + 3.25 * Math.pow(miss * 0.9731 - 0.02, versionStr === '3.0' ? 15 : 13); + // 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: Map + metricsMap: Map ): number => 8.22 * getMetricNumericValue(BaseMetric.ATTACK_VECTOR, metricsMap) * @@ -110,6 +222,17 @@ export const calculateExploitability = ( 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 calculateMExploitability = ( + metricsMap: Map +): number => + 8.22 * + getMetricNumericValue(EnvironmentalMetric.ATTACK_VECTOR, metricsMap) * + getMetricNumericValue(EnvironmentalMetric.ATTACK_COMPLEXITY, metricsMap) * + getMetricNumericValue(EnvironmentalMetric.PRIVILEGES_REQUIRED, metricsMap) * + getMetricNumericValue(EnvironmentalMetric.USER_INTERACTION, metricsMap); + // https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding const roundUp = (input: number): number => { const intInput = Math.round(input * 100000); @@ -119,24 +242,128 @@ const roundUp = (input: number): number => { : (Math.floor(intInput / 10000) + 1) / 10; }; +// populate temp and env metrics if not provided +export const populateUndefinedMetrics = ( + metricsMap: Map +): Map => { + [...temporalMetrics, ...environmentalMetrics].map((metric) => { + if (![...metricsMap.keys()].includes(metric)) { + metricsMap.set( + metric, + metricsIndex[metric] ? metricsMap.get(metricsIndex[metric]) : 'X' + ); + } + if (metricsMap.get(metric) === 'X') { + metricsMap.set( + metric, + metricsMap.get(metricsIndex[metric]) + ? metricsMap.get(metricsIndex[metric]) + : 'X' + ); + } + }); + + return metricsMap; +}; + +// https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations +// If ModifiedImpact <= 0 => 0; else +// If ModifiedScope is Unchanged => Roundup (Roundup [Minimum ([ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) +// If ModifiedScope is Changed => Roundup (Roundup [Minimum (1.08 × [ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) +export const calculateEnvironmentalScore = ( + cvssString: string +): ScoreResult => { + const { versionStr } = validate(cvssString); + let { metricsMap } = validate(cvssString); + + metricsMap = populateUndefinedMetrics(metricsMap); + const miss = calculateMiss(metricsMap); + const impact = calculateMImpact(metricsMap, miss, versionStr); + const exploitability = calculateMExploitability(metricsMap); + const scopeUnchanged = metricsMap.get(EnvironmentalMetric.SCOPE) === 'U'; + + const score = + impact <= 0 + ? 0 + : scopeUnchanged + ? roundUp( + roundUp(Math.min(impact + exploitability, 10)) * + getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * + getMetricNumericValue( + TemporalMetric.REMEDIATION_LEVEL, + metricsMap + ) * + getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) + ) + : roundUp( + roundUp(Math.min(1.08 * (impact + exploitability), 10)) * + getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * + getMetricNumericValue( + TemporalMetric.REMEDIATION_LEVEL, + metricsMap + ) * + getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) + ); + + return { + score, + metricsMap, + impact: impact <= 0 ? 0 : roundUp(impact), + exploitability: impact <= 0 ? 0 : roundUp(exploitability) + }; +}; + // https://www.first.org/cvss/v3.1/specification-document#7-1-Base-Metrics-Equations // If Impact <= 0 => 0; else // If Scope is Unchanged => Roundup (Minimum [(Impact + Exploitability), 10]) // If Scope is Changed => Roundup (Minimum [1.08 × (Impact + Exploitability), 10]) -export const calculateBaseScore = (cvssString: string): number => { - validate(cvssString); +export const calculateBaseScore = (cvssString: string): ScoreResult => { + const { metricsMap } = validate(cvssString); - const metricsMap: Map = parseMetricsAsMap( - cvssString - ); const iss = calculateIss(metricsMap); const impact = calculateImpact(metricsMap, iss); const exploitability = calculateExploitability(metricsMap); const scopeUnchanged = metricsMap.get(BaseMetric.SCOPE) === 'U'; - return impact <= 0 - ? 0 - : scopeUnchanged - ? roundUp(Math.min(impact + exploitability, 10)) - : roundUp(Math.min(1.08 * (impact + exploitability), 10)); + const score = + impact <= 0 + ? 0 + : scopeUnchanged + ? roundUp(Math.min(impact + exploitability, 10)) + : roundUp(Math.min(1.08 * (impact + exploitability), 10)); + + return { + score, + metricsMap, + impact: impact <= 0 ? 0 : roundUp(impact), + exploitability: impact <= 0 ? 0 : roundUp(exploitability) + }; +}; + +// https://www.first.org/cvss/v3.1/specification-document#7-2-Temporal-Metrics-Equations +// Roundup (BaseScore × ExploitCodeMaturity × RemediationLevel × ReportConfidence) +export const calculateTemporalScore = (cvssString: string): ScoreResult => { + const { metricsMap } = validate(cvssString); + // populate temp metrics if not provided + [...temporalMetrics].map((metric) => { + if (![...metricsMap.keys()].includes(metric)) { + metricsMap.set(metric, 'X'); + } + }); + + const { score, impact, exploitability } = calculateBaseScore(cvssString); + + const tempScore = roundUp( + score * + getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) * + getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * + getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap) + ); + + return { + score: tempScore, + metricsMap, + impact, + exploitability + }; }; diff --git a/src/validator.ts b/src/validator.ts index e6d5d72..9b369cd 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,8 +1,16 @@ import { - BaseMetric, + Metric, + MetricValue, + Metrics, baseMetrics, - BaseMetricValue, - baseMetricValues + temporalMetrics, + environmentalMetrics, + AllMetricValues, + baseMetricValues, + temporalMetricValues, + environmentalMetricValues, + TemporalMetric, + EnvironmentalMetric } from './models'; import { humanizeBaseMetric, humanizeBaseMetricValue } from './humanizer'; import { parseMetricsAsMap, parseVector, parseVersion } from './parser'; @@ -29,11 +37,20 @@ const validateVector = (vectorStr: string | null): void => { } }; -const checkUnknownBaseMetrics = (metricsMap: Map): void => { +const checkUnknownMetrics = ( + metricsMap: Map, + knownMetrics?: Metrics +): void => { + const allKnownMetrics = knownMetrics || [ + ...baseMetrics, + ...temporalMetrics, + ...environmentalMetrics + ]; + [...metricsMap.keys()].forEach((userMetric: string) => { - if (!baseMetrics.includes(userMetric as BaseMetric)) { + if (!allKnownMetrics.includes(userMetric as Metric)) { throw new Error( - `Unknown CVSS metric "${userMetric}". Allowed metrics: ${baseMetrics.join( + `Unknown CVSS metric "${userMetric}". Allowed metrics: ${allKnownMetrics.join( ', ' )}` ); @@ -41,33 +58,42 @@ const checkUnknownBaseMetrics = (metricsMap: Map): void => { }); }; -const checkMandatoryBaseMetrics = (metricsMap: Map): void => { - baseMetrics.forEach((baseMetric: BaseMetric) => { - if (!metricsMap.has(baseMetric)) { +const checkMandatoryMetrics = ( + metricsMap: Map, + metrics: Metrics = baseMetrics +): void => { + metrics.forEach((metric: Metric) => { + if (!metricsMap.has(metric)) { // eslint-disable-next-line max-len throw new Error( - `Missing mandatory CVSS base metric ${baseMetric} (${humanizeBaseMetric( - baseMetric + `Missing mandatory CVSS metric ${metrics} (${humanizeBaseMetric( + metric )})` ); } }); }; -const checkBaseMetricsValues = (metricsMap: Map): void => { - baseMetrics.forEach((baseMetric: BaseMetric) => { - const userValue = metricsMap.get(baseMetric); - if (!baseMetricValues[baseMetric].includes(userValue as BaseMetricValue)) { - const allowedValuesHumanized = baseMetricValues[baseMetric] +const checkMetricsValues = ( + metricsMap: Map, + metrics: Metrics, + metricsValues: AllMetricValues +): void => { + metrics.forEach((metric: Metric) => { + const userValue = metricsMap.get(metric); + if (!userValue) { + return; + } + if (!metricsValues[metric].includes(userValue as MetricValue)) { + const allowedValuesHumanized = metricsValues[metric] .map( - (value: BaseMetricValue) => - `${value} (${humanizeBaseMetricValue(value, baseMetric)})` + (value: MetricValue) => + `${value} (${humanizeBaseMetricValue(value, metric)})` ) .join(', '); - // eslint-disable-next-line max-len throw new Error( - `Invalid value for CVSS metric ${baseMetric} (${humanizeBaseMetric( - baseMetric + `Invalid value for CVSS metric ${metric} (${humanizeBaseMetric( + metric )})${ userValue ? `: ${userValue}` : '' }. Allowed values: ${allowedValuesHumanized}` @@ -76,10 +102,31 @@ const checkBaseMetricsValues = (metricsMap: Map): void => { }); }; -export const validate = (cvssStr: string): void => { +type ValidationResult = { + isTemporal: boolean; + isEnvironmental: boolean; + metricsMap: Map; + versionStr: string | null; +}; + +/** + * Validate that the given string is a valid cvss vector + * @param cvssStr + */ +export const validate = (cvssStr: string): ValidationResult => { if (!cvssStr || !cvssStr.startsWith('CVSS:')) { throw new Error('CVSS vector must start with "CVSS:"'); } + const allKnownMetrics = [ + ...baseMetrics, + ...temporalMetrics, + ...environmentalMetrics + ]; + const allKnownMetricsValues = { + ...baseMetricValues, + ...temporalMetricValues, + ...environmentalMetricValues + }; const versionStr = parseVersion(cvssStr); validateVersion(versionStr); @@ -87,8 +134,22 @@ export const validate = (cvssStr: string): void => { const vectorStr = parseVector(cvssStr); validateVector(vectorStr); - const metricsMap: Map = parseMetricsAsMap(cvssStr); - checkUnknownBaseMetrics(metricsMap); - checkMandatoryBaseMetrics(metricsMap); - checkBaseMetricsValues(metricsMap); + const metricsMap = parseMetricsAsMap(cvssStr); + checkMandatoryMetrics(metricsMap); + checkUnknownMetrics(metricsMap, allKnownMetrics); + checkMetricsValues(metricsMap, allKnownMetrics, allKnownMetricsValues); + + const isTemporal = [...metricsMap.keys()].some((metric) => + temporalMetrics.includes(metric as TemporalMetric) + ); + const isEnvironmental = [...metricsMap.keys()].some((metric) => + environmentalMetrics.includes(metric as EnvironmentalMetric) + ); + + return { + metricsMap, + isTemporal, + isEnvironmental, + versionStr + }; }; diff --git a/tests/score-calculator.spec.ts b/tests/score-calculator.spec.ts index 874821d..dd1aca0 100644 --- a/tests/score-calculator.spec.ts +++ b/tests/score-calculator.spec.ts @@ -1,59 +1,95 @@ -import { calculateBaseScore } from '../src'; +import { + calculateBaseScore, + calculateEnvironmentalScore, + calculateTemporalScore +} from '../src'; import { expect } from 'chai'; -describe('calculator', () => { - it('should calculate "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N" score as 8.6', () => { - const result = calculateBaseScore( - 'CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N' - ); - expect(result).to.equal(8.6); - }); - - it('should calculate "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" score as 10.0', () => { - const result = calculateBaseScore( - 'CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H' - ); - expect(result).to.equal(10); - }); - - it('should calculate "CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:N" score as 0.0', () => { - const result = calculateBaseScore( - 'CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:N' - ); - expect(result).to.equal(0); - }); - - // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle - it('should calculate "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" score as 7.5', () => { - const result = calculateBaseScore( - 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' - ); - expect(result).to.equal(7.5); - }); - - // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle - it('should calculate "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" score as 7.8', () => { - const result = calculateBaseScore( - 'CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H' - ); - expect(result).to.equal(7.8); - }); - - // https://www.first.org/cvss/user-guide#3-6-Vulnerable-Components-Protected-by-a-Firewall - it('should calculate "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N" score as 6.4', () => { - const result = calculateBaseScore( - 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N' - ); - expect(result).to.equal(6.4); - }); - - it('should calculate "CVSS:3.1/S:C/C:L/I:L/A:N/AV:N/AC:L/PR:L/UI:N" (non-normalized order) score as 6.4', () => { - const result = calculateBaseScore( - 'CVSS:3.1/S:C/C:L/I:L/A:N/AV:N/AC:L/PR:L/UI:N' - ); - expect(result).to.equal(6.4); - }); +// CVSS => base, temporal, environmental +const cvssTests = { + 'CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N': [8.6, 8.6, 8.6], + 'CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H': [10.0, 10.0, 10.0], + 'CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:N': [0.0, 0.0, 0.0], + 'CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H': [7.8, 7.8, 7.8], // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle + 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N': [7.5, 7.5, 7.5], // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle + 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N': [6.4, 6.4, 6.4], // https://www.first.org/cvss/user-guide#3-6-Vulnerable-Components-Protected-by-a-Firewall + 'CVSS:3.1/S:C/C:L/I:L/A:N/AV:N/AC:L/PR:L/UI:N': [6.4, 6.4, 6.4], // non-normalized order + 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N': [8.6, 8.6, 8.6], + 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H': [10.0, 10.0, 10.0], + 'CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:N': [0.0, 0.0, 0.0], + 'CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H': [7.8, 7.8, 7.8], + 'CVSS:3.1/AV:A/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:U/RL:O/RC:U/CR:M/IR:M/AR:M/MAV:A/MAC:H/MPR:L/MUI:N/MS:X/MC:N/MI:H/MA:X': [ + 5.1, + 4.1, + 5.2 + ], + 'CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N/E:U/RL:T/RC:C/CR:X/IR:L/AR:L/MAV:N/MAC:H/MPR:L/MUI:N/MS:U/MC:L/MI:L/MA:L': [ + 4.6, + 4.1, + 3.6 + ], + 'CVSS:3.1/AV:A/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:N/E:P/RL:W/RC:C/IR:L/AR:L/MAV:A/MAC:H/MPR:L/MUI:N/MS:C/MI:L/MA:L': [ + 3.0, + 2.8, + 4.0 + ], + 'CVSS:3.1/AV:A/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:U/RL:O/RC:U/CR:H/IR:H/AR:L/MAV:P/MAC:H/MPR:H/MUI:R/MS:C/MC:N/MI:N/MA:N': [ + 5.1, + 4.1, + 0.0 + ], + 'CVSS:3.1/AV:A/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:H/RL:U/RC:C/CR:M/IR:M/AR:M/MAV:N/MAC:L/MPR:N/MUI:N/MS:C/MC:H/MI:H/MA:H': [ + 5.1, + 5.1, + 10.0 + ], + 'CVSS:3.1/AV:A/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:L/CR:M/IR:M/AR:L/MAV:N/MAC:H/MPR:N/MUI:R/MS:U/MC:N/MI:N/MA:L': [ + 3.8, + 3.8, + 2.4 + ], + 'CVSS:3.1/AV:A/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:L/CR:H/IR:M/AR:H/MAV:A/MAC:H/MPR:N/MUI:R/MC:N/MI:H/MA:N': [ + 3.8, + 3.8, + 4.8 + ], + 'CVSS:3.1/AV:A/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:L/CR:H/IR:M/AR:H/MAV:A/MAC:H/MPR:N/MUI:R/MS:C/MC:N/MI:H/MA:N': [ + 3.8, + 3.8, + 5.6 + ], + 'CVSS:3.1/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L/E:H/RL:U/RC:C/MAV:P/MAC:H/MPR:N/MUI:R/MS:C/MC:L': [ + 6.2, + 6.2, + 6.1 + ], + 'CVSS:3.0/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L/E:H/RL:U/RC:C/MAV:P/MAC:H/MPR:N/MUI:R/MS:C/MC:L': [ + 6.2, + 6.2, + 6.2 + ], + 'CVSS:3.0/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L': [6.2, 6.2, 6.2], + 'CVSS:3.0/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L/CR:H': [6.2, 6.2, 6.4], + 'CVSS:3.0/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L/CR:H/IR:H/MAV:L/MUI:R/MS:U/MC:L': [ + 6.2, + 6.2, + 7.0 + ], + 'CVSS:3.1/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L': [6.2, 6.2, 6.1], + 'CVSS:3.1/AV:P/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L/MUI:R/MS:U/MC:L': [ + 6.2, + 6.2, + 5.1 + ], + 'CVSS:3.1/AV:P/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:U/RL:O/RC:U/CR:M/IR:M/AR:M/MAV:A/MAC:L/MPR:L/MUI:N/MC:N/MI:H': [ + 4.4, + 3.5, + 6.1 + ], + 'CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N/RL:O/CR:L': [8.6, 8.2, 6.0] +}; +describe('Calculator', () => { // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle it('should throw an exception on empty value', () => { expect(() => calculateBaseScore('')).to.throw(); @@ -61,7 +97,7 @@ describe('calculator', () => { // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle it('should throw an exception on missing metric', () => { - expect(() => calculateBaseScore('CVSS:3.0/A:H')).to.throw(); + expect(() => calculateBaseScore('CVSS:3.1/A:H')).to.throw(); }); // https://www.first.org/cvss/user-guide#3-1-CVSS-Scoring-in-the-Exploit-Life-Cycle @@ -71,3 +107,36 @@ describe('calculator', () => { ).to.throw(); }); }); + +describe('Calculate correctly base scores', () => { + Object.entries(cvssTests).map((entry) => { + const cvss = entry[0]; + const baseScore = entry[1][0]; + it(`should calculate a score of ${baseScore} for ${cvss}`, () => { + const { score } = calculateBaseScore(cvss); + expect(score).to.equal(baseScore); + }); + }); +}); + +describe('Calculate correctly temporal scores', () => { + Object.entries(cvssTests).map((entry) => { + const cvss = entry[0]; + const temporalScore = entry[1][1]; + it(`should calculate a score of ${temporalScore} for ${cvss}`, () => { + const { score } = calculateTemporalScore(cvss); + expect(score).to.equal(temporalScore); + }); + }); +}); + +describe('Calculate correctly environmental scores', () => { + Object.entries(cvssTests).map((entry) => { + const cvss = entry[0]; + const environmentalScore = entry[1][2]; + it(`should calculate a score of ${environmentalScore} for ${cvss}`, () => { + const { score } = calculateEnvironmentalScore(cvss); + expect(score).to.equal(environmentalScore); + }); + }); +}); diff --git a/tests/validator.spec.ts b/tests/validator.spec.ts index c80fad9..2b6f54e 100644 --- a/tests/validator.spec.ts +++ b/tests/validator.spec.ts @@ -61,4 +61,18 @@ describe('parser', () => { validate('CVSS:3.1/AV:N/AC:L//PR:N/UI:N/S:C/C:H/I:H/A:H') ).to.throw('Invalid'); }); + + it('should produce exception on double separator', () => { + expect(() => + validate('CVSS:3.1/AV:N/AC:L//PR:N/UI:N/S:C/C:H/I:H/A:H') + ).to.throw('Invalid'); + }); + + it('should not throw when validating extra scopes', () => { + expect(() => + validate( + 'CVSS:3.0/AV:A/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:H/RL:U/RC:C/CR:M/IR:M/AR:M/MAV:N/MAC:L/MPR:N/MUI:N/MS:C/MC:H/MI:H/MA:H' + ) + ).not.to.throw(); + }); }); From 31426ccf13062a8e6ea2614a254fd0da9f5bbf3c Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Wed, 24 Feb 2021 11:54:47 +0100 Subject: [PATCH 04/12] feat(metrics): refactos based on code reviews --- src/models.ts | 6 +++--- src/score-calculator.ts | 10 +++++----- src/validator.ts | 24 ++++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/models.ts b/src/models.ts index 00a665d..cb4a710 100644 --- a/src/models.ts +++ b/src/models.ts @@ -29,7 +29,7 @@ export enum EnvironmentalMetric { AVAILABILITY_REQUIREMENT = 'AR' } -export const baseMetrics: ReadonlyArray = [ +export const baseMetricMap: ReadonlyArray = [ BaseMetric.ATTACK_VECTOR, BaseMetric.ATTACK_COMPLEXITY, BaseMetric.PRIVILEGES_REQUIRED, @@ -40,13 +40,13 @@ export const baseMetrics: ReadonlyArray = [ BaseMetric.AVAILABILITY ]; -export const temporalMetrics: Metrics = [ +export const temporalMetricMap: Metrics = [ TemporalMetric.EXPLOITABILITY, TemporalMetric.REMEDIATION_LEVEL, TemporalMetric.REPORT_CONFIDENCE ]; -export const environmentalMetrics: Metrics = [ +export const environmentalMetricMap: Metrics = [ EnvironmentalMetric.ATTACK_VECTOR, EnvironmentalMetric.ATTACK_COMPLEXITY, EnvironmentalMetric.PRIVILEGES_REQUIRED, diff --git a/src/score-calculator.ts b/src/score-calculator.ts index 809cb36..0a664a5 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -3,14 +3,14 @@ import { BaseMetric, BaseMetricValue, EnvironmentalMetric, - environmentalMetrics, + environmentalMetricMap, EnvironmentalMetricValue, Metric, MetricValue, metricsIndex, ScoreResult, TemporalMetric, - temporalMetrics, + temporalMetricMap, TemporalMetricValue } from './models'; import { validate } from './validator'; @@ -200,7 +200,7 @@ export const calculateImpact = ( // 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 -// Note : Math.pow is 15 in 3.0 but 13 int 3.1 +// Note : Math.pow is 15 in 3.0 but 13 in 3.1 export const calculateMImpact = ( metricsMap: Map, miss: number, @@ -246,7 +246,7 @@ const roundUp = (input: number): number => { export const populateUndefinedMetrics = ( metricsMap: Map ): Map => { - [...temporalMetrics, ...environmentalMetrics].map((metric) => { + [...temporalMetricMap, ...environmentalMetricMap].map((metric) => { if (![...metricsMap.keys()].includes(metric)) { metricsMap.set( metric, @@ -345,7 +345,7 @@ export const calculateBaseScore = (cvssString: string): ScoreResult => { export const calculateTemporalScore = (cvssString: string): ScoreResult => { const { metricsMap } = validate(cvssString); // populate temp metrics if not provided - [...temporalMetrics].map((metric) => { + [...temporalMetricMap].map((metric) => { if (![...metricsMap.keys()].includes(metric)) { metricsMap.set(metric, 'X'); } diff --git a/src/validator.ts b/src/validator.ts index 9b369cd..a500731 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -2,9 +2,9 @@ import { Metric, MetricValue, Metrics, - baseMetrics, - temporalMetrics, - environmentalMetrics, + baseMetricMap, + temporalMetricMap, + environmentalMetricMap, AllMetricValues, baseMetricValues, temporalMetricValues, @@ -42,9 +42,9 @@ const checkUnknownMetrics = ( knownMetrics?: Metrics ): void => { const allKnownMetrics = knownMetrics || [ - ...baseMetrics, - ...temporalMetrics, - ...environmentalMetrics + ...baseMetricMap, + ...temporalMetricMap, + ...environmentalMetricMap ]; [...metricsMap.keys()].forEach((userMetric: string) => { @@ -60,7 +60,7 @@ const checkUnknownMetrics = ( const checkMandatoryMetrics = ( metricsMap: Map, - metrics: Metrics = baseMetrics + metrics: Metrics = baseMetricMap ): void => { metrics.forEach((metric: Metric) => { if (!metricsMap.has(metric)) { @@ -118,9 +118,9 @@ export const validate = (cvssStr: string): ValidationResult => { throw new Error('CVSS vector must start with "CVSS:"'); } const allKnownMetrics = [ - ...baseMetrics, - ...temporalMetrics, - ...environmentalMetrics + ...baseMetricMap, + ...temporalMetricMap, + ...environmentalMetricMap ]; const allKnownMetricsValues = { ...baseMetricValues, @@ -140,10 +140,10 @@ export const validate = (cvssStr: string): ValidationResult => { checkMetricsValues(metricsMap, allKnownMetrics, allKnownMetricsValues); const isTemporal = [...metricsMap.keys()].some((metric) => - temporalMetrics.includes(metric as TemporalMetric) + temporalMetricMap.includes(metric as TemporalMetric) ); const isEnvironmental = [...metricsMap.keys()].some((metric) => - environmentalMetrics.includes(metric as EnvironmentalMetric) + environmentalMetricMap.includes(metric as EnvironmentalMetric) ); return { From 1ab83f641e8f5b0ae8857f8d8e2eb012943242ec Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Wed, 24 Feb 2021 11:58:07 +0100 Subject: [PATCH 05/12] feat(metrics): remove model used only once from models --- src/models.ts | 7 ------- src/score-calculator.ts | 8 +++++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/models.ts b/src/models.ts index cb4a710..d8d0592 100644 --- a/src/models.ts +++ b/src/models.ts @@ -137,10 +137,3 @@ export type AllMetricValues = | typeof baseMetricValues | typeof temporalMetricValues | typeof environmentalMetricValues; - -export type ScoreResult = { - score: number; - impact: number; - exploitability: number; - metricsMap: Map; -}; diff --git a/src/score-calculator.ts b/src/score-calculator.ts index 0a664a5..793f883 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -8,7 +8,6 @@ import { Metric, MetricValue, metricsIndex, - ScoreResult, TemporalMetric, temporalMetricMap, TemporalMetricValue @@ -266,6 +265,13 @@ export const populateUndefinedMetrics = ( return metricsMap; }; +export type ScoreResult = { + score: number; + impact: number; + exploitability: number; + metricsMap: Map; +}; + // https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations // If ModifiedImpact <= 0 => 0; else // If ModifiedScope is Unchanged => Roundup (Roundup [Minimum ([ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) From 340e208de6f89643f75b26b7dd81d9087dbb6310 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Wed, 24 Feb 2021 12:22:19 +0100 Subject: [PATCH 06/12] feat(metrics): remove score results as an object --- src/score-calculator.ts | 42 +++++++--------------------------- tests/score-calculator.spec.ts | 6 ++--- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/src/score-calculator.ts b/src/score-calculator.ts index 793f883..9e874ef 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -265,20 +265,11 @@ export const populateUndefinedMetrics = ( return metricsMap; }; -export type ScoreResult = { - score: number; - impact: number; - exploitability: number; - metricsMap: Map; -}; - // https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations // If ModifiedImpact <= 0 => 0; else // If ModifiedScope is Unchanged => Roundup (Roundup [Minimum ([ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) // If ModifiedScope is Changed => Roundup (Roundup [Minimum (1.08 × [ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) -export const calculateEnvironmentalScore = ( - cvssString: string -): ScoreResult => { +export const calculateEnvironmentalScore = (cvssString: string): number => { const { versionStr } = validate(cvssString); let { metricsMap } = validate(cvssString); @@ -311,19 +302,14 @@ export const calculateEnvironmentalScore = ( getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) ); - return { - score, - metricsMap, - impact: impact <= 0 ? 0 : roundUp(impact), - exploitability: impact <= 0 ? 0 : roundUp(exploitability) - }; + return score; }; // https://www.first.org/cvss/v3.1/specification-document#7-1-Base-Metrics-Equations // If Impact <= 0 => 0; else // If Scope is Unchanged => Roundup (Minimum [(Impact + Exploitability), 10]) // If Scope is Changed => Roundup (Minimum [1.08 × (Impact + Exploitability), 10]) -export const calculateBaseScore = (cvssString: string): ScoreResult => { +export const calculateBaseScore = (cvssString: string): number => { const { metricsMap } = validate(cvssString); const iss = calculateIss(metricsMap); @@ -338,17 +324,12 @@ export const calculateBaseScore = (cvssString: string): ScoreResult => { ? roundUp(Math.min(impact + exploitability, 10)) : roundUp(Math.min(1.08 * (impact + exploitability), 10)); - return { - score, - metricsMap, - impact: impact <= 0 ? 0 : roundUp(impact), - exploitability: impact <= 0 ? 0 : roundUp(exploitability) - }; + return score; }; // https://www.first.org/cvss/v3.1/specification-document#7-2-Temporal-Metrics-Equations // Roundup (BaseScore × ExploitCodeMaturity × RemediationLevel × ReportConfidence) -export const calculateTemporalScore = (cvssString: string): ScoreResult => { +export const calculateTemporalScore = (cvssString: string): number => { const { metricsMap } = validate(cvssString); // populate temp metrics if not provided [...temporalMetricMap].map((metric) => { @@ -357,19 +338,12 @@ export const calculateTemporalScore = (cvssString: string): ScoreResult => { } }); - const { score, impact, exploitability } = calculateBaseScore(cvssString); - - const tempScore = roundUp( - score * + const score = roundUp( + calculateBaseScore(cvssString) * getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) * getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap) ); - return { - score: tempScore, - metricsMap, - impact, - exploitability - }; + return score; }; diff --git a/tests/score-calculator.spec.ts b/tests/score-calculator.spec.ts index dd1aca0..7f673dd 100644 --- a/tests/score-calculator.spec.ts +++ b/tests/score-calculator.spec.ts @@ -113,7 +113,7 @@ describe('Calculate correctly base scores', () => { const cvss = entry[0]; const baseScore = entry[1][0]; it(`should calculate a score of ${baseScore} for ${cvss}`, () => { - const { score } = calculateBaseScore(cvss); + const score = calculateBaseScore(cvss); expect(score).to.equal(baseScore); }); }); @@ -124,7 +124,7 @@ describe('Calculate correctly temporal scores', () => { const cvss = entry[0]; const temporalScore = entry[1][1]; it(`should calculate a score of ${temporalScore} for ${cvss}`, () => { - const { score } = calculateTemporalScore(cvss); + const score = calculateTemporalScore(cvss); expect(score).to.equal(temporalScore); }); }); @@ -135,7 +135,7 @@ describe('Calculate correctly environmental scores', () => { const cvss = entry[0]; const environmentalScore = entry[1][2]; it(`should calculate a score of ${environmentalScore} for ${cvss}`, () => { - const { score } = calculateEnvironmentalScore(cvss); + const score = calculateEnvironmentalScore(cvss); expect(score).to.equal(environmentalScore); }); }); From 2310de785a6e000af9b020ee1fb729037275ead3 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Wed, 24 Feb 2021 12:31:10 +0100 Subject: [PATCH 07/12] feat(metrics): remove additional validator tests --- tests/validator.spec.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/validator.spec.ts b/tests/validator.spec.ts index 2b6f54e..c80fad9 100644 --- a/tests/validator.spec.ts +++ b/tests/validator.spec.ts @@ -61,18 +61,4 @@ describe('parser', () => { validate('CVSS:3.1/AV:N/AC:L//PR:N/UI:N/S:C/C:H/I:H/A:H') ).to.throw('Invalid'); }); - - it('should produce exception on double separator', () => { - expect(() => - validate('CVSS:3.1/AV:N/AC:L//PR:N/UI:N/S:C/C:H/I:H/A:H') - ).to.throw('Invalid'); - }); - - it('should not throw when validating extra scopes', () => { - expect(() => - validate( - 'CVSS:3.0/AV:A/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:H/RL:U/RC:C/CR:M/IR:M/AR:M/MAV:N/MAC:L/MPR:N/MUI:N/MS:C/MC:H/MI:H/MA:H' - ) - ).not.to.throw(); - }); }); From 733695e59483ffd5199e887b8a1198d03b33f3a3 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Thu, 25 Feb 2021 10:05:53 +0100 Subject: [PATCH 08/12] feat(metrics): re-add some validator tests --- tests/validator.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/validator.spec.ts b/tests/validator.spec.ts index c80fad9..2b6f54e 100644 --- a/tests/validator.spec.ts +++ b/tests/validator.spec.ts @@ -61,4 +61,18 @@ describe('parser', () => { validate('CVSS:3.1/AV:N/AC:L//PR:N/UI:N/S:C/C:H/I:H/A:H') ).to.throw('Invalid'); }); + + it('should produce exception on double separator', () => { + expect(() => + validate('CVSS:3.1/AV:N/AC:L//PR:N/UI:N/S:C/C:H/I:H/A:H') + ).to.throw('Invalid'); + }); + + it('should not throw when validating extra scopes', () => { + expect(() => + validate( + 'CVSS:3.0/AV:A/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L/E:H/RL:U/RC:C/CR:M/IR:M/AR:M/MAV:N/MAC:L/MPR:N/MUI:N/MS:C/MC:H/MI:H/MA:H' + ) + ).not.to.throw(); + }); }); From 63e9d901556897e51ca0c5f60a7e3ba8356d7c30 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Thu, 25 Feb 2021 10:15:15 +0100 Subject: [PATCH 09/12] feat(metrics): add functions which return full results (map, impact, exploitability) --- src/score-calculator.ts | 89 ++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/src/score-calculator.ts b/src/score-calculator.ts index 9e874ef..f055f98 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -265,11 +265,53 @@ export const populateUndefinedMetrics = ( return metricsMap; }; +export type ScoreResult = { + score: number; + impact: number; + exploitability: number; + metricsMap: Map; +}; + +// https://www.first.org/cvss/v3.1/specification-document#7-1-Base-Metrics-Equations +// If Impact <= 0 => 0; else +// If Scope is Unchanged => Roundup (Minimum [(Impact + Exploitability), 10]) +// If Scope is Changed => Roundup (Minimum [1.08 × (Impact + Exploitability), 10]) +export const calculateBaseResult = (cvssString: string): ScoreResult => { + const { metricsMap } = validate(cvssString); + + const iss = calculateIss(metricsMap); + const impact = calculateImpact(metricsMap, iss); + const exploitability = calculateExploitability(metricsMap); + const scopeUnchanged = metricsMap.get(BaseMetric.SCOPE) === 'U'; + + const score = + impact <= 0 + ? 0 + : scopeUnchanged + ? roundUp(Math.min(impact + exploitability, 10)) + : roundUp(Math.min(1.08 * (impact + exploitability), 10)); + + return { + score, + metricsMap, + impact: impact <= 0 ? 0 : roundUp(impact), + exploitability: impact <= 0 ? 0 : roundUp(exploitability) + }; +}; + +export const calculateBaseScore = (cvssString: string): number => { + const { score } = calculateBaseResult(cvssString); + + return score; +}; + // https://www.first.org/cvss/v3.1/specification-document#7-3-Environmental-Metrics-Equations // If ModifiedImpact <= 0 => 0; else // If ModifiedScope is Unchanged => Roundup (Roundup [Minimum ([ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) // If ModifiedScope is Changed => Roundup (Roundup [Minimum (1.08 × [ModifiedImpact + ModifiedExploitability], 10)] × ExploitCodeMaturity × RemediationLevel × ReportConfidence) -export const calculateEnvironmentalScore = (cvssString: string): number => { +export const calculateEnvironmentalResult = ( + cvssString: string +): ScoreResult => { const { versionStr } = validate(cvssString); let { metricsMap } = validate(cvssString); @@ -302,34 +344,23 @@ export const calculateEnvironmentalScore = (cvssString: string): number => { getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) ); - return score; + return { + score, + metricsMap, + impact: impact <= 0 ? 0 : roundUp(impact), + exploitability: impact <= 0 ? 0 : roundUp(exploitability) + }; }; -// https://www.first.org/cvss/v3.1/specification-document#7-1-Base-Metrics-Equations -// If Impact <= 0 => 0; else -// If Scope is Unchanged => Roundup (Minimum [(Impact + Exploitability), 10]) -// If Scope is Changed => Roundup (Minimum [1.08 × (Impact + Exploitability), 10]) -export const calculateBaseScore = (cvssString: string): number => { - const { metricsMap } = validate(cvssString); - - const iss = calculateIss(metricsMap); - const impact = calculateImpact(metricsMap, iss); - const exploitability = calculateExploitability(metricsMap); - const scopeUnchanged = metricsMap.get(BaseMetric.SCOPE) === 'U'; - - const score = - impact <= 0 - ? 0 - : scopeUnchanged - ? roundUp(Math.min(impact + exploitability, 10)) - : roundUp(Math.min(1.08 * (impact + exploitability), 10)); +export const calculateEnvironmentalScore = (cvssString: string): number => { + const { score } = calculateEnvironmentalResult(cvssString); return score; }; // https://www.first.org/cvss/v3.1/specification-document#7-2-Temporal-Metrics-Equations // Roundup (BaseScore × ExploitCodeMaturity × RemediationLevel × ReportConfidence) -export const calculateTemporalScore = (cvssString: string): number => { +export const calculateTemporalResult = (cvssString: string): ScoreResult => { const { metricsMap } = validate(cvssString); // populate temp metrics if not provided [...temporalMetricMap].map((metric) => { @@ -337,13 +368,25 @@ export const calculateTemporalScore = (cvssString: string): number => { metricsMap.set(metric, 'X'); } }); + const { score, impact, exploitability } = calculateBaseResult(cvssString); - const score = roundUp( - calculateBaseScore(cvssString) * + const tempScore = roundUp( + score * getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) * getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap) ); + return { + score: tempScore, + metricsMap, + impact, + exploitability + }; +}; + +export const calculateTemporalScore = (cvssString: string): number => { + const { score } = calculateTemporalResult(cvssString); + return score; }; From 63f2f0df862f7e1a35a269a8e039c008d425cf39 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Thu, 4 Mar 2021 11:00:43 +0100 Subject: [PATCH 10/12] feat(metrics): refactos based on code reviews --- src/models.ts | 91 ++++++++++++---------------- src/score-calculator.ts | 128 ++++++++++++++++++++++++++-------------- 2 files changed, 122 insertions(+), 97 deletions(-) diff --git a/src/models.ts b/src/models.ts index d8d0592..9afda45 100644 --- a/src/models.ts +++ b/src/models.ts @@ -10,23 +10,23 @@ export enum BaseMetric { } export enum TemporalMetric { - EXPLOITABILITY = 'E', + EXPLOIT_CODE_MATURITY = 'E', REMEDIATION_LEVEL = 'RL', REPORT_CONFIDENCE = 'RC' } export enum EnvironmentalMetric { - ATTACK_VECTOR = 'MAV', - ATTACK_COMPLEXITY = 'MAC', - PRIVILEGES_REQUIRED = 'MPR', - USER_INTERACTION = 'MUI', - SCOPE = 'MS', - CONFIDENTIALITY = 'MC', - INTEGRITY = 'MI', - AVAILABILITY = 'MA', CONFIDENTIALITY_REQUIREMENT = 'CR', INTEGRITY_REQUIREMENT = 'IR', - AVAILABILITY_REQUIREMENT = 'AR' + AVAILABILITY_REQUIREMENT = 'AR', + MODIFIED_ATTACK_VECTOR = 'MAV', + MODIFIED_ATTACK_COMPLEXITY = 'MAC', + MODIFIED_PRIVILEGES_REQUIRED = 'MPR', + MODIFIED_USER_INTERACTION = 'MUI', + MODIFIED_SCOPE = 'MS', + MODIFIED_CONFIDENTIALITY = 'MC', + MODIFIED_INTEGRITY = 'MI', + MODIFIED_AVAILABILITY = 'MA' } export const baseMetricMap: ReadonlyArray = [ @@ -41,23 +41,23 @@ export const baseMetricMap: ReadonlyArray = [ ]; export const temporalMetricMap: Metrics = [ - TemporalMetric.EXPLOITABILITY, + TemporalMetric.EXPLOIT_CODE_MATURITY, TemporalMetric.REMEDIATION_LEVEL, TemporalMetric.REPORT_CONFIDENCE ]; export const environmentalMetricMap: Metrics = [ - EnvironmentalMetric.ATTACK_VECTOR, - EnvironmentalMetric.ATTACK_COMPLEXITY, - EnvironmentalMetric.PRIVILEGES_REQUIRED, - EnvironmentalMetric.USER_INTERACTION, - EnvironmentalMetric.SCOPE, - EnvironmentalMetric.CONFIDENTIALITY, - EnvironmentalMetric.INTEGRITY, - EnvironmentalMetric.AVAILABILITY, EnvironmentalMetric.AVAILABILITY_REQUIREMENT, EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT, - EnvironmentalMetric.INTEGRITY_REQUIREMENT + EnvironmentalMetric.INTEGRITY_REQUIREMENT, + EnvironmentalMetric.MODIFIED_ATTACK_VECTOR, + EnvironmentalMetric.MODIFIED_ATTACK_COMPLEXITY, + EnvironmentalMetric.MODIFIED_PRIVILEGES_REQUIRED, + EnvironmentalMetric.MODIFIED_USER_INTERACTION, + EnvironmentalMetric.MODIFIED_SCOPE, + EnvironmentalMetric.MODIFIED_CONFIDENTIALITY, + EnvironmentalMetric.MODIFIED_INTEGRITY, + EnvironmentalMetric.MODIFIED_AVAILABILITY ]; export const baseMetricValues: MetricValues = { @@ -71,45 +71,33 @@ export const baseMetricValues: MetricValues = { [BaseMetric.AVAILABILITY]: ['N', 'L', 'H'] }; -export const environmentalMetricValues: MetricValues< - EnvironmentalMetric, - EnvironmentalMetricValue -> = { - [EnvironmentalMetric.ATTACK_VECTOR]: ['N', 'A', 'L', 'P', 'X'], - [EnvironmentalMetric.ATTACK_COMPLEXITY]: ['L', 'H', 'X'], - [EnvironmentalMetric.PRIVILEGES_REQUIRED]: ['N', 'L', 'H', 'X'], - [EnvironmentalMetric.USER_INTERACTION]: ['N', 'R', 'X'], - [EnvironmentalMetric.SCOPE]: ['U', 'C', 'X'], - [EnvironmentalMetric.CONFIDENTIALITY]: ['N', 'L', 'H', 'X'], - [EnvironmentalMetric.INTEGRITY]: ['N', 'L', 'H', 'X'], - [EnvironmentalMetric.AVAILABILITY]: ['N', 'L', 'H', 'X'], - [EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT]: ['M', 'L', 'H', 'X'], - [EnvironmentalMetric.INTEGRITY_REQUIREMENT]: ['M', 'L', 'H', 'X'], - [EnvironmentalMetric.AVAILABILITY_REQUIREMENT]: ['M', 'L', 'H', 'X'] -}; - export const temporalMetricValues: MetricValues< TemporalMetric, TemporalMetricValue > = { - [TemporalMetric.EXPLOITABILITY]: ['X', 'U', 'P', 'F', 'H'], - [TemporalMetric.REMEDIATION_LEVEL]: ['X', 'O', 'T', 'W', 'U'], - [TemporalMetric.REPORT_CONFIDENCE]: ['X', 'U', 'R', 'C'] + [TemporalMetric.EXPLOIT_CODE_MATURITY]: ['X', 'H', 'F', 'P', 'U'], + [TemporalMetric.REMEDIATION_LEVEL]: ['X', 'U', 'W', 'T', 'O'], + [TemporalMetric.REPORT_CONFIDENCE]: ['X', 'C', 'R', 'U'] }; -export const metricsIndex: { [key: string]: BaseMetric } = { - 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 +export const environmentalMetricValues: MetricValues< + EnvironmentalMetric, + EnvironmentalMetricValue +> = { + [EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT]: ['X', 'H', 'M', 'L'], + [EnvironmentalMetric.INTEGRITY_REQUIREMENT]: ['X', 'H', 'M', 'L'], + [EnvironmentalMetric.AVAILABILITY_REQUIREMENT]: ['X', 'H', 'M', 'L'], + [EnvironmentalMetric.MODIFIED_ATTACK_VECTOR]: ['X', 'N', 'A', 'L', 'P'], + [EnvironmentalMetric.MODIFIED_ATTACK_COMPLEXITY]: ['X', 'L', 'H'], + [EnvironmentalMetric.MODIFIED_PRIVILEGES_REQUIRED]: ['X', 'N', 'L', 'H'], + [EnvironmentalMetric.MODIFIED_USER_INTERACTION]: ['X', 'N', 'R'], + [EnvironmentalMetric.MODIFIED_SCOPE]: ['X', 'U', 'C'], + [EnvironmentalMetric.MODIFIED_CONFIDENTIALITY]: ['X', 'N', 'L', 'H'], + [EnvironmentalMetric.MODIFIED_INTEGRITY]: ['X', 'N', 'L', 'H'], + [EnvironmentalMetric.MODIFIED_AVAILABILITY]: ['X', 'N', 'L', 'H'] }; export type Metric = BaseMetric | TemporalMetric | EnvironmentalMetric; -export type AnyMetric = BaseMetric & TemporalMetric & EnvironmentalMetric; export type BaseMetricValue = 'A' | 'C' | 'H' | 'L' | 'N' | 'P' | 'R' | 'U'; export type TemporalMetricValue = | 'X' @@ -126,8 +114,7 @@ export type EnvironmentalMetricValue = BaseMetricValue | 'M' | 'X'; export type MetricValue = | BaseMetricValue | TemporalMetricValue - | EnvironmentalMetricValue - | any; + | EnvironmentalMetricValue; export type MetricValues< M extends Metric = Metric, V extends MetricValue = MetricValue diff --git a/src/score-calculator.ts b/src/score-calculator.ts index f055f98..15d20b3 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -1,5 +1,4 @@ import { - AnyMetric, BaseMetric, BaseMetricValue, EnvironmentalMetric, @@ -7,7 +6,6 @@ import { EnvironmentalMetricValue, Metric, MetricValue, - metricsIndex, TemporalMetric, temporalMetricMap, TemporalMetricValue @@ -33,7 +31,13 @@ const temporalMetricValueScores: Record< TemporalMetric, Partial> | null > = { - [TemporalMetric.EXPLOITABILITY]: { X: 1, U: 0.91, F: 0.97, P: 0.94, H: 1 }, + [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 } }; @@ -42,19 +46,6 @@ const environmentalMetricValueScores: Record< EnvironmentalMetric, Partial> | null > = { - [EnvironmentalMetric.ATTACK_VECTOR]: - baseMetricValueScores[BaseMetric.ATTACK_VECTOR], - [EnvironmentalMetric.ATTACK_COMPLEXITY]: - baseMetricValueScores[BaseMetric.ATTACK_COMPLEXITY], - [EnvironmentalMetric.PRIVILEGES_REQUIRED]: null, // scope-dependent: see getPrivilegesRequiredNumericValue() - [EnvironmentalMetric.USER_INTERACTION]: - baseMetricValueScores[BaseMetric.USER_INTERACTION], - [EnvironmentalMetric.SCOPE]: baseMetricValueScores[BaseMetric.SCOPE], - [EnvironmentalMetric.CONFIDENTIALITY]: - baseMetricValueScores[BaseMetric.CONFIDENTIALITY], - [EnvironmentalMetric.INTEGRITY]: baseMetricValueScores[BaseMetric.INTEGRITY], - [EnvironmentalMetric.AVAILABILITY]: - baseMetricValueScores[BaseMetric.AVAILABILITY], [EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT]: { M: 1, L: 0.5, @@ -62,7 +53,26 @@ const environmentalMetricValueScores: Record< 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.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, // scope-dependent: see getPrivilegesRequiredNumericValue() + [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 = ( @@ -100,18 +110,24 @@ const getMetricNumericValue = ( metric: Metric, metricsMap: Map ): number => { - const value = getMetricValue(metric as AnyMetric, metricsMap); + const value = getMetricValue( + (metric as BaseMetric) || TemporalMetric || EnvironmentalMetric, + metricsMap + ); if (metric === BaseMetric.PRIVILEGES_REQUIRED) { return getPrivilegesRequiredNumericValue( value, - getMetricValue(BaseMetric.SCOPE as AnyMetric, metricsMap) + getMetricValue(BaseMetric.SCOPE as BaseMetric, metricsMap) ); } - if (metric === EnvironmentalMetric.PRIVILEGES_REQUIRED) { + if (metric === EnvironmentalMetric.MODIFIED_PRIVILEGES_REQUIRED) { return getPrivilegesRequiredNumericValue( value, - getMetricValue(EnvironmentalMetric.SCOPE as AnyMetric, metricsMap) + getMetricValue( + EnvironmentalMetric.MODIFIED_SCOPE as EnvironmentalMetric, + metricsMap + ) ); } @@ -151,7 +167,7 @@ export const calculateMiss = (metricsMap: Map): number => { metricsMap ); const mConfidentiality = getMetricNumericValue( - EnvironmentalMetric.CONFIDENTIALITY, + EnvironmentalMetric.MODIFIED_CONFIDENTIALITY, metricsMap ); @@ -160,7 +176,7 @@ export const calculateMiss = (metricsMap: Map): number => { metricsMap ); const mIntegrity = getMetricNumericValue( - EnvironmentalMetric.INTEGRITY, + EnvironmentalMetric.MODIFIED_INTEGRITY, metricsMap ); @@ -169,7 +185,7 @@ export const calculateMiss = (metricsMap: Map): number => { metricsMap ); const mAvailability = getMetricNumericValue( - EnvironmentalMetric.AVAILABILITY, + EnvironmentalMetric.MODIFIED_AVAILABILITY, metricsMap ); @@ -185,7 +201,7 @@ export const calculateMiss = (metricsMap: Map): number => { // 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) +// If Scope is Changed 7.52 × (ISS - 0.029) - 3.25 × (ISS - 0.02)^15 export const calculateImpact = ( metricsMap: Map, iss: number @@ -197,7 +213,7 @@ export const calculateImpact = ( // 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 +// If ModifiedScope is Changed 7.52 × (MISS - 0.029) - 3.25 × (MISS × 0.9731 - 0.02)^13 // ModifiedExploitability = 8.22 × ModifiedAttackVector × ModifiedAttackComplexity × ModifiedPrivilegesRequired × ModifiedUserInteraction // Note : Math.pow is 15 in 3.0 but 13 in 3.1 export const calculateMImpact = ( @@ -205,7 +221,7 @@ export const calculateMImpact = ( miss: number, versionStr: string | null ): number => - metricsMap.get(EnvironmentalMetric.SCOPE) === 'U' + metricsMap.get(EnvironmentalMetric.MODIFIED_SCOPE) === 'U' ? 6.42 * miss : 7.52 * (miss - 0.029) - 3.25 * Math.pow(miss * 0.9731 - 0.02, versionStr === '3.0' ? 15 : 13); @@ -227,10 +243,22 @@ export const calculateMExploitability = ( metricsMap: Map ): number => 8.22 * - getMetricNumericValue(EnvironmentalMetric.ATTACK_VECTOR, metricsMap) * - getMetricNumericValue(EnvironmentalMetric.ATTACK_COMPLEXITY, metricsMap) * - getMetricNumericValue(EnvironmentalMetric.PRIVILEGES_REQUIRED, metricsMap) * - getMetricNumericValue(EnvironmentalMetric.USER_INTERACTION, metricsMap); + 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 const roundUp = (input: number): number => { @@ -241,24 +269,27 @@ const roundUp = (input: number): number => { : (Math.floor(intInput / 10000) + 1) / 10; }; +export const modifiedMetricsMap: { [key: string]: BaseMetric } = { + 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 +}; + // populate temp and env metrics if not provided export const populateUndefinedMetrics = ( metricsMap: Map ): Map => { [...temporalMetricMap, ...environmentalMetricMap].map((metric) => { if (![...metricsMap.keys()].includes(metric)) { - metricsMap.set( - metric, - metricsIndex[metric] ? metricsMap.get(metricsIndex[metric]) : 'X' - ); + metricsMap.set(metric, metricsMap.get(modifiedMetricsMap[metric]) || 'X'); } if (metricsMap.get(metric) === 'X') { - metricsMap.set( - metric, - metricsMap.get(metricsIndex[metric]) - ? metricsMap.get(metricsIndex[metric]) - : 'X' - ); + metricsMap.set(metric, metricsMap.get(modifiedMetricsMap[metric]) || 'X'); } }); @@ -319,7 +350,8 @@ export const calculateEnvironmentalResult = ( const miss = calculateMiss(metricsMap); const impact = calculateMImpact(metricsMap, miss, versionStr); const exploitability = calculateMExploitability(metricsMap); - const scopeUnchanged = metricsMap.get(EnvironmentalMetric.SCOPE) === 'U'; + const scopeUnchanged = + metricsMap.get(EnvironmentalMetric.MODIFIED_SCOPE) === 'U'; const score = impact <= 0 @@ -327,7 +359,10 @@ export const calculateEnvironmentalResult = ( : scopeUnchanged ? roundUp( roundUp(Math.min(impact + exploitability, 10)) * - getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * + getMetricNumericValue( + TemporalMetric.EXPLOIT_CODE_MATURITY, + metricsMap + ) * getMetricNumericValue( TemporalMetric.REMEDIATION_LEVEL, metricsMap @@ -336,7 +371,10 @@ export const calculateEnvironmentalResult = ( ) : roundUp( roundUp(Math.min(1.08 * (impact + exploitability), 10)) * - getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * + getMetricNumericValue( + TemporalMetric.EXPLOIT_CODE_MATURITY, + metricsMap + ) * getMetricNumericValue( TemporalMetric.REMEDIATION_LEVEL, metricsMap @@ -373,7 +411,7 @@ export const calculateTemporalResult = (cvssString: string): ScoreResult => { const tempScore = roundUp( score * getMetricNumericValue(TemporalMetric.REPORT_CONFIDENCE, metricsMap) * - getMetricNumericValue(TemporalMetric.EXPLOITABILITY, metricsMap) * + getMetricNumericValue(TemporalMetric.EXPLOIT_CODE_MATURITY, metricsMap) * getMetricNumericValue(TemporalMetric.REMEDIATION_LEVEL, metricsMap) ); From a8bd0aee70b0bba488ab8ff7bc59c42da8632c7f Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Fri, 5 Mar 2021 15:44:21 +0100 Subject: [PATCH 11/12] feat(metrics): changes based on reviews --- src/score-calculator.ts | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/score-calculator.ts b/src/score-calculator.ts index 15d20b3..6eddecd 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -280,16 +280,35 @@ export const modifiedMetricsMap: { [key: string]: BaseMetric } = { MA: BaseMetric.AVAILABILITY }; -// populate temp and env metrics if not provided -export const populateUndefinedMetrics = ( +// 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: Map ): Map => { - [...temporalMetricMap, ...environmentalMetricMap].map((metric) => { - if (![...metricsMap.keys()].includes(metric)) { - metricsMap.set(metric, metricsMap.get(modifiedMetricsMap[metric]) || 'X'); + [...temporalMetricMap].forEach((metric) => { + if (!metricsMap.has(metric)) { + metricsMap.set(metric, 'X'); + } + }); + + return metricsMap; +}; + +export const populateEnvironmentalMetricDefaults = ( + metricsMap: Map +): Map => { + [...environmentalMetricMap].forEach((metric: EnvironmentalMetric) => { + if (!metricsMap.has(metric)) { + metricsMap.set(metric, 'X'); } + if (metricsMap.get(metric) === 'X') { - metricsMap.set(metric, metricsMap.get(modifiedMetricsMap[metric]) || 'X'); + metricsMap.set( + metric, + metricsMap.has(modifiedMetricsMap[metric]) + ? (metricsMap.get(modifiedMetricsMap[metric]) as MetricValue) + : 'X' + ); } }); @@ -346,7 +365,8 @@ export const calculateEnvironmentalResult = ( const { versionStr } = validate(cvssString); let { metricsMap } = validate(cvssString); - metricsMap = populateUndefinedMetrics(metricsMap); + metricsMap = populateTemporalMetricDefaults(metricsMap); + metricsMap = populateEnvironmentalMetricDefaults(metricsMap); const miss = calculateMiss(metricsMap); const impact = calculateMImpact(metricsMap, miss, versionStr); const exploitability = calculateMExploitability(metricsMap); From ab9dda5b347720968dd2e840a97353d4b7dfb695 Mon Sep 17 00:00:00 2001 From: Alexandre Jose Date: Tue, 9 Mar 2021 15:00:38 +0100 Subject: [PATCH 12/12] feat(metrics): changes based on reviews --- src/models.ts | 6 +++--- src/score-calculator.ts | 10 +++++----- src/validator.ts | 24 ++++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/models.ts b/src/models.ts index 9afda45..9c9ca6f 100644 --- a/src/models.ts +++ b/src/models.ts @@ -29,7 +29,7 @@ export enum EnvironmentalMetric { MODIFIED_AVAILABILITY = 'MA' } -export const baseMetricMap: ReadonlyArray = [ +export const baseMetrics: ReadonlyArray = [ BaseMetric.ATTACK_VECTOR, BaseMetric.ATTACK_COMPLEXITY, BaseMetric.PRIVILEGES_REQUIRED, @@ -40,13 +40,13 @@ export const baseMetricMap: ReadonlyArray = [ BaseMetric.AVAILABILITY ]; -export const temporalMetricMap: Metrics = [ +export const temporalMetrics: Metrics = [ TemporalMetric.EXPLOIT_CODE_MATURITY, TemporalMetric.REMEDIATION_LEVEL, TemporalMetric.REPORT_CONFIDENCE ]; -export const environmentalMetricMap: Metrics = [ +export const environmentalMetrics: Metrics = [ EnvironmentalMetric.AVAILABILITY_REQUIREMENT, EnvironmentalMetric.CONFIDENTIALITY_REQUIREMENT, EnvironmentalMetric.INTEGRITY_REQUIREMENT, diff --git a/src/score-calculator.ts b/src/score-calculator.ts index 6eddecd..0b3ea33 100644 --- a/src/score-calculator.ts +++ b/src/score-calculator.ts @@ -2,12 +2,12 @@ import { BaseMetric, BaseMetricValue, EnvironmentalMetric, - environmentalMetricMap, + environmentalMetrics, EnvironmentalMetricValue, Metric, MetricValue, TemporalMetric, - temporalMetricMap, + temporalMetrics, TemporalMetricValue } from './models'; import { validate } from './validator'; @@ -285,7 +285,7 @@ export const modifiedMetricsMap: { [key: string]: BaseMetric } = { export const populateTemporalMetricDefaults = ( metricsMap: Map ): Map => { - [...temporalMetricMap].forEach((metric) => { + [...temporalMetrics].forEach((metric) => { if (!metricsMap.has(metric)) { metricsMap.set(metric, 'X'); } @@ -297,7 +297,7 @@ export const populateTemporalMetricDefaults = ( export const populateEnvironmentalMetricDefaults = ( metricsMap: Map ): Map => { - [...environmentalMetricMap].forEach((metric: EnvironmentalMetric) => { + [...environmentalMetrics].forEach((metric: EnvironmentalMetric) => { if (!metricsMap.has(metric)) { metricsMap.set(metric, 'X'); } @@ -421,7 +421,7 @@ export const calculateEnvironmentalScore = (cvssString: string): number => { export const calculateTemporalResult = (cvssString: string): ScoreResult => { const { metricsMap } = validate(cvssString); // populate temp metrics if not provided - [...temporalMetricMap].map((metric) => { + [...temporalMetrics].map((metric) => { if (![...metricsMap.keys()].includes(metric)) { metricsMap.set(metric, 'X'); } diff --git a/src/validator.ts b/src/validator.ts index a500731..9b369cd 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -2,9 +2,9 @@ import { Metric, MetricValue, Metrics, - baseMetricMap, - temporalMetricMap, - environmentalMetricMap, + baseMetrics, + temporalMetrics, + environmentalMetrics, AllMetricValues, baseMetricValues, temporalMetricValues, @@ -42,9 +42,9 @@ const checkUnknownMetrics = ( knownMetrics?: Metrics ): void => { const allKnownMetrics = knownMetrics || [ - ...baseMetricMap, - ...temporalMetricMap, - ...environmentalMetricMap + ...baseMetrics, + ...temporalMetrics, + ...environmentalMetrics ]; [...metricsMap.keys()].forEach((userMetric: string) => { @@ -60,7 +60,7 @@ const checkUnknownMetrics = ( const checkMandatoryMetrics = ( metricsMap: Map, - metrics: Metrics = baseMetricMap + metrics: Metrics = baseMetrics ): void => { metrics.forEach((metric: Metric) => { if (!metricsMap.has(metric)) { @@ -118,9 +118,9 @@ export const validate = (cvssStr: string): ValidationResult => { throw new Error('CVSS vector must start with "CVSS:"'); } const allKnownMetrics = [ - ...baseMetricMap, - ...temporalMetricMap, - ...environmentalMetricMap + ...baseMetrics, + ...temporalMetrics, + ...environmentalMetrics ]; const allKnownMetricsValues = { ...baseMetricValues, @@ -140,10 +140,10 @@ export const validate = (cvssStr: string): ValidationResult => { checkMetricsValues(metricsMap, allKnownMetrics, allKnownMetricsValues); const isTemporal = [...metricsMap.keys()].some((metric) => - temporalMetricMap.includes(metric as TemporalMetric) + temporalMetrics.includes(metric as TemporalMetric) ); const isEnvironmental = [...metricsMap.keys()].some((metric) => - environmentalMetricMap.includes(metric as EnvironmentalMetric) + environmentalMetrics.includes(metric as EnvironmentalMetric) ); return {