Skip to content

Commit 2427172

Browse files
committed
Support multiple CVEs
1 parent 15601cf commit 2427172

File tree

3 files changed

+182
-68
lines changed

3 files changed

+182
-68
lines changed

README.md

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ See CISA KEV Catalog at [https://www.cisa.gov/known-exploited-vulnerabilities](h
2626
### Usage via npx
2727

2828
```bash
29-
## Run the tool with a CVE ID
30-
npx cve-risk-scores@latest CVE-YYYY-XXXXX
29+
## Run the tool with a list of CVE IDs separated by comma
30+
npx cve-risk-scores@latest "CVE-YYYY-XXXX1, CVE-YYYY-XXXX2,..."
3131
```
3232

3333
### Usage via global install option
@@ -36,8 +36,8 @@ npx cve-risk-scores@latest CVE-YYYY-XXXXX
3636
## Install the tool globally
3737
npm install -g cve-risk-scores@latest
3838

39-
## Run the tool with a CVE ID
40-
cve-risk-scores CVE-YYYY-XXXXX
39+
## Run the tool with a list of CVE IDs separated by comma
40+
cve-risk-scores "CVE-YYYY-XXXX1, CVE-YYYY-XXXX2,..."
4141
```
4242

4343
### Options
@@ -54,6 +54,8 @@ Options:
5454
[number] [default: 0]
5555
-s, --score CVSS score threshold to fail the audit
5656
[number] [default: 0]
57+
-d, --delay Delay between each CVE audit, in seconds
58+
[number] [default: 0]
5759
--help Show help [boolean]
5860

5961
```
@@ -81,16 +83,42 @@ Audit will fail if any of these conditions are met.
8183

8284
```bash
8385
# Run with default options
84-
cve-risk-scores CVE-2023-20273
86+
cve-risk-scores "CVE-2021-21295, CVE-2017-7525"
87+
88+
Auditing 1 of 2 CVE-2021-21295 at 10/25/2023, 8:48:11 PM
89+
90+
91+
EPSS score (probability of exploitation) : 89.162%
92+
93+
No CISA KEV data found for CVE CVE-2021-21295
94+
95+
CVSS v3.1 Base Score: 5.9 (MEDIUM)
96+
97+
Exploitability Score: 2.2 Impact Score : 3.6
98+
99+
----------------------------------------
100+
101+
Auditing 2 of 2 CVE-2017-7525 at 10/25/2023, 8:48:11 PM
85102

86-
Auditing CVE-2023-20273 at 10/25/2023, 5:55:24 PM
87103

104+
EPSS score (probability of exploitation) : 69.982%
88105

89-
EPSS score is : 0.01182 (0.83503%)
106+
No CISA KEV data found for CVE CVE-2017-7525
90107

91-
CISA KEV Date Added: 2023-10-23, Due Date: 2023-10-27
108+
CVSS v3.1 Base Score: 9.8 (CRITICAL)
92109

93-
CVSS v3.1 Base Score: 7.2 (HIGH)
110+
Exploitability Score: 3.9 Impact Score : 5.9
111+
112+
----------------------------------------
113+
114+
Audit Summary
115+
116+
┌─────────┬──────────────────┬────────────┬─────────────────┬───────────────────┐
117+
│ (index) │ CVE ID │ EPSS Score │ CVSS Base Score │ CISA KEV Due Date │
118+
├─────────┼──────────────────┼────────────┼─────────────────┼───────────────────┤
119+
│ 0 │ 'CVE-2021-21295' │ 89.162 │ 5.9 │ 'N/A'
120+
│ 1 │ 'CVE-2017-7525' │ 69.982 │ 9.8 │ 'N/A'
121+
└─────────┴──────────────────┴────────────┴─────────────────┴───────────────────┘
94122

95123
```
96124

@@ -100,6 +128,16 @@ On first run, the tool will create a folder named .epss in the ${HOME} or "/tmp"
100128
This folder will contain the raw EPSS Data file and uncompressed CSV file.
101129
If you would like to choose a different folder, you may set the `EPSS_DATA_FOLDER` environment variable to the desired folder.
102130

131+
## Future Roadmap
132+
133+
- [ ] Download NVD Database and use it for offline mode
134+
135+
## Rate Limits
136+
137+
> NVD API may be rate limited. If you run into any issues, you may try with smaller batches of CVE IDs.
138+
139+
- Use the `--delay` option to add a delay between each CVE audit. This will help avoid rate limiting issues.
140+
103141
## How to contribute
104142

105143
If you would like to contribute to this project, feel free to fork and create PR if you can.

bin/index.js

Lines changed: 134 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -96,91 +96,160 @@ async function loadScores(refresh = false) {
9696

9797
// Check CVSS Score in NVD database for the CVE
9898
async function checkCVSS(cveID) {
99-
const response = await fetch(
100-
`https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${cveID}`
101-
);
102-
const data = await response.json();
103-
if (data.totalResults == 0) {
104-
return;
105-
}
99+
try {
100+
const response = await fetch(
101+
`https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${cveID}`
102+
);
103+
104+
const data = await response.json();
105+
if (data.totalResults == 0) {
106+
return;
107+
}
106108

107-
const cvssScoreV31 = data.vulnerabilities[0].cve.metrics.cvssMetricV31;
108-
return cvssScoreV31 ? cvssScoreV31[0] : null;
109+
const cvssScoreV31 = data.vulnerabilities[0].cve.metrics.cvssMetricV31;
110+
return cvssScoreV31 ? cvssScoreV31[0] : null;
111+
} catch (err) {
112+
console.log(`Error fetching CVSS score for ${cveID}`);
113+
console.error(err);
114+
return null;
115+
}
109116
}
110117

111118
async function audit(
112-
cveID,
119+
cveIDStr,
113120
verbose = false,
114121
threshold = 0.0,
115122
failOnPastDue = false,
116-
score = 0.0
123+
score = 0.0,
124+
delay = 0.0
117125
) {
118126
let tabularData = [];
127+
let cveIDs = [];
128+
let counter = 0;
119129

120-
const cveRegex = new RegExp(`^CVE-\\d{4}-\\d{4,}$`);
121-
if (!cveRegex.test(cveID)) {
122-
console.warn(
123-
`\n Invalid CVE number ${cveID}. Expecting number as: CVE-YYYY-XXXX.\n`
124-
);
125-
process.exit(1);
130+
let cveAuditFail = false;
131+
let epssAuditFail = false;
132+
let kevAuditFail = false;
133+
134+
// If multiple CVEs are provided, audit each one
135+
if (cveIDStr.includes(",")) {
136+
cveIDs = cveIDStr.split(",").map((cve) => cve.trim());
137+
} else {
138+
cveIDs.push(cveIDStr.trim());
126139
}
127140

128-
const today = new Date();
141+
for (const cveID of cveIDs) {
142+
const cveRegex = new RegExp(`^CVE-\\d{4}-\\d{4,}$`);
129143

130-
console.log(`\n Auditing ${cveID} at ${new Date().toLocaleString()} \n`);
144+
let epssScore = null;
145+
let kevDueDate = null;
146+
let cvssScore = null;
147+
148+
if (!cveRegex.test(cveID)) {
149+
console.warn(
150+
`\n Invalid CVE number ${cveID}. Expecting number as: CVE-YYYY-XXXX. Skipping.\n`
151+
);
152+
// continue the loop
153+
continue;
154+
}
155+
156+
const today = new Date();
157+
158+
counter++;
131159

132-
if (!epssScores[cveID]) {
133-
console.log(` No EPSS score found for CVE ${cveID}\n`);
134-
} else {
135-
const { epss, percentile } = epssScores[cveID];
136160
console.log(
137-
`\n EPSS score (probability of exploitation) : ${+Number(
138-
epss * 100.0
139-
).toFixed(3)}% \n`
161+
`\n Auditing ${counter} of ${
162+
cveIDs.length
163+
} ${cveID} at ${new Date().toLocaleString()} \n`
140164
);
141165

142-
// If EPSS score is above threshold, fail the audit
143-
if (Number(threshold) > 0.0 && parseFloat(epss) > threshold) {
144-
console.warn(
145-
` EPSS score is above threshold of ${threshold}. Failing the audit.\n`
166+
if (!epssScores[cveID]) {
167+
console.log(` No EPSS score found for CVE ${cveID}\n`);
168+
} else {
169+
const { epss, percentile } = epssScores[cveID];
170+
171+
epssScore = +Number(epss * 100.0).toFixed(3);
172+
173+
console.log(
174+
`\n EPSS score (probability of exploitation) : ${+Number(
175+
epss * 100.0
176+
).toFixed(3)}% \n`
146177
);
147-
process.exit(2);
178+
179+
// If EPSS score is above threshold, fail the audit
180+
if (Number(threshold) > 0.0 && parseFloat(epss) > threshold) {
181+
console.warn(
182+
` EPSS score is above threshold of ${threshold}. Failing the audit.\n`
183+
);
184+
epssAuditFail = true;
185+
}
148186
}
149-
}
150187

151-
if (!kevData[cveID]) {
152-
console.log(` No CISA KEV data found for CVE ${cveID}\n`);
153-
} else {
154-
const { dateAdded, dueDate } = kevData[cveID];
155-
console.log(` CISA KEV Date Added: ${dateAdded}, Due Date: ${dueDate}\n`);
188+
if (!kevData[cveID]) {
189+
console.log(` No CISA KEV data found for CVE ${cveID}\n`);
190+
} else {
191+
const { dateAdded, dueDate } = kevData[cveID];
192+
kevDueDate = dueDate;
156193

157-
// If CVE is past due date, fail the audit
158-
if (failOnPastDue && new Date(dueDate) < today) {
159-
console.warn(
160-
`\n CVE is past due date of ${dueDate}. Failing the audit.\n`
161-
);
162-
process.exit(2);
194+
console.log(` CISA KEV Date Added: ${dateAdded}, Due Date: ${dueDate}\n`);
195+
196+
// If CVE is past due date, fail the audit
197+
if (failOnPastDue && new Date(dueDate) < today) {
198+
console.warn(
199+
`\n CVE is past due date of ${dueDate}. Failing the audit.\n`
200+
);
201+
kevAuditFail = true;
202+
}
163203
}
164-
}
165204

166-
const cvssScoreV31 = await checkCVSS(cveID);
167-
if (!cvssScoreV31) {
168-
console.log(` No CVSS score found for CVE ${cveID}\n`);
169-
} else {
170-
const { exploitabilityScore, impactScore } = cvssScoreV31;
171-
const { baseScore, baseSeverity } = cvssScoreV31.cvssData;
172-
console.log(` CVSS v3.1 Base Score: ${baseScore} (${baseSeverity})
205+
const cvssScoreV31 = await checkCVSS(cveID);
206+
if (!cvssScoreV31) {
207+
console.log(` No CVSS score found for CVE ${cveID}\n`);
208+
} else {
209+
const { exploitabilityScore, impactScore } = cvssScoreV31;
210+
const { baseScore, baseSeverity } = cvssScoreV31.cvssData;
211+
console.log(` CVSS v3.1 Base Score: ${baseScore} (${baseSeverity})
173212
\n\t Exploitability Score: ${exploitabilityScore} Impact Score : ${impactScore} \n`);
174213

175-
// If CVSS score is above threshold, fail the audiat
176-
if (Number(score) > 0.0 && parseFloat(baseScore) > score) {
177-
console.warn(
178-
` CVSS v3.1 Base Score is above threshold of ${score}. Failing the audit.\n`
179-
);
180-
process.exit(2);
214+
cvssScore = baseScore;
215+
216+
// If CVSS score is above threshold, fail the audiat
217+
if (Number(score) > 0.0 && parseFloat(baseScore) > score) {
218+
console.warn(
219+
` CVSS v3.1 Base Score is above threshold of ${score}. Failing the audit.\n`
220+
);
221+
cveAuditFail = true;
222+
}
223+
}
224+
225+
tabularData.push({
226+
"CVE ID": cveID,
227+
"EPSS Score (%)": epssScore ? epssScore : "N/A",
228+
"CVSS Base Score": cvssScore ? cvssScore : "N/A",
229+
"CISA KEV Due Date": kevDueDate ? kevDueDate : "N/A",
230+
});
231+
232+
// print a line separator
233+
console.log("-".repeat(40));
234+
235+
// Try adding a delay to avoid rate limiting
236+
if (Number(delay) > 0.0 && cveIDs.length > 2 && counter < cveIDs.length) {
237+
await new Promise((resolve) => setTimeout(resolve, Number(delay) * 1000));
181238
}
182239
}
183240

241+
if (tabularData.length > 0) {
242+
console.log("\n Audit Summary \n");
243+
console.table(tabularData);
244+
}
245+
246+
if (cveAuditFail || epssAuditFail || kevAuditFail) {
247+
console.warn(
248+
`\n Audit failed. Please review the CVEs above and take appropriate action.\n`
249+
);
250+
process.exit(2);
251+
}
252+
184253
process.exit(0);
185254
}
186255

@@ -212,6 +281,12 @@ async function audit(
212281
type: "number",
213282
default: 0.0,
214283
})
284+
.option("d", {
285+
alias: "delay",
286+
describe: "Delay between each CVE audit, in seconds",
287+
type: "number",
288+
default: 0.0,
289+
})
215290
.help(true).argv;
216291

217292
// If no CVE number is provided, print help
@@ -231,7 +306,8 @@ async function audit(
231306
options.verbose,
232307
options.threshold,
233308
options["fail-on-past-duedate"],
234-
options.score
309+
options.score,
310+
options["delay"]
235311
);
236312
} catch (err) {
237313
console.error(err);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cve-risk-scores",
3-
"version": "0.0.4",
3+
"version": "0.0.5",
44
"description": "Check risk scores for CVEs",
55
"main": "bin/index.js",
66
"bin": {

0 commit comments

Comments
 (0)