From 33e449f52058992e10b05170c8f504e87c5e69f7 Mon Sep 17 00:00:00 2001
From: Jordan Ramos
Date: Fri, 22 May 2026 14:15:06 -0600
Subject: [PATCH] Add Jira Tickets, CCP Metrics, and Remediation Status export
cards
New export cards on the Exports page:
- Jira Tickets: All tickets, open/active only, by-CVE multi-sheet
- CCP Compliance Metrics: Current snapshot, non-compliant devices,
trend history, full multi-sheet report
- Remediation Status: Cross-domain report combining CVEs, Jira tickets,
Archer exceptions, and Ivanti findings into a per-CVE progress view
---
frontend/src/components/pages/ExportsPage.js | 311 +++++++++++++++++++
1 file changed, 311 insertions(+)
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.
+
+
+
);