Add Atlas metrics reporting, security audit tracker, and spec documents

This commit is contained in:
root
2026-04-24 17:30:06 +00:00
parent 8bf8dc55dd
commit 5ffedad02f
11 changed files with 2101 additions and 22 deletions

View File

@@ -507,6 +507,229 @@ function FPWorkflowDonut({ counts, total, centerLabel = 'FP TOTAL' }) {
);
}
// ---------------------------------------------------------------------------
// Atlas Donut Charts — Coverage, Plan Type, Plan Status
// ---------------------------------------------------------------------------
const PLAN_TYPE_DEFS = [
{ key: 'decommission', label: 'Decommission', color: '#EF4444' },
{ key: 'remediation', label: 'Remediation', color: '#0EA5E9' },
{ key: 'false_positive', label: 'False Positive', color: '#A855F7' },
{ key: 'risk_acceptance', label: 'Risk Acceptance', color: '#F59E0B' },
{ key: 'scan_exclusion', label: 'Scan Exclusion', color: '#64748B' },
];
function getStatusColor(status) {
if (status === 'active') return '#10B981';
if (status === 'expired') return '#EF4444';
if (status === 'completed') return '#0EA5E9';
return '#64748B';
}
function AtlasCoverageDonut({ hostsWithPlans, hostsWithoutPlans, totalHosts }) {
const SIZE = 180;
const CX = SIZE / 2;
const CY = SIZE / 2;
const OUTER = 72;
const INNER = 48;
if (totalHosts === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No data run Atlas Sync</p>
</div>
);
}
const segments = [
{ label: 'With Plans', count: hostsWithPlans, color: '#10B981', start: 0, end: (hostsWithPlans / totalHosts) * 360 },
{ label: 'Without Plans', count: hostsWithoutPlans, color: '#F59E0B', start: (hostsWithPlans / totalHosts) * 360, end: 360 },
].filter((s) => s.count > 0);
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem' }}>
<svg width={SIZE} height={SIZE} style={{ flexShrink: 0 }}>
<circle cx={CX} cy={CY} r={OUTER + 1} fill="none" stroke="rgba(10,18,32,0.8)" strokeWidth="2" />
{segments.map((seg) => (
<path
key={seg.label}
d={donutArcPath(CX, CY, OUTER, INNER, seg.start, seg.end)}
fill={seg.color}
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{totalHosts.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
HOSTS
</text>
</svg>
{/* Legend */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{segments.map((seg) => (
<div key={seg.label} style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{seg.label}
</div>
<div style={{ fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: '700', color: '#E2E8F0', lineHeight: 1.2 }}>
{seg.count.toLocaleString()}
<span style={{ fontSize: '0.68rem', fontWeight: '400', color: '#64748B', marginLeft: '0.4rem' }}>
({((seg.count / totalHosts) * 100).toFixed(1)}%)
</span>
</div>
</div>
</div>
))}
</div>
</div>
);
}
function AtlasPlanTypeDonut({ plansByType, totalPlans }) {
const SIZE = 180;
const CX = SIZE / 2;
const CY = SIZE / 2;
const OUTER = 72;
const INNER = 48;
if (totalPlans === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No plans run Atlas Sync</p>
</div>
);
}
let cursor = 0;
const segments = PLAN_TYPE_DEFS.map((def) => {
const count = plansByType[def.key] || 0;
const start = cursor;
const end = count > 0 ? cursor + (count / totalPlans) * 360 : cursor;
if (count > 0) cursor = end;
return { ...def, count, start, end };
}).filter(s => s.count > 0);
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem' }}>
<svg width={SIZE} height={SIZE} style={{ flexShrink: 0 }}>
<circle cx={CX} cy={CY} r={OUTER + 1} fill="none" stroke="rgba(10,18,32,0.8)" strokeWidth="2" />
{segments.map((seg) => (
<path
key={seg.key}
d={donutArcPath(CX, CY, OUTER, INNER, seg.start, seg.end)}
fill={seg.color}
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{totalPlans.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
PLANS
</text>
</svg>
{/* Legend */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
{segments.map((seg) => (
<div key={seg.key} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
{seg.label}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
{seg.count}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
({((seg.count / totalPlans) * 100).toFixed(0)}%)
</span>
</div>
</div>
))}
</div>
</div>
);
}
function AtlasPlanStatusDonut({ plansByStatus, totalPlans }) {
const SIZE = 180;
const CX = SIZE / 2;
const CY = SIZE / 2;
const OUTER = 72;
const INNER = 48;
if (totalPlans === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No plans run Atlas Sync</p>
</div>
);
}
const entries = Object.entries(plansByStatus).filter(([, count]) => count > 0);
let cursor = 0;
const segments = entries.map(([status, count]) => {
const start = cursor;
const end = cursor + (count / totalPlans) * 360;
cursor = end;
return {
key: status,
label: status.charAt(0).toUpperCase() + status.slice(1),
color: getStatusColor(status),
count,
start,
end,
};
});
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem' }}>
<svg width={SIZE} height={SIZE} style={{ flexShrink: 0 }}>
<circle cx={CX} cy={CY} r={OUTER + 1} fill="none" stroke="rgba(10,18,32,0.8)" strokeWidth="2" />
{segments.map((seg) => (
<path
key={seg.key}
d={donutArcPath(CX, CY, OUTER, INNER, seg.start, seg.end)}
fill={seg.color}
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{totalPlans.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
STATUS
</text>
</svg>
{/* Legend */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
{segments.map((seg) => (
<div key={seg.key} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
{seg.label}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
{seg.count}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
({((seg.count / totalPlans) * 100).toFixed(0)}%)
</span>
</div>
</div>
))}
</div>
</div>
);
}
function SortIcon({ colKey, sort }) {
if (sort.field !== colKey) return <ChevronsUpDown style={{ width: '11px', height: '11px', opacity: 0.3, marginLeft: '3px', flexShrink: 0 }} />;
return sort.dir === 'asc'
@@ -3632,6 +3855,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
const hoverTimerRef = useRef(null);
// Atlas action plan state
const [metricsTab, setMetricsTab] = useState('ivanti');
const [atlasStatusMap, setAtlasStatusMap] = useState(new Map());
const [atlasSyncing, setAtlasSyncing] = useState(false);
const [atlasError, setAtlasError] = useState(null);
@@ -3640,6 +3864,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
const [atlasSelectedHostName, setAtlasSelectedHostName] = useState(null);
const [atlasSelectedFindingId, setAtlasSelectedFindingId] = useState(null);
// Atlas metrics state (for Atlas Coverage tab donut charts)
const [atlasMetrics, setAtlasMetrics] = useState(null);
const [atlasMetricsLoading, setAtlasMetricsLoading] = useState(false);
const [atlasMetricsError, setAtlasMetricsError] = useState(null);
const updateColumns = useCallback((newOrder) => {
setColumnOrder(newOrder);
saveColumnOrder(newOrder);
@@ -3758,6 +3987,25 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
}
}, []);
const fetchAtlasMetrics = useCallback(async () => {
setAtlasMetricsLoading(true);
setAtlasMetricsError(null);
try {
const res = await fetch(`${API_BASE}/atlas/metrics`, { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setAtlasMetrics(data);
} else {
const err = await res.json().catch(() => ({}));
setAtlasMetricsError(err.error || 'Failed to fetch Atlas metrics');
}
} catch (err) {
setAtlasMetricsError(err.message);
} finally {
setAtlasMetricsLoading(false);
}
}, []);
const fetchFindings = async () => {
setLoading(true);
try {
@@ -3799,6 +4047,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
fetchQueue();
fetchFpSubmissions();
fetchAtlasStatus();
fetchAtlasMetrics();
}, []); // eslint-disable-line
// Set/clear a single column filter
@@ -4234,7 +4483,41 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
<h2 style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '600', color: '#F59E0B', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(245,158,11,0.4)', margin: 0 }}>
Metric Graphs
</h2>
<div style={{ marginLeft: 'auto', display: 'flex', gap: '0.25rem' }} role="tablist">
{[{ key: 'ivanti', label: 'Ivanti Findings' }, { key: 'atlas', label: 'Atlas Coverage' }].map(tab => {
const isActive = metricsTab === tab.key;
return (
<button
key={tab.key}
role="tab"
aria-selected={isActive}
tabIndex={0}
onClick={() => setMetricsTab(tab.key)}
onKeyDown={(e) => { if (e.key === 'Enter') setMetricsTab(tab.key); }}
style={{
background: 'transparent',
border: 'none',
borderBottom: isActive ? '2px solid #F59E0B' : '2px solid transparent',
color: isActive ? '#F59E0B' : '#64748B',
fontFamily: 'monospace',
fontSize: '0.7rem',
textTransform: 'uppercase',
letterSpacing: '0.08em',
padding: '0.375rem 0.75rem',
cursor: 'pointer',
transition: 'background 0.15s, color 0.15s'
}}
onMouseEnter={(e) => { if (!isActive) e.currentTarget.style.background = 'rgba(245, 158, 11, 0.06)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
>
{tab.label}
</button>
);
})}
</div>
</div>
<div role="tabpanel">
{metricsTab === 'ivanti' && (
<div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
{/* Open vs Closed donut */}
<div style={{ flex: '0 0 auto' }}>
@@ -4289,12 +4572,68 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
<FPWorkflowDonut counts={fpCounts.idCounts} total={fpCounts.idTotal} centerLabel="FP TICKETS" />
</div>
</div>
)}
{metricsTab === 'atlas' && (
(atlasMetricsLoading || (!atlasMetrics && !atlasMetricsError)) ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 0', gap: '0.5rem' }}>
<Loader style={{ width: '24px', height: '24px', color: '#0EA5E9', animation: 'spin 1s linear infinite' }} />
</div>
) : atlasMetricsError ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem', gap: '0.375rem' }}>
<AlertCircle style={{ width: '14px', height: '14px', color: '#EF4444', flexShrink: 0 }} />
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#FCA5A5' }}>{atlasMetricsError}</span>
</div>
) : (
<div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
{/* Host Coverage donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Host Coverage
</div>
<AtlasCoverageDonut
hostsWithPlans={atlasMetrics.hostsWithPlans}
hostsWithoutPlans={atlasMetrics.hostsWithoutPlans}
totalHosts={atlasMetrics.totalHosts}
/>
</div>
{/* Divider */}
<div style={{ width: '1px', background: 'rgba(255,255,255,0.06)', alignSelf: 'stretch', flexShrink: 0 }} />
{/* Plan Types donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Plan Types
</div>
<AtlasPlanTypeDonut
plansByType={atlasMetrics.plansByType}
totalPlans={atlasMetrics.totalPlans}
/>
</div>
{/* Divider */}
<div style={{ width: '1px', background: 'rgba(255,255,255,0.06)', alignSelf: 'stretch', flexShrink: 0 }} />
{/* Plan Status donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Plan Status
</div>
<AtlasPlanStatusDonut
plansByStatus={atlasMetrics.plansByStatus}
totalPlans={atlasMetrics.totalPlans}
/>
</div>
</div>
)
)}
</div>
</div>
{/* ----------------------------------------------------------------
Panel 1.5 — Open vs Closed trend over time
---------------------------------------------------------------- */}
<IvantiCountsChart />
{metricsTab === 'ivanti' && <IvantiCountsChart />}
{/* ----------------------------------------------------------------
Panel 2 — Findings table
@@ -4483,6 +4822,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
throw new Error(data.error || 'Atlas sync failed');
}
await fetchAtlasStatus();
await fetchAtlasMetrics();
} catch (err) {
setAtlasError(err.message);
} finally {