-
+
{hostName || 'Unknown Host'}
diff --git a/frontend/src/components/pages/ExportsPage.js b/frontend/src/components/pages/ExportsPage.js
index 08ea97a..fe560e0 100644
--- a/frontend/src/components/pages/ExportsPage.js
+++ b/frontend/src/components/pages/ExportsPage.js
@@ -1,7 +1,8 @@
import React, { useState, useCallback } from 'react';
import * as XLSX from 'xlsx';
-import { Download, Loader, AlertCircle, BarChart2, FileText, Shield, Tag, CheckCircle, X } from 'lucide-react';
+import { Download, Loader, AlertCircle, BarChart2, FileText, Shield, Tag, CheckCircle, X, Database } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
+import AtlasIcon from '../AtlasIcon';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const EXC_PATTERN = /EXC-\d+/i;
@@ -122,6 +123,31 @@ async function fetchCompliance() {
return res.json();
}
+async function fetchAtlasStatus() {
+ const res = await fetch(`${API_BASE}/atlas/status`, { credentials: 'include' });
+ if (!res.ok) throw new Error(`Atlas status returned ${res.status}`);
+ return res.json();
+}
+
+async function fetchAtlasAndFindings() {
+ const [atlasRows, findings] = await Promise.all([fetchAtlasStatus(), fetchFindings()]);
+ // Build a lookup from hostId → finding details (hostname, IP, BU, etc.)
+ const hostMap = {};
+ findings.forEach(f => {
+ if (f.hostId && !hostMap[f.hostId]) {
+ hostMap[f.hostId] = {
+ hostName: f.overrides?.hostName ?? f.hostName ?? '',
+ ipAddress: f.ipAddress ?? '',
+ dns: f.overrides?.dns ?? f.dns ?? '',
+ buOwnership: f.buOwnership ?? '',
+ findingCount: 0,
+ };
+ }
+ if (f.hostId && hostMap[f.hostId]) hostMap[f.hostId].findingCount++;
+ });
+ return { atlasRows, hostMap };
+}
+
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
@@ -333,6 +359,70 @@ export default function ExportsPage() {
toXLSX([headers, ...rows], 'Compliance', `compliance-report-${dateStr()}.xlsx`);
});
+ // ---- Card 6: Atlas Action Plans ----
+
+ const ATLAS_HEADERS = ['Host ID', 'Hostname', 'IP Address', 'Business Unit', 'Open Findings', 'Active Plans', 'Plan Type', 'Commit Date', 'Status', 'Qualys ID', 'Findings ID', 'VNR', 'EXC', 'Last Synced'];
+
+ function atlasRow(atlasEntry, hostInfo) {
+ const plans = JSON.parse(atlasEntry.plans_json || '[]');
+ const activePlans = plans.filter(p => p.status === 'active');
+ const h = hostInfo || {};
+ if (activePlans.length === 0) {
+ return [[
+ atlasEntry.host_id, h.hostName || '', h.ipAddress || '', h.buOwnership || '',
+ h.findingCount || '', 0, '', '', 'No Plan', '', '', '', '', atlasEntry.synced_at || '',
+ ]];
+ }
+ return activePlans.map(p => [
+ atlasEntry.host_id, h.hostName || '', h.ipAddress || '', h.buOwnership || '',
+ h.findingCount || '', activePlans.length,
+ (p.plan_type || '').replace(/_/g, ' '), p.commit_date || '', p.status || '',
+ p.qualys_id || '', p.active_host_findings_id || '',
+ p.jira_vnr || '', p.archer_exc || '', atlasEntry.synced_at || '',
+ ]);
+ }
+
+ const exportAtlasStatus = () => run('atlas-status', async () => {
+ const { atlasRows, hostMap } = await fetchAtlasAndFindings();
+ const rows = atlasRows.flatMap(a => atlasRow(a, hostMap[a.host_id]));
+ toXLSX([ATLAS_HEADERS, ...rows], 'Atlas Status', `atlas-action-plans-${dateStr()}.xlsx`);
+ });
+
+ const exportAtlasGaps = () => run('atlas-gaps', async () => {
+ const { atlasRows, hostMap } = await fetchAtlasAndFindings();
+ const gaps = atlasRows.filter(a => !a.has_action_plan);
+ const rows = gaps.flatMap(a => atlasRow(a, hostMap[a.host_id]));
+ toXLSX([ATLAS_HEADERS, ...rows], 'Coverage Gaps', `atlas-coverage-gaps-${dateStr()}.xlsx`);
+ });
+
+ const exportAtlasFull = () => run('atlas-full', async () => {
+ const { atlasRows, hostMap } = await fetchAtlasAndFindings();
+ const withPlans = atlasRows.filter(a => a.has_action_plan);
+ const withoutPlans = atlasRows.filter(a => !a.has_action_plan);
+ const sheets = [
+ { name: 'Active Plans', rows: [ATLAS_HEADERS, ...withPlans.flatMap(a => atlasRow(a, hostMap[a.host_id]))] },
+ { name: 'No Plan', rows: [ATLAS_HEADERS, ...withoutPlans.flatMap(a => atlasRow(a, hostMap[a.host_id]))] },
+ ];
+ // Add history sheet with inactive plans
+ const historyHeaders = ['Host ID', 'Hostname', 'Plan Type', 'Commit Date', 'Status', 'Qualys ID', 'Findings ID', 'VNR', 'EXC', 'Created'];
+ const historyRows = [];
+ atlasRows.forEach(a => {
+ const plans = JSON.parse(a.plans_json || '[]');
+ const inactive = plans.filter(p => p.status !== 'active');
+ const h = hostMap[a.host_id] || {};
+ inactive.forEach(p => {
+ historyRows.push([
+ a.host_id, h.hostName || '',
+ (p.plan_type || '').replace(/_/g, ' '), p.commit_date || '', p.status || '',
+ p.qualys_id || '', p.active_host_findings_id || '',
+ p.jira_vnr || '', p.archer_exc || '', p.created_at ? p.created_at.split('T')[0] : '',
+ ]);
+ });
+ });
+ sheets.push({ name: 'History', rows: [historyHeaders, ...historyRows] });
+ toMultiXLSX(sheets, `atlas-full-report-${dateStr()}.xlsx`);
+ });
+
// ---- Render ----
if (!canExport()) {
@@ -465,6 +555,25 @@ export default function ExportsPage() {
+ {/* ── Card 6: Atlas Action Plans ── */}
+