Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Handle arbitrary metrics order and add testing #60

Merged
merged 6 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Run Tests
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install modules
run: yarn
- name: Run tests
run: yarn test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
node_modules/
.DS_Store
113 changes: 59 additions & 54 deletions cvss40.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ class Vector {
},
// Environmental (14 metrics)
ENVIRONMENTAL: {
"CR": ["X", "H", "M", "L"],
"IR": ["X", "H", "M", "L"],
"AR": ["X", "H", "M", "L"],
"CR": ["X", "H", "M", "L"],
"IR": ["X", "H", "M", "L"],
"AR": ["X", "H", "M", "L"],
"MAV": ["X", "N", "A", "L", "P"],
"MAC": ["X", "L", "H"],
"MAT": ["X", "N", "P"],
Expand All @@ -88,12 +88,12 @@ class Vector {
},
// Supplemental (6 metrics)
SUPPLEMENTAL: {
"S": ["X", "N", "P"],
"S": ["X", "N", "P"],
"AU": ["X", "N", "Y"],
"R": ["X", "A", "U", "I"],
"V": ["X", "D", "C"],
"R": ["X", "A", "U", "I"],
"V": ["X", "D", "C"],
"RE": ["X", "L", "M", "H"],
"U": ["X", "Clear", "Green", "Amber", "Red"],
"U": ["X", "Clear", "Green", "Amber", "Red"],
}
};

Expand Down Expand Up @@ -381,39 +381,48 @@ class Vector {

// Check if the prefix is correct
if (metrics.shift() !== "CVSS:4.0") {
console.error("Error: invalid vector, missing CVSS v4.0 prefix from vector: " + vector);
return false;
throw new Error(`Invalid vector, missing \`CVSS:4.0 prefix\` from vector: \`${vector}\``);
}

const expectedMetrics = Object.entries(Vector.ALL_METRICS);
let mandatoryMetricIndex = 0;
const malformedMetric = metrics.find(metric => metric.split(':').length !== 2);

for (let metric of metrics) {
const [key, value] = metric.split(':');
if (malformedMetric) {
throw new Error(`Invalid vector, malformed substring \`${malformedMetric}\` in vector: \`${vector}\``);
}

// Check if there are too many metric values
if (!expectedMetrics[mandatoryMetricIndex]) {
console.error("Error: invalid vector, too many metric values");
return false;
}
const metricsLookup = metrics.reduce((lookup, metric) => {
const [metricType, metricValue] = metric.split(':');
lookup[metricType] = metricValue;
return lookup;
}, {});

// Find the current expected metric
while (expectedMetrics[mandatoryMetricIndex] && expectedMetrics[mandatoryMetricIndex][0] !== key) {
// Check for missing mandatory metrics
if (mandatoryMetricIndex < 11) {
console.error("Error: invalid vector, missing mandatory metrics");
return false;
}
mandatoryMetricIndex++;
}
const requiredMetrics = Object.keys(Vector.METRICS.BASE);

if (!requiredMetrics.every(metricType => metricType in metricsLookup)) {
throw new Error(`Invalid CVSS v4.0 vector: Missing required metrics in \`${vector}\``);
}

// Check if the value is valid for the given metric
if (!expectedMetrics[mandatoryMetricIndex][1].includes(value)) {
console.error(`Error: invalid vector, for key ${key}, value ${value} is not in ${expectedMetrics[mandatoryMetricIndex][1]}`);
return false;
if (metrics.length > Object.keys(metricsLookup).length) {
throw new Error(`Invalid CVSS v4.0 vector: Duplicated metric types in \`${vector}\``);
}

const definedMetrics = Vector.ALL_METRICS;

if (metrics.length > Object.keys(definedMetrics).length) {
// This was here before but probably is impossible to reach because of the previous checks for duplicated and undefined keys
throw new Error(`Invalid CVSS v4.0 vector: Unknown/excessive metric types in \`${vector}\``);
}

for (let [metricType, metricValue] of Object.entries(metricsLookup)) {

if (!metricType in Vector.ALL_METRICS) {
throw new Error(`Invalid CVSS v4.0 vector: Unknown metric \`${metricType}\` in \`${vector}\``);
}

mandatoryMetricIndex++;
// Check if the value is valid for the given metric type
if (!definedMetrics[metricType].includes(metricValue)) {
throw new Error(`Invalid CVSS v4.0 vector \`${vector}\`: For metricType \`${metricType}\`, value \`${metricValue}\` is invalid. Valid, defined metric values for \`${metricType}\` are: ${definedMetrics[metricType]}.`);
}
}

return true;
Expand All @@ -437,13 +446,10 @@ class Vector {
*/
updateMetricsFromVectorString(vector) {
if (!vector) {
throw new Error("The vector string cannot be null, undefined, or empty.");
throw new Error(`The vector string cannot be null, undefined, or empty in ${vector}`);
}

// Validate the CVSS v4.0 string vector
if (!this.validateStringVector(vector)) {
throw new Error("Invalid CVSS v4.0 vector: " + vector);
}
this.validateStringVector(vector);

let metrics = vector.split('/');

Expand Down Expand Up @@ -780,21 +786,21 @@ class CVSS40 {
// It is used when looking for the highest vector part of the
// combinations produced by the MacroVector respective highest
static METRIC_LEVELS = {
"AV": {"N": 0.0, "A": 0.1, "L": 0.2, "P": 0.3},
"PR": {"N": 0.0, "L": 0.1, "H": 0.2},
"UI": {"N": 0.0, "P": 0.1, "A": 0.2},
"AC": {'L': 0.0, 'H': 0.1},
"AT": {'N': 0.0, 'P': 0.1},
"VC": {'H': 0.0, 'L': 0.1, 'N': 0.2},
"VI": {'H': 0.0, 'L': 0.1, 'N': 0.2},
"VA": {'H': 0.0, 'L': 0.1, 'N': 0.2},
"SC": {'H': 0.1, 'L': 0.2, 'N': 0.3},
"SI": {'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3},
"SA": {'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3},
"CR": {'H': 0.0, 'M': 0.1, 'L': 0.2},
"IR": {'H': 0.0, 'M': 0.1, 'L': 0.2},
"AR": {'H': 0.0, 'M': 0.1, 'L': 0.2},
"E": {'U': 0.2, 'P': 0.1, 'A': 0}
"AV": { "N": 0.0, "A": 0.1, "L": 0.2, "P": 0.3 },
"PR": { "N": 0.0, "L": 0.1, "H": 0.2 },
"UI": { "N": 0.0, "P": 0.1, "A": 0.2 },
"AC": { 'L': 0.0, 'H': 0.1 },
"AT": { 'N': 0.0, 'P': 0.1 },
"VC": { 'H': 0.0, 'L': 0.1, 'N': 0.2 },
"VI": { 'H': 0.0, 'L': 0.1, 'N': 0.2 },
"VA": { 'H': 0.0, 'L': 0.1, 'N': 0.2 },
"SC": { 'H': 0.1, 'L': 0.2, 'N': 0.3 },
"SI": { 'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3 },
"SA": { 'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3 },
"CR": { 'H': 0.0, 'M': 0.1, 'L': 0.2 },
"IR": { 'H': 0.0, 'M': 0.1, 'L': 0.2 },
"AR": { 'H': 0.0, 'M': 0.1, 'L': 0.2 },
"E": { 'U': 0.2, 'P': 0.1, 'A': 0 }
};

static MAX_COMPOSED = {
Expand Down Expand Up @@ -880,7 +886,7 @@ class CVSS40 {
// If the input is a string, create a new Vector object from the string
this.vector = new Vector(input);
} else {
throw new Error("Invalid input type for CVSS40 constructor. Expected a string or a Vector object.");
throw new Error(`Invalid input type for CVSSv4.0 constructor. Expected a string or a Vector object in ${vector}`);
}

// Calculate the score
Expand Down Expand Up @@ -1208,4 +1214,3 @@ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
window.CVSS40 = CVSS40;
window.Vector = Vector;
}

29 changes: 29 additions & 0 deletions cvss40.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { CVSS40 } = require('./cvss40');
const fs = require('fs');
const path = require('path');

const testDataPaths = fs.readdirSync('./data').map(fileName => ({
path: path.join('./data', fileName),
name: fileName,
}));

describe('CVSS 4.0', () => {
const testData = testDataPaths.reduce((data, file) => {
const fileData = fs.readFileSync(file.path, 'utf8');
const lineEntries = fileData.split('\n');
const scoredVectors = lineEntries.map(vectorScore => {
const vectorScorePair = vectorScore.trim().split(' - ');
return (vectorScorePair.length !== 2) ? null : { vector: vectorScorePair[0], score: parseFloat(vectorScorePair[1]) };
}).filter(Boolean);
data[file.name] = scoredVectors;
return data;
}, {});

Object.entries(testData).forEach(([fileName, vectorScores]) => {
it(`should calculate scores in ${fileName} correctly`, () => {
vectorScores.forEach(({ vector, score }) => {
expect(new CVSS40(vector).score).toBe(score);
});
});
});
});
Loading