diff --git a/.gitea/issue_template/enhancement.yaml b/.gitea/issue_template/enhancement.yaml new file mode 100644 index 0000000..0878f89 --- /dev/null +++ b/.gitea/issue_template/enhancement.yaml @@ -0,0 +1,84 @@ +name: Enhancement +about: Suggest an improvement to an existing feature or functionality +title: "[Enhancement] " +labels: + - kind/enhancement + - status/triage +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest an improvement! This template is for enhancements to **existing** features. If you'd like to request a brand new feature, please use the Feature Request template instead. + + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: Describe how the existing feature currently works. + placeholder: "Currently, when I do X, it works like..." + validations: + required: true + + - type: textarea + id: proposed-improvement + attributes: + label: Proposed Improvement + description: Describe how you'd like the existing feature to be improved. + placeholder: "I'd like it to also do Y, or behave differently by..." + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Why would this improvement be valuable? What problem does it solve? + placeholder: "This would help because..." + validations: + required: true + + - type: dropdown + id: area + attributes: + label: Area of the Application + description: Which part of the application does this enhancement relate to? + options: + - Dashboard / CVE List + - CVE Details + - Document Management + - User Management + - Authentication + - Audit Logging + - API + - Other + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this enhancement to your workflow? + options: + - Nice to have + - Important + - Critical + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives or Workarounds + description: Are there any current workarounds or alternative approaches you've considered? + placeholder: "Currently I work around this by..." + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Add any other context, screenshots, or mockups about the enhancement here. + validations: + required: false diff --git a/backend/.env.example b/backend/.env.example index 81d2de0..0f61a35 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,3 +2,7 @@ PORT=3001 API_HOST=localhost CORS_ORIGINS=http://localhost:3000 + +# 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 +NVD_API_KEY= diff --git a/backend/routes/nvdLookup.js b/backend/routes/nvdLookup.js new file mode 100644 index 0000000..c82413f --- /dev/null +++ b/backend/routes/nvdLookup.js @@ -0,0 +1,94 @@ +// NVD CVE Lookup Routes +const express = require('express'); + +const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/; + +function createNvdLookupRouter(db, requireAuth) { + const router = express.Router(); + + // All routes require authentication + router.use(requireAuth(db)); + + // Lookup CVE details from NVD API 2.0 + router.get('/lookup/:cveId', async (req, res) => { + const { cveId } = req.params; + + if (!CVE_ID_PATTERN.test(cveId)) { + return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' }); + } + + const url = `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${encodeURIComponent(cveId)}`; + const headers = {}; + if (process.env.NVD_API_KEY) { + headers['apiKey'] = process.env.NVD_API_KEY; + } + + try { + const response = await fetch(url, { + headers, + signal: AbortSignal.timeout(10000) + }); + + if (response.status === 404) { + return res.status(404).json({ error: 'CVE not found in NVD.' }); + } + + if (response.status === 429) { + return res.status(429).json({ error: 'NVD API rate limit exceeded. Try again later.' }); + } + + if (!response.ok) { + return res.status(502).json({ error: `NVD API returned status ${response.status}.` }); + } + + const data = await response.json(); + + if (!data.vulnerabilities || data.vulnerabilities.length === 0) { + return res.status(404).json({ error: 'CVE not found in NVD.' }); + } + + const vuln = data.vulnerabilities[0].cve; + + // Extract English description + const descriptionEntry = vuln.descriptions?.find(d => d.lang === 'en'); + const description = descriptionEntry ? descriptionEntry.value : ''; + + // Extract severity with cascade: CVSS v3.1 → v3.0 → v2.0 + let severity = null; + const metrics = vuln.metrics || {}; + + if (metrics.cvssMetricV31 && metrics.cvssMetricV31.length > 0) { + severity = metrics.cvssMetricV31[0].cvssData?.baseSeverity; + } else if (metrics.cvssMetricV30 && metrics.cvssMetricV30.length > 0) { + severity = metrics.cvssMetricV30[0].cvssData?.baseSeverity; + } else if (metrics.cvssMetricV2 && metrics.cvssMetricV2.length > 0) { + severity = metrics.cvssMetricV2[0].baseSeverity; + } + + // Map NVD severity strings to app levels + const severityMap = { + 'CRITICAL': 'Critical', + 'HIGH': 'High', + 'MEDIUM': 'Medium', + 'LOW': 'Low' + }; + severity = severity ? (severityMap[severity.toUpperCase()] || 'Medium') : 'Medium'; + + // Extract published date (YYYY-MM-DD) + const publishedRaw = vuln.published; + const published_date = publishedRaw ? publishedRaw.split('T')[0] : ''; + + res.json({ description, severity, published_date }); + } catch (err) { + if (err.name === 'TimeoutError' || err.name === 'AbortError') { + return res.status(504).json({ error: 'NVD API request timed out.' }); + } + console.error('NVD lookup error:', err); + res.status(502).json({ error: 'Failed to reach NVD API.' }); + } + }); + + return router; +} + +module.exports = createNvdLookupRouter; diff --git a/backend/server.js b/backend/server.js index 3246bad..d2ede73 100644 --- a/backend/server.js +++ b/backend/server.js @@ -17,6 +17,7 @@ const createAuthRouter = require('./routes/auth'); const createUsersRouter = require('./routes/users'); const createAuditLogRouter = require('./routes/auditLog'); const logAudit = require('./helpers/auditLog'); +const createNvdLookupRouter = require('./routes/nvdLookup'); const app = express(); const PORT = process.env.PORT || 3001; @@ -56,6 +57,9 @@ app.use('/api/users', createUsersRouter(db, requireAuth, requireRole, logAudit)) // Audit log routes (admin only) app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireRole)); +// NVD lookup routes (authenticated users) +app.use('/api/nvd', createNvdLookupRouter(db, requireAuth)); + // Simple storage - upload to temp directory first const storage = multer.diskStorage({ destination: (req, file, cb) => { @@ -125,6 +129,14 @@ app.get('/api/cves', requireAuth(db), (req, res) => { }); }); +// Get distinct CVE IDs for NVD sync (authenticated users) +app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => { + db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(rows.map(r => r.cve_id)); + }); +}); + // Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR (authenticated users) app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { const { cveId } = req.params; @@ -262,6 +274,73 @@ app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'adm }); }); +// Bulk sync CVE data from NVD (editor or admin) +app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { + const { updates } = req.body; + if (!Array.isArray(updates) || updates.length === 0) { + return res.status(400).json({ error: 'No updates provided' }); + } + + let updated = 0; + const errors = []; + let completed = 0; + + db.serialize(() => { + updates.forEach((entry) => { + const fields = []; + const values = []; + if (entry.description !== null && entry.description !== undefined) { + fields.push('description = ?'); + values.push(entry.description); + } + if (entry.severity !== null && entry.severity !== undefined) { + fields.push('severity = ?'); + values.push(entry.severity); + } + if (entry.published_date !== null && entry.published_date !== undefined) { + fields.push('published_date = ?'); + values.push(entry.published_date); + } + if (fields.length === 0) { + completed++; + if (completed === updates.length) sendResponse(); + return; + } + fields.push('updated_at = CURRENT_TIMESTAMP'); + values.push(entry.cve_id); + + db.run( + `UPDATE cves SET ${fields.join(', ')} WHERE cve_id = ?`, + values, + function(err) { + if (err) { + errors.push({ cve_id: entry.cve_id, error: err.message }); + } else { + updated += this.changes; + } + completed++; + if (completed === updates.length) sendResponse(); + } + ); + }); + }); + + function sendResponse() { + logAudit(db, { + userId: req.user.id, + username: req.user.username, + action: 'cve_nvd_sync', + entityType: 'cve', + entityId: null, + details: { count: updated, cve_ids: updates.map(u => u.cve_id) }, + ipAddress: req.ip + }); + const result = { message: 'NVD sync completed', updated }; + if (errors.length > 0) result.errors = errors; + res.json(result); + } +}); + // ========== DOCUMENT ENDPOINTS ========== // Get documents for a CVE - FILTER BY VENDOR (authenticated users) diff --git a/frontend/src/App.js b/frontend/src/App.js index b7a9f3e..1e0c907 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,10 +1,11 @@ import React, { useState, useEffect } from 'react'; -import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus } from 'lucide-react'; +import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw } from 'lucide-react'; import { useAuth } from './contexts/AuthContext'; import LoginForm from './components/LoginForm'; import UserMenu from './components/UserMenu'; import UserManagement from './components/UserManagement'; import AuditLog from './components/AuditLog'; +import NvdSyncModal from './components/NvdSyncModal'; 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'; @@ -29,6 +30,7 @@ export default function App() { const [showAddCVE, setShowAddCVE] = useState(false); const [showUserManagement, setShowUserManagement] = useState(false); const [showAuditLog, setShowAuditLog] = useState(false); + const [showNvdSync, setShowNvdSync] = useState(false); const [newCVE, setNewCVE] = useState({ cve_id: '', vendor: '', @@ -37,6 +39,42 @@ export default function App() { published_date: new Date().toISOString().split('T')[0] }); const [uploadingFile, setUploadingFile] = useState(false); + const [nvdLoading, setNvdLoading] = useState(false); + const [nvdError, setNvdError] = useState(null); + const [nvdAutoFilled, setNvdAutoFilled] = useState(false); + + const lookupNVD = async (cveId) => { + const trimmed = cveId.trim(); + if (!/^CVE-\d{4}-\d{4,}$/.test(trimmed)) return; + + setNvdLoading(true); + setNvdError(null); + setNvdAutoFilled(false); + + try { + const response = await fetch(`${API_BASE}/nvd/lookup/${encodeURIComponent(trimmed)}`, { + credentials: 'include' + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'NVD lookup failed'); + } + + const data = await response.json(); + setNewCVE(prev => ({ + ...prev, + description: prev.description || data.description, + severity: data.severity, + published_date: data.published_date || prev.published_date + })); + setNvdAutoFilled(true); + } catch (err) { + setNvdError(err.message); + } finally { + setNvdLoading(false); + } + }; const fetchCVEs = async () => { setLoading(true); @@ -163,6 +201,9 @@ export default function App() { description: '', published_date: new Date().toISOString().split('T')[0] }); + setNvdLoading(false); + setNvdError(null); + setNvdAutoFilled(false); fetchCVEs(); } catch (err) { alert(`Error: ${err.message}`); @@ -297,6 +338,15 @@ export default function App() {
Query vulnerabilities, manage vendors, and attach documentation