Merge branch 'fix/issue-1'
Merges fixed code from the issue-1 branch back into master: fixes issues with Hardcoded IP across deployments, the App.js src using .env file and multivendor support
This commit was merged in pull request #3.
This commit is contained in:
4
backend/.env.example
Normal file
4
backend/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# Backend Configuration
|
||||
PORT=3001
|
||||
API_HOST=localhost
|
||||
CORS_ORIGINS=http://localhost:3000
|
||||
128
backend/migrate_multivendor.js
Normal file
128
backend/migrate_multivendor.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const db = new sqlite3.Database('./cve_database.db');
|
||||
|
||||
console.log('🔄 Starting database migration for multi-vendor support...\n');
|
||||
|
||||
db.serialize(() => {
|
||||
// Backup existing data
|
||||
console.log('📦 Creating backup tables...');
|
||||
db.run(`CREATE TABLE IF NOT EXISTS cves_backup AS SELECT * FROM cves`, (err) => {
|
||||
if (err) console.error('Backup error:', err);
|
||||
else console.log('✓ CVEs backed up');
|
||||
});
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS documents_backup AS SELECT * FROM documents`, (err) => {
|
||||
if (err) console.error('Backup error:', err);
|
||||
else console.log('✓ Documents backed up');
|
||||
});
|
||||
|
||||
// Drop old table
|
||||
console.log('\n🗑️ Dropping old cves table...');
|
||||
db.run(`DROP TABLE IF EXISTS cves`, (err) => {
|
||||
if (err) {
|
||||
console.error('Drop error:', err);
|
||||
return;
|
||||
}
|
||||
console.log('✓ Old table dropped');
|
||||
|
||||
// Create new table with UNIQUE(cve_id, vendor) instead of UNIQUE(cve_id)
|
||||
console.log('\n🏗️ Creating new cves table with multi-vendor support...');
|
||||
db.run(`
|
||||
CREATE TABLE cves (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cve_id VARCHAR(20) NOT NULL,
|
||||
vendor VARCHAR(100) NOT NULL,
|
||||
severity VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
published_date DATE,
|
||||
status VARCHAR(50) DEFAULT 'Open',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(cve_id, vendor)
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) {
|
||||
console.error('Create error:', err);
|
||||
return;
|
||||
}
|
||||
console.log('✓ New table created with UNIQUE(cve_id, vendor)');
|
||||
|
||||
// Restore data
|
||||
console.log('\n📥 Restoring data...');
|
||||
db.run(`INSERT INTO cves SELECT * FROM cves_backup`, (err) => {
|
||||
if (err) {
|
||||
console.error('Restore error:', err);
|
||||
return;
|
||||
}
|
||||
console.log('✓ Data restored');
|
||||
|
||||
// Recreate indexes
|
||||
console.log('\n🔍 Creating indexes...');
|
||||
db.run(`CREATE INDEX idx_cve_id ON cves(cve_id)`, () => {
|
||||
console.log('✓ Index: idx_cve_id');
|
||||
});
|
||||
db.run(`CREATE INDEX idx_vendor ON cves(vendor)`, () => {
|
||||
console.log('✓ Index: idx_vendor');
|
||||
});
|
||||
db.run(`CREATE INDEX idx_severity ON cves(severity)`, () => {
|
||||
console.log('✓ Index: idx_severity');
|
||||
});
|
||||
db.run(`CREATE INDEX idx_status ON cves(status)`, () => {
|
||||
console.log('✓ Index: idx_status');
|
||||
});
|
||||
|
||||
// Update view
|
||||
console.log('\n👁️ Updating cve_document_status view...');
|
||||
db.run(`DROP VIEW IF EXISTS cve_document_status`, (err) => {
|
||||
if (err) console.error('Drop view error:', err);
|
||||
|
||||
db.run(`
|
||||
CREATE VIEW cve_document_status AS
|
||||
SELECT
|
||||
c.id as record_id,
|
||||
c.cve_id,
|
||||
c.vendor,
|
||||
c.severity,
|
||||
c.status,
|
||||
COUNT(DISTINCT d.id) as total_documents,
|
||||
COUNT(DISTINCT CASE WHEN d.type = 'advisory' THEN d.id END) as advisory_count,
|
||||
COUNT(DISTINCT CASE WHEN d.type = 'email' THEN d.id END) as email_count,
|
||||
COUNT(DISTINCT CASE WHEN d.type = 'screenshot' THEN d.id END) as screenshot_count,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT CASE WHEN d.type = 'advisory' THEN d.id END) > 0
|
||||
THEN 'Complete'
|
||||
ELSE 'Missing Required Docs'
|
||||
END as compliance_status
|
||||
FROM cves c
|
||||
LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor
|
||||
GROUP BY c.id, c.cve_id, c.vendor, c.severity, c.status
|
||||
`, (err) => {
|
||||
if (err) {
|
||||
console.error('Create view error:', err);
|
||||
} else {
|
||||
console.log('✓ View recreated');
|
||||
}
|
||||
|
||||
console.log('\n✅ Migration complete!');
|
||||
console.log('\n📊 Summary:');
|
||||
|
||||
db.get('SELECT COUNT(*) as count FROM cves', (err, row) => {
|
||||
if (!err) console.log(` Total CVE entries: ${row.count}`);
|
||||
|
||||
db.get('SELECT COUNT(DISTINCT cve_id) as count FROM cves', (err, row) => {
|
||||
if (!err) console.log(` Unique CVE IDs: ${row.count}`);
|
||||
|
||||
console.log('\n💡 Next steps:');
|
||||
console.log(' 1. Restart backend: pkill -f "node server.js" && node server.js &');
|
||||
console.log(' 2. Replace frontend/src/App.js with multi-vendor version');
|
||||
console.log(' 3. Test by adding same CVE with multiple vendors\n');
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
// CVE Management Backend API
|
||||
// Install: npm install express sqlite3 multer cors
|
||||
// Install: npm install express sqlite3 multer cors dotenv
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
@@ -9,11 +11,21 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3001;
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const API_HOST = process.env.API_HOST || 'localhost';
|
||||
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: ['http://localhost:3000', 'http://192.168.2.117:3000'],
|
||||
origin: CORS_ORIGINS,
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
@@ -94,7 +106,7 @@ app.get('/api/cves', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Check if CVE exists and get its status
|
||||
// Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR
|
||||
app.get('/api/cves/check/:cveId', (req, res) => {
|
||||
const { cveId } = req.params;
|
||||
|
||||
@@ -105,80 +117,53 @@ app.get('/api/cves/check/:cveId', (req, res) => {
|
||||
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
|
||||
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.get(query, [cveId], (err, row) => {
|
||||
db.all(query, [cveId], (err, rows) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
if (!row) {
|
||||
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,
|
||||
cve: row,
|
||||
addressed: true,
|
||||
has_required_docs: row.has_advisory > 0,
|
||||
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)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Create new CVE entry
|
||||
app.post('/api/cves', (req, res) => {
|
||||
const { cve_id, vendor, severity, description, published_date } = req.body;
|
||||
// NEW ENDPOINT: Get all vendors for a specific CVE
|
||||
app.get('/api/cves/:cveId/vendors', (req, res) => {
|
||||
const { cveId } = req.params;
|
||||
|
||||
const query = `
|
||||
INSERT INTO cves (cve_id, vendor, severity, description, published_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
SELECT vendor, severity, status, description, published_date
|
||||
FROM cves
|
||||
WHERE cve_id = ?
|
||||
ORDER BY vendor
|
||||
`;
|
||||
|
||||
db.run(query, [cve_id, vendor, severity, description, published_date], function(err) {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
res.json({
|
||||
id: this.lastID,
|
||||
cve_id,
|
||||
message: 'CVE created successfully'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Update CVE status
|
||||
app.patch('/api/cves/:cveId/status', (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, [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
|
||||
app.get('/api/cves/:cveId/documents', (req, res) => {
|
||||
const { cveId } = req.params;
|
||||
|
||||
const query = `SELECT * FROM documents WHERE cve_id = ? ORDER BY uploaded_at DESC`;
|
||||
|
||||
db.all(query, [cveId], (err, rows) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
@@ -187,17 +172,110 @@ app.get('/api/cves/:cveId/documents', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Upload document
|
||||
app.post('/api/cves/:cveId/documents', upload.single('file'), (req, res) => {
|
||||
|
||||
// Create new CVE entry - ALLOW MULTIPLE VENDORS
|
||||
app.post('/api/cves', (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
|
||||
app.patch('/api/cves/:cveId/status', (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
|
||||
app.get('/api/cves/:cveId/documents', (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
|
||||
app.post('/api/cves/:cveId/documents', (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' });
|
||||
}
|
||||
|
||||
@@ -213,14 +291,15 @@ app.post('/api/cves/:cveId/documents', upload.single('file'), (req, res) => {
|
||||
fs.renameSync(file.path, finalPath);
|
||||
|
||||
const query = `
|
||||
INSERT INTO documents (cve_id, name, type, file_path, file_size, mime_type, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
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,
|
||||
@@ -229,6 +308,7 @@ app.post('/api/cves/:cveId/documents', upload.single('file'), (req, res) => {
|
||||
notes
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.error('DATABASE ERROR:', err);
|
||||
// If database insert fails, delete the file
|
||||
if (fs.existsSync(finalPath)) {
|
||||
fs.unlinkSync(finalPath);
|
||||
@@ -245,8 +325,8 @@ app.post('/api/cves/:cveId/documents', upload.single('file'), (req, res) => {
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete document
|
||||
app.delete('/api/documents/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
@@ -308,5 +388,6 @@ app.get('/api/stats', (req, res) => {
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`CVE API server running on http://localhost:${PORT}`);
|
||||
console.log(`CVE API server running on http://${API_HOST}:${PORT}`);
|
||||
console.log(`CORS origins: ${CORS_ORIGINS.join(', ')}`);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Setup Script for CVE Database
|
||||
// This creates a fresh database ready for new CVE entries
|
||||
// This creates a fresh database with multi-vendor support built-in
|
||||
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const fs = require('fs');
|
||||
@@ -18,19 +18,21 @@ function initializeDatabase() {
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS cves (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cve_id VARCHAR(20) UNIQUE NOT NULL,
|
||||
cve_id VARCHAR(20) NOT NULL,
|
||||
vendor VARCHAR(100) NOT NULL,
|
||||
severity VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
published_date DATE,
|
||||
status VARCHAR(50) DEFAULT 'Open',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(cve_id, vendor)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cve_id VARCHAR(20) NOT NULL,
|
||||
vendor VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
@@ -54,6 +56,7 @@ function initializeDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_severity ON cves(severity);
|
||||
CREATE INDEX IF NOT EXISTS idx_status ON cves(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_cve_id ON documents(cve_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(type);
|
||||
|
||||
INSERT OR IGNORE INTO required_documents (vendor, document_type, is_mandatory, description) VALUES
|
||||
@@ -66,6 +69,7 @@ function initializeDatabase() {
|
||||
|
||||
CREATE VIEW IF NOT EXISTS cve_document_status AS
|
||||
SELECT
|
||||
c.id as record_id,
|
||||
c.cve_id,
|
||||
c.vendor,
|
||||
c.severity,
|
||||
@@ -80,8 +84,8 @@ function initializeDatabase() {
|
||||
ELSE 'Missing Required Docs'
|
||||
END as compliance_status
|
||||
FROM cves c
|
||||
LEFT JOIN documents d ON c.cve_id = d.cve_id
|
||||
GROUP BY c.cve_id, c.vendor, c.severity, c.status;
|
||||
LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor
|
||||
GROUP BY c.id, c.cve_id, c.vendor, c.severity, c.status;
|
||||
`;
|
||||
|
||||
db.exec(schema, (err) => {
|
||||
@@ -107,17 +111,21 @@ function createUploadsDirectory() {
|
||||
|
||||
// Add sample CVE data (optional - for testing)
|
||||
async function addSampleData(db) {
|
||||
console.log('\n📝 Would you like to add sample CVE data for testing? (y/n)');
|
||||
console.log('\n📝 Adding sample CVE data for testing...');
|
||||
|
||||
// For automated setup, we'll skip this. Uncomment the code below if you want samples.
|
||||
|
||||
/*
|
||||
const sampleCVEs = [
|
||||
{
|
||||
cve_id: 'CVE-2024-SAMPLE-1',
|
||||
vendor: 'Microsoft',
|
||||
severity: 'Critical',
|
||||
description: 'Sample vulnerability for testing',
|
||||
description: 'Sample remote code execution vulnerability',
|
||||
published_date: '2024-01-15'
|
||||
},
|
||||
{
|
||||
cve_id: 'CVE-2024-SAMPLE-1',
|
||||
vendor: 'Cisco',
|
||||
severity: 'High',
|
||||
description: 'Sample remote code execution vulnerability',
|
||||
published_date: '2024-01-15'
|
||||
}
|
||||
];
|
||||
@@ -131,16 +139,37 @@ async function addSampleData(db) {
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
console.log(` ✓ Added sample CVE: ${cve.cve_id}`);
|
||||
console.log(` ✓ Added sample: ${cve.cve_id} / ${cve.vendor}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
console.log('ℹ️ Skipping sample data - you can add CVEs through the API or dashboard');
|
||||
console.log('ℹ️ Sample data added - demonstrates multi-vendor support');
|
||||
}
|
||||
|
||||
// Verify database structure
|
||||
async function verifySetup(db) {
|
||||
return new Promise((resolve) => {
|
||||
db.get('SELECT sql FROM sqlite_master WHERE type="table" AND name="cves"', (err, row) => {
|
||||
if (err) {
|
||||
console.error('Warning: Could not verify setup:', err);
|
||||
} else {
|
||||
console.log('\n📋 CVEs table structure:');
|
||||
console.log(row.sql);
|
||||
|
||||
// Check if UNIQUE constraint is correct
|
||||
if (row.sql.includes('UNIQUE(cve_id, vendor)')) {
|
||||
console.log('\n✅ Multi-vendor support: ENABLED');
|
||||
} else {
|
||||
console.log('\n⚠️ Warning: Multi-vendor constraint may not be set correctly');
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Display setup summary
|
||||
@@ -151,33 +180,37 @@ function displaySummary() {
|
||||
console.log('\n📊 What was created:');
|
||||
console.log(' ✓ SQLite database (cve_database.db)');
|
||||
console.log(' ✓ Tables: cves, documents, required_documents');
|
||||
console.log(' ✓ Multi-vendor support with UNIQUE(cve_id, vendor)');
|
||||
console.log(' ✓ Vendor column in documents table');
|
||||
console.log(' ✓ Indexes for fast queries');
|
||||
console.log(' ✓ Document compliance view');
|
||||
console.log(' ✓ Uploads directory for file storage');
|
||||
console.log('\n📁 File structure will be:');
|
||||
console.log(' uploads/');
|
||||
console.log(' └── CVE-XXXX-XXXX/');
|
||||
console.log(' └── VendorName/');
|
||||
console.log(' ├── advisory.pdf');
|
||||
console.log(' ├── email.pdf');
|
||||
console.log(' └── screenshot.png');
|
||||
console.log(' ├── Vendor1/');
|
||||
console.log(' │ ├── advisory.pdf');
|
||||
console.log(' │ └── screenshot.png');
|
||||
console.log(' └── Vendor2/');
|
||||
console.log(' └── advisory.pdf');
|
||||
console.log('\n🚀 Next steps:');
|
||||
console.log(' 1. Start the backend API:');
|
||||
console.log(' → cd backend && node server.js');
|
||||
console.log(' 2. Start the frontend:');
|
||||
console.log(' → cd frontend && npm start');
|
||||
console.log(' 3. Open http://localhost:3000');
|
||||
console.log(' 4. Start adding CVEs and uploading documents!');
|
||||
console.log('\n💡 Tips:');
|
||||
console.log(' • Use the Quick Check to verify CVE status');
|
||||
console.log(' • Upload documents through the dashboard');
|
||||
console.log(' • Documents are auto-organized by CVE ID → Vendor');
|
||||
console.log(' 4. Start adding CVEs with multiple vendors!');
|
||||
console.log('\n💡 Key Features:');
|
||||
console.log(' • Add same CVE-ID with different vendors');
|
||||
console.log(' • Each vendor has separate document storage');
|
||||
console.log(' • Quick Check shows all vendors for a CVE');
|
||||
console.log(' • Document compliance tracking per vendor');
|
||||
console.log(' • Required docs: Advisory (mandatory for most vendors)\n');
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
console.log('🚀 CVE Database Setup\n');
|
||||
console.log('🚀 CVE Database Setup (Multi-Vendor Support)\n');
|
||||
console.log('════════════════════════════════════════\n');
|
||||
|
||||
try {
|
||||
@@ -187,13 +220,16 @@ async function main() {
|
||||
// Initialize database
|
||||
const db = await initializeDatabase();
|
||||
|
||||
// Optionally add sample data
|
||||
// Add sample data
|
||||
await addSampleData(db);
|
||||
|
||||
// Verify setup
|
||||
await verifySetup(db);
|
||||
|
||||
// Close database connection
|
||||
db.close((err) => {
|
||||
if (err) console.error('Error closing database:', err);
|
||||
else console.log('✓ Database connection closed');
|
||||
else console.log('\n✓ Database connection closed');
|
||||
|
||||
// Display summary
|
||||
displaySummary();
|
||||
|
||||
5
frontend/.env.example
Normal file
5
frontend/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Frontend Configuration
|
||||
# API_BASE should include the /api path
|
||||
REACT_APP_API_BASE=http://localhost:3001/api
|
||||
# API_HOST is used for direct file URLs (no /api)
|
||||
REACT_APP_API_HOST=http://localhost:3001
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2 } from 'lucide-react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus } from 'lucide-react';
|
||||
|
||||
const API_BASE = 'http://192.168.2.117:3001/api';
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001';
|
||||
|
||||
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
||||
|
||||
@@ -10,6 +11,7 @@ export default function App() {
|
||||
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
|
||||
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
|
||||
const [selectedCVE, setSelectedCVE] = useState(null);
|
||||
const [selectedVendorView, setSelectedVendorView] = useState(null);
|
||||
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
||||
const [cves, setCves] = useState([]);
|
||||
const [vendors, setVendors] = useState(['All Vendors']);
|
||||
@@ -73,14 +75,15 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDocuments = async (cveId) => {
|
||||
if (cveDocuments[cveId]) return;
|
||||
const fetchDocuments = async (cveId, vendor) => {
|
||||
const key = `${cveId}-${vendor}`;
|
||||
if (cveDocuments[key]) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`);
|
||||
const response = await fetch(`${API_BASE}/cves/${cveId}/documents?vendor=${vendor}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch documents');
|
||||
const data = await response.json();
|
||||
setCveDocuments(prev => ({ ...prev, [cveId]: data }));
|
||||
setCveDocuments(prev => ({ ...prev, [key]: data }));
|
||||
} catch (err) {
|
||||
console.error('Error fetching documents:', err);
|
||||
}
|
||||
@@ -100,12 +103,15 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDocuments = async (cveId) => {
|
||||
if (selectedCVE === cveId) {
|
||||
const handleViewDocuments = async (cveId, vendor) => {
|
||||
const key = `${cveId}-${vendor}`;
|
||||
if (selectedCVE === cveId && selectedVendorView === vendor) {
|
||||
setSelectedCVE(null);
|
||||
setSelectedVendorView(null);
|
||||
} else {
|
||||
setSelectedCVE(cveId);
|
||||
await fetchDocuments(cveId);
|
||||
setSelectedVendorView(vendor);
|
||||
await fetchDocuments(cveId, vendor);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,9 +146,12 @@ export default function App() {
|
||||
body: JSON.stringify(newCVE)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to add CVE');
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to add CVE');
|
||||
}
|
||||
|
||||
alert(`CVE ${newCVE.cve_id} added successfully!`);
|
||||
alert(`CVE ${newCVE.cve_id} added successfully for vendor: ${newCVE.vendor}!`);
|
||||
setShowAddCVE(false);
|
||||
setNewCVE({
|
||||
cve_id: '',
|
||||
@@ -192,8 +201,9 @@ export default function App() {
|
||||
if (!response.ok) throw new Error('Failed to upload document');
|
||||
|
||||
alert(`Document uploaded successfully!`);
|
||||
delete cveDocuments[cveId];
|
||||
await fetchDocuments(cveId);
|
||||
const key = `${cveId}-${vendor}`;
|
||||
delete cveDocuments[key];
|
||||
await fetchDocuments(cveId, vendor);
|
||||
fetchCVEs();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
@@ -205,7 +215,7 @@ export default function App() {
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
const handleDeleteDocument = async (docId, cveId) => {
|
||||
const handleDeleteDocument = async (docId, cveId, vendor) => {
|
||||
if (!window.confirm('Are you sure you want to delete this document?')) {
|
||||
return;
|
||||
}
|
||||
@@ -218,20 +228,30 @@ export default function App() {
|
||||
if (!response.ok) throw new Error('Failed to delete document');
|
||||
|
||||
alert('Document deleted successfully!');
|
||||
delete cveDocuments[cveId];
|
||||
await fetchDocuments(cveId);
|
||||
const key = `${cveId}-${vendor}`;
|
||||
delete cveDocuments[key];
|
||||
await fetchDocuments(cveId, vendor);
|
||||
fetchCVEs();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCVEs = cves;
|
||||
// Group CVEs by CVE ID
|
||||
const groupedCVEs = cves.reduce((acc, cve) => {
|
||||
if (!acc[cve.cve_id]) {
|
||||
acc[cve.cve_id] = [];
|
||||
}
|
||||
acc[cve.cve_id].push(cve);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const filteredGroupedCVEs = groupedCVEs;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header with Charter Branding */}
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">CVE Dashboard</h1>
|
||||
@@ -241,8 +261,8 @@ export default function App() {
|
||||
onClick={() => setShowAddCVE(true)}
|
||||
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors flex items-center gap-2 shadow-md"
|
||||
>
|
||||
<span className="text-xl">+</span>
|
||||
Add New CVE
|
||||
<Plus className="w-5 h-5" />
|
||||
Add CVE/Vendor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +272,7 @@ export default function App() {
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Add New CVE</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Add CVE Entry</h2>
|
||||
<button
|
||||
onClick={() => setShowAddCVE(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
@@ -261,6 +281,13 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Tip:</strong> You can add the same CVE-ID multiple times with different vendors.
|
||||
Each vendor will have its own documents folder.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAddCVE} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -274,6 +301,7 @@ export default function App() {
|
||||
onChange={(e) => setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()})}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Can be the same as existing CVE if adding another vendor</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -288,6 +316,7 @@ export default function App() {
|
||||
onChange={(e) => setNewCVE({...newCVE, vendor: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Must be unique for this CVE-ID</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -338,7 +367,7 @@ export default function App() {
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
|
||||
>
|
||||
Add CVE
|
||||
Add CVE Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -354,7 +383,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Check with Charter Blue */}
|
||||
{/* Quick Check */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg shadow-md p-6 mb-6 border-2 border-[#0476D9]">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Quick CVE Status Check</h2>
|
||||
<div className="flex gap-3">
|
||||
@@ -388,24 +417,30 @@ export default function App() {
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-green-900">✓ CVE Addressed</p>
|
||||
<div className="mt-2 space-y-1 text-sm text-gray-700">
|
||||
<p><strong>Vendor:</strong> {quickCheckResult.cve.vendor}</p>
|
||||
<p><strong>Severity:</strong> {quickCheckResult.cve.severity}</p>
|
||||
<p><strong>Status:</strong> {quickCheckResult.cve.status}</p>
|
||||
<p><strong>Documents:</strong> {quickCheckResult.cve.total_documents} attached</p>
|
||||
<div className="mt-2 flex gap-3">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{quickCheckResult.compliance.advisory ? '✓' : '✗'} Advisory
|
||||
<p className="font-medium text-green-900">✓ CVE Addressed ({quickCheckResult.vendors.length} vendor{quickCheckResult.vendors.length > 1 ? 's' : ''})</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{quickCheckResult.vendors.map((vendorInfo, idx) => (
|
||||
<div key={idx} className="p-3 bg-white rounded border border-green-200">
|
||||
<p className="font-semibold text-gray-900 mb-2">{vendorInfo.vendor}</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-700 mb-2">
|
||||
<p><strong>Severity:</strong> {vendorInfo.severity}</p>
|
||||
<p><strong>Status:</strong> {vendorInfo.status}</p>
|
||||
<p><strong>Documents:</strong> {vendorInfo.total_documents} attached</p>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{vendorInfo.compliance.advisory ? '✓' : '✗'} Advisory
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{quickCheckResult.compliance.email ? '✓' : '○'} Email
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{vendorInfo.compliance.email ? '✓' : '○'} Email
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{quickCheckResult.compliance.screenshot ? '✓' : '○'} Screenshot
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{vendorInfo.compliance.screenshot ? '✓' : '○'} Screenshot
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -475,7 +510,8 @@ export default function App() {
|
||||
{/* Results Summary */}
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<p className="text-gray-600">
|
||||
Found {filteredCVEs.length} CVE{filteredCVEs.length !== 1 ? 's' : ''}
|
||||
Found {Object.keys(filteredGroupedCVEs).length} CVE{Object.keys(filteredGroupedCVEs).length !== 1 ? 's' : ''}
|
||||
({cves.length} vendor entr{cves.length !== 1 ? 'ies' : 'y'})
|
||||
</p>
|
||||
{selectedDocuments.length > 0 && (
|
||||
<button
|
||||
@@ -488,7 +524,7 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CVE List */}
|
||||
{/* CVE List - Grouped by CVE ID */}
|
||||
{loading ? (
|
||||
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<Loader className="w-12 h-12 text-[#0476D9] mx-auto mb-4 animate-spin" />
|
||||
@@ -508,16 +544,33 @@ export default function App() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredCVEs.map(cve => {
|
||||
const documents = cveDocuments[cve.cve_id] || [];
|
||||
{Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => (
|
||||
<div key={cveId} className="bg-white rounded-lg shadow-md border-2 border-gray-200">
|
||||
<div className="p-6">
|
||||
{/* CVE Header */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">{cveId}</h3>
|
||||
<p className="text-gray-600 mb-3">{vendorEntries[0].description}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>Published: {vendorEntries[0].published_date}</span>
|
||||
<span>•</span>
|
||||
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vendor Entries */}
|
||||
<div className="space-y-3">
|
||||
{vendorEntries.map((cve) => {
|
||||
const key = `${cve.cve_id}-${cve.vendor}`;
|
||||
const documents = cveDocuments[key] || [];
|
||||
const isExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor;
|
||||
|
||||
return (
|
||||
<div key={cve.cve_id} className="bg-white rounded-lg shadow-md border border-gray-200">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div key={cve.id} className="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">{cve.cve_id}</h3>
|
||||
<h4 className="text-lg font-semibold text-gray-900">{cve.vendor}</h4>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
|
||||
{cve.severity}
|
||||
</span>
|
||||
@@ -525,10 +578,7 @@ export default function App() {
|
||||
{cve.doc_status === 'Complete' ? '✓ Docs Complete' : '⚠ Incomplete'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-2">{cve.description}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>Vendor: <span className="font-medium text-gray-700">{cve.vendor}</span></span>
|
||||
<span>Published: {cve.published_date}</span>
|
||||
<span>Status: <span className="font-medium text-gray-700">{cve.status}</span></span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
@@ -537,27 +587,27 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleViewDocuments(cve.cve_id)}
|
||||
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
||||
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{selectedCVE === cve.cve_id ? 'Hide' : 'View'} Documents
|
||||
{isExpanded ? 'Hide' : 'View'} Documents
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Documents Section */}
|
||||
{selectedCVE === cve.cve_id && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
{isExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-300">
|
||||
<h5 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
Attached Documents ({documents.length})
|
||||
</h4>
|
||||
Documents for {cve.vendor} ({documents.length})
|
||||
</h5>
|
||||
{documents.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{documents.map(doc => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="flex items-center justify-between p-3 bg-white rounded-lg hover:bg-gray-50 transition-colors border border-gray-200"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<input
|
||||
@@ -577,7 +627,7 @@ export default function App() {
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href={`http://192.168.2.117:3001/${doc.file_path}`}
|
||||
href={`${API_HOST}/${doc.file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-3 py-1 text-sm text-[#0476D9] hover:bg-blue-50 rounded transition-colors border border-[#0476D9]"
|
||||
@@ -585,7 +635,7 @@ export default function App() {
|
||||
View
|
||||
</a>
|
||||
<button
|
||||
onClick={() => handleDeleteDocument(doc.id, cve.cve_id)}
|
||||
onClick={() => handleDeleteDocument(doc.id, cve.cve_id, cve.vendor)}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors border border-red-600 flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
@@ -604,18 +654,21 @@ export default function App() {
|
||||
className="mt-3 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50 border border-gray-300"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
{uploadingFile ? 'Uploading...' : 'Upload New Document'}
|
||||
{uploadingFile ? 'Uploading...' : 'Upload Document'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredCVEs.length === 0 && !loading && (
|
||||
{Object.keys(filteredGroupedCVEs).length === 0 && !loading && (
|
||||
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No CVEs Found</h3>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.2.1",
|
||||
"multer": "^2.0.2",
|
||||
"sqlite3": "^5.1.7"
|
||||
|
||||
22
start-servers.sh
Executable file
22
start-servers.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
echo "Starting CVE Dashboard servers..."
|
||||
|
||||
# Start backend
|
||||
cd backend
|
||||
nohup node server.js > backend.log 2>&1 &
|
||||
BACKEND_PID=$!
|
||||
echo "Backend started (PID: $BACKEND_PID)"
|
||||
|
||||
# Start frontend
|
||||
cd ../frontend
|
||||
nohup npm start > frontend.log 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
echo "Frontend started (PID: $FRONTEND_PID)"
|
||||
|
||||
# Save PIDs
|
||||
echo $BACKEND_PID > ../backend.pid
|
||||
echo $FRONTEND_PID > ../frontend.pid
|
||||
|
||||
echo "✓ Both servers running in background"
|
||||
echo " Backend: http://localhost:3001"
|
||||
echo " Frontend: http://localhost:3000"
|
||||
18
stop-servers.sh
Executable file
18
stop-servers.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
echo "Stopping CVE Dashboard servers..."
|
||||
|
||||
if [ -f backend.pid ]; then
|
||||
kill $(cat backend.pid) 2>/dev/null
|
||||
rm backend.pid
|
||||
echo "✓ Backend stopped"
|
||||
fi
|
||||
|
||||
if [ -f frontend.pid ]; then
|
||||
kill $(cat frontend.pid) 2>/dev/null
|
||||
rm frontend.pid
|
||||
echo "✓ Frontend stopped"
|
||||
fi
|
||||
|
||||
pkill -f "node server.js"
|
||||
pkill -f "react-scripts start"
|
||||
echo "All servers stopped"
|
||||
Reference in New Issue
Block a user