Initial commit of ld-sysinfo-server backend
This commit is contained in:
1
scripts/.enrichment_resume
Normal file
1
scripts/.enrichment_resume
Normal file
@@ -0,0 +1 @@
|
||||
CVE-1999-0057
|
||||
7
scripts/.env.local
Normal file
7
scripts/.env.local
Normal file
@@ -0,0 +1,7 @@
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASSWORD=6DRR4xWvHBhSqLGtIOEKa7gHjKnX33Hf
|
||||
DB_NAME=db_ld-spring-backend
|
||||
|
||||
NVD_API_KEY=42b4f093-e8c4-4110-a7d1-6ab2ba6234aa
|
||||
NVD_MAX_RANGE_DAYS=30
|
||||
1
scripts/.last_synced_date
Normal file
1
scripts/.last_synced_date
Normal file
@@ -0,0 +1 @@
|
||||
2001-08-20T00:00:00.000Z
|
||||
48
scripts/cve-sync.log
Normal file
48
scripts/cve-sync.log
Normal file
@@ -0,0 +1,48 @@
|
||||
[27 May 2025, 12:53:07 pm] 🚀 📡 CVE sync launched in background.
|
||||
[27 May 2025, 12:53:08 pm] 🧪 Getting CVEs from the last 7 of days
|
||||
[27 May 2025, 12:53:08 pm] 🧪 Getting CVEs from the last 7 of days
|
||||
[27 May 2025, 12:53:08 pm] 🚀 CVE sync started
|
||||
[27 May 2025, 12:53:08 pm] 🔄 Initializing script...
|
||||
[27 May 2025, 12:53:08 pm] 📍 Launching script
|
||||
[27 May 2025, 12:53:08 pm] 📅 Starting CVE sync from 2025-05-20T04:53:08.063Z to 2025-05-27T04:53:08.063Z
|
||||
[27 May 2025, 12:53:08 pm] 📡 Fetching modified CVEs from 20 May 2025 to 27 May 2025...
|
||||
[27 May 2025, 12:53:08 pm] 🚀 CVE sync started
|
||||
[27 May 2025, 12:53:08 pm] 🔄 Initializing script...
|
||||
[27 May 2025, 12:53:08 pm] 📍 Launching script
|
||||
[27 May 2025, 12:53:08 pm] 📅 Starting CVE sync from 2025-05-20T04:53:08.063Z to 2025-05-27T04:53:08.063Z
|
||||
[27 May 2025, 12:53:08 pm] 📡 Fetching modified CVEs from 20 May 2025 to 27 May 2025...
|
||||
[27 May 2025, 12:53:10 pm] 📄 Page 1 — Processing 2000 CVEs from index 0 of ~2212
|
||||
[27 May 2025, 12:53:10 pm] 📄 Page 1 — Processing 2000 CVEs from index 0 of ~2212
|
||||
[27 May 2025, 12:53:27 pm] 📄 Page 2 — Processing 212 CVEs from index 2000 of ~2212
|
||||
[27 May 2025, 12:53:27 pm] 📄 Page 2 — Processing 212 CVEs from index 2000 of ~2212
|
||||
[27 May 2025, 12:53:33 pm] ✅ CVE import complete!
|
||||
[27 May 2025, 12:53:33 pm] ✅ CVE import complete!
|
||||
[27 May 2025, 12:53:33 pm] ✅ fetchCVE.js finished with exit code: 0
|
||||
[27 May 2025, 12:54:46 pm] 🚀 📡 CVE sync launched in background.
|
||||
[27 May 2025, 12:54:46 pm] 🧪 Getting CVEs from the last 30 of days
|
||||
[27 May 2025, 12:54:46 pm] 🧪 Getting CVEs from the last 30 of days
|
||||
[27 May 2025, 12:54:46 pm] 🚀 CVE sync started
|
||||
[27 May 2025, 12:54:46 pm] 🔄 Initializing script...
|
||||
[27 May 2025, 12:54:46 pm] 📍 Launching script
|
||||
[27 May 2025, 12:54:46 pm] 📅 Starting CVE sync from 2025-04-27T04:54:46.513Z to 2025-05-27T04:54:46.513Z
|
||||
[27 May 2025, 12:54:46 pm] 📡 Fetching modified CVEs from 27 Apr 2025 to 27 May 2025...
|
||||
[27 May 2025, 12:54:46 pm] 🚀 CVE sync started
|
||||
[27 May 2025, 12:54:46 pm] 🔄 Initializing script...
|
||||
[27 May 2025, 12:54:46 pm] 📍 Launching script
|
||||
[27 May 2025, 12:54:46 pm] 📅 Starting CVE sync from 2025-04-27T04:54:46.513Z to 2025-05-27T04:54:46.513Z
|
||||
[27 May 2025, 12:54:46 pm] 📡 Fetching modified CVEs from 27 Apr 2025 to 27 May 2025...
|
||||
[27 May 2025, 12:54:49 pm] 📄 Page 1 — Processing 2000 CVEs from index 0 of ~10257
|
||||
[27 May 2025, 12:54:49 pm] 📄 Page 1 — Processing 2000 CVEs from index 0 of ~10257
|
||||
[27 May 2025, 12:55:55 pm] 📄 Page 2 — Processing 2000 CVEs from index 2000 of ~10257
|
||||
[27 May 2025, 12:55:55 pm] 📄 Page 2 — Processing 2000 CVEs from index 2000 of ~10257
|
||||
[27 May 2025, 12:56:18 pm] 📄 Page 3 — Processing 2000 CVEs from index 4000 of ~10257
|
||||
[27 May 2025, 12:56:18 pm] 📄 Page 3 — Processing 2000 CVEs from index 4000 of ~10257
|
||||
[27 May 2025, 12:56:35 pm] 📄 Page 4 — Processing 2000 CVEs from index 6000 of ~10257
|
||||
[27 May 2025, 12:56:35 pm] 📄 Page 4 — Processing 2000 CVEs from index 6000 of ~10257
|
||||
[27 May 2025, 12:57:03 pm] 📄 Page 5 — Processing 2000 CVEs from index 8000 of ~10257
|
||||
[27 May 2025, 12:57:03 pm] 📄 Page 5 — Processing 2000 CVEs from index 8000 of ~10257
|
||||
[27 May 2025, 12:57:15 pm] 📄 Page 6 — Processing 257 CVEs from index 10000 of ~10257
|
||||
[27 May 2025, 12:57:15 pm] 📄 Page 6 — Processing 257 CVEs from index 10000 of ~10257
|
||||
[27 May 2025, 12:57:21 pm] ✅ CVE import complete!
|
||||
[27 May 2025, 12:57:21 pm] ✅ CVE import complete!
|
||||
[27 May 2025, 12:57:21 pm] ✅ fetchCVE.js finished with exit code: 0
|
||||
177
scripts/enrichCVE_MSRC.js
Normal file
177
scripts/enrichCVE_MSRC.js
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import axios from 'axios';
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
const DB = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return null;
|
||||
const date = new Date(isoString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
function getLocalizedText(entry, fallback = '') {
|
||||
if (!entry || typeof entry !== 'object') return fallback;
|
||||
|
||||
// MSRC: sometimes the object directly has "Value"
|
||||
if ('Value' in entry && typeof entry.Value === 'string') {
|
||||
return entry.Value;
|
||||
}
|
||||
|
||||
// If it's localized (e.g., { "en-US": { Value: ... } })
|
||||
if (entry['en-US']?.Value) return entry['en-US'].Value;
|
||||
|
||||
// Fallback: first valid value
|
||||
for (const key in entry) {
|
||||
if (entry[key]?.Value) return entry[key].Value;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
|
||||
function extractDescription(notes = [], fallback = '') {
|
||||
const note = notes.find(n =>
|
||||
n.Title === 'Description' &&
|
||||
n.Type === 2 &&
|
||||
typeof n.Value === 'string' &&
|
||||
n.Value.trim().length > 0
|
||||
);
|
||||
return note?.Value || fallback;
|
||||
}
|
||||
|
||||
function extractAffectedProducts(vuln) {
|
||||
const fixed = vuln.ProductStatuses?.Fixed ?? [];
|
||||
const known = vuln.ProductStatuses?.KnownAffected ?? [];
|
||||
const underInvestigation = vuln.ProductStatuses?.UnderInvestigation ?? [];
|
||||
return [...new Set([...fixed, ...known, ...underInvestigation])].join(', ');
|
||||
}
|
||||
|
||||
async function fetchCVRFDoc(cvrfId) {
|
||||
const url = `https://api.msrc.microsoft.com/cvrf/v3.0/cvrf/${cvrfId}`;
|
||||
try {
|
||||
const res = await axios.get(url, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to fetch CVRF for ${cvrfId}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function enrichCVE(cve, data) {
|
||||
const [rows] = await DB.execute(
|
||||
`SELECT title, severity, description, cvss_score, cvss_vector FROM cves WHERE id = ?`,
|
||||
[cve]
|
||||
);
|
||||
|
||||
if (rows.length === 0) return;
|
||||
|
||||
const existing = rows[0];
|
||||
const updateFields = {
|
||||
title: existing.title || data.title,
|
||||
severity: existing.severity || data.severity,
|
||||
description: (existing.description?.length ?? 0) < 20 ? data.description : existing.description,
|
||||
cvss_score: existing.cvss_score ?? data.cvssScore,
|
||||
cvss_vector: existing.cvss_vector || data.cvssVector,
|
||||
};
|
||||
|
||||
await DB.execute(
|
||||
`UPDATE cves
|
||||
SET title = ?, severity = ?, description = ?, cvss_score = ?, cvss_vector = ?
|
||||
WHERE id = ?`,
|
||||
[updateFields.title, updateFields.severity, updateFields.description, updateFields.cvss_score, updateFields.cvss_vector, cve]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
async function storeVulnerability(v, published, modified) {
|
||||
const cve = v.CVE;
|
||||
console.log(`⏳ Processing ${cve}...`);
|
||||
|
||||
const title = getLocalizedText(v.Title, '');
|
||||
console.debug(`🧪 Raw Title for ${cve}:`, JSON.stringify(v.Title, null, 2));
|
||||
console.debug(`➡️ Parsed Title: "${title}"`);
|
||||
const description = extractDescription(v.Notes, title);
|
||||
const affectedProducts = extractAffectedProducts(v);
|
||||
const severity = v.Threats?.find(t => t.Type === 'Severity')?.Description?.Value || '';
|
||||
const exploitability = v.Threats?.find(t => t.Type === 'Exploitability')?.Description?.Value || '';
|
||||
const cvssSet = v.CVSSScoreSets?.[0];
|
||||
const cvssScore = cvssSet?.BaseScore || null;
|
||||
const cvssVector = cvssSet?.Vector || '';
|
||||
const cweList = Array.isArray(v.CWE) ? v.CWE : [];
|
||||
|
||||
await DB.execute(
|
||||
`INSERT INTO microsoft_cves (cve_id, title, severity, published_date, last_modified_date,
|
||||
affected_products, description, exploitability, cvss_score, cvss_vector)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_modified_date = VALUES(last_modified_date),
|
||||
affected_products = VALUES(affected_products),
|
||||
severity = VALUES(severity),
|
||||
description = VALUES(description),
|
||||
exploitability = VALUES(exploitability),
|
||||
cvss_score = VALUES(cvss_score),
|
||||
cvss_vector = VALUES(cvss_vector)`,
|
||||
[cve, title, severity, published, modified, affectedProducts, description, exploitability, cvssScore, cvssVector]
|
||||
);
|
||||
|
||||
for (const { ID: cweId, Value: cweName } of cweList) {
|
||||
if (!cweId) continue;
|
||||
|
||||
await DB.execute(
|
||||
`INSERT IGNORE INTO cwe_entries (cwe_id, cwe_name) VALUES (?, ?)`,
|
||||
[cweId, cweName || null]
|
||||
);
|
||||
|
||||
await DB.execute(
|
||||
`INSERT IGNORE INTO cve_cwe (cve_id, cwe_id) VALUES (?, ?)`,
|
||||
[cve, cweId]
|
||||
);
|
||||
}
|
||||
|
||||
await enrichCVE(cve, { title, severity, description, cvssScore, cvssVector });
|
||||
console.log(`✅ Updated ${cve} in master table`);
|
||||
}
|
||||
|
||||
function getPreviousMonthID() {
|
||||
const now = new Date();
|
||||
now.setMonth(now.getMonth() - 1);
|
||||
const year = now.getFullYear();
|
||||
const month = now.toLocaleString('en-US', { month: 'short' }); // e.g., "Mar"
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
|
||||
async function enrichPreviousMonth() {
|
||||
const cvrfId = getPreviousMonthID();
|
||||
console.log(`📡 Fetching CVEs for ${cvrfId}...`);
|
||||
|
||||
const data = await fetchCVRFDoc(cvrfId);
|
||||
if (!data) return;
|
||||
|
||||
const published = formatDate(data.DocumentTracking?.InitialReleaseDate);
|
||||
const modified = formatDate(data.DocumentTracking?.CurrentReleaseDate);
|
||||
|
||||
const vulns = data.Vulnerability || [];
|
||||
console.log(`📋 Found ${vulns.length} vulnerabilities.`);
|
||||
|
||||
for (const v of vulns) {
|
||||
await storeVulnerability(v, published, modified);
|
||||
}
|
||||
|
||||
console.log(`🎉 Enrichment complete for ${cvrfId}`);
|
||||
await DB.end();
|
||||
}
|
||||
|
||||
console.log("🚀 Starting MSRC enrichment of CVEs...");
|
||||
await enrichPreviousMonth();
|
||||
208
scripts/fetchCVE.js
Normal file
208
scripts/fetchCVE.js
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
const logFile = fs.createWriteStream('cve-sync.log', {
|
||||
flags: 'a',
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
function log(msg) {
|
||||
const now = new Date();
|
||||
|
||||
// Generate the locale string with hour12 enabled (e.g. "14 Apr 2025, 08:14:42 AM")
|
||||
const raw = now.toLocaleString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
// Regex to convert only the AM/PM to lowercase
|
||||
const formatted = raw.replace(/\b(AM|PM)\b/, (match) => match.toLowerCase());
|
||||
|
||||
const timestamp = `[${formatted}]`;
|
||||
const line = `${timestamp} ${msg}`;
|
||||
console.log(line);
|
||||
logFile.write(`${line}\n`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return null;
|
||||
const date = new Date(isoString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
function formatShortDate(isoString) {
|
||||
return new Date(isoString).toLocaleDateString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).replace(/\b([A-Z])([a-z]+)\b/, (_, a, b) => a + b); // Capitalize only first letter of month
|
||||
}
|
||||
|
||||
|
||||
function extractCpeParts(cpe) {
|
||||
const parts = cpe.split(':');
|
||||
return {
|
||||
vendor: parts[3] || null,
|
||||
product: parts[4] || null,
|
||||
version: parts[5] || null
|
||||
};
|
||||
}
|
||||
|
||||
function addDaysToISO(dateISO, days) {
|
||||
const date = new Date(dateISO);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
const DB = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
|
||||
const BASE_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
|
||||
const API_KEY = process.env.NVD_API_KEY;
|
||||
const RESULTS_PER_PAGE = 2000;
|
||||
let MAX_RANGE_DAYS = Number(process.env.NVD_MAX_RANGE_DAYS || 7);
|
||||
|
||||
log(`🧪 Getting CVEs from the last ${MAX_RANGE_DAYS} of days`);
|
||||
|
||||
if (MAX_RANGE_DAYS > 120) {
|
||||
log("⚠️ MAX_RANGE_DAYS exceeds NVD API limit. Defaulting to 120.");
|
||||
MAX_RANGE_DAYS = 120;
|
||||
}
|
||||
if (isNaN(MAX_RANGE_DAYS) || MAX_RANGE_DAYS < 1) {
|
||||
log("⚠️ Invalid MAX_RANGE_DAYS. Using default of 7.");
|
||||
MAX_RANGE_DAYS = 7;
|
||||
}
|
||||
|
||||
async function fetchCVEPage(startIndex, startDate, endDate) {
|
||||
try {
|
||||
const res = await axios.get(BASE_URL, {
|
||||
params: {
|
||||
startIndex,
|
||||
resultsPerPage: RESULTS_PER_PAGE,
|
||||
lastModStartDate: startDate,
|
||||
lastModEndDate: endDate,
|
||||
},
|
||||
headers: API_KEY ? { apiKey: API_KEY } : {}
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
log(`❌ API error: ${err.response?.status} - ${err.response?.data?.message || err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function processCVE(cveWrapper) {
|
||||
const cve = cveWrapper.cve;
|
||||
const cveId = cve.id;
|
||||
const desc = cve.descriptions.find(d => d.lang === 'en')?.value ?? '';
|
||||
const published = formatDate(cve.published);
|
||||
const modified = formatDate(cve.lastModified);
|
||||
const severity = cve.metrics?.cvssMetricV31?.[0]?.cvssData?.baseSeverity ?? null;
|
||||
const score = cve.metrics?.cvssMetricV31?.[0]?.cvssData?.baseScore ?? null;
|
||||
|
||||
try {
|
||||
await DB.execute(
|
||||
`INSERT INTO cves (id, description, published_date, last_modified_date, severity, cvss_score)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE last_modified_date = VALUES(last_modified_date)`,
|
||||
[cveId, desc, published, modified, severity, score]
|
||||
);
|
||||
} catch (err) {
|
||||
log(`❌ Error inserting CVE ${cveId}: ${err.message}`);
|
||||
}
|
||||
|
||||
const configurations = cve.configurations ?? [];
|
||||
for (const node of configurations) {
|
||||
for (const match of node.nodes?.flatMap(n => n.cpeMatch ?? []) ?? []) {
|
||||
const cpe = match.criteria;
|
||||
const vulnerable = match.vulnerable ? 1 : 0;
|
||||
const start = match.versionStartIncluding || match.versionStartExcluding || null;
|
||||
const end = match.versionEndIncluding || match.versionEndExcluding || null;
|
||||
|
||||
const { vendor, product, version } = extractCpeParts(cpe);
|
||||
|
||||
try {
|
||||
await DB.execute(
|
||||
`INSERT INTO cpe_matches (cve_id, cpe_uri, version_start, version_end, vulnerable, vendor, product, version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[cveId, cpe, start, end, vulnerable, vendor, product, version]
|
||||
);
|
||||
} catch (err) {
|
||||
log(`⚠️ Error inserting CPE for CVE ${cveId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getMostRecentModifiedDateFromDB() {
|
||||
const [rows] = await DB.query(`SELECT MAX(last_modified_date) AS lastMod FROM cves`);
|
||||
const lastMod = rows[0]?.lastMod;
|
||||
return lastMod ? new Date(lastMod).toISOString() : '2020-01-01T00:00:00.000Z';
|
||||
}
|
||||
|
||||
async function importCVEFeed() {
|
||||
const now = new Date();
|
||||
const endDate = now.toISOString();
|
||||
|
||||
const startDateObj = new Date(now);
|
||||
startDateObj.setDate(startDateObj.getDate() - MAX_RANGE_DAYS);
|
||||
const startDate = startDateObj.toISOString();
|
||||
|
||||
log(`🚀 CVE sync started`);
|
||||
log(`🔄 Initializing script...`);
|
||||
log(`📍 Launching script`);
|
||||
log(`📅 Starting CVE sync from ${startDate} to ${endDate}`);
|
||||
|
||||
const humanStart = formatShortDate(startDate);
|
||||
const humanEnd = formatShortDate(endDate);
|
||||
log(`📡 Fetching modified CVEs from ${humanStart} to ${humanEnd}...`);
|
||||
|
||||
let startIndex = 0;
|
||||
let totalResults = Infinity;
|
||||
let pageCount = 0;
|
||||
|
||||
do {
|
||||
const data = await fetchCVEPage(startIndex, startDate, endDate);
|
||||
const vulnerabilities = data.vulnerabilities || [];
|
||||
totalResults = data.totalResults ?? vulnerabilities.length;
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
log(`⚠️ No CVEs returned at index ${startIndex}`);
|
||||
break;
|
||||
}
|
||||
|
||||
log(`📄 Page ${++pageCount} — Processing ${vulnerabilities.length} CVEs from index ${startIndex} of ~${totalResults}`);
|
||||
|
||||
for (const vuln of vulnerabilities) {
|
||||
await processCVE(vuln);
|
||||
}
|
||||
|
||||
startIndex += RESULTS_PER_PAGE;
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
} while (startIndex < totalResults);
|
||||
|
||||
log('✅ CVE import complete!');
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
|
||||
importCVEFeed().catch((err) => {
|
||||
log(`❌ Fatal error during import: ${err.message}`);
|
||||
logFile.end();
|
||||
});
|
||||
457
scripts/fetchCVE_v2.js
Normal file
457
scripts/fetchCVE_v2.js
Normal file
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
import fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
const logFile = fs.createWriteStream('cve-sync.log', {
|
||||
flags: 'a',
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const RESUME_FILE = '.enrichment_resume';
|
||||
const DB = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
const BASE_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
|
||||
const API_KEY = process.env.NVD_API_KEY;
|
||||
const RESULTS_PER_PAGE = 2000;
|
||||
let MAX_RANGE_DAYS = Number(process.env.NVD_MAX_RANGE_DAYS || 120);
|
||||
log(`🧪 Getting CVEs from the last ${MAX_RANGE_DAYS} of days`);
|
||||
|
||||
if (MAX_RANGE_DAYS > 120) {
|
||||
log("⚠️ MAX_RANGE_DAYS exceeds NVD API limit. Defaulting to 120.");
|
||||
MAX_RANGE_DAYS = 120;
|
||||
}
|
||||
if (isNaN(MAX_RANGE_DAYS) || MAX_RANGE_DAYS < 1) {
|
||||
log("⚠️ Invalid MAX_RANGE_DAYS. Using default of 7.");
|
||||
MAX_RANGE_DAYS = 7;
|
||||
}
|
||||
|
||||
function saveLastProcessedCVE(cveId) {
|
||||
fs.writeFileSync(RESUME_FILE, cveId, 'utf8');
|
||||
}
|
||||
|
||||
function loadLastProcessedCVE() {
|
||||
if (!fs.existsSync(RESUME_FILE)) return null;
|
||||
return fs.readFileSync(RESUME_FILE, 'utf8');
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
const now = new Date();
|
||||
|
||||
// Generate the locale string with hour12 enabled (e.g. "14 Apr 2025, 08:14:42 AM")
|
||||
const raw = now.toLocaleString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
// Regex to convert only the AM/PM to lowercase
|
||||
const formatted = raw.replace(/\b(AM|PM)\b/, (match) => match.toLowerCase());
|
||||
|
||||
const timestamp = `[${formatted}]`;
|
||||
const line = `${timestamp} ${msg}`;
|
||||
console.log(line);
|
||||
logFile.write(`${line}\n`);
|
||||
}
|
||||
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return null;
|
||||
const date = new Date(isoString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
function formatShortDate(isoString) {
|
||||
return new Date(isoString).toLocaleDateString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).replace(/\b([A-Z])([a-z]+)\b/, (_, a, b) => a + b); // Capitalize only first letter of month
|
||||
}
|
||||
|
||||
function extractCpeParts(cpe) {
|
||||
const parts = cpe.split(':');
|
||||
return {
|
||||
vendor: parts[3] || null,
|
||||
product: parts[4] || null,
|
||||
version: parts[5] || null
|
||||
};
|
||||
}
|
||||
|
||||
function addDaysToISO(dateISO, days) {
|
||||
const date = new Date(dateISO);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
async function fetchCVEPage(startIndex, startDate, endDate, extraOptions = {}) {
|
||||
try {
|
||||
const res = await axios.get(BASE_URL, {
|
||||
params: {
|
||||
pubStartDate: startDate,
|
||||
pubEndDate: endDate,
|
||||
startIndex,
|
||||
resultsPerPage: RESULTS_PER_PAGE,
|
||||
...extraOptions,
|
||||
},
|
||||
headers: API_KEY ? { apiKey: API_KEY } : {}
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
log(`❌ API error: ${err.response?.status} - ${err.response?.data?.message || err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function processCVE(cveWrapper) {
|
||||
const cve = cveWrapper.cve;
|
||||
const cveId = cve.id;
|
||||
|
||||
const title = cve.titles?.find(t => t.lang === 'en')?.title || '';
|
||||
const desc = cve.descriptions?.find(d => d.lang === 'en')?.value || '';
|
||||
const published = formatDate(cve.published);
|
||||
const modified = formatDate(cve.lastModified);
|
||||
|
||||
// CVSSv2
|
||||
const metricV2 = cve.metrics?.cvssMetricV2?.[0];
|
||||
const severityV2 = metricV2?.cvssData?.baseSeverity || null;
|
||||
const scoreV2 = metricV2?.cvssData?.baseScore || null;
|
||||
const vectorV2 = metricV2?.cvssData?.vectorString || '';
|
||||
|
||||
// CVSSv3
|
||||
const metricV3 = cve.metrics?.cvssMetricV31?.[0];
|
||||
const severityV3 = metricV3?.cvssData?.baseSeverity || null;
|
||||
const scoreV3 = metricV3?.cvssData?.baseScore || null;
|
||||
const vectorV3 = metricV3?.cvssData?.vectorString || '';
|
||||
|
||||
// CVSSv4
|
||||
const metricV4 = cve.metrics?.cvssMetricV40?.[0] || cve.metrics?.cvssMetricV4?.[0];
|
||||
const severityV4 = metricV4?.cvssData?.baseSeverity || null;
|
||||
const scoreV4 = metricV4?.cvssData?.baseScore || null;
|
||||
const vectorV4 = metricV4?.cvssData?.vectorString || '';
|
||||
|
||||
// CWE IDs
|
||||
const cweIds = (cve.weaknesses || [])
|
||||
.flatMap(w => w.description || [])
|
||||
.filter(desc => desc.lang === 'en')
|
||||
.map(desc => desc.value)
|
||||
.join(',');
|
||||
|
||||
// References
|
||||
const references = (cve.references || [])
|
||||
.map(ref => ref.url)
|
||||
.join(',');
|
||||
|
||||
// Tags
|
||||
const cveTags = cve.cveMetadata?.cveTags || [];
|
||||
const hasKev = cveTags.includes('Known_Exploited_Vulnerability');
|
||||
const hasCertNotes = cveTags.includes('CERT-VN');
|
||||
const hasCertAlerts = cveTags.includes('US-CERT-TA');
|
||||
|
||||
try {
|
||||
await DB.execute(
|
||||
`INSERT INTO cves (
|
||||
id, title, description, published_date, last_modified_date,
|
||||
severity_v2, cvss_score_v2, cvss_vector_v2,
|
||||
severity_v3, cvss_score_v3, cvss_vector_v3,
|
||||
severity_v4, cvss_score_v4, cvss_vector_v4,
|
||||
cwe_ids, \`references\`, hasKev, hasCertNotes, hasCertAlerts, source
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_modified_date = VALUES(last_modified_date),
|
||||
severity_v2 = IFNULL(severity_v2, VALUES(severity_v2)),
|
||||
cvss_score_v2 = IFNULL(cvss_score_v2, VALUES(cvss_score_v2)),
|
||||
cvss_vector_v2 = IFNULL(cvss_vector_v2, VALUES(cvss_vector_v2)),
|
||||
severity_v3 = IFNULL(severity_v3, VALUES(severity_v3)),
|
||||
cvss_score_v3 = IFNULL(cvss_score_v3, VALUES(cvss_score_v3)),
|
||||
cvss_vector_v3 = IFNULL(cvss_vector_v3, VALUES(cvss_vector_v3)),
|
||||
severity_v4 = IFNULL(severity_v4, VALUES(severity_v4)),
|
||||
cvss_score_v4 = IFNULL(cvss_score_v4, VALUES(cvss_score_v4)),
|
||||
cvss_vector_v4 = IFNULL(cvss_vector_v4, VALUES(cvss_vector_v4)),
|
||||
cwe_ids = IFNULL(cwe_ids, VALUES(cwe_ids)),
|
||||
\`references\` = IFNULL(\`references\`, VALUES(\`references\`)),
|
||||
hasKev = VALUES(hasKev),
|
||||
hasCertNotes = VALUES(hasCertNotes),
|
||||
hasCertAlerts = VALUES(hasCertAlerts),
|
||||
source = VALUES(source)
|
||||
`,
|
||||
[
|
||||
cveId, title, desc, published, modified,
|
||||
severityV2, scoreV2, vectorV2,
|
||||
severityV3, scoreV3, vectorV3,
|
||||
severityV4, scoreV4, vectorV4,
|
||||
cweIds, references, hasKev ? 1 : 0, hasCertNotes ? 1 : 0, hasCertAlerts ? 1 : 0, 'NVD'
|
||||
]
|
||||
);
|
||||
} catch (err) {
|
||||
log(`❌ Error inserting CVE ${cveId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMostRecentModifiedDateFromDB() {
|
||||
const [rows] = await DB.query(`SELECT MAX(last_modified_date) AS lastMod FROM cves`);
|
||||
const lastMod = rows[0]?.lastMod;
|
||||
return lastMod ? new Date(lastMod).toISOString() : '2020-01-01T00:00:00.000Z';
|
||||
}
|
||||
|
||||
async function importCVEFeed() {
|
||||
const now = new Date();
|
||||
const endDate = now.toISOString();
|
||||
|
||||
const startDateObj = new Date(now);
|
||||
startDateObj.setDate(startDateObj.getDate() - MAX_RANGE_DAYS);
|
||||
const startDate = startDateObj.toISOString();
|
||||
|
||||
log(`🚀 CVE sync started`);
|
||||
log(`🔄 Initializing script...`);
|
||||
log(`📍 Launching script`);
|
||||
log(`📅 Starting CVE sync from ${startDate} to ${endDate}`);
|
||||
|
||||
const humanStart = formatShortDate(startDate);
|
||||
const humanEnd = formatShortDate(endDate);
|
||||
log(`📡 Fetching modified CVEs from ${humanStart} to ${humanEnd}...`);
|
||||
|
||||
let startIndex = 0;
|
||||
let totalResults = Infinity;
|
||||
let pageCount = 0;
|
||||
|
||||
do {
|
||||
const data = await fetchCVEPage(startIndex, startDate, endDate);
|
||||
const vulnerabilities = data.vulnerabilities || [];
|
||||
totalResults = data.totalResults ?? vulnerabilities.length;
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
log(`⚠️ No CVEs returned at index ${startIndex}`);
|
||||
break;
|
||||
}
|
||||
|
||||
log(`📄 Page ${++pageCount} — Processing ${vulnerabilities.length} CVEs from index ${startIndex} of ~${totalResults}`);
|
||||
|
||||
for (const vuln of vulnerabilities) {
|
||||
await processCVE(vuln);
|
||||
}
|
||||
|
||||
startIndex += RESULTS_PER_PAGE;
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
} while (startIndex < totalResults);
|
||||
|
||||
log('✅ CVE import complete!');
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
async function importCVEFeedBackfill() {
|
||||
const now = new Date();
|
||||
const resumeFrom = loadLastSyncedDate();
|
||||
let startFrom = resumeFrom ? new Date(resumeFrom) : now;
|
||||
|
||||
const MAX_RANGE_DAYS = 120;
|
||||
|
||||
log(resumeFrom
|
||||
? `🔁 Resuming CVE backfill from ${formatShortDate(startFrom.toISOString())}`
|
||||
: `⏮️ Starting CVE backfill from today (${formatShortDate(startFrom.toISOString())})`
|
||||
);
|
||||
|
||||
while (true) {
|
||||
const end = new Date(startFrom);
|
||||
const start = new Date(startFrom);
|
||||
start.setDate(start.getDate() - MAX_RANGE_DAYS + 1); // 120-day window
|
||||
|
||||
const startISO = start.toISOString();
|
||||
const endISO = end.toISOString();
|
||||
const humanRange = `${formatShortDate(startISO)} to ${formatShortDate(endISO)}`;
|
||||
|
||||
log(`📡 Fetching published CVEs from ${humanRange}...`);
|
||||
|
||||
let startIndex = 0;
|
||||
let totalResults = Infinity;
|
||||
let pageCount = 0;
|
||||
|
||||
try {
|
||||
do {
|
||||
const data = await fetchCVEPage(startIndex, startISO, endISO);
|
||||
const vulnerabilities = data.vulnerabilities || [];
|
||||
totalResults = data.totalResults ?? vulnerabilities.length;
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
log(`⚠️ No CVEs returned for ${humanRange} at index ${startIndex}`);
|
||||
break;
|
||||
}
|
||||
|
||||
log(`📄 Page ${++pageCount} — ${vulnerabilities.length} CVEs from index ${startIndex}`);
|
||||
|
||||
for (const vuln of vulnerabilities) {
|
||||
await processCVE(vuln);
|
||||
}
|
||||
|
||||
startIndex += RESULTS_PER_PAGE;
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
} while (startIndex < totalResults);
|
||||
|
||||
// Move the window backward
|
||||
saveLastSyncedDate(start.toISOString());
|
||||
startFrom = start;
|
||||
} catch (err) {
|
||||
log(`❌ Error during ${humanRange}: ${err.message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (start < new Date('2002-01-01')) {
|
||||
log(`🛑 Reached earliest supported CVE publication date — halting backfill.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log('✅ CVE backfill complete!');
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
async function importCVEEnrichmentFromDB() {
|
||||
log(`🔍 Starting CVE enrichment from existing database records...`);
|
||||
|
||||
const lastProcessed = loadLastProcessedCVE();
|
||||
if (lastProcessed) {
|
||||
log(`🔁 Resuming enrichment from after ${lastProcessed}`);
|
||||
}
|
||||
|
||||
const [rows] = await DB.query(`
|
||||
SELECT id
|
||||
FROM cves
|
||||
WHERE severity_v3 IS NULL OR cvss_score_v3 IS NULL OR cvss_vector_v3 IS NULL
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
|
||||
if (rows.length === 0) {
|
||||
log(`✅ All CVEs seem enriched! Nothing to do.`);
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let startIndex = 0;
|
||||
if (lastProcessed) {
|
||||
const resumeIndex = rows.findIndex(r => r.id === lastProcessed);
|
||||
if (resumeIndex !== -1 && resumeIndex + 1 < rows.length) {
|
||||
startIndex = resumeIndex + 1; // Start *after* last processed
|
||||
}
|
||||
}
|
||||
|
||||
log(`📄 Found ${rows.length} CVEs needing enrichment, starting at index ${startIndex}.`);
|
||||
|
||||
for (let i = startIndex; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const cveId = row.id;
|
||||
|
||||
try {
|
||||
const res = await axios.get(BASE_URL, {
|
||||
params: { cveId },
|
||||
headers: API_KEY ? { apiKey: API_KEY } : {}
|
||||
});
|
||||
|
||||
const vulnerabilities = res.data.vulnerabilities || [];
|
||||
if (vulnerabilities.length > 0) {
|
||||
await processCVE(vulnerabilities[0]);
|
||||
log(`✅ Enriched ${cveId}`);
|
||||
} else {
|
||||
log(`⚠️ No data found for ${cveId}`);
|
||||
}
|
||||
|
||||
saveLastProcessedCVE(cveId); // ✨ Save after each successful CVE
|
||||
await new Promise(r => setTimeout(r, 6000)); // Sleep to respect rate limit
|
||||
} catch (err) {
|
||||
log(`❌ Failed to fetch/enrich ${cveId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
log('✅ CVE enrichment batch complete!');
|
||||
fs.unlinkSync(RESUME_FILE); // ✨ Cleanup resume file if done
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
async function importCVEEnrichmentFast() {
|
||||
log(`🚀 Starting FAST CVE enrichment by modified date...`);
|
||||
|
||||
const now = new Date();
|
||||
const earliestDate = new Date('2002-01-01T00:00:00.000Z');
|
||||
let endDateObj = new Date(); // most recent
|
||||
const STEP_DAYS = 120;
|
||||
|
||||
while (endDateObj > earliestDate) {
|
||||
const startDateObj = new Date(endDateObj);
|
||||
startDateObj.setDate(startDateObj.getDate() - STEP_DAYS);
|
||||
|
||||
const startISO = startDateObj.toISOString();
|
||||
const endISO = endDateObj.toISOString();
|
||||
const humanRange = `${formatShortDate(startISO)} to ${formatShortDate(endISO)}`;
|
||||
|
||||
log(`📅 Fetching modified CVEs from ${humanRange}`);
|
||||
|
||||
let startIndex = 0;
|
||||
let totalResults = 0;
|
||||
let processedCount = 0;
|
||||
let pageCount = 0;
|
||||
|
||||
try {
|
||||
const initial = await fetchCVEPage(0, startISO, endISO);
|
||||
totalResults = initial.totalResults || 0;
|
||||
|
||||
if (totalResults === 0) {
|
||||
log(`⚠️ No CVEs to process in this window.`);
|
||||
endDateObj = startDateObj;
|
||||
continue;
|
||||
}
|
||||
|
||||
log(`📦 Found ${totalResults} CVEs to enrich from ${humanRange}`);
|
||||
|
||||
while (startIndex < totalResults) {
|
||||
const data = startIndex === 0 ? initial : await fetchCVEPage(startIndex, startISO, endISO);
|
||||
const vulnerabilities = data.vulnerabilities || [];
|
||||
|
||||
log(`📄 Page ${++pageCount} — ${vulnerabilities.length} CVEs (Index ${startIndex})`);
|
||||
|
||||
for (const vuln of vulnerabilities) {
|
||||
await processCVE(vuln);
|
||||
processedCount++;
|
||||
if (processedCount % 100 === 0 || processedCount === totalResults) {
|
||||
const pct = ((processedCount / totalResults) * 100).toFixed(1);
|
||||
log(`📊 Progress: ${processedCount}/${totalResults} CVEs (${pct}%)`);
|
||||
}
|
||||
}
|
||||
|
||||
startIndex += RESULTS_PER_PAGE;
|
||||
await new Promise(r => setTimeout(r, 6000)); // API rate limit
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
log(`❌ Error during enrichment for ${humanRange}: ${err.message}`);
|
||||
}
|
||||
|
||||
endDateObj = startDateObj; // 🕒 step backward
|
||||
}
|
||||
|
||||
log('✅ Full enrichment pass complete!');
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
importCVEEnrichmentFast().catch((err) => {
|
||||
log(`❌ Fatal error during enrichment: ${err.message}`);
|
||||
logFile.end();
|
||||
});
|
||||
|
||||
298
scripts/fetchCVE_withMORE.js
Normal file
298
scripts/fetchCVE_withMORE.js
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
const logFile = fs.createWriteStream('cve-sync.log', {
|
||||
flags: 'a',
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
const RESUME_FILE = '.last_synced_date';
|
||||
function saveLastSyncedDate(dateStr) {
|
||||
fs.writeFileSync(RESUME_FILE, dateStr);
|
||||
}
|
||||
|
||||
function loadLastSyncedDate() {
|
||||
if (!fs.existsSync(RESUME_FILE)) return null;
|
||||
return fs.readFileSync(RESUME_FILE, 'utf8');
|
||||
}
|
||||
|
||||
|
||||
function log(msg) {
|
||||
const now = new Date();
|
||||
|
||||
// Generate the locale string with hour12 enabled (e.g. "14 Apr 2025, 08:14:42 AM")
|
||||
const raw = now.toLocaleString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
// Regex to convert only the AM/PM to lowercase
|
||||
const formatted = raw.replace(/\b(AM|PM)\b/, (match) => match.toLowerCase());
|
||||
|
||||
const timestamp = `[${formatted}]`;
|
||||
const line = `${timestamp} ${msg}`;
|
||||
console.log(line);
|
||||
logFile.write(`${line}\n`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return null;
|
||||
const date = new Date(isoString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
function formatShortDate(isoString) {
|
||||
return new Date(isoString).toLocaleDateString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).replace(/\b([A-Z])([a-z]+)\b/, (_, a, b) => a + b); // Capitalize only first letter of month
|
||||
}
|
||||
|
||||
|
||||
function extractCpeParts(cpe) {
|
||||
const parts = cpe.split(':');
|
||||
return {
|
||||
vendor: parts[3] || null,
|
||||
product: parts[4] || null,
|
||||
version: parts[5] || null
|
||||
};
|
||||
}
|
||||
|
||||
function addDaysToISO(dateISO, days) {
|
||||
const date = new Date(dateISO);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
const DB = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
|
||||
const BASE_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
|
||||
const API_KEY = process.env.NVD_API_KEY;
|
||||
const RESULTS_PER_PAGE = 2000;
|
||||
let MAX_RANGE_DAYS = Number(process.env.NVD_MAX_RANGE_DAYS || 120);
|
||||
|
||||
log(`🧪 Getting CVEs from the last ${MAX_RANGE_DAYS} of days`);
|
||||
|
||||
if (MAX_RANGE_DAYS > 120) {
|
||||
log("⚠️ MAX_RANGE_DAYS exceeds NVD API limit. Defaulting to 120.");
|
||||
MAX_RANGE_DAYS = 120;
|
||||
}
|
||||
if (isNaN(MAX_RANGE_DAYS) || MAX_RANGE_DAYS < 1) {
|
||||
log("⚠️ Invalid MAX_RANGE_DAYS. Using default of 7.");
|
||||
MAX_RANGE_DAYS = 7;
|
||||
}
|
||||
|
||||
async function fetchCVEPage(startIndex, startDate, endDate) {
|
||||
try {
|
||||
const res = await axios.get(BASE_URL, {
|
||||
params: {
|
||||
pubStartDate: startDate,
|
||||
pubEndDate: endDate,
|
||||
startIndex,
|
||||
resultsPerPage: RESULTS_PER_PAGE,
|
||||
},
|
||||
headers: API_KEY ? { apiKey: API_KEY } : {}
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
log(`❌ API error: ${err.response?.status} - ${err.response?.data?.message || err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function processCVE(cveWrapper) {
|
||||
const cve = cveWrapper.cve;
|
||||
const cveId = cve.id;
|
||||
const title = cve.titles?.find(t => t.lang === 'en')?.title || '';
|
||||
const desc = cve.descriptions.find(d => d.lang === 'en')?.value ?? '';
|
||||
const published = formatDate(cve.published);
|
||||
const modified = formatDate(cve.lastModified);
|
||||
const metric = cve.metrics?.cvssMetricV31?.[0];
|
||||
const severity = metric?.cvssData?.baseSeverity ?? null;
|
||||
const score = metric?.cvssData?.baseScore ?? null;
|
||||
const vector = metric?.cvssData?.vectorString ?? '';
|
||||
|
||||
try {
|
||||
await DB.execute(
|
||||
`INSERT INTO cves (id, title, description, published_date, last_modified_date, severity, cvss_score, cvss_vector, source)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_modified_date = VALUES(last_modified_date),
|
||||
severity = IFNULL(severity, VALUES(severity)),
|
||||
cvss_score = IFNULL(cvss_score, VALUES(cvss_score)),
|
||||
cvss_vector = IFNULL(cvss_vector, VALUES(cvss_vector)),
|
||||
title = IFNULL(title, VALUES(title)),
|
||||
source = IFNULL(source, VALUES(source))`,
|
||||
[cveId, title, desc, published, modified, severity, score, vector, 'NVD']
|
||||
);
|
||||
} catch (err) {
|
||||
log(`❌ Error inserting CVE ${cveId}: ${err.message}`);
|
||||
}
|
||||
|
||||
const configurations = cve.configurations ?? [];
|
||||
for (const node of configurations) {
|
||||
for (const match of node.nodes?.flatMap(n => n.cpeMatch ?? []) ?? []) {
|
||||
const cpe = match.criteria;
|
||||
const vulnerable = match.vulnerable ? 1 : 0;
|
||||
const start = match.versionStartIncluding || match.versionStartExcluding || null;
|
||||
const end = match.versionEndIncluding || match.versionEndExcluding || null;
|
||||
|
||||
const { vendor, product, version } = extractCpeParts(cpe);
|
||||
|
||||
try {
|
||||
await DB.execute(
|
||||
`INSERT INTO cpe_matches (cve_id, cpe_uri, version_start, version_end, vulnerable, vendor, product, version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[cveId, cpe, start, end, vulnerable, vendor, product, version]
|
||||
);
|
||||
} catch (err) {
|
||||
log(`⚠️ Error inserting CPE for CVE ${cveId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getMostRecentModifiedDateFromDB() {
|
||||
const [rows] = await DB.query(`SELECT MAX(last_modified_date) AS lastMod FROM cves`);
|
||||
const lastMod = rows[0]?.lastMod;
|
||||
return lastMod ? new Date(lastMod).toISOString() : '2020-01-01T00:00:00.000Z';
|
||||
}
|
||||
|
||||
async function importCVEFeed() {
|
||||
const now = new Date();
|
||||
const endDate = now.toISOString();
|
||||
|
||||
const startDateObj = new Date(now);
|
||||
startDateObj.setDate(startDateObj.getDate() - MAX_RANGE_DAYS);
|
||||
const startDate = startDateObj.toISOString();
|
||||
|
||||
log(`🚀 CVE sync started`);
|
||||
log(`🔄 Initializing script...`);
|
||||
log(`📍 Launching script`);
|
||||
log(`📅 Starting CVE sync from ${startDate} to ${endDate}`);
|
||||
|
||||
const humanStart = formatShortDate(startDate);
|
||||
const humanEnd = formatShortDate(endDate);
|
||||
log(`📡 Fetching modified CVEs from ${humanStart} to ${humanEnd}...`);
|
||||
|
||||
let startIndex = 0;
|
||||
let totalResults = Infinity;
|
||||
let pageCount = 0;
|
||||
|
||||
do {
|
||||
const data = await fetchCVEPage(startIndex, startDate, endDate);
|
||||
const vulnerabilities = data.vulnerabilities || [];
|
||||
totalResults = data.totalResults ?? vulnerabilities.length;
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
log(`⚠️ No CVEs returned at index ${startIndex}`);
|
||||
break;
|
||||
}
|
||||
|
||||
log(`📄 Page ${++pageCount} — Processing ${vulnerabilities.length} CVEs from index ${startIndex} of ~${totalResults}`);
|
||||
|
||||
for (const vuln of vulnerabilities) {
|
||||
await processCVE(vuln);
|
||||
}
|
||||
|
||||
startIndex += RESULTS_PER_PAGE;
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
} while (startIndex < totalResults);
|
||||
|
||||
log('✅ CVE import complete!');
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
async function importCVEFeedBackfill() {
|
||||
const now = new Date();
|
||||
const resumeFrom = loadLastSyncedDate();
|
||||
let startFrom = resumeFrom ? new Date(resumeFrom) : now;
|
||||
|
||||
const MAX_RANGE_DAYS = 120;
|
||||
|
||||
log(resumeFrom
|
||||
? `🔁 Resuming CVE backfill from ${formatShortDate(startFrom.toISOString())}`
|
||||
: `⏮️ Starting CVE backfill from today (${formatShortDate(startFrom.toISOString())})`
|
||||
);
|
||||
|
||||
while (true) {
|
||||
const end = new Date(startFrom);
|
||||
const start = new Date(startFrom);
|
||||
start.setDate(start.getDate() - MAX_RANGE_DAYS + 1); // 120-day window
|
||||
|
||||
const startISO = start.toISOString();
|
||||
const endISO = end.toISOString();
|
||||
const humanRange = `${formatShortDate(startISO)} to ${formatShortDate(endISO)}`;
|
||||
|
||||
log(`📡 Fetching published CVEs from ${humanRange}...`);
|
||||
|
||||
let startIndex = 0;
|
||||
let totalResults = Infinity;
|
||||
let pageCount = 0;
|
||||
|
||||
try {
|
||||
do {
|
||||
const data = await fetchCVEPage(startIndex, startISO, endISO);
|
||||
const vulnerabilities = data.vulnerabilities || [];
|
||||
totalResults = data.totalResults ?? vulnerabilities.length;
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
log(`⚠️ No CVEs returned for ${humanRange} at index ${startIndex}`);
|
||||
break;
|
||||
}
|
||||
|
||||
log(`📄 Page ${++pageCount} — ${vulnerabilities.length} CVEs from index ${startIndex}`);
|
||||
|
||||
for (const vuln of vulnerabilities) {
|
||||
await processCVE(vuln);
|
||||
}
|
||||
|
||||
startIndex += RESULTS_PER_PAGE;
|
||||
await new Promise((r) => setTimeout(r, 6000));
|
||||
} while (startIndex < totalResults);
|
||||
|
||||
// Move the window backward
|
||||
saveLastSyncedDate(start.toISOString());
|
||||
startFrom = start;
|
||||
} catch (err) {
|
||||
log(`❌ Error during ${humanRange}: ${err.message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (start < new Date('2002-01-01')) {
|
||||
log(`🛑 Reached earliest supported CVE publication date — halting backfill.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log('✅ CVE backfill complete!');
|
||||
await DB.end();
|
||||
logFile.end();
|
||||
}
|
||||
|
||||
|
||||
|
||||
//importCVEFeed().catch((err) => {
|
||||
importCVEFeedBackfill(9000) // ~25 years (goes back to 2000)
|
||||
.catch((err) => {
|
||||
log(`❌ Fatal error during import: ${err.message}`);
|
||||
logFile.end();
|
||||
});
|
||||
79
scripts/fetchKEV.js
Normal file
79
scripts/fetchKEV.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// fetchKEV.js
|
||||
import fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
const KEV_URL = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
|
||||
|
||||
const DB = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
fs.writeFileSync('kev-sync.log', '', { flag: 'w' }); // 👈 Reset the log file cleanly
|
||||
|
||||
function log(msg) {
|
||||
const now = new Date().toLocaleString('en-AU', {
|
||||
day: '2-digit', month: 'short', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
}).replace(/\b(AM|PM)\b/, m => m.toLowerCase());
|
||||
|
||||
const line = `[${now}] ${msg}`;
|
||||
console.log(line);
|
||||
fs.appendFileSync('kev-sync.log', `${line}\n`);
|
||||
}
|
||||
|
||||
function safeDate(input) {
|
||||
return input ? new Date(input).toISOString().slice(0, 10) : null;
|
||||
}
|
||||
|
||||
async function fetchAndInsertKEVs() {
|
||||
log('🚀 Starting KEV fetch from CISA...');
|
||||
|
||||
const { data } = await axios.get(KEV_URL);
|
||||
const vulns = data.vulnerabilities;
|
||||
|
||||
let inserted = 0;
|
||||
|
||||
for (const v of vulns) {
|
||||
try {
|
||||
await DB.execute(`
|
||||
INSERT INTO kev_catalog (
|
||||
cve_id, vendor_project, product, vulnerability_name,
|
||||
date_added, short_description, required_action, due_date,
|
||||
known_ransomware_campaign_use, notes
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
date_added = VALUES(date_added),
|
||||
short_description = VALUES(short_description),
|
||||
required_action = VALUES(required_action),
|
||||
due_date = VALUES(due_date),
|
||||
known_ransomware_campaign_use = VALUES(known_ransomware_campaign_use),
|
||||
notes = VALUES(notes)
|
||||
`, [
|
||||
v.cveID,
|
||||
v.vendorProject,
|
||||
v.product,
|
||||
v.vulnerabilityName,
|
||||
safeDate(v.dateAdded),
|
||||
v.shortDescription,
|
||||
v.requiredAction,
|
||||
safeDate(v.dueDate),
|
||||
v.knownRansomwareCampaignUse ?? null,
|
||||
v.notes ?? null
|
||||
]);
|
||||
inserted++;
|
||||
} catch (err) {
|
||||
log(`❌ Failed to insert ${v.cveID}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
log(`✅ Finished KEV sync. Inserted/updated: ${inserted}`);
|
||||
await DB.end();
|
||||
}
|
||||
|
||||
fetchAndInsertKEVs().catch(err => {
|
||||
log(`❌ Uncaught error: ${err.message}`);
|
||||
DB.end();
|
||||
});
|
||||
193
scripts/fetchMSRC.js
Normal file
193
scripts/fetchMSRC.js
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import axios from 'axios';
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
import readline from 'readline';
|
||||
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
const DB = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
|
||||
function formatDate(isoString) {
|
||||
if (!isoString) return null;
|
||||
const date = new Date(isoString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
function getPreviousMonthID() {
|
||||
const now = new Date();
|
||||
now.setMonth(now.getMonth() - 1);
|
||||
const year = now.getFullYear();
|
||||
const month = now.toLocaleString('en-US', { month: 'short' }); // e.g. "Mar"
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
|
||||
function getLocalizedText(entry, fallback = '') {
|
||||
if (!entry || typeof entry !== 'object') return fallback;
|
||||
|
||||
// Try to get English (US) first
|
||||
if (entry['en-US']?.Value) return entry['en-US'].Value;
|
||||
|
||||
// Fallback: get first entry with a Value field
|
||||
for (const key in entry) {
|
||||
if (entry[key]?.Value) {
|
||||
return entry[key].Value;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
|
||||
function extractDescription(notes = [], fallback = '') {
|
||||
const note = notes.find(n =>
|
||||
n.Title === 'Description' &&
|
||||
n.Type === 2 &&
|
||||
typeof n.Value === 'string' &&
|
||||
n.Value.trim().length > 0
|
||||
);
|
||||
return note?.Value || fallback;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function extractAffectedProducts(vuln) {
|
||||
const fixed = vuln.ProductStatuses?.Fixed ?? [];
|
||||
const known = vuln.ProductStatuses?.KnownAffected ?? [];
|
||||
const underInvestigation = vuln.ProductStatuses?.UnderInvestigation ?? [];
|
||||
return [...new Set([...fixed, ...known, ...underInvestigation])].join(', ');
|
||||
}
|
||||
|
||||
async function fetchCVRFDoc(cvrfId) {
|
||||
const url = `https://api.msrc.microsoft.com/cvrf/v3.0/cvrf/${cvrfId}`;
|
||||
try {
|
||||
const res = await axios.get(url, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to fetch CVRF for ${cvrfId}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function storeVulnerability(v, published, modified) {
|
||||
const cve = v.CVE;
|
||||
console.log(`⏳ Saving ${cve}...`);
|
||||
const title = getLocalizedText(v.Title, '');
|
||||
const description = extractDescription(v.Notes, title);
|
||||
const affectedProducts = extractAffectedProducts(v);
|
||||
const severity = v.Threats?.find(t => t.Type === 'Severity')?.Description?.Value || '';
|
||||
const impact = v.Threats?.find(t => t.Type === 'Impact')?.Description?.Value || '';
|
||||
const exploitability = v.Threats?.find(t => t.Type === 'Exploitability')?.Description?.Value || '';
|
||||
const cvssSet = v.CVSSScoreSets?.[0];
|
||||
const cvssScore = cvssSet?.BaseScore || null;
|
||||
const cvssVector = cvssSet?.Vector || '';
|
||||
const cweList = Array.isArray(v.CWE) ? v.CWE : [];
|
||||
|
||||
if (!title) {
|
||||
console.warn(`⚠️ No title found for ${cve}`);
|
||||
console.debug('Title raw:', JSON.stringify(v.Title, null, 2));
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
console.warn(`⚠️ No description for ${cve}`);
|
||||
console.debug('Notes raw:', JSON.stringify(v.Notes, null, 2));
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Insert or update the main CVE entry (no cwe column now)
|
||||
await DB.execute(
|
||||
`INSERT INTO microsoft_cves (cve_id, title, severity, published_date, last_modified_date,
|
||||
affected_products, description, exploitability, cvss_score, cvss_vector)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE last_modified_date = VALUES(last_modified_date),
|
||||
affected_products = VALUES(affected_products),
|
||||
severity = VALUES(severity),
|
||||
description = VALUES(description),
|
||||
exploitability = VALUES(exploitability),
|
||||
cvss_score = VALUES(cvss_score),
|
||||
cvss_vector = VALUES(cvss_vector)`,
|
||||
[cve, title, severity, published, modified, affectedProducts, description, exploitability, cvssScore, cvssVector]
|
||||
);
|
||||
|
||||
// Insert CWE entries and links
|
||||
for (const { ID: cweId, Value: cweName } of cweList) {
|
||||
if (!cweId) continue;
|
||||
|
||||
// Insert into cwe_entries if not already present
|
||||
await DB.execute(
|
||||
`INSERT IGNORE INTO cwe_entries (cwe_id, cwe_name) VALUES (?, ?)`,
|
||||
[cweId, cweName || null]
|
||||
);
|
||||
|
||||
// Link CVE to CWE
|
||||
await DB.execute(
|
||||
`INSERT IGNORE INTO cve_cwe (cve_id, cwe_id) VALUES (?, ?)`,
|
||||
[cve, cweId]
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✅ Saved ${cve} — ${severity}, ${cvssScore ?? '?'}`);
|
||||
} catch (err) {
|
||||
console.error(`❌ DB error for ${cve}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function waitForKeypress() {
|
||||
return new Promise((resolve) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question('🔄 Press any key to continue to the next 10 CVEs...', () => {
|
||||
rl.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function importPreviousMonth() {
|
||||
const cvrfId = getPreviousMonthID();
|
||||
console.log(`📡 Fetching CVEs for ${cvrfId}...`);
|
||||
|
||||
const data = await fetchCVRFDoc(cvrfId);
|
||||
if (!data) {
|
||||
console.log("❌ No data returned from MSRC.");
|
||||
return;
|
||||
}
|
||||
|
||||
const published = formatDate(data.DocumentTracking?.InitialReleaseDate);
|
||||
const modified = formatDate(data.DocumentTracking?.CurrentReleaseDate);
|
||||
|
||||
const vulns = data.Vulnerability || [];
|
||||
console.log(`📋 Found ${vulns.length} vulnerabilities to import.`);
|
||||
|
||||
const batchSize = 10;
|
||||
|
||||
for (let i = 0; i < vulns.length; i += batchSize) {
|
||||
const batch = vulns.slice(i, i + batchSize);
|
||||
console.log(`🔄 Processing batch ${i / batchSize + 1} (${batch.length} items)...`);
|
||||
|
||||
for (const v of batch) {
|
||||
await storeVulnerability(v, published, modified);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Finished importing ${vulns.length} CVEs for ${cvrfId}`);
|
||||
await DB.end();
|
||||
}
|
||||
console.log("🚀 Starting MSRC sync now...");
|
||||
await importPreviousMonth();
|
||||
|
||||
5
scripts/kev-sync.log
Normal file
5
scripts/kev-sync.log
Normal file
@@ -0,0 +1,5 @@
|
||||
[29 Apr 2025, 11:56:08 am] 🚀 Starting KEV fetch from CISA...
|
||||
[29 Apr 2025, 11:56:08 am] 🚀 Starting KEV fetch from CISA...
|
||||
[29 Apr 2025, 11:56:08 am] ✅ Finished KEV sync. Inserted/updated: 1326
|
||||
[29 Apr 2025, 11:56:08 am] ✅ Finished KEV sync. Inserted/updated: 1326
|
||||
[29 Apr 2025, 11:56:08 am] ✅ fetchKEV.js finished with exit code: 0
|
||||
6978
scripts/msrc-sync.log
Normal file
6978
scripts/msrc-sync.log
Normal file
File diff suppressed because it is too large
Load Diff
13
scripts/runSpringSync_Manual.js
Normal file
13
scripts/runSpringSync_Manual.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// runSpringSync_Manual.js
|
||||
import axios from 'axios';
|
||||
|
||||
async function triggerSpringSync() {
|
||||
try {
|
||||
const res = await axios.post('https://sys.psg.net.au:8443/api/admin/scripts/run-scheduled-sync');
|
||||
console.log(res.data);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to trigger:", err.response?.data || err.message);
|
||||
}
|
||||
}
|
||||
|
||||
triggerSpringSync();
|
||||
Reference in New Issue
Block a user