release: v1.0.0 — clean README, changelog, full reference manual, dead code removal, package metadata
This commit is contained in:
59
CHANGELOG.md
Normal file
59
CHANGELOG.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v1.0.0 — 2026-05-01
|
||||||
|
|
||||||
|
First official release. Consolidates all features developed since initial commit into a stable, documented, deployment-ready package.
|
||||||
|
|
||||||
|
### Core Platform
|
||||||
|
- CVE tracking with multi-vendor support, document storage, and NVD API auto-fill
|
||||||
|
- Session-based authentication with four user groups (Admin, Standard_User, Leadership, Read_Only)
|
||||||
|
- Full audit logging of all state-changing actions
|
||||||
|
- Dark tactical intelligence UI theme with monospace typography
|
||||||
|
|
||||||
|
### Ivanti Integration
|
||||||
|
- Live sync of open host findings from Ivanti/RiskSense API (auto-sync every 24h)
|
||||||
|
- Reporting page with donut metric charts, advanced per-column filtering, inline editing
|
||||||
|
- FP workflow submission directly to Ivanti API with file attachments
|
||||||
|
- Ivanti Queue — personal staging list for batch FP, Archer, CARD, and Granite workflows
|
||||||
|
- Queue item redirect between workflow types after completion
|
||||||
|
- Row visibility controls with localStorage persistence
|
||||||
|
|
||||||
|
### Archive and Anomaly Tracking
|
||||||
|
- Automatic detection of disappeared and returned findings across syncs
|
||||||
|
- BU drift checker — classifies archived findings by reason (BU reassignment, severity drift, closed on platform, decommissioned)
|
||||||
|
- Return classification — explains why findings came back (BU reassigned back, severity re-escalated, etc.)
|
||||||
|
- Findings Trend chart with archive activity sparkline and shift reason tooltips
|
||||||
|
- Anomaly banner for significant archive events
|
||||||
|
|
||||||
|
### Compliance (AEO Posture)
|
||||||
|
- Weekly NTS_AEO xlsx upload with diff preview (new, resolved, recurring)
|
||||||
|
- Schema drift detection with breaking/silent-miss/cosmetic classification
|
||||||
|
- Admin config reconciliation for parser updates
|
||||||
|
- Per-team metric health cards with grouped categories and variant pills
|
||||||
|
- Device-level violation tracking with timestamped notes history
|
||||||
|
- Multi-metric note grouping
|
||||||
|
- Upload rollback support
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
- Jira Data Center — create, sync, and track tickets linked to CVE/vendor pairs
|
||||||
|
- Archer — risk acceptance exception tracking (EXC numbers)
|
||||||
|
- Atlas InfoSec — action plan cache, bulk creation from row selection, metrics reporting
|
||||||
|
- CARD API — Granite/CARD asset lookup for network device workflows
|
||||||
|
- NVD API — auto-fill CVE metadata with bulk sync support
|
||||||
|
|
||||||
|
### Knowledge Base
|
||||||
|
- Internal document library with inline PDF and Markdown rendering
|
||||||
|
- Category-based browsing and search
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- Full-page admin panel with user management, audit log, and system info tabs
|
||||||
|
- Themed confirm modals replacing browser dialogs
|
||||||
|
- User profile panel with self-service password change
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- Consolidated `setup.js` with complete database schema (27 tables, all indexes and triggers)
|
||||||
|
- systemd service files for persistent deployment
|
||||||
|
- GitLab CI/CD pipeline (install, lint, test, build, deploy)
|
||||||
|
- GPG-signed commits for code provenance
|
||||||
|
- Organized documentation structure (api, design, guides, security, testing, troubleshooting)
|
||||||
|
- Migration scripts documented and retained for existing deployment upgrades
|
||||||
@@ -3,6 +3,10 @@ PORT=3001
|
|||||||
API_HOST=localhost
|
API_HOST=localhost
|
||||||
CORS_ORIGINS=http://localhost:3000
|
CORS_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
|
# Session secret — REQUIRED. Server will not start without this.
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
|
SESSION_SECRET=
|
||||||
|
|
||||||
# NVD API Key (optional - increases rate limit from 5 to 50 requests per 30s)
|
# NVD API Key (optional - increases rate limit from 5 to 50 requests per 30s)
|
||||||
# Request one at https://nvd.nist.gov/developers/request-an-api-key
|
# Request one at https://nvd.nist.gov/developers/request-an-api-key
|
||||||
NVD_API_KEY=
|
NVD_API_KEY=
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Migration script: Add audit_logs table
|
|
||||||
// Run: node migrate-audit-log.js
|
|
||||||
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const DB_FILE = './cve_database.db';
|
|
||||||
const BACKUP_FILE = `./cve_database_backup_${Date.now()}.db`;
|
|
||||||
|
|
||||||
function run(db, sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.run(sql, params, function(err) {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(this);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(db, sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.get(sql, params, (err, row) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(row);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrate() {
|
|
||||||
console.log('╔════════════════════════════════════════════════════════╗');
|
|
||||||
console.log('║ CVE Database Migration: Add Audit Logs ║');
|
|
||||||
console.log('╚════════════════════════════════════════════════════════╝\n');
|
|
||||||
|
|
||||||
if (!fs.existsSync(DB_FILE)) {
|
|
||||||
console.log('❌ Database not found. Run setup.js for fresh install.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup database
|
|
||||||
console.log('📦 Creating backup...');
|
|
||||||
fs.copyFileSync(DB_FILE, BACKUP_FILE);
|
|
||||||
console.log(` ✓ Backup saved to: ${BACKUP_FILE}\n`);
|
|
||||||
|
|
||||||
const db = new sqlite3.Database(DB_FILE);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if table already exists
|
|
||||||
const exists = await get(db,
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='audit_logs'"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
console.log('⏭️ audit_logs table already exists, nothing to do.');
|
|
||||||
} else {
|
|
||||||
console.log('1️⃣ Creating audit_logs table...');
|
|
||||||
await run(db, `
|
|
||||||
CREATE TABLE audit_logs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER,
|
|
||||||
username VARCHAR(50) NOT NULL,
|
|
||||||
action VARCHAR(50) NOT NULL,
|
|
||||||
entity_type VARCHAR(50) NOT NULL,
|
|
||||||
entity_id VARCHAR(100),
|
|
||||||
details TEXT,
|
|
||||||
ip_address VARCHAR(45),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
console.log(' ✓ Table created');
|
|
||||||
|
|
||||||
console.log('2️⃣ Creating indexes...');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at)');
|
|
||||||
console.log(' ✓ Indexes created');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n╔════════════════════════════════════════════════════════╗');
|
|
||||||
console.log('║ MIGRATION COMPLETE! ║');
|
|
||||||
console.log('╚════════════════════════════════════════════════════════╝');
|
|
||||||
console.log('\n📋 Summary:');
|
|
||||||
console.log(' ✓ audit_logs table ready');
|
|
||||||
console.log(`\n💾 Backup saved: ${BACKUP_FILE}`);
|
|
||||||
console.log('\n🚀 Restart your server to apply changes.\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ Migration failed:', error.message);
|
|
||||||
console.log(`\n🔄 To restore from backup: cp ${BACKUP_FILE} ${DB_FILE}`);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
migrate();
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Migration script: v1.0.0 -> v1.1.0
|
|
||||||
// Adds: users, sessions tables, multi-vendor support, vendor column in documents
|
|
||||||
// Run: node migrate-to-1.1.js
|
|
||||||
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const bcrypt = require('bcryptjs');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const DB_FILE = './cve_database.db';
|
|
||||||
const BACKUP_FILE = `./cve_database_backup_${Date.now()}.db`;
|
|
||||||
|
|
||||||
async function migrate() {
|
|
||||||
console.log('╔════════════════════════════════════════════════════════╗');
|
|
||||||
console.log('║ CVE Database Migration: v1.0.0 → v1.1.0 ║');
|
|
||||||
console.log('╚════════════════════════════════════════════════════════╝\n');
|
|
||||||
|
|
||||||
// Check if database exists
|
|
||||||
if (!fs.existsSync(DB_FILE)) {
|
|
||||||
console.log('❌ Database not found. Run setup.js for fresh install.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup database
|
|
||||||
console.log('📦 Creating backup...');
|
|
||||||
fs.copyFileSync(DB_FILE, BACKUP_FILE);
|
|
||||||
console.log(` ✓ Backup saved to: ${BACKUP_FILE}\n`);
|
|
||||||
|
|
||||||
const db = new sqlite3.Database(DB_FILE);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Run migrations in sequence
|
|
||||||
await addUsersTable(db);
|
|
||||||
await addSessionsTable(db);
|
|
||||||
await addVendorToDocuments(db);
|
|
||||||
await updateCvesConstraint(db);
|
|
||||||
await createDefaultAdmin(db);
|
|
||||||
await updateView(db);
|
|
||||||
|
|
||||||
console.log('\n╔════════════════════════════════════════════════════════╗');
|
|
||||||
console.log('║ MIGRATION COMPLETE! ║');
|
|
||||||
console.log('╚════════════════════════════════════════════════════════╝');
|
|
||||||
console.log('\n📋 Summary:');
|
|
||||||
console.log(' ✓ Users table added');
|
|
||||||
console.log(' ✓ Sessions table added');
|
|
||||||
console.log(' ✓ Vendor column added to documents');
|
|
||||||
console.log(' ✓ Multi-vendor constraint applied to cves');
|
|
||||||
console.log(' ✓ Default admin user created (admin/admin123)');
|
|
||||||
console.log(`\n💾 Backup saved: ${BACKUP_FILE}`);
|
|
||||||
console.log('\n🚀 Restart your server to apply changes.\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ Migration failed:', error.message);
|
|
||||||
console.log(`\n🔄 To restore from backup: cp ${BACKUP_FILE} ${DB_FILE}`);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function run(db, sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.run(sql, params, function(err) {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(this);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(db, sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.get(sql, params, (err, row) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(row);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function all(db, sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.all(sql, params, (err, rows) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(rows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addUsersTable(db) {
|
|
||||||
console.log('1️⃣ Adding users table...');
|
|
||||||
|
|
||||||
const exists = await get(db,
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
console.log(' ⏭️ Users table already exists, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await run(db, `
|
|
||||||
CREATE TABLE users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
|
|
||||||
is_active BOOLEAN DEFAULT 1,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_login TIMESTAMP,
|
|
||||||
CHECK (role IN ('admin', 'editor', 'viewer'))
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)');
|
|
||||||
console.log(' ✓ Users table created');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addSessionsTable(db) {
|
|
||||||
console.log('2️⃣ Adding sessions table...');
|
|
||||||
|
|
||||||
const exists = await get(db,
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
console.log(' ⏭️ Sessions table already exists, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await run(db, `
|
|
||||||
CREATE TABLE sessions (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
session_id VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)');
|
|
||||||
console.log(' ✓ Sessions table created');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addVendorToDocuments(db) {
|
|
||||||
console.log('3️⃣ Adding vendor column to documents...');
|
|
||||||
|
|
||||||
// Check if vendor column exists
|
|
||||||
const columns = await all(db, "PRAGMA table_info(documents)");
|
|
||||||
const hasVendor = columns.some(col => col.name === 'vendor');
|
|
||||||
|
|
||||||
if (hasVendor) {
|
|
||||||
console.log(' ⏭️ Vendor column already exists, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add vendor column
|
|
||||||
await run(db, "ALTER TABLE documents ADD COLUMN vendor VARCHAR(100)");
|
|
||||||
|
|
||||||
// Populate vendor from the cves table based on cve_id
|
|
||||||
await run(db, `
|
|
||||||
UPDATE documents
|
|
||||||
SET vendor = (
|
|
||||||
SELECT c.vendor
|
|
||||||
FROM cves c
|
|
||||||
WHERE c.cve_id = documents.cve_id
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
WHERE vendor IS NULL
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Set default for any remaining nulls
|
|
||||||
await run(db, "UPDATE documents SET vendor = 'Unknown' WHERE vendor IS NULL");
|
|
||||||
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor)');
|
|
||||||
console.log(' ✓ Vendor column added and populated');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateCvesConstraint(db) {
|
|
||||||
console.log('4️⃣ Updating CVEs table for multi-vendor support...');
|
|
||||||
|
|
||||||
// Check current schema
|
|
||||||
const tableInfo = await get(db,
|
|
||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='cves'"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tableInfo.sql.includes('UNIQUE(cve_id, vendor)')) {
|
|
||||||
console.log(' ⏭️ Multi-vendor constraint already exists, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLite doesn't support ALTER CONSTRAINT, so we need to rebuild the table
|
|
||||||
console.log(' 📋 Rebuilding table with new constraint...');
|
|
||||||
|
|
||||||
// Create new table with correct schema
|
|
||||||
await run(db, `
|
|
||||||
CREATE TABLE cves_new (
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Copy data
|
|
||||||
await run(db, `
|
|
||||||
INSERT INTO cves_new (id, cve_id, vendor, severity, description, published_date, status, created_at, updated_at)
|
|
||||||
SELECT id, cve_id, vendor, severity, description, published_date, status, created_at, updated_at
|
|
||||||
FROM cves
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Drop old table
|
|
||||||
await run(db, 'DROP TABLE cves');
|
|
||||||
|
|
||||||
// Rename new table
|
|
||||||
await run(db, 'ALTER TABLE cves_new RENAME TO cves');
|
|
||||||
|
|
||||||
// Recreate indexes
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_cve_id ON cves(cve_id)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_vendor ON cves(vendor)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_severity ON cves(severity)');
|
|
||||||
await run(db, 'CREATE INDEX IF NOT EXISTS idx_status ON cves(status)');
|
|
||||||
|
|
||||||
console.log(' ✓ Multi-vendor constraint applied');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createDefaultAdmin(db) {
|
|
||||||
console.log('5️⃣ Creating default admin user...');
|
|
||||||
|
|
||||||
const exists = await get(db, "SELECT id FROM users WHERE username = 'admin'");
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
console.log(' ⏭️ Admin user already exists, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
|
||||||
|
|
||||||
await run(db, `
|
|
||||||
INSERT INTO users (username, email, password_hash, role, is_active)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, ['admin', 'admin@localhost', passwordHash, 'admin', 1]);
|
|
||||||
|
|
||||||
console.log(' ✓ Admin user created (admin/admin123)');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateView(db) {
|
|
||||||
console.log('6️⃣ Updating document status view...');
|
|
||||||
|
|
||||||
// Drop old view if exists
|
|
||||||
await run(db, 'DROP VIEW IF EXISTS cve_document_status');
|
|
||||||
|
|
||||||
// Create updated view with multi-vendor support
|
|
||||||
await run(db, `
|
|
||||||
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
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log(' ✓ View updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run migration
|
|
||||||
migrate();
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Migration: Add jira_tickets table
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const dbPath = path.join(__dirname, 'cve_database.db');
|
|
||||||
const db = new sqlite3.Database(dbPath);
|
|
||||||
|
|
||||||
console.log('Starting JIRA tickets migration...');
|
|
||||||
|
|
||||||
db.serialize(() => {
|
|
||||||
// Create jira_tickets table
|
|
||||||
db.run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS jira_tickets (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
cve_id TEXT NOT NULL,
|
|
||||||
vendor TEXT NOT NULL,
|
|
||||||
ticket_key TEXT NOT NULL,
|
|
||||||
url TEXT,
|
|
||||||
summary TEXT,
|
|
||||||
status TEXT DEFAULT 'Open' CHECK(status IN ('Open', 'In Progress', 'Closed')),
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (cve_id, vendor) REFERENCES cves(cve_id, vendor) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`, (err) => {
|
|
||||||
if (err) console.error('Error creating table:', err);
|
|
||||||
else console.log('✓ jira_tickets table created');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_jira_tickets_cve ON jira_tickets(cve_id, vendor)');
|
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_jira_tickets_status ON jira_tickets(status)');
|
|
||||||
|
|
||||||
console.log('✓ Indexes created');
|
|
||||||
});
|
|
||||||
|
|
||||||
db.close(() => {
|
|
||||||
console.log('Migration complete!');
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
1080
docs/guides/full-reference-manual.md
Normal file
1080
docs/guides/full-reference-manual.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,598 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader } from 'lucide-react';
|
|
||||||
|
|
||||||
const API_BASE = 'http://192.168.2.117:3001/api';
|
|
||||||
|
|
||||||
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
|
|
||||||
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
|
|
||||||
const [selectedCVE, setSelectedCVE] = useState(null);
|
|
||||||
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
|
||||||
const [cves, setCves] = useState([]);
|
|
||||||
const [vendors, setVendors] = useState(['All Vendors']);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [cveDocuments, setCveDocuments] = useState({});
|
|
||||||
const [quickCheckCVE, setQuickCheckCVE] = useState('');
|
|
||||||
const [quickCheckResult, setQuickCheckResult] = useState(null);
|
|
||||||
const [showAddCVE, setShowAddCVE] = useState(false);
|
|
||||||
const [newCVE, setNewCVE] = useState({
|
|
||||||
cve_id: '',
|
|
||||||
vendor: '',
|
|
||||||
severity: 'Medium',
|
|
||||||
description: '',
|
|
||||||
published_date: new Date().toISOString().split('T')[0]
|
|
||||||
});
|
|
||||||
const [uploadingFile, setUploadingFile] = useState(false);
|
|
||||||
|
|
||||||
// Fetch CVEs from API
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCVEs();
|
|
||||||
fetchVendors();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Refetch when filters change
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCVEs();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [searchQuery, selectedVendor, selectedSeverity]);
|
|
||||||
|
|
||||||
const fetchCVEs = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (searchQuery) params.append('search', searchQuery);
|
|
||||||
if (selectedVendor !== 'All Vendors') params.append('vendor', selectedVendor);
|
|
||||||
if (selectedSeverity !== 'All Severities') params.append('severity', selectedSeverity);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/cves?${params}`);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch CVEs');
|
|
||||||
const data = await response.json();
|
|
||||||
setCves(data);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
console.error('Error fetching CVEs:', err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchVendors = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/vendors`);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch vendors');
|
|
||||||
const data = await response.json();
|
|
||||||
setVendors(['All Vendors', ...data]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching vendors:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDocuments = async (cveId) => {
|
|
||||||
if (cveDocuments[cveId]) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch documents');
|
|
||||||
const data = await response.json();
|
|
||||||
setCveDocuments(prev => ({ ...prev, [cveId]: data }));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching documents:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const quickCheckCVEStatus = async () => {
|
|
||||||
if (!quickCheckCVE.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`);
|
|
||||||
if (!response.ok) throw new Error('Failed to check CVE');
|
|
||||||
const data = await response.json();
|
|
||||||
setQuickCheckResult(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error checking CVE:', err);
|
|
||||||
setQuickCheckResult({ error: err.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDocuments = async (cveId) => {
|
|
||||||
if (selectedCVE === cveId) {
|
|
||||||
setSelectedCVE(null);
|
|
||||||
} else {
|
|
||||||
setSelectedCVE(cveId);
|
|
||||||
await fetchDocuments(cveId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSeverityColor = (severity) => {
|
|
||||||
const colors = {
|
|
||||||
'Critical': 'bg-red-100 text-red-800',
|
|
||||||
'High': 'bg-orange-100 text-orange-800',
|
|
||||||
'Medium': 'bg-yellow-100 text-yellow-800',
|
|
||||||
'Low': 'bg-blue-100 text-blue-800'
|
|
||||||
};
|
|
||||||
return colors[severity] || 'bg-gray-100 text-gray-800';
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDocumentSelection = (docId) => {
|
|
||||||
setSelectedDocuments(prev =>
|
|
||||||
prev.includes(docId)
|
|
||||||
? prev.filter(id => id !== docId)
|
|
||||||
: [...prev, docId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportSelectedDocuments = () => {
|
|
||||||
alert(`Exporting ${selectedDocuments.length} documents for report attachment`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddCVE = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/cves`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(newCVE)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to add CVE');
|
|
||||||
|
|
||||||
alert(`CVE ${newCVE.cve_id} added successfully!`);
|
|
||||||
setShowAddCVE(false);
|
|
||||||
setNewCVE({
|
|
||||||
cve_id: '',
|
|
||||||
vendor: '',
|
|
||||||
severity: 'Medium',
|
|
||||||
description: '',
|
|
||||||
published_date: new Date().toISOString().split('T')[0]
|
|
||||||
});
|
|
||||||
fetchCVEs();
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Error: ${err.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileUpload = async (cveId, vendor) => {
|
|
||||||
const fileInput = document.createElement('input');
|
|
||||||
fileInput.type = 'file';
|
|
||||||
fileInput.accept = '.pdf,.png,.jpg,.jpeg,.txt,.doc,.docx';
|
|
||||||
|
|
||||||
fileInput.onchange = async (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const docType = prompt(
|
|
||||||
'Document type (advisory, email, screenshot, patch, other):',
|
|
||||||
'advisory'
|
|
||||||
);
|
|
||||||
if (!docType) return;
|
|
||||||
|
|
||||||
const notes = prompt('Notes (optional):');
|
|
||||||
|
|
||||||
setUploadingFile(true);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
formData.append('cveId', cveId);
|
|
||||||
formData.append('vendor', vendor);
|
|
||||||
formData.append('type', docType);
|
|
||||||
if (notes) formData.append('notes', notes);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to upload document');
|
|
||||||
|
|
||||||
alert(`Document uploaded successfully!`);
|
|
||||||
delete cveDocuments[cveId];
|
|
||||||
await fetchDocuments(cveId);
|
|
||||||
fetchCVEs();
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Error: ${err.message}`);
|
|
||||||
} finally {
|
|
||||||
setUploadingFile(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fileInput.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredCVEs = cves;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 p-6">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
{/* 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>
|
|
||||||
<p className="text-gray-600">Query vulnerabilities, manage vendors, and attach documentation</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAddCVE(true)}
|
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<span className="text-xl">+</span>
|
|
||||||
Add New CVE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add CVE Modal */}
|
|
||||||
{showAddCVE && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAddCVE(false)}
|
|
||||||
className="text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
<XCircle className="w-6 h-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleAddCVE} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
CVE ID *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="CVE-2024-1234"
|
|
||||||
value={newCVE.cve_id}
|
|
||||||
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-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Vendor *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="Microsoft, Cisco, Oracle, etc."
|
|
||||||
value={newCVE.vendor}
|
|
||||||
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-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Severity *
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={newCVE.severity}
|
|
||||||
onChange={(e) => setNewCVE({...newCVE, severity: e.target.value})}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="Critical">Critical</option>
|
|
||||||
<option value="High">High</option>
|
|
||||||
<option value="Medium">Medium</option>
|
|
||||||
<option value="Low">Low</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Description *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
required
|
|
||||||
placeholder="Brief description of the vulnerability"
|
|
||||||
value={newCVE.description}
|
|
||||||
onChange={(e) => setNewCVE({...newCVE, description: e.target.value})}
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Published Date *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
value={newCVE.published_date}
|
|
||||||
onChange={(e) => setNewCVE({...newCVE, published_date: e.target.value})}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
Add CVE
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowAddCVE(false)}
|
|
||||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Quick Check */}
|
|
||||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg shadow-sm p-6 mb-6 border border-blue-200">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Quick CVE Status Check</h2>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter CVE ID (e.g., CVE-2024-1234)"
|
|
||||||
value={quickCheckCVE}
|
|
||||||
onChange={(e) => setQuickCheckCVE(e.target.value)}
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && quickCheckCVEStatus()}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={quickCheckCVEStatus}
|
|
||||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
Check Status
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{quickCheckResult && (
|
|
||||||
<div className={`mt-4 p-4 rounded-lg ${quickCheckResult.exists ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'}`}>
|
|
||||||
{quickCheckResult.error ? (
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<XCircle className="w-5 h-5 text-red-600 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-red-900">Error</p>
|
|
||||||
<p className="text-sm text-red-700">{quickCheckResult.error}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : quickCheckResult.exists ? (
|
|
||||||
<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
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-yellow-900">Not Found</p>
|
|
||||||
<p className="text-sm text-yellow-700">This CVE has not been addressed yet. No entry exists in the database.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search and Filters */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="md:col-span-1">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
<Search className="inline w-4 h-4 mr-1" />
|
|
||||||
Search CVEs
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="CVE ID or description..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
<Filter className="inline w-4 h-4 mr-1" />
|
|
||||||
Vendor
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={selectedVendor}
|
|
||||||
onChange={(e) => setSelectedVendor(e.target.value)}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
|
||||||
{vendors.map(vendor => (
|
|
||||||
<option key={vendor} value={vendor}>{vendor}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
<AlertCircle className="inline w-4 h-4 mr-1" />
|
|
||||||
Severity
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={selectedSeverity}
|
|
||||||
onChange={(e) => setSelectedSeverity(e.target.value)}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
|
||||||
{severityLevels.map(level => (
|
|
||||||
<option key={level} value={level}>{level}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Results Summary */}
|
|
||||||
<div className="mb-4 flex justify-between items-center">
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Found {filteredCVEs.length} CVE{filteredCVEs.length !== 1 ? 's' : ''}
|
|
||||||
</p>
|
|
||||||
{selectedDocuments.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={exportSelectedDocuments}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
Export {selectedDocuments.length} Document{selectedDocuments.length !== 1 ? 's' : ''} for Report
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CVE List */}
|
|
||||||
{loading ? (
|
|
||||||
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
|
|
||||||
<Loader className="w-12 h-12 text-blue-600 mx-auto mb-4 animate-spin" />
|
|
||||||
<p className="text-gray-600">Loading CVEs...</p>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
|
|
||||||
<XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Error Loading CVEs</h3>
|
|
||||||
<p className="text-gray-600 mb-4">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={fetchCVEs}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{filteredCVEs.map(cve => {
|
|
||||||
const documents = cveDocuments[cve.cve_id] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={cve.cve_id} className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex justify-between items-start mb-4">
|
|
||||||
<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>
|
|
||||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
|
|
||||||
{cve.severity}
|
|
||||||
</span>
|
|
||||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${cve.doc_status === 'Complete' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
|
||||||
{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" />
|
|
||||||
{cve.document_count} document{cve.document_count !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleViewDocuments(cve.cve_id)}
|
|
||||||
className="px-4 py-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
{selectedCVE === cve.cve_id ? '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">
|
|
||||||
<FileText className="w-4 h-4" />
|
|
||||||
Attached Documents ({documents.length})
|
|
||||||
</h4>
|
|
||||||
{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"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 flex-1">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedDocuments.includes(doc.id)}
|
|
||||||
onChange={() => toggleDocumentSelection(doc.id)}
|
|
||||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<FileText className="w-5 h-5 text-gray-400" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
|
|
||||||
<p className="text-xs text-gray-500 capitalize">
|
|
||||||
{doc.type} • {doc.file_size}
|
|
||||||
{doc.notes && ` • ${doc.notes}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={`http://localhost:3001/${doc.file_path}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="px-3 py-1 text-sm text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
|
|
||||||
disabled={uploadingFile}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Upload className="w-4 h-4" />
|
|
||||||
{uploadingFile ? 'Uploading...' : 'Upload New Document'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{filteredCVEs.length === 0 && !loading && (
|
|
||||||
<div className="bg-white rounded-lg shadow-sm 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>
|
|
||||||
<p className="text-gray-600">Try adjusting your search criteria or filters</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import logo from './logo.svg';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.js</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "cve-dashboard",
|
"name": "cve-dashboard",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "STEAM Security Dashboard — vulnerability management for NTS-AEO",
|
||||||
"main": "index.js",
|
"author": "Jordan Ramos <jordan.ramos@spectrum.com>",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user