Files
cve-dashboard/backend/server.js

745 lines
27 KiB
JavaScript
Raw Normal View History

2026-01-27 04:06:03 +00:00
// CVE Management Backend API
// Install: npm install express sqlite3 multer cors dotenv bcryptjs cookie-parser
require('dotenv').config();
2026-01-27 04:06:03 +00:00
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const multer = require('multer');
const cors = require('cors');
const cookieParser = require('cookie-parser');
2026-01-27 04:06:03 +00:00
const path = require('path');
const fs = require('fs');
// Auth imports
const { requireAuth, requireRole } = require('./middleware/auth');
const createAuthRouter = require('./routes/auth');
const createUsersRouter = require('./routes/users');
2026-01-29 15:10:29 -07:00
const createAuditLogRouter = require('./routes/auditLog');
const logAudit = require('./helpers/auditLog');
const createNvdLookupRouter = require('./routes/nvdLookup');
2026-01-27 04:06:03 +00:00
const app = express();
const PORT = process.env.PORT || 3001;
const API_HOST = process.env.API_HOST || 'localhost';
const SESSION_SECRET = process.env.SESSION_SECRET || 'default-secret-change-me';
const CORS_ORIGINS = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',')
: ['http://localhost:3000'];
2026-01-27 04:06:03 +00:00
// Log all incoming requests
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
next();
});
2026-01-27 04:06:03 +00:00
// Middleware
app.use(cors({
origin: CORS_ORIGINS,
2026-01-27 04:06:03 +00:00
credentials: true
}));
app.use(express.json());
app.use(cookieParser());
2026-01-27 04:06:03 +00:00
app.use('/uploads', express.static('uploads'));
// Database connection
const db = new sqlite3.Database('./cve_database.db', (err) => {
if (err) console.error('Database connection error:', err);
else console.log('Connected to CVE database');
});
// Auth routes (public)
2026-01-29 15:10:29 -07:00
app.use('/api/auth', createAuthRouter(db, logAudit));
// User management routes (admin only)
2026-01-29 15:10:29 -07:00
app.use('/api/users', createUsersRouter(db, requireAuth, requireRole, logAudit));
// Audit log routes (admin only)
app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireRole));
// NVD lookup routes (authenticated users)
app.use('/api/nvd', createNvdLookupRouter(db, requireAuth));
2026-01-27 04:06:03 +00:00
// Simple storage - upload to temp directory first
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const tempDir = 'uploads/temp';
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
cb(null, tempDir);
},
filename: (req, file, cb) => {
const timestamp = Date.now();
cb(null, `${timestamp}-${file.originalname}`);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
});
// ========== CVE ENDPOINTS ==========
// Get all CVEs with optional filters (authenticated users)
app.get('/api/cves', requireAuth(db), (req, res) => {
2026-01-27 04:06:03 +00:00
const { search, vendor, severity, status } = req.query;
let query = `
SELECT c.*,
COUNT(d.id) as document_count,
CASE
WHEN COUNT(CASE WHEN d.type = 'advisory' THEN 1 END) > 0
THEN 'Complete'
ELSE 'Incomplete'
END as doc_status
FROM cves c
LEFT JOIN documents d ON c.cve_id = d.cve_id
WHERE 1=1
`;
const params = [];
if (search) {
query += ` AND (c.cve_id LIKE ? OR c.description LIKE ?)`;
params.push(`%${search}%`, `%${search}%`);
}
if (vendor && vendor !== 'All Vendors') {
query += ` AND c.vendor = ?`;
params.push(vendor);
}
if (severity && severity !== 'All Severities') {
query += ` AND c.severity = ?`;
params.push(severity);
}
if (status) {
query += ` AND c.status = ?`;
params.push(status);
}
query += ` GROUP BY c.id ORDER BY c.published_date DESC`;
db.all(query, params, (err, rows) => {
if (err) {
console.error('Error fetching CVEs:', err);
return res.status(500).json({ error: err.message });
}
res.json(rows);
});
});
// Get distinct CVE IDs for NVD sync (authenticated users)
app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => {
db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows.map(r => r.cve_id));
});
});
// Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR (authenticated users)
app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => {
2026-01-27 04:06:03 +00:00
const { cveId } = req.params;
const query = `
SELECT c.*,
COUNT(d.id) as total_documents,
COUNT(CASE WHEN d.type = 'advisory' THEN 1 END) as has_advisory,
COUNT(CASE WHEN d.type = 'email' THEN 1 END) as has_email,
COUNT(CASE WHEN d.type = 'screenshot' THEN 1 END) as has_screenshot
FROM cves c
LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor
2026-01-27 04:06:03 +00:00
WHERE c.cve_id = ?
GROUP BY c.id
`;
db.all(query, [cveId], (err, rows) => {
2026-01-27 04:06:03 +00:00
if (err) {
return res.status(500).json({ error: err.message });
}
if (!rows || rows.length === 0) {
2026-01-27 04:06:03 +00:00
return res.json({
exists: false,
message: 'CVE not found - not yet addressed'
});
}
// Return all vendor entries for this CVE
2026-01-27 04:06:03 +00:00
res.json({
exists: true,
vendors: rows.map(row => ({
vendor: row.vendor,
severity: row.severity,
status: row.status,
total_documents: row.total_documents,
compliance: {
advisory: row.has_advisory > 0,
email: row.has_email > 0,
screenshot: row.has_screenshot > 0
}
})),
2026-01-27 04:06:03 +00:00
addressed: true,
has_required_docs: rows.some(row => row.has_advisory > 0)
2026-01-27 04:06:03 +00:00
});
});
});
// NEW ENDPOINT: Get all vendors for a specific CVE (authenticated users)
app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => {
const { cveId } = req.params;
const query = `
SELECT vendor, severity, status, description, published_date
FROM cves
WHERE cve_id = ?
ORDER BY vendor
`;
db.all(query, [cveId], (err, rows) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json(rows);
});
});
// Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin)
app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
console.log('=== ADD CVE REQUEST ===');
console.log('Body:', req.body);
console.log('=======================');
2026-01-27 04:06:03 +00:00
const { cve_id, vendor, severity, description, published_date } = req.body;
2026-01-27 04:06:03 +00:00
const query = `
INSERT INTO cves (cve_id, vendor, severity, description, published_date)
VALUES (?, ?, ?, ?, ?)
`;
console.log('Query:', query);
console.log('Values:', [cve_id, vendor, severity, description, published_date]);
2026-01-27 04:06:03 +00:00
db.run(query, [cve_id, vendor, severity, description, published_date], function(err) {
if (err) {
console.error('DATABASE ERROR:', err); // Make sure this is here
// ... rest of error handling
// Check if it's a duplicate CVE_ID + Vendor combination
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(409).json({
error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.'
});
}
2026-01-27 04:06:03 +00:00
return res.status(500).json({ error: err.message });
}
2026-01-29 15:10:29 -07:00
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_create',
entityType: 'cve',
entityId: cve_id,
details: { vendor, severity },
ipAddress: req.ip
});
res.json({
id: this.lastID,
2026-01-27 04:06:03 +00:00
cve_id,
2026-01-29 15:10:29 -07:00
message: `CVE created successfully for vendor: ${vendor}`
2026-01-27 04:06:03 +00:00
});
});
});
// Update CVE status (editor or admin)
app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
2026-01-27 04:06:03 +00:00
const { cveId } = req.params;
const { status } = req.body;
const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`;
2026-01-29 15:10:29 -07:00
db.run(query, [status, cveId], function(err) {
2026-01-27 04:06:03 +00:00
if (err) {
return res.status(500).json({ error: err.message });
}
2026-01-29 15:10:29 -07:00
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_update_status',
entityType: 'cve',
entityId: cveId,
details: { status },
ipAddress: req.ip
});
2026-01-27 04:06:03 +00:00
res.json({ message: 'Status updated successfully', changes: this.changes });
});
});
// Bulk sync CVE data from NVD (editor or admin)
app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
const { updates } = req.body;
if (!Array.isArray(updates) || updates.length === 0) {
return res.status(400).json({ error: 'No updates provided' });
}
let updated = 0;
const errors = [];
let completed = 0;
db.serialize(() => {
updates.forEach((entry) => {
const fields = [];
const values = [];
if (entry.description !== null && entry.description !== undefined) {
fields.push('description = ?');
values.push(entry.description);
}
if (entry.severity !== null && entry.severity !== undefined) {
fields.push('severity = ?');
values.push(entry.severity);
}
if (entry.published_date !== null && entry.published_date !== undefined) {
fields.push('published_date = ?');
values.push(entry.published_date);
}
if (fields.length === 0) {
completed++;
if (completed === updates.length) sendResponse();
return;
}
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(entry.cve_id);
db.run(
`UPDATE cves SET ${fields.join(', ')} WHERE cve_id = ?`,
values,
function(err) {
if (err) {
errors.push({ cve_id: entry.cve_id, error: err.message });
} else {
updated += this.changes;
}
completed++;
if (completed === updates.length) sendResponse();
}
);
});
});
function sendResponse() {
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_nvd_sync',
entityType: 'cve',
entityId: null,
details: { count: updated, cve_ids: updates.map(u => u.cve_id) },
ipAddress: req.ip
});
const result = { message: 'NVD sync completed', updated };
if (errors.length > 0) result.errors = errors;
res.json(result);
}
});
// ========== CVE EDIT & DELETE ENDPOINTS ==========
// Edit single CVE entry (editor or admin)
app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
const { id } = req.params;
const { cve_id, vendor, severity, description, published_date, status } = req.body;
// Fetch existing row first
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, existing) => {
if (err) return res.status(500).json({ error: err.message });
if (!existing) return res.status(404).json({ error: 'CVE entry not found' });
const before = { cve_id: existing.cve_id, vendor: existing.vendor, severity: existing.severity, description: existing.description, published_date: existing.published_date, status: existing.status };
const newCveId = cve_id !== undefined ? cve_id : existing.cve_id;
const newVendor = vendor !== undefined ? vendor : existing.vendor;
const cveIdChanged = newCveId !== existing.cve_id;
const vendorChanged = newVendor !== existing.vendor;
const doUpdate = () => {
// Build dynamic SET clause
const fields = [];
const values = [];
if (cve_id !== undefined) { fields.push('cve_id = ?'); values.push(cve_id); }
if (vendor !== undefined) { fields.push('vendor = ?'); values.push(vendor); }
if (severity !== undefined) { fields.push('severity = ?'); values.push(severity); }
if (description !== undefined) { fields.push('description = ?'); values.push(description); }
if (published_date !== undefined) { fields.push('published_date = ?'); values.push(published_date); }
if (status !== undefined) { fields.push('status = ?'); values.push(status); }
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
db.run(`UPDATE cves SET ${fields.join(', ')} WHERE id = ?`, values, function(updateErr) {
if (updateErr) return res.status(500).json({ error: updateErr.message });
const after = {
cve_id: newCveId, vendor: newVendor,
severity: severity !== undefined ? severity : existing.severity,
description: description !== undefined ? description : existing.description,
published_date: published_date !== undefined ? published_date : existing.published_date,
status: status !== undefined ? status : existing.status
};
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_edit',
entityType: 'cve',
entityId: newCveId,
details: { before, after },
ipAddress: req.ip
});
res.json({ message: 'CVE updated successfully', changes: this.changes });
});
};
if (cveIdChanged || vendorChanged) {
// Check UNIQUE constraint
db.get('SELECT id FROM cves WHERE cve_id = ? AND vendor = ? AND id != ?', [newCveId, newVendor, id], (checkErr, conflict) => {
if (checkErr) return res.status(500).json({ error: checkErr.message });
if (conflict) return res.status(409).json({ error: 'A CVE entry with this CVE ID and vendor already exists.' });
// Rename document directory
const oldDir = path.join('uploads', existing.cve_id, existing.vendor);
const newDir = path.join('uploads', newCveId, newVendor);
if (fs.existsSync(oldDir)) {
const newParent = path.join('uploads', newCveId);
if (!fs.existsSync(newParent)) {
fs.mkdirSync(newParent, { recursive: true });
}
fs.renameSync(oldDir, newDir);
// Clean up old cve_id directory if empty
const oldParent = path.join('uploads', existing.cve_id);
if (fs.existsSync(oldParent)) {
const remaining = fs.readdirSync(oldParent);
if (remaining.length === 0) fs.rmdirSync(oldParent);
}
}
// Update documents table - file paths
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [existing.cve_id, existing.vendor], (docErr, docs) => {
if (docErr) return res.status(500).json({ error: docErr.message });
const oldPrefix = path.join('uploads', existing.cve_id, existing.vendor);
const newPrefix = path.join('uploads', newCveId, newVendor);
let docUpdated = 0;
const totalDocs = docs.length;
const finishDocUpdate = () => {
if (docUpdated >= totalDocs) doUpdate();
};
if (totalDocs === 0) {
doUpdate();
} else {
docs.forEach((doc) => {
const newFilePath = doc.file_path.replace(oldPrefix, newPrefix);
db.run('UPDATE documents SET cve_id = ?, vendor = ?, file_path = ? WHERE id = ?',
[newCveId, newVendor, newFilePath, doc.id],
(docUpdateErr) => {
if (docUpdateErr) console.error('Error updating document:', docUpdateErr);
docUpdated++;
finishDocUpdate();
}
);
});
}
});
});
} else {
doUpdate();
}
});
});
// Delete entire CVE - all vendors (editor or admin) - MUST be before /:id route
app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
const { cveId } = req.params;
// Get all rows for this CVE ID to know what we're deleting
db.all('SELECT * FROM cves WHERE cve_id = ?', [cveId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' });
// Delete all documents from DB
db.run('DELETE FROM documents WHERE cve_id = ?', [cveId], (docErr) => {
if (docErr) console.error('Error deleting documents:', docErr);
// Delete all CVE rows
db.run('DELETE FROM cves WHERE cve_id = ?', [cveId], function(cveErr) {
if (cveErr) return res.status(500).json({ error: cveErr.message });
// Remove upload directory
const cveDir = path.join('uploads', cveId);
if (fs.existsSync(cveDir)) {
fs.rmSync(cveDir, { recursive: true, force: true });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_delete',
entityType: 'cve',
entityId: cveId,
details: { type: 'all_vendors', vendors: rows.map(r => r.vendor), count: rows.length },
ipAddress: req.ip
});
res.json({ message: `Deleted CVE ${cveId} and all ${rows.length} vendor entries`, deleted: this.changes });
});
});
});
});
// Delete single CVE vendor entry (editor or admin)
app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
const { id } = req.params;
db.get('SELECT * FROM cves WHERE id = ?', [id], (err, cve) => {
if (err) return res.status(500).json({ error: err.message });
if (!cve) return res.status(404).json({ error: 'CVE entry not found' });
// Delete associated documents from DB
db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (docErr, docs) => {
if (docErr) console.error('Error fetching documents:', docErr);
// Delete document files from disk
if (docs && docs.length > 0) {
docs.forEach(doc => {
if (doc.file_path && fs.existsSync(doc.file_path)) {
fs.unlinkSync(doc.file_path);
}
});
}
// Delete documents from DB
db.run('DELETE FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (delDocErr) => {
if (delDocErr) console.error('Error deleting documents from DB:', delDocErr);
// Delete CVE row
db.run('DELETE FROM cves WHERE id = ?', [id], function(delErr) {
if (delErr) return res.status(500).json({ error: delErr.message });
// Clean up directories
const vendorDir = path.join('uploads', cve.cve_id, cve.vendor);
if (fs.existsSync(vendorDir)) {
fs.rmSync(vendorDir, { recursive: true, force: true });
}
const cveDir = path.join('uploads', cve.cve_id);
if (fs.existsSync(cveDir)) {
const remaining = fs.readdirSync(cveDir);
if (remaining.length === 0) fs.rmdirSync(cveDir);
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_delete',
entityType: 'cve',
entityId: cve.cve_id,
details: { type: 'single_vendor', vendor: cve.vendor, severity: cve.severity },
ipAddress: req.ip
});
res.json({ message: `Deleted ${cve.vendor} entry for ${cve.cve_id}` });
});
});
});
});
});
2026-01-27 04:06:03 +00:00
// ========== DOCUMENT ENDPOINTS ==========
// Get documents for a CVE - FILTER BY VENDOR (authenticated users)
app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => {
2026-01-27 04:06:03 +00:00
const { cveId } = req.params;
const { vendor } = req.query; // NEW: Optional vendor filter
2026-01-27 04:06:03 +00:00
let query = `SELECT * FROM documents WHERE cve_id = ?`;
let params = [cveId];
2026-01-27 04:06:03 +00:00
if (vendor) {
query += ` AND vendor = ?`;
params.push(vendor);
}
query += ` ORDER BY uploaded_at DESC`;
db.all(query, params, (err, rows) => {
2026-01-27 04:06:03 +00:00
if (err) {
return res.status(500).json({ error: err.message });
}
res.json(rows);
});
});
// Upload document - ADD ERROR HANDLING FOR MULTER (editor or admin)
app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => {
upload.single('file')(req, res, (err) => {
2026-01-27 04:06:03 +00:00
if (err) {
console.error('MULTER ERROR:', err);
return res.status(500).json({ error: 'File upload failed: ' + err.message });
2026-01-27 04:06:03 +00:00
}
console.log('=== UPLOAD REQUEST RECEIVED ===');
console.log('CVE ID:', req.params.cveId);
console.log('Body:', req.body);
console.log('File:', req.file);
console.log('================================');
const { cveId } = req.params;
const { type, notes, vendor } = req.body;
const file = req.file;
if (!file) {
console.error('ERROR: No file uploaded');
return res.status(400).json({ error: 'No file uploaded' });
}
if (!vendor) {
console.error('ERROR: Vendor is required');
return res.status(400).json({ error: 'Vendor is required' });
}
// Move file from temp to proper location
const finalDir = path.join('uploads', cveId, vendor);
if (!fs.existsSync(finalDir)) {
fs.mkdirSync(finalDir, { recursive: true });
}
const finalPath = path.join(finalDir, file.filename);
// Move file from temp to final location
fs.renameSync(file.path, finalPath);
const query = `
INSERT INTO documents (cve_id, vendor, name, type, file_path, file_size, mime_type, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
const fileSizeKB = (file.size / 1024).toFixed(2) + ' KB';
db.run(query, [
cveId,
vendor,
file.originalname,
type,
finalPath,
fileSizeKB,
file.mimetype,
notes
], function(err) {
if (err) {
console.error('DATABASE ERROR:', err);
// If database insert fails, delete the file
if (fs.existsSync(finalPath)) {
fs.unlinkSync(finalPath);
}
return res.status(500).json({ error: err.message });
2026-01-27 04:06:03 +00:00
}
2026-01-29 15:10:29 -07:00
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'document_upload',
entityType: 'document',
entityId: cveId,
details: { vendor, type, filename: file.originalname },
ipAddress: req.ip
});
res.json({
id: this.lastID,
message: 'Document uploaded successfully',
file: {
name: file.originalname,
path: finalPath,
size: fileSizeKB
}
});
2026-01-27 04:06:03 +00:00
});
});
});
// Delete document (admin only)
app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => {
2026-01-27 04:06:03 +00:00
const { id } = req.params;
// First get the file path to delete the actual file
db.get('SELECT file_path FROM documents WHERE id = ?', [id], (err, row) => {
if (err) {
return res.status(500).json({ error: err.message });
}
if (row && fs.existsSync(row.file_path)) {
fs.unlinkSync(row.file_path);
}
db.run('DELETE FROM documents WHERE id = ?', [id], function(err) {
if (err) {
return res.status(500).json({ error: err.message });
}
2026-01-29 15:10:29 -07:00
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'document_delete',
entityType: 'document',
entityId: id,
details: { file_path: row ? row.file_path : null },
ipAddress: req.ip
});
2026-01-27 04:06:03 +00:00
res.json({ message: 'Document deleted successfully' });
});
});
});
// ========== UTILITY ENDPOINTS ==========
// Get all vendors (authenticated users)
app.get('/api/vendors', requireAuth(db), (req, res) => {
2026-01-27 04:06:03 +00:00
const query = `SELECT DISTINCT vendor FROM cves ORDER BY vendor`;
db.all(query, [], (err, rows) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json(rows.map(r => r.vendor));
});
});
// Get statistics (authenticated users)
app.get('/api/stats', requireAuth(db), (req, res) => {
2026-01-27 04:06:03 +00:00
const query = `
SELECT
COUNT(DISTINCT c.id) as total_cves,
COUNT(DISTINCT CASE WHEN c.severity = 'Critical' THEN c.id END) as critical_count,
COUNT(DISTINCT CASE WHEN c.status = 'Addressed' THEN c.id END) as addressed_count,
COUNT(DISTINCT d.id) as total_documents,
COUNT(DISTINCT CASE WHEN cd.compliance_status = 'Complete' THEN c.id END) as compliant_count
FROM cves c
LEFT JOIN documents d ON c.cve_id = d.cve_id
LEFT JOIN cve_document_status cd ON c.cve_id = cd.cve_id
`;
db.get(query, [], (err, row) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json(row);
});
});
// Start server
app.listen(PORT, () => {
console.log(`CVE API server running on http://${API_HOST}:${PORT}`);
console.log(`CORS origins: ${CORS_ORIGINS.join(', ')}`);
2026-01-27 04:06:03 +00:00
});