Skip to content

Instantly share code, notes, and snippets.

@yesmar
Last active January 27, 2018 01:04
Show Gist options
  • Save yesmar/f1dd98987ef541a5da34b1b4878ef3ff to your computer and use it in GitHub Desktop.
Save yesmar/f1dd98987ef541a5da34b1b4878ef3ff to your computer and use it in GitHub Desktop.
Compute CVSSv3 score from vector
// cvss.js: Compute CVSSv3 score from vector.
// Copyright © 2018, Ramsey Dow. All rights reserved.
// SPDX-License-Identifier: BSD-2-Clause
// Developed under the influence of Rick and Morty. TINY RICK!
const vectorToObject = (string) => {
const metrics = {};
if (isEmpty(string)) throw 'Empty string';
const scratch = string.split('/');
if (scratch.length !== 8 && scratch.length !== 9) throw 'Unexpected number of tokens';
for (const index in scratch) {
metric = scratch[index].split(':');
if (metric.length !== 2) throw 'Invalid number of metric tokens';
if (metric[0].toUpperCase() === 'CVSS') {
if (metric[1] !== '3.0') throw `${metric[1]} is an unsupported CVSS version`;
} else {
metrics[metric[0].toUpperCase()] = metric[1];
}
}
return metrics;
}
const validateBaseMetricsFields = (metrics) => {
for (metric in metrics) {
switch (metric.toUpperCase()) {
case 'AV': break;
case 'AC': break;
case 'PR': break;
case 'UI': break;
case 'S': break;
case 'C': break;
case 'I': break;
case 'A': break;
default: throw `${metric} is an invalid metric`;
}
}
}
const attackVectorValue = (metrics) => {
switch (metrics['AV'].toUpperCase()) {
case 'N': return 0.85;
case 'A': return 0.62;
case 'L': return 0.55;
case 'P': return 0.2;
default: throw `${metrics['AV']} is an illegal Attack Vector value`;
}
}
const attackComplexityValue = (metrics) => {
switch (metrics['AC'].toUpperCase()) {
case 'L': return 0.77;
case 'H': return 0.44;
default: throw `${metrics['AC']} is an illegal Attack Complexity value`;
}
}
const privilegesRequiredValue = (metrics) => {
switch (metrics['PR'].toUpperCase()) {
case 'N': return 0.85;
case 'L':
switch (metrics['S'].toUpperCase()) {
case 'C': return 0.68;
case 'U': return 0.62;
}
case 'H':
switch (metrics['S'].toUpperCase()) {
case 'C': return 0.5;
case 'U': return 0.27;
}
default: throw `${metrics['PR']} is an illegal Privileges Required value`;
}
}
const userInteractionValue = (metrics) => {
switch (metrics['UI'].toUpperCase()) {
case 'N': return 0.85;
case 'R': return 0.62;
default: throw `${metrics['AV']} is an illegal User Interaction value`;
}
}
const impactValue = (metrics, value) => {
let metricName = '';
switch (value.toUpperCase()) {
case 'C':
metricName = 'Confidentiality';
break;
case 'I':
metricName = 'Integrity';
break;
case 'A':
metricName = 'Availability';
break;
default: throw `${value} is an illegal Impact type`;
}
switch (metrics[value].toUpperCase()) {
case 'H': return 0.56;
case 'L': return 0.22;
case 'N': return 0;
default: throw `${metrics[value]} is an illegal ${metricName} Impact value`;
}
}
let qualitativeSeverity = (score) => {
if (score >= 9.0 && score <= 10.0) {
return 'Critical';
} else if (score >= 7.0 && score <= 8.9) {
return 'High';
} else if (score >= 4.0 && score <= 6.9) {
return 'Medium';
} else if (score >= 0.1 && score <= 3.9) {
return 'Low';
} else {
return 'None'; //=>0.0
}
}
// Return the smallest number, specified to one decimal place, >= its input.
// roundUp(4.02) === 4.1, roundUp(4.00) === 4.0
let roundUp = (n) => {
return Math.ceil(n * 10) / 10;
}
let isEmpty = (string) => {
return (!string || 0 === string.length);
}
let basename = (path) => {
return path.split(/[\\/]/).pop();
}
// Main program logic…
const program = basename(process.argv[1]);
if (process.argv.length !== 3) {
console.log(`Usage: ${program} <[CVSS:3.0/]vector>`);
process.exit(1);
}
let metrics = {};
const state = {};
const results = {};
try {
metrics = vectorToObject(process.argv[2]);
validateBaseMetricsFields(metrics);
state.av = attackVectorValue(metrics);
state.ac = attackComplexityValue(metrics);
state.pr = privilegesRequiredValue(metrics);
state.ui = userInteractionValue(metrics);
//We need to validate the Scope value prior to using it.
switch (metrics['S'].toUpperCase()) {
case 'C': break;
case 'U': break;
default: throw `${metrics['S']} is an illegal Scope value`;
}
state.c = impactValue(metrics, 'C');
state.i = impactValue(metrics, 'I');
state.a = impactValue(metrics, 'A');
}
catch(err) {
results.status = 'error';
results.data = {
message: err
};
const output = JSON.stringify(results);
console.log(output);
process.exit();
}
// Compute exploitability.
state.exploitabilitySubscore = 8.22 * state.av * state.ac * state.pr * state.ui;
// Compute impact.
state.impactSubscoreBase = 1 - ((1 - state.c) * (1 - state.i) * (1 - state.a));
if (metrics['S'].toUpperCase() === 'U') {
state.impactSubscore = 6.42 * state.impactSubscoreBase;
} else {
state.impactSubscore = 7.52 * (state.impactSubscoreBase - 0.029) - 3.25 * Math.pow((state.impactSubscoreBase - 0.02), 15);
}
// Compute base score.
if (state.impactSubscore <= 0) {
state.score = 0.0;
} else {
if (metrics['S'].toUpperCase() === 'U') {
state.score = roundUp(Math.min(state.impactSubscore + state.exploitabilitySubscore, 10));
} else {
state.score = roundUp(Math.min(1.08 * (state.impactSubscore + state.exploitabilitySubscore), 10));
}
}
results.status = 'ok';
results.data = {
score: `${state.score}`,
severity: qualitativeSeverity(state.score)
};
const output = JSON.stringify(results);
console.log(output);
@yesmar
Copy link
Author

yesmar commented Jan 26, 2018

Usage is simple:

node ./cvss.js CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

The CVSS:3.0/ portion of the vector is optional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment