Add Atlas exports and custom Atlas InfoSec icon
Exports page: - Add Atlas Action Plans export card with three reports: Full Status, Coverage Gaps, and Full Report (multi-sheet with active, gaps, history) - Reports join Atlas cache with Ivanti findings for hostname, IP, BU context Atlas icon: - Add AtlasIcon SVG component matching the Atlas InfoSec logo (badge with globe) - Replace Database icon with AtlasIcon on exports card, sync button, and panel header
This commit is contained in:
48
frontend/src/components/AtlasIcon.js
Normal file
48
frontend/src/components/AtlasIcon.js
Normal file
@@ -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 (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
fill="none"
|
||||||
|
stroke={c}
|
||||||
|
strokeWidth="1.75"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ ...style, width: w, height: h, flexShrink: 0 }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* Badge / card outline — rounded rectangle */}
|
||||||
|
<rect x="3" y="2" width="18" height="20" rx="3" ry="3" />
|
||||||
|
|
||||||
|
{/* Globe circle */}
|
||||||
|
<circle cx="12" cy="11" r="5.5" />
|
||||||
|
|
||||||
|
{/* Globe horizontal line */}
|
||||||
|
<line x1="6.5" y1="11" x2="17.5" y2="11" />
|
||||||
|
|
||||||
|
{/* Globe vertical line */}
|
||||||
|
<line x1="12" y1="5.5" x2="12" y2="16.5" />
|
||||||
|
|
||||||
|
{/* Globe left meridian arc */}
|
||||||
|
<path d="M9.5 5.8C8.6 7.3 8 9 8 11s0.6 3.7 1.5 5.2" />
|
||||||
|
|
||||||
|
{/* Globe right meridian arc */}
|
||||||
|
<path d="M14.5 5.8C15.4 7.3 16 9 16 11s-0.6 3.7-1.5 5.2" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { X, Loader, Shield, Plus, Edit3, Check, AlertCircle, ChevronDown } from 'lucide-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';
|
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
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
<div style={{ minWidth: 0, flex: 1, marginRight: '0.75rem' }}>
|
<div style={{ minWidth: 0, flex: 1, marginRight: '0.75rem' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.3rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.3rem' }}>
|
||||||
<Shield style={{ width: 16, height: 16, color: ACCENT, flexShrink: 0 }} />
|
<AtlasIcon style={{ width: 16, height: 16, color: ACCENT, flexShrink: 0 }} />
|
||||||
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#E2E8F0', wordBreak: 'break-all', lineHeight: 1.3 }}>
|
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#E2E8F0', wordBreak: 'break-all', lineHeight: 1.3 }}>
|
||||||
{hostName || 'Unknown Host'}
|
{hostName || 'Unknown Host'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import * as XLSX from 'xlsx';
|
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 { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import AtlasIcon from '../AtlasIcon';
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
const EXC_PATTERN = /EXC-\d+/i;
|
const EXC_PATTERN = /EXC-\d+/i;
|
||||||
@@ -122,6 +123,31 @@ async function fetchCompliance() {
|
|||||||
return res.json();
|
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
|
// Sub-components
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -333,6 +359,70 @@ export default function ExportsPage() {
|
|||||||
toXLSX([headers, ...rows], 'Compliance', `compliance-report-${dateStr()}.xlsx`);
|
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 ----
|
// ---- Render ----
|
||||||
|
|
||||||
if (!canExport()) {
|
if (!canExport()) {
|
||||||
@@ -465,6 +555,25 @@ export default function ExportsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</ExportCard>
|
</ExportCard>
|
||||||
|
|
||||||
|
{/* ── Card 6: Atlas Action Plans ── */}
|
||||||
|
<ExportCard
|
||||||
|
color="#A855F7" colorRgb="168,85,247"
|
||||||
|
icon={AtlasIcon}
|
||||||
|
title="Atlas Action Plans"
|
||||||
|
description="Export Atlas InfoSec action plan status for all synced hosts. Includes plan type, commit date, and coverage status. Three report types: full status, coverage gaps only, and a multi-sheet workbook with active plans, gaps, and plan history."
|
||||||
|
>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem' }}>
|
||||||
|
<ExportBtn label="Full Status" exportKey="atlas-status" loading={loading} color="#A855F7" colorRgb="168,85,247" onClick={exportAtlasStatus} />
|
||||||
|
<ExportBtn label="Coverage Gaps" exportKey="atlas-gaps" loading={loading} color="#A855F7" colorRgb="168,85,247" onClick={exportAtlasGaps} />
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<ExportBtn label="Full Report (multi-sheet)" exportKey="atlas-full" loading={loading} color="#A855F7" colorRgb="168,85,247" onClick={exportAtlasFull} />
|
||||||
|
</div>
|
||||||
|
<p style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#334155', margin: '0.75rem 0 0', lineHeight: 1.5 }}>
|
||||||
|
"Full Report" creates three sheets: Active Plans, No Plan, and History (overridden plans).
|
||||||
|
</p>
|
||||||
|
</ExportCard>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import CveTooltip from '../CveTooltip';
|
|||||||
import RedirectModal from '../RedirectModal';
|
import RedirectModal from '../RedirectModal';
|
||||||
import AtlasBadge from '../AtlasBadge';
|
import AtlasBadge from '../AtlasBadge';
|
||||||
import AtlasSlideOutPanel from '../AtlasSlideOutPanel';
|
import AtlasSlideOutPanel from '../AtlasSlideOutPanel';
|
||||||
|
import AtlasIcon from '../AtlasIcon';
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
const STORAGE_KEY = 'steam_findings_columns_v2';
|
const STORAGE_KEY = 'steam_findings_columns_v2';
|
||||||
@@ -4508,7 +4509,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
>
|
>
|
||||||
{atlasSyncing
|
{atlasSyncing
|
||||||
? <Loader style={{ width: 13, height: 13, animation: 'spin 1s linear infinite' }} />
|
? <Loader style={{ width: 13, height: 13, animation: 'spin 1s linear infinite' }} />
|
||||||
: <Database style={{ width: 13, height: 13 }} />}
|
: <AtlasIcon style={{ width: 13, height: 13 }} />}
|
||||||
Atlas
|
Atlas
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user