Skip to content

Commit f7c34e7

Browse files
committed
Reapply "Merge pull request #60 from RedHatProductSecurity/feature/add-testing"
This reverts commit bea2884.
1 parent bea2884 commit f7c34e7

13 files changed

+584917
-54
lines changed

.github/workflows/main.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: Run Tests
2+
on: push
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- name: Install modules
9+
run: yarn
10+
- name: Run tests
11+
run: yarn test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
22
node_modules/
3+
.DS_Store

cvss40.js

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ class Vector {
7171
},
7272
// Environmental (14 metrics)
7373
ENVIRONMENTAL: {
74-
"CR": ["X", "H", "M", "L"],
75-
"IR": ["X", "H", "M", "L"],
76-
"AR": ["X", "H", "M", "L"],
74+
"CR": ["X", "H", "M", "L"],
75+
"IR": ["X", "H", "M", "L"],
76+
"AR": ["X", "H", "M", "L"],
7777
"MAV": ["X", "N", "A", "L", "P"],
7878
"MAC": ["X", "L", "H"],
7979
"MAT": ["X", "N", "P"],
@@ -88,12 +88,12 @@ class Vector {
8888
},
8989
// Supplemental (6 metrics)
9090
SUPPLEMENTAL: {
91-
"S": ["X", "N", "P"],
91+
"S": ["X", "N", "P"],
9292
"AU": ["X", "N", "Y"],
93-
"R": ["X", "A", "U", "I"],
94-
"V": ["X", "D", "C"],
93+
"R": ["X", "A", "U", "I"],
94+
"V": ["X", "D", "C"],
9595
"RE": ["X", "L", "M", "H"],
96-
"U": ["X", "Clear", "Green", "Amber", "Red"],
96+
"U": ["X", "Clear", "Green", "Amber", "Red"],
9797
}
9898
};
9999

@@ -381,39 +381,48 @@ class Vector {
381381

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

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

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

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

400-
// Find the current expected metric
401-
while (expectedMetrics[mandatoryMetricIndex] && expectedMetrics[mandatoryMetricIndex][0] !== key) {
402-
// Check for missing mandatory metrics
403-
if (mandatoryMetricIndex < 11) {
404-
console.error("Error: invalid vector, missing mandatory metrics");
405-
return false;
406-
}
407-
mandatoryMetricIndex++;
408-
}
399+
const requiredMetrics = Object.keys(Vector.METRICS.BASE);
400+
401+
if (!requiredMetrics.every(metricType => metricType in metricsLookup)) {
402+
throw new Error(`Invalid CVSS v4.0 vector: Missing required metrics in \`${vector}\``);
403+
}
409404

410-
// Check if the value is valid for the given metric
411-
if (!expectedMetrics[mandatoryMetricIndex][1].includes(value)) {
412-
console.error(`Error: invalid vector, for key ${key}, value ${value} is not in ${expectedMetrics[mandatoryMetricIndex][1]}`);
413-
return false;
405+
if (metrics.length > Object.keys(metricsLookup).length) {
406+
throw new Error(`Invalid CVSS v4.0 vector: Duplicated metric types in \`${vector}\``);
407+
}
408+
409+
const definedMetrics = Vector.ALL_METRICS;
410+
411+
if (metrics.length > Object.keys(definedMetrics).length) {
412+
// This was here before but probably is impossible to reach because of the previous checks for duplicated and undefined keys
413+
throw new Error(`Invalid CVSS v4.0 vector: Unknown/excessive metric types in \`${vector}\``);
414+
}
415+
416+
for (let [metricType, metricValue] of Object.entries(metricsLookup)) {
417+
418+
if (!metricType in Vector.ALL_METRICS) {
419+
throw new Error(`Invalid CVSS v4.0 vector: Unknown metric \`${metricType}\` in \`${vector}\``);
414420
}
415421

416-
mandatoryMetricIndex++;
422+
// Check if the value is valid for the given metric type
423+
if (!definedMetrics[metricType].includes(metricValue)) {
424+
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]}.`);
425+
}
417426
}
418427

419428
return true;
@@ -437,13 +446,10 @@ class Vector {
437446
*/
438447
updateMetricsFromVectorString(vector) {
439448
if (!vector) {
440-
throw new Error("The vector string cannot be null, undefined, or empty.");
449+
throw new Error(`The vector string cannot be null, undefined, or empty in ${vector}`);
441450
}
442451

443-
// Validate the CVSS v4.0 string vector
444-
if (!this.validateStringVector(vector)) {
445-
throw new Error("Invalid CVSS v4.0 vector: " + vector);
446-
}
452+
this.validateStringVector(vector);
447453

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

@@ -780,21 +786,21 @@ class CVSS40 {
780786
// It is used when looking for the highest vector part of the
781787
// combinations produced by the MacroVector respective highest
782788
static METRIC_LEVELS = {
783-
"AV": {"N": 0.0, "A": 0.1, "L": 0.2, "P": 0.3},
784-
"PR": {"N": 0.0, "L": 0.1, "H": 0.2},
785-
"UI": {"N": 0.0, "P": 0.1, "A": 0.2},
786-
"AC": {'L': 0.0, 'H': 0.1},
787-
"AT": {'N': 0.0, 'P': 0.1},
788-
"VC": {'H': 0.0, 'L': 0.1, 'N': 0.2},
789-
"VI": {'H': 0.0, 'L': 0.1, 'N': 0.2},
790-
"VA": {'H': 0.0, 'L': 0.1, 'N': 0.2},
791-
"SC": {'H': 0.1, 'L': 0.2, 'N': 0.3},
792-
"SI": {'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3},
793-
"SA": {'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3},
794-
"CR": {'H': 0.0, 'M': 0.1, 'L': 0.2},
795-
"IR": {'H': 0.0, 'M': 0.1, 'L': 0.2},
796-
"AR": {'H': 0.0, 'M': 0.1, 'L': 0.2},
797-
"E": {'U': 0.2, 'P': 0.1, 'A': 0}
789+
"AV": { "N": 0.0, "A": 0.1, "L": 0.2, "P": 0.3 },
790+
"PR": { "N": 0.0, "L": 0.1, "H": 0.2 },
791+
"UI": { "N": 0.0, "P": 0.1, "A": 0.2 },
792+
"AC": { 'L': 0.0, 'H': 0.1 },
793+
"AT": { 'N': 0.0, 'P': 0.1 },
794+
"VC": { 'H': 0.0, 'L': 0.1, 'N': 0.2 },
795+
"VI": { 'H': 0.0, 'L': 0.1, 'N': 0.2 },
796+
"VA": { 'H': 0.0, 'L': 0.1, 'N': 0.2 },
797+
"SC": { 'H': 0.1, 'L': 0.2, 'N': 0.3 },
798+
"SI": { 'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3 },
799+
"SA": { 'S': 0.0, 'H': 0.1, 'L': 0.2, 'N': 0.3 },
800+
"CR": { 'H': 0.0, 'M': 0.1, 'L': 0.2 },
801+
"IR": { 'H': 0.0, 'M': 0.1, 'L': 0.2 },
802+
"AR": { 'H': 0.0, 'M': 0.1, 'L': 0.2 },
803+
"E": { 'U': 0.2, 'P': 0.1, 'A': 0 }
798804
};
799805

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

886892
// Calculate the score
@@ -1208,4 +1214,3 @@ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
12081214
window.CVSS40 = CVSS40;
12091215
window.Vector = Vector;
12101216
}
1211-

cvss40.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const { CVSS40 } = require('./cvss40');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const testDataPaths = fs.readdirSync('./data').map(fileName => ({
6+
path: path.join('./data', fileName),
7+
name: fileName,
8+
}));
9+
10+
describe('CVSS 4.0', () => {
11+
const testData = testDataPaths.reduce((data, file) => {
12+
const fileData = fs.readFileSync(file.path, 'utf8');
13+
const lineEntries = fileData.split('\n');
14+
const scoredVectors = lineEntries.map(vectorScore => {
15+
const vectorScorePair = vectorScore.trim().split(' - ');
16+
return (vectorScorePair.length !== 2) ? null : { vector: vectorScorePair[0], score: parseFloat(vectorScorePair[1]) };
17+
}).filter(Boolean);
18+
data[file.name] = scoredVectors;
19+
return data;
20+
}, {});
21+
22+
Object.entries(testData).forEach(([fileName, vectorScores]) => {
23+
it(`should calculate scores in ${fileName} correctly`, () => {
24+
vectorScores.forEach(({ vector, score }) => {
25+
expect(new CVSS40(vector).score).toBe(score);
26+
});
27+
});
28+
});
29+
});

0 commit comments

Comments
 (0)