import React, { useState, useCallback } from 'react';
import * as XLSX from 'xlsx';
import { Download, Loader, AlertCircle, BarChart2, FileText, Shield, Tag, CheckCircle, X } from 'lucide-react';
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 (
);
}
// ---------------------------------------------------------------------------
// Main page
// ---------------------------------------------------------------------------
export default function ExportsPage() {
const [loading, setLoading] = useState(null);
const [error, setError] = useState(null);
const [cveStatus, setCveStatus] = useState('');
const [missingOnly, setMissingOnly] = useState(false);
const run = useCallback(async (key, fn) => {
setLoading(key);
setError(null);
try {
await fn();
} catch (e) {
console.error('[Export]', e);
setError(e.message || 'Export failed — check console for details');
} finally {
setLoading(null);
}
}, []);
// ---- Card 1: Ivanti Findings ----
const exportFullFindings = () => run('ivanti-full', async () => {
const findings = await fetchFindings();
toXLSX(
[FINDING_HEADERS, ...findings.map(findingRow)],
'All Findings',
`findings-full-${dateStr()}.xlsx`,
);
});
const exportPending = () => run('ivanti-pending', async () => {
const findings = await fetchFindings();
const rows = findings.filter(f => classifyFinding(f) === 'pending').map(findingRow);
toXLSX([FINDING_HEADERS, ...rows], 'Pending Action', `findings-pending-${dateStr()}.xlsx`);
});
const exportOverdue = () => run('ivanti-overdue', async () => {
const findings = await fetchFindings();
const today = dateStr();
const rows = findings.filter(f => {
if (!f.dueDate && !(f.slaStatus || '').toLowerCase().includes('overdue')) return false;
return f.dueDate < today || (f.slaStatus || '').toUpperCase() === 'OVERDUE';
}).map(findingRow);
toXLSX([FINDING_HEADERS, ...rows], 'Overdue', `findings-overdue-${dateStr()}.xlsx`);
});
const exportByBU = () => run('ivanti-bu', async () => {
const findings = await fetchFindings();
const groups = {};
findings.forEach(f => {
const bu = f.buOwnership || 'Unknown';
if (!groups[bu]) groups[bu] = [];
groups[bu].push(f);
});
const sheets = Object.entries(groups)
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, rows]) => ({ name, rows: [FINDING_HEADERS, ...rows.map(findingRow)] }));
if (sheets.length === 0) sheets.push({ name: 'No Data', rows: [FINDING_HEADERS] });
toMultiXLSX(sheets, `findings-by-bu-${dateStr()}.xlsx`);
});
// ---- Card 2: FP Workflow Summary ----
const exportFPSummary = () => run('fp-summary', async () => {
const findings = await fetchFindings();
const fpMap = {};
findings.forEach(f => {
if (!f.workflow?.id) return;
const id = f.workflow.id;
if (!fpMap[id]) fpMap[id] = { id, state: f.workflow.state || '', count: 0, hosts: new Set(), bus: new Set(), cves: new Set() };
fpMap[id].count++;
const host = f.overrides?.hostName ?? f.hostName;
if (host) fpMap[id].hosts.add(host);
if (f.buOwnership) fpMap[id].bus.add(f.buOwnership);
(f.cves || []).forEach(c => fpMap[id].cves.add(c));
});
const headers = ['FP# ID', 'State', 'Finding Count', 'Hosts', 'Business Units', 'CVEs'];
const rows = Object.values(fpMap)
.sort((a, b) => a.id.localeCompare(b.id))
.map(e => [e.id, e.state, e.count, [...e.hosts].join(', '), [...e.bus].join(', '), [...e.cves].join(', ')]);
toXLSX([headers, ...rows], 'FP Workflows', `fp-workflow-summary-${dateStr()}.xlsx`);
});
// ---- Card 3: CVE Database ----
const exportCVEs = (fmt) => run(`cves-${fmt}`, async () => {
const data = await fetchCVEs(cveStatus);
const headers = ['CVE ID', 'Vendor', 'Severity', 'Status', 'Published Date', 'Description', 'Documents'];
const rows = data.map(c => [c.cve_id, c.vendor, c.severity, c.status, c.published_date ?? '', c.description ?? '', c.document_count ?? 0]);
if (fmt === 'csv') {
toCSV([headers, ...rows], `cve-database-${dateStr()}.csv`);
} else {
toXLSX([headers, ...rows], 'CVEs', `cve-database-${dateStr()}.xlsx`);
}
});
// ---- Card 4: Archer Tickets ----
const exportArcher = () => run('archer', async () => {
const data = await fetchArcher();
const headers = ['EXC Number', 'Status', 'CVE ID', 'Vendor', 'Archer URL', 'Created'];
const rows = data.map(t => [t.exc_number, t.status, t.cve_id ?? '', t.vendor ?? '', t.archer_url ?? '', t.created_at ?? '']);
toXLSX([headers, ...rows], 'Archer Tickets', `archer-tickets-${dateStr()}.xlsx`);
});
// ---- Card 5: Compliance Report ----
const exportCompliance = () => run('compliance', async () => {
const data = await fetchCompliance();
const filtered = missingOnly ? data.filter(r => r.compliance_status !== 'Complete') : data;
const headers = ['CVE ID', 'Vendor', 'Severity', 'Status', 'Total Docs', 'Advisory Docs', 'Email Docs', 'Screenshot Docs', 'Compliance Status'];
const rows = filtered.map(r => [r.cve_id, r.vendor, r.severity, r.status, r.total_documents, r.advisory_count, r.email_count, r.screenshot_count, r.compliance_status]);
toXLSX([headers, ...rows], 'Compliance', `compliance-report-${dateStr()}.xlsx`);
});
// ---- Render ----
return (
{/* Page header */}
Exports
{/* Error banner */}
{error && (
{error}
)}
{/* Card grid */}
{/* ── Card 1: Ivanti Findings ── */}
"By Business Unit" creates one sheet per BU in a single workbook.
{/* ── Card 2: FP Workflow Summary ── */}
{/* ── Card 3: CVE Database ── */}
Status
exportCVEs('csv')} />
exportCVEs('xlsx')} />
{/* ── Card 4: Archer Tickets ── */}
{/* ── Card 5: Compliance Report ── */}
);
}