diff --git a/frontend/src/components/pages/ExportsPage.js b/frontend/src/components/pages/ExportsPage.js index 95e20ad..b8c6dfd 100644 --- a/frontend/src/components/pages/ExportsPage.js +++ b/frontend/src/components/pages/ExportsPage.js @@ -132,6 +132,42 @@ async function fetchAtlasStatus() { return res.json(); } +async function fetchJiraTickets() { + const res = await fetch(`${API_BASE}/jira-tickets`, { credentials: 'include' }); + if (!res.ok) throw new Error(`Jira tickets returned ${res.status}`); + return res.json(); +} + +async function fetchCCPStats() { + const res = await fetch(`${API_BASE}/compliance/vcl-multi/stats`, { credentials: 'include' }); + if (!res.ok) throw new Error(`CCP stats returned ${res.status}`); + return res.json(); +} + +async function fetchCCPVerticals() { + const res = await fetch(`${API_BASE}/compliance/vcl-multi/verticals`, { credentials: 'include' }); + if (!res.ok) throw new Error(`CCP verticals returned ${res.status}`); + return res.json(); +} + +async function fetchCCPMetrics() { + const res = await fetch(`${API_BASE}/compliance/vcl-multi/metrics`, { credentials: 'include' }); + if (!res.ok) throw new Error(`CCP metrics returned ${res.status}`); + return res.json(); +} + +async function fetchCCPTrend() { + const res = await fetch(`${API_BASE}/compliance/vcl-multi/trend`, { credentials: 'include' }); + if (!res.ok) throw new Error(`CCP trend returned ${res.status}`); + return res.json(); +} + +async function fetchCCPVerticalMetrics(code) { + const res = await fetch(`${API_BASE}/compliance/vcl-multi/vertical/${encodeURIComponent(code)}/metrics`, { credentials: 'include' }); + if (!res.ok) throw new Error(`CCP vertical metrics returned ${res.status}`); + return res.json(); +} + async function fetchAtlasAndFindings(teamsParam) { const [atlasRows, findings] = await Promise.all([fetchAtlasStatus(), fetchFindings(teamsParam)]); // Build a lookup from hostId → finding details (hostname, IP, BU, etc.) @@ -430,6 +466,234 @@ export default function ExportsPage() { toMultiXLSX(sheets, `atlas-full-report-${dateStr()}.xlsx`); }); + // ---- Card 7: Jira Tickets ---- + + const exportJiraAll = () => run('jira-all', async () => { + const tickets = await fetchJiraTickets(); + const headers = ['Ticket Key', 'CVE', 'Vendor', 'Summary', 'Status', 'Source', 'URL', 'Last Synced', 'Created']; + const rows = tickets.map(t => [ + t.ticket_key, t.cve_id, t.vendor || '', t.summary || '', t.status || 'Open', + t.source_context || 'cve', t.url || '', + t.last_synced_at ? new Date(t.last_synced_at).toLocaleDateString() : 'Never', + t.created_at ? new Date(t.created_at).toLocaleDateString() : '', + ]); + toXLSX([headers, ...rows], 'All Tickets', `jira-tickets-all-${dateStr()}.xlsx`); + }); + + const exportJiraOpen = () => run('jira-open', async () => { + const tickets = await fetchJiraTickets(); + const closedStatuses = ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined']; + const open = tickets.filter(t => { + const lower = (t.status || '').toLowerCase(); + return !closedStatuses.some(s => lower.includes(s)); + }); + const headers = ['Ticket Key', 'CVE', 'Vendor', 'Summary', 'Status', 'Source', 'URL', 'Last Synced', 'Created']; + const rows = open.map(t => [ + t.ticket_key, t.cve_id, t.vendor || '', t.summary || '', t.status || 'Open', + t.source_context || 'cve', t.url || '', + t.last_synced_at ? new Date(t.last_synced_at).toLocaleDateString() : 'Never', + t.created_at ? new Date(t.created_at).toLocaleDateString() : '', + ]); + toXLSX([headers, ...rows], 'Open Tickets', `jira-tickets-open-${dateStr()}.xlsx`); + }); + + const exportJiraByCVE = () => run('jira-by-cve', async () => { + const tickets = await fetchJiraTickets(); + const groups = {}; + tickets.forEach(t => { + const key = t.cve_id || 'No CVE'; + if (!groups[key]) groups[key] = []; + groups[key].push(t); + }); + const headers = ['Ticket Key', 'Vendor', 'Summary', 'Status', 'Source', 'URL', 'Last Synced']; + const sheets = Object.entries(groups) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([cve, tix]) => ({ + name: cve.slice(0, 31), + rows: [headers, ...tix.map(t => [ + t.ticket_key, t.vendor || '', t.summary || '', t.status || 'Open', + t.source_context || 'cve', t.url || '', + t.last_synced_at ? new Date(t.last_synced_at).toLocaleDateString() : 'Never', + ])], + })); + if (sheets.length === 0) sheets.push({ name: 'No Data', rows: [headers] }); + toMultiXLSX(sheets, `jira-tickets-by-cve-${dateStr()}.xlsx`); + }); + + // ---- Card 8: CCP Metrics ---- + + const exportCCPSnapshot = () => run('ccp-snapshot', async () => { + const stats = await fetchCCPStats(); + const verticals = stats.verticals || []; + const headers = ['Vertical', 'Total Devices', 'Non-Compliant', 'Compliance %', 'Failing Metrics', 'Report Date']; + const rows = verticals.map(v => [ + v.vertical || v.code || '', + v.total_devices ?? v.totalDevices ?? '', + v.non_compliant_devices ?? v.nonCompliantDevices ?? '', + v.compliance_pct != null ? `${Number(v.compliance_pct).toFixed(1)}%` : (v.compliancePct != null ? `${Number(v.compliancePct).toFixed(1)}%` : ''), + v.failing_metrics ?? v.failingMetrics ?? '', + v.report_date ?? v.reportDate ?? '', + ]); + toXLSX([headers, ...rows], 'CCP Snapshot', `ccp-compliance-snapshot-${dateStr()}.xlsx`); + }); + + const exportCCPNonCompliant = () => run('ccp-noncompliant', async () => { + const verticals = await fetchCCPVerticals(); + const allRows = []; + for (const v of verticals) { + const code = v.code || v.vertical; + if (!code) continue; + try { + const metrics = await fetchCCPVerticalMetrics(code); + const metricList = metrics.metrics || metrics || []; + metricList.forEach(m => { + const devices = m.devices || []; + devices.forEach(d => { + allRows.push([ + code, m.metric_id || m.metricId || '', m.metric_desc || m.metricDesc || '', + d.hostname || '', d.ip_address || d.ipAddress || '', d.device_type || d.deviceType || '', + d.team || '', + ]); + }); + }); + } catch (e) { + // Skip verticals that fail + } + } + const headers = ['Vertical', 'Metric ID', 'Metric Description', 'Hostname', 'IP Address', 'Device Type', 'Team']; + toXLSX([headers, ...allRows], 'Non-Compliant Devices', `ccp-non-compliant-devices-${dateStr()}.xlsx`); + }); + + const exportCCPTrend = () => run('ccp-trend', async () => { + const trend = await fetchCCPTrend(); + const snapshots = trend.snapshots || trend || []; + const headers = ['Date', 'Vertical', 'Total Devices', 'Non-Compliant', 'Compliance %']; + const rows = snapshots.flatMap(s => { + const date = s.report_date || s.reportDate || s.date || ''; + const verts = s.verticals || [s]; + return verts.map(v => [ + date, + v.vertical || v.code || '', + v.total_devices ?? v.totalDevices ?? '', + v.non_compliant_devices ?? v.nonCompliantDevices ?? '', + v.compliance_pct != null ? `${Number(v.compliance_pct).toFixed(1)}%` : '', + ]); + }); + toXLSX([headers, ...rows], 'Trend', `ccp-compliance-trend-${dateStr()}.xlsx`); + }); + + const exportCCPFull = () => run('ccp-full', async () => { + const [stats, trend] = await Promise.all([fetchCCPStats(), fetchCCPTrend()]); + const verticals = stats.verticals || []; + const snapshots = trend.snapshots || trend || []; + + // Sheet 1: Summary + const summaryHeaders = ['Vertical', 'Total Devices', 'Non-Compliant', 'Compliance %', 'Failing Metrics', 'Report Date']; + const summaryRows = verticals.map(v => [ + v.vertical || v.code || '', + v.total_devices ?? v.totalDevices ?? '', + v.non_compliant_devices ?? v.nonCompliantDevices ?? '', + v.compliance_pct != null ? `${Number(v.compliance_pct).toFixed(1)}%` : '', + v.failing_metrics ?? v.failingMetrics ?? '', + v.report_date ?? v.reportDate ?? '', + ]); + + // Sheet 2: Trend + const trendHeaders = ['Date', 'Vertical', 'Total Devices', 'Non-Compliant', 'Compliance %']; + const trendRows = snapshots.flatMap(s => { + const date = s.report_date || s.reportDate || s.date || ''; + const verts = s.verticals || [s]; + return verts.map(v => [ + date, v.vertical || v.code || '', + v.total_devices ?? v.totalDevices ?? '', + v.non_compliant_devices ?? v.nonCompliantDevices ?? '', + v.compliance_pct != null ? `${Number(v.compliance_pct).toFixed(1)}%` : '', + ]); + }); + + toMultiXLSX([ + { name: 'Summary', rows: [summaryHeaders, ...summaryRows] }, + { name: 'Trend', rows: [trendHeaders, ...trendRows] }, + ], `ccp-full-report-${dateStr()}.xlsx`); + }); + + // ---- Card 9: Remediation Status (Cross-Domain) ---- + + const exportRemediationStatus = () => run('remediation', async () => { + const [cves, tickets, archer, findings] = await Promise.all([ + fetchCVEs(''), + fetchJiraTickets(), + fetchArcher(), + fetchFindings(teamsParam), + ]); + + // Build lookup maps + const ticketsByCVE = {}; + tickets.forEach(t => { + const key = `${t.cve_id}|${t.vendor || ''}`; + if (!ticketsByCVE[key]) ticketsByCVE[key] = []; + ticketsByCVE[key].push(t); + }); + + const archerByCVE = {}; + archer.forEach(a => { + const key = `${a.cve_id}|${a.vendor || ''}`; + if (!archerByCVE[key]) archerByCVE[key] = []; + archerByCVE[key].push(a); + }); + + const findingsByCVE = {}; + findings.forEach(f => { + (f.cves || []).forEach(cve => { + if (!findingsByCVE[cve]) findingsByCVE[cve] = []; + findingsByCVE[cve].push(f); + }); + }); + + const headers = [ + 'CVE ID', 'Vendor', 'Severity', 'CVE Status', + 'Jira Tickets', 'Jira Statuses', + 'Archer EXC#', 'Archer Status', + 'Ivanti Findings', 'Overdue Findings', + 'Overall Progress', + ]; + + const rows = cves.map(c => { + const key = `${c.cve_id}|${c.vendor}`; + const cveTickets = ticketsByCVE[key] || []; + const cveArcher = archerByCVE[key] || []; + const cveFindings = findingsByCVE[c.cve_id] || []; + const today = dateStr(); + const overdueCount = cveFindings.filter(f => f.dueDate && f.dueDate < today).length; + + // Determine overall progress + let progress = 'Not Started'; + if (cveTickets.length > 0 || cveArcher.length > 0) { + const closedKeywords = ['closed', 'done', 'resolved', 'complete', 'completed']; + const allTicketsClosed = cveTickets.length > 0 && cveTickets.every(t => closedKeywords.some(s => (t.status || '').toLowerCase().includes(s))); + const allArcherAccepted = cveArcher.length > 0 && cveArcher.every(a => a.status === 'Accepted'); + if (allTicketsClosed && (cveArcher.length === 0 || allArcherAccepted)) { + progress = 'Complete'; + } else { + progress = 'In Progress'; + } + } + + return [ + c.cve_id, c.vendor, c.severity, c.status, + cveTickets.map(t => t.ticket_key).join(', '), + cveTickets.map(t => `${t.ticket_key}: ${t.status || 'Open'}`).join('; '), + cveArcher.map(a => a.exc_number).join(', '), + cveArcher.map(a => `${a.exc_number}: ${a.status}`).join('; '), + cveFindings.length, + overdueCount, + progress, + ]; + }); + + toXLSX([headers, ...rows], 'Remediation Status', `remediation-status-${dateStr()}.xlsx`); + }); + // ---- Render ---- if (!canExport()) { @@ -581,6 +845,53 @@ export default function ExportsPage() {

+ {/* ── Card 7: Jira Tickets ── */} + +
+ + +
+
+ +
+
+ + {/* ── Card 8: CCP Metrics ── */} + +
+ + + + +
+

+ "Non-Compliant Devices" fetches per-metric device lists for all verticals — may take a moment. +

+
+ + {/* ── Card 9: Remediation Status (Cross-Domain) ── */} + + +

+ Pulls from CVE database, Jira tickets, Archer tickets, and Ivanti findings cache. Best for leadership status updates. +

+
+ );