408 lines
13 KiB
JavaScript
408 lines
13 KiB
JavaScript
// CVE Management Backend API
|
|
// Install: npm install express sqlite3 multer cors dotenv bcryptjs cookie-parser
|
|
|
|
require('dotenv').config();
|
|
|
|
const express = require('express');
|
|
const sqlite3 = require('sqlite3').verbose();
|
|
const multer = require('multer');
|
|
const cors = require('cors');
|
|
const cookieParser = require('cookie-parser');
|
|
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');
|
|
|
|
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'];
|
|
|
|
// Log all incoming requests
|
|
app.use((req, res, next) => {
|
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
|
next();
|
|
});
|
|
|
|
// Middleware
|
|
app.use(cors({
|
|
origin: CORS_ORIGINS,
|
|
credentials: true
|
|
}));
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
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)
|
|
app.use('/api/auth', createAuthRouter(db));
|
|
|
|
// User management routes (admin only)
|
|
app.use('/api/users', createUsersRouter(db, requireAuth, requireRole));
|
|
|
|
// 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) => {
|
|
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);
|
|
});
|
|
});
|
|
|
|
// Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR (authenticated users)
|
|
app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => {
|
|
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
|
|
WHERE c.cve_id = ?
|
|
GROUP BY c.id
|
|
`;
|
|
|
|
db.all(query, [cveId], (err, rows) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
if (!rows || rows.length === 0) {
|
|
return res.json({
|
|
exists: false,
|
|
message: 'CVE not found - not yet addressed'
|
|
});
|
|
}
|
|
|
|
// Return all vendor entries for this CVE
|
|
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
|
|
}
|
|
})),
|
|
addressed: true,
|
|
has_required_docs: rows.some(row => row.has_advisory > 0)
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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('=======================');
|
|
|
|
const { cve_id, vendor, severity, description, published_date } = req.body;
|
|
|
|
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]);
|
|
|
|
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.'
|
|
});
|
|
}
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
res.json({
|
|
id: this.lastID,
|
|
cve_id,
|
|
message: `CVE created successfully for vendor: ${vendor}`
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
// Update CVE status (editor or admin)
|
|
app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => {
|
|
const { cveId } = req.params;
|
|
const { status } = req.body;
|
|
|
|
const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`;
|
|
|
|
db.run(query, [
|
|
vendor,status, cveId], function(err) {
|
|
if (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
res.json({ message: 'Status updated successfully', changes: this.changes });
|
|
});
|
|
});
|
|
|
|
// ========== DOCUMENT ENDPOINTS ==========
|
|
|
|
// Get documents for a CVE - FILTER BY VENDOR (authenticated users)
|
|
app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => {
|
|
const { cveId } = req.params;
|
|
const { vendor } = req.query; // NEW: Optional vendor filter
|
|
|
|
let query = `SELECT * FROM documents WHERE cve_id = ?`;
|
|
let params = [cveId];
|
|
|
|
if (vendor) {
|
|
query += ` AND vendor = ?`;
|
|
params.push(vendor);
|
|
}
|
|
|
|
query += ` ORDER BY uploaded_at DESC`;
|
|
|
|
db.all(query, params, (err, rows) => {
|
|
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) => {
|
|
if (err) {
|
|
console.error('MULTER ERROR:', err);
|
|
return res.status(500).json({ error: 'File upload failed: ' + err.message });
|
|
}
|
|
|
|
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 });
|
|
}
|
|
res.json({
|
|
id: this.lastID,
|
|
message: 'Document uploaded successfully',
|
|
file: {
|
|
name: file.originalname,
|
|
path: finalPath,
|
|
size: fileSizeKB
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
// Delete document (admin only)
|
|
app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => {
|
|
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 });
|
|
}
|
|
res.json({ message: 'Document deleted successfully' });
|
|
});
|
|
});
|
|
});
|
|
|
|
// ========== UTILITY ENDPOINTS ==========
|
|
|
|
// Get all vendors (authenticated users)
|
|
app.get('/api/vendors', requireAuth(db), (req, res) => {
|
|
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) => {
|
|
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(', ')}`);
|
|
});
|