Skip to content

Commit 04b5b41

Browse files
committed
Add quality report generator
1 parent 43b4f61 commit 04b5b41

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
# CVE Quality Report Generator
3+
4+
Eg.,
5+
6+
$ node report.js [path where CVE JSON records are kept] > report.html
7+
8+
$ node report.js ~/Documents/GitHub/cvelistV5/cves > index.html
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const validateCve = require('../Node_Validator/dist/cve5validator.js')
4+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
5+
6+
const ignore = {
7+
'': 1,
8+
'/cveMetadata/state': 1,
9+
'/cveMetadata': 1,
10+
'/dataVersion': 1,
11+
'/containers/cna/references/url': 0
12+
}
13+
var cnas = {};
14+
var cnaIndex = {};
15+
var errorStat = {};
16+
var warnStat = {};
17+
var errorCount = {};
18+
var yStat = {};
19+
var invalid = 0;
20+
var warns = 0;
21+
var total = 0;
22+
23+
const start = `
24+
<html><head><title>CVE Quality Report</title><style>
25+
body { font-family:Roboto Mono,sans-serif; margin:3em; }
26+
summary { cursor: pointer; }
27+
a { text-decoration: none; color: #356cac; }
28+
.grid { display:grid; gap: 5px; grid-template-columns: repeat(auto-fill, minmax(8em,1fr)); }
29+
</style></head>
30+
<body>`
31+
32+
async function loadCNAs(data) {
33+
for(c of data) {
34+
try {
35+
var em = c.contact[0].email[0].emailAddr;
36+
var host= em.substr(em.indexOf('@')+1);
37+
u = new URL('https://www.'+host);
38+
c.i = u.href;
39+
c.n = c.organizationName;
40+
} catch(e) {
41+
}
42+
cnas[c.shortName]=c;
43+
}
44+
45+
}
46+
async function getCNAs() {
47+
//var data = require('./CNAsList.json');
48+
//loadCNAs(data); return;
49+
const cnaList = 'https://raw.githubusercontent.com/CVEProject/cve-website/dev/src/assets/data/CNAsList.json'
50+
const res = await fetch(cnaList);
51+
if (res.ok) {
52+
const data = await res.json();
53+
loadCNAs(data);
54+
}
55+
}
56+
57+
function cveLink(id) {
58+
return 'https://github.com/CVEProject/cvelistV5/tree/main/cves/' + cveRepoPath(id);
59+
}
60+
61+
function cveRepoPath(value) {
62+
var realId = value.match(/(CVE-(\d{4})-(\d{1,12})(\d{3}))/);
63+
if (realId) {
64+
var id = realId[1];
65+
var year = realId[2];
66+
var bucket = realId[3];
67+
return (year + '/' + bucket + 'xxx/' + id + '.json')
68+
}
69+
}
70+
71+
async function getReport(dir) {
72+
const files = fs.readdirSync(dir, { withFileTypes: true });
73+
for (const file of files) {
74+
if (file.isDirectory()) {
75+
getReport(path.join(dir, file.name));
76+
} else {
77+
if (file.name.match((/CVE-(\d{4})-(\d{1,12})(\d{3})/))) {
78+
var cveFile = fs.readFileSync(path.join(dir, file.name));
79+
var cve = JSON.parse(cveFile);
80+
var v = valididate(cve);
81+
var q = qualityCheck(cve);
82+
total++;
83+
if(!v) {
84+
invalid++;
85+
}
86+
if(!q) {
87+
warns++;
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
/* Example error
95+
{
96+
instancePath: '/cveMetadata/state',
97+
schemaPath: '#/properties/state/enum',
98+
keyword: 'enum',
99+
params: { allowedValues: [Array] },
100+
message: 'must be equal to one of the allowed values'
101+
},
102+
*/
103+
104+
function addError(cve, err) {
105+
var id = cve.cveMetadata.cveId;
106+
107+
var shortName = cve.cveMetadata?.assignerShortName || 'default';
108+
if(!cnaIndex[shortName]) {
109+
cnaIndex[shortName] = [];
110+
}
111+
112+
//remove oneOf numbers
113+
var path = err?.instancePath?.replace(/\/\d+\/?/g, "/");
114+
if (!ignore[path]) {
115+
var prop = err.params?.additionalProperty || '';
116+
var e = `Problem: <b>${err.keyword} ${prop}!</b> - ${err.message}`;
117+
if (!errorStat[shortName]) {
118+
errorStat[shortName] = {}
119+
errorCount[shortName] = 0
120+
}
121+
if (!errorStat[shortName][path]) {
122+
errorStat[shortName][path] = {}
123+
}
124+
if (!errorStat[shortName][path][e]) {
125+
errorStat[shortName][path][e] = []
126+
}
127+
errorStat[shortName][path][e].push(id);
128+
errseen = true;
129+
}
130+
}
131+
132+
function valididate(cve) {
133+
var valid = validateCve(cve);
134+
errseen = false;
135+
if (!valid) {
136+
validateCve.errors.forEach(err=>{addError(cve, err)});
137+
}
138+
return !errseen;
139+
}
140+
141+
function qualityCheck(cve) {
142+
var warned = false;
143+
var c = checkCVSS(cve);
144+
if(c){
145+
addError(cve, c);
146+
warned = true;
147+
}
148+
/* c = checkLinkRot(cve);
149+
if(c) {
150+
addError(cve, c);
151+
warned = true;
152+
}*/
153+
if(warned) {
154+
return false;
155+
} else {
156+
return true;
157+
}
158+
}
159+
160+
const four04List = [
161+
'www.securityfocus.com',
162+
'osvdb.org',
163+
'online.securityfocus.com',
164+
'patches.sgi.com',
165+
'docs.info.apple.com',
166+
'h20000.www2.hp.com',
167+
'labs.idefense.com',
168+
'wiki.rpath.com',
169+
'source.codeaurora.org',
170+
'code.wireshark.org',
171+
'h20564.www2.hp.com',
172+
'www.linux-mandrake.com',
173+
'erpscan.io',
174+
'downloads.securityfocus.com',
175+
'www.atstake.com',
176+
'hermes.opensuse.org',
177+
'itrc.hp.com',
178+
'ftp.caldera.com',
179+
'packetstorm.linuxsecurity.com',
180+
'www1.itrc.hp.com'
181+
]
182+
183+
const four04 = {};
184+
for (const key of four04List) {
185+
four04[key] = 1;
186+
}
187+
188+
function checkLinkRot(cve) {
189+
if(cve.containers?.cna?.references) {
190+
for(r of cve.containers?.cna?.references) {
191+
try{
192+
var u = new URL(r.url);
193+
if (four04[u.host] && !(r.tags && r.tags.includes('broken-link'))) {
194+
return {
195+
instancePath: '/containers/cna/references',
196+
schemaPath: '#/properties/url',
197+
keyword: 'Broken link to ' + u.host,
198+
params: { },
199+
message: 'Reference points to defunct site. Replace or add a broken-link tag.'
200+
}
201+
}
202+
} catch(e) {
203+
console.log('Error parsing URL' + r.url)
204+
}
205+
}
206+
}
207+
return false;
208+
}
209+
210+
function checkCVSS(cve) {
211+
if(cve.containers.cna?.metrics) {
212+
for(m of cve.containers.cna?.metrics) {
213+
var cvss = m.cvssV3_1 || m.cvssV3_0;
214+
if(cvss) {
215+
if ((cvss.baseSeverity == 'CRITICAL' && cvss.baseScore >= 9 && cvss.baseScore <= 10)
216+
|| (cvss.baseSeverity == 'HIGH' && cvss.baseScore >= 7 && cvss.baseScore < 9)
217+
|| (cvss.baseSeverity == 'MEDIUM' && cvss.baseScore >= 4 && cvss.baseScore < 7)
218+
|| (cvss.baseSeverity == 'LOW' && cvss.baseScore >= 0.1 && cvss.baseScore < 4)
219+
|| (cvss.baseSeverity == 'NONE' && cvss.baseScore == 0)) {
220+
//console.log('valid CVSS ');
221+
} else {
222+
return {
223+
instancePath: '/containers/cna/metrics',
224+
schemaPath: '#/properties', // TODO?
225+
keyword: 'Bad CVSS',
226+
params: { },
227+
message: 'Mismatched CVSS score and level'
228+
}
229+
}
230+
}
231+
}
232+
}
233+
return false;
234+
}
235+
236+
async function checkAffected(cve) {
237+
238+
}
239+
240+
241+
run(process.argv[2]);
242+
243+
async function run(dir) {
244+
await getCNAs();
245+
await getReport(dir);
246+
await printReport();
247+
}
248+
249+
250+
const docs = {
251+
'/containers/cna/affected/product:maxLength': "Product name is too long! If you are listing multiple products, please use separate product objects.",
252+
'/containers/cna/affected/product:minLength': "A product name is required.",
253+
'/containers/cna/affected/versions/version:maxLength': "Version name is too long! If you are listing multiple versions, please encode as an array of version objects.",
254+
'/containers/cna/metrics/cvssV3_0:required': "CVSS objects are incomplete. Please provide a valid vectorString at the minimum in your CVE-JSON v4 submission."
255+
}
256+
257+
258+
function printReport() {
259+
console.log(start +
260+
`<h2>CVE Quality Workgroup Report</h2><h3> ${total} CVE analyzed: Found ${invalid} schema errors, ${warns} quality issues</h2>`)
261+
/*for (const y in yStat) {
262+
console.log(`<li>year ${y} - ${yStat[y]}</li>`)
263+
}*/
264+
265+
Object.keys(errorStat).sort().forEach(shortName => {
266+
var i = cnas[shortName]?.i;
267+
var name = cnas[shortName]?.n ? cnas[shortName]?.n : shortName;
268+
console.log(`<h3 id=${shortName}><img style="vertical-align:middle" width=32 height=32 src="https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(i)}/"> ${name} <a href="#${shortName}">[link]</a></h3>`)
269+
for (const k in errorStat[shortName]) {
270+
var alist = errorStat[shortName][k];
271+
for (const a in alist) {
272+
var ids = [...new Set(alist[a])];
273+
console.log(`<blockquote><details id="${shortName}-${k}-${a}"><summary>[${ids.length} CVEs] ${a} - <i>field ${k}</i> <a href="#${shortName}-${k}-${a}">[link]</a>:</summary>`)
274+
if(docs[shortName + ':' + k]) {
275+
console.log(`<p>`+docs[shortName + ':' + k]+'</p>')
276+
}
277+
console.log('<blockquote class="grid">')
278+
for (const c of ids.sort()) {
279+
console.log(` <a href="${cveLink(c)}">${c}</a>`)
280+
}
281+
console.log('</blockquote></details></blockquote>')
282+
}
283+
}
284+
});
285+
286+
console.log('</body></html>');
287+
}
288+
/* var index = start + '<h2>CVE Quality Workgroup Report: CVE Records Indexed by CNAs</h2><div class="grid">';
289+
for(x of Object.keys(cnaIndex).sort(new Intl.Collator('en',{numeric:true}).compare)) {
290+
var i = cnas[x]?.i;
291+
var name = cnas[x]?.n ? cnas[x]?.n : x;
292+
index = index + `<img style="vertical-align:middle" width=32 height=32 src="https://www.google.com/s2/favicons?sz=32&domain_url=${encodeURIComponent(i)}/"><br>${name}<br>${cnaIndex[x].length} records<br><br>`;
293+
//var report = start + `<h2>CVE Quality Workgroup Report: CVEs records belonging to ${name}</h2><blockquote class="grid">`;
294+
for (c in cnaIndex[x].sort(new Intl.Collator('en',{numeric:true}).compare)) {
295+
index += ` <a href="${cveLink(cnaIndex[x][c])}">${cnaIndex[x][c]}</a>`
296+
}
297+
//report = report + '</body</html>';
298+
//fs.writeFileSync('./reports/'+x+'.html',report);
299+
}
300+
fs.writeFileSync('./reports/index.html',index + '</div></body></html>');
301+
}
302+
303+
rl.on('line', validate)
304+
rl.on('close', report)
305+
306+
*/

0 commit comments

Comments
 (0)