diff --git a/frontend/src/components/AtlasIcon.js b/frontend/src/components/AtlasIcon.js new file mode 100644 index 0000000..4b94479 --- /dev/null +++ b/frontend/src/components/AtlasIcon.js @@ -0,0 +1,48 @@ +import React from 'react'; + +// --------------------------------------------------------------------------- +// AtlasIcon — SVG recreation of the Atlas InfoSec logo icon. +// A rounded badge/card shape with a globe (crosshair grid) inside. +// Accepts the same props as lucide-react icons: style, width, height, color. +// ⚠️ CONVENTION: Uses raw SVG instead of lucide-react. Acceptable here because +// this is a custom brand icon (Atlas InfoSec logo) with no lucide-react equivalent. +// --------------------------------------------------------------------------- +export default function AtlasIcon({ style, width, height, color, ...props }) { + const w = width || (style && style.width) || 24; + const h = height || (style && style.height) || 24; + const c = color || (style && style.color) || 'currentColor'; + + return ( + + {/* Badge / card outline — rounded rectangle */} + + + {/* Globe circle */} + + + {/* Globe horizontal line */} + + + {/* Globe vertical line */} + + + {/* Globe left meridian arc */} + + + {/* Globe right meridian arc */} + + + ); +} diff --git a/frontend/src/components/AtlasSlideOutPanel.js b/frontend/src/components/AtlasSlideOutPanel.js index ba01355..1565ff6 100644 --- a/frontend/src/components/AtlasSlideOutPanel.js +++ b/frontend/src/components/AtlasSlideOutPanel.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { X, Loader, Shield, Plus, Edit3, Check, AlertCircle, ChevronDown } from 'lucide-react'; +import AtlasIcon from './AtlasIcon'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; @@ -623,7 +624,7 @@ export default function AtlasSlideOutPanel({ hostId, hostName, findingId, qualys
- + {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 ── */} + +
+ + +
+
+ +
+

+ "Full Report" creates three sheets: Active Plans, No Plan, and History (overridden plans). +

+
+
); diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index 9ea821e..f9014fd 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -8,6 +8,7 @@ import CveTooltip from '../CveTooltip'; import RedirectModal from '../RedirectModal'; import AtlasBadge from '../AtlasBadge'; import AtlasSlideOutPanel from '../AtlasSlideOutPanel'; +import AtlasIcon from '../AtlasIcon'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const STORAGE_KEY = 'steam_findings_columns_v2'; @@ -4508,7 +4509,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { > {atlasSyncing ? - : } + : } Atlas