diff --git a/backend/server.js b/backend/server.js index 02bfd4f..e2b9c83 100644 --- a/backend/server.js +++ b/backend/server.js @@ -302,6 +302,17 @@ app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => { }); +// Compliance export — reads from cve_document_status view +app.get('/api/cves/compliance', requireAuth(db), (req, res) => { + db.all('SELECT * FROM cve_document_status ORDER BY cve_id, vendor', [], (err, rows) => { + if (err) { + console.error('Error fetching compliance data:', err); + return res.status(500).json({ error: 'Internal server error.' }); + } + res.json(rows); + }); +}); + // Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin) app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { cve_id, vendor, severity, description, published_date } = req.body; diff --git a/frontend/src/components/pages/ExportsPage.js b/frontend/src/components/pages/ExportsPage.js index afbf95b..42da1ea 100644 --- a/frontend/src/components/pages/ExportsPage.js +++ b/frontend/src/components/pages/ExportsPage.js @@ -1,25 +1,460 @@ -import React from 'react'; -import { Download } from 'lucide-react'; +import React, { useState, useCallback } from 'react'; +import * as XLSX from 'xlsx'; +import { Download, Loader, AlertCircle, BarChart2, FileText, Shield, Tag, CheckCircle, X } from 'lucide-react'; -export default function ExportsPage() { - return ( -
-
-
- -
-

- Exports -

-

- Under construction — coming soon -

-
-
- ); +const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; +const EXC_PATTERN = /EXC-\d+/i; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +function classifyFinding(f) { + if (f.workflow != null) return 'fp'; + if (EXC_PATTERN.test(f.note || '')) return 'archer'; + return 'pending'; +} + +const dateStr = () => new Date().toISOString().slice(0, 10); + +function triggerDownload(blob, filename) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function autoFit(ws, rows) { + if (!rows[0]) return; + ws['!cols'] = rows[0].map((_, ci) => ({ + wch: Math.min(60, Math.max(10, ...rows.map(r => String(r[ci] ?? '').length))) + })); +} + +function toXLSX(rows, sheetName, filename) { + const ws = XLSX.utils.aoa_to_sheet(rows); + autoFit(ws, rows); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, sheetName); + XLSX.writeFile(wb, filename); +} + +function toMultiXLSX(sheets, filename) { + const wb = XLSX.utils.book_new(); + sheets.forEach(({ name, rows }) => { + const ws = XLSX.utils.aoa_to_sheet(rows); + autoFit(ws, rows); + XLSX.utils.book_append_sheet(wb, ws, String(name || 'Unknown').slice(0, 31)); + }); + XLSX.writeFile(wb, filename); +} + +function toCSV(rows, filename) { + const csv = rows.map(row => + row.map(cell => { + const s = String(cell ?? ''); + return (s.includes(',') || s.includes('"') || s.includes('\n')) + ? `"${s.replace(/"/g, '""')}"` : s; + }).join(',') + ).join('\r\n'); + triggerDownload(new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }), filename); +} + +// --------------------------------------------------------------------------- +// Finding column definitions +// --------------------------------------------------------------------------- +const FINDING_HEADERS = [ + 'Finding ID', 'Title', 'Severity Score', 'Severity Group', + 'Host', 'IP Address', 'DNS', 'Due Date', 'SLA Status', + 'Business Unit', 'FP# ID', 'FP# State', 'Last Found', 'CVEs', 'Notes', +]; + +function findingRow(f) { + return [ + f.id, + f.title, + f.severity != null ? Number(f.severity).toFixed(2) : '', + f.vrrGroup ?? '', + f.overrides?.hostName ?? f.hostName ?? '', + f.ipAddress ?? '', + f.overrides?.dns ?? f.dns ?? '', + f.dueDate ?? '', + f.slaStatus ?? '', + f.buOwnership ?? '', + f.workflow?.id ?? '', + f.workflow?.state ?? '', + f.lastFoundOn ?? '', + (f.cves || []).join(', '), + f.note ?? '', + ]; +} + +// --------------------------------------------------------------------------- +// API fetchers +// --------------------------------------------------------------------------- +async function fetchFindings() { + const res = await fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' }); + if (!res.ok) throw new Error(`Ivanti findings returned ${res.status}`); + const data = await res.json(); + return data.findings || []; +} + +async function fetchCVEs(status) { + const url = status ? `${API_BASE}/cves?status=${encodeURIComponent(status)}` : `${API_BASE}/cves`; + const res = await fetch(url, { credentials: 'include' }); + if (!res.ok) throw new Error(`CVE list returned ${res.status}`); + return res.json(); +} + +async function fetchArcher() { + const res = await fetch(`${API_BASE}/archer-tickets`, { credentials: 'include' }); + if (!res.ok) throw new Error(`Archer tickets returned ${res.status}`); + return res.json(); +} + +async function fetchCompliance() { + const res = await fetch(`${API_BASE}/cves/compliance`, { credentials: 'include' }); + if (!res.ok) throw new Error(`Compliance data returned ${res.status}`); + return res.json(); +} + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- +function ExportCard({ color, colorRgb, icon: Icon, title, description, children }) { + return ( +
+
+ +

+ {title} +

+
+

+ {description} +

+
+ {children} +
+
+ ); +} + +function ExportBtn({ label, exportKey, loading, color, colorRgb, onClick, disabled }) { + const isLoading = loading === exportKey; + return ( + + ); +} + +function Toggle({ label, checked, onChange, color, colorRgb }) { + return ( +