WIP: fix/issue-1 #3

Manually merged
jramos merged 3 commits from fix/issue-1 into master 2026-01-28 12:13:51 -07:00
5 changed files with 420 additions and 158 deletions
Showing only changes of commit b9421ea0e9 - Show all commits

View 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();
});
});
});
});
});
});
});
});

View File

@@ -13,7 +13,7 @@ const PORT = 3001;
// Middleware
app.use(cors({
origin: ['http://localhost:3000', 'http://192.168.2.117:3000'],
origin: ['http://localhost:3000', 'http://71.85.90.6:3000'],
credentials: true
}));
app.use(express.json());
@@ -94,7 +94,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,37 +105,63 @@ 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
// NEW ENDPOINT: Get all vendors for a specific CVE
app.get('/api/cves/:cveId/vendors', (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
app.post('/api/cves', (req, res) => {
const { cve_id, vendor, severity, description, published_date } = req.body;
@@ -146,16 +172,23 @@ app.post('/api/cves', (req, res) => {
db.run(query, [cve_id, vendor, severity, description, published_date], function(err) {
if (err) {
// 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'
message: `CVE created successfully for vendor: ${vendor}`
});
});
});
// Update CVE status
app.patch('/api/cves/:cveId/status', (req, res) => {
const { cveId } = req.params;
@@ -173,13 +206,22 @@ app.patch('/api/cves/:cveId/status', (req, res) => {
// ========== DOCUMENT ENDPOINTS ==========
// Get documents for a CVE
// 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
const query = `SELECT * FROM documents WHERE cve_id = ? ORDER BY uploaded_at DESC`;
let query = `SELECT * FROM documents WHERE cve_id = ?`;
let params = [cveId];
db.all(query, [cveId], (err, rows) => {
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 });
}

View File

@@ -1,7 +1,7 @@
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 = 'http://71.85.90.6:3001/api';
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
@@ -10,6 +10,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 +74,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 +102,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 +145,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 +200,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 +214,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 +227,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 +260,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 +271,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 +280,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 +300,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 +315,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 +366,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 +382,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 +416,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 +509,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 +523,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 +543,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 +577,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 +586,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 +626,7 @@ export default function App() {
</div>
<div className="flex gap-2">
<a
href={`http://192.168.2.117:3001/${doc.file_path}`}
href={`http://71.85.90.6:3001/${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 +634,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 +653,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>

22
start-servers.sh Executable file
View 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
View 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"