2026-03-18 11:39:26 -06:00
import React , { useState , useCallback } from 'react' ;
import * as XLSX from 'xlsx' ;
import { Download , Loader , AlertCircle , BarChart2 , FileText , Shield , Tag , CheckCircle , X } from 'lucide-react' ;
2026-03-11 11:47:03 -06:00
2026-03-18 11:39:26 -06:00
const API _BASE = process . env . REACT _APP _API _BASE || 'http://localhost:3001/api' ;
const EXC _PATTERN = /EXC-\d+/i ;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function classifyFinding ( f ) {
if ( f . workflow != null ) return 'fp' ;
if ( EXC _PATTERN . test ( f . note || '' ) ) return 'archer' ;
return 'pending' ;
}
const dateStr = ( ) => new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
function triggerDownload ( blob , filename ) {
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = filename ;
document . body . appendChild ( a ) ;
a . click ( ) ;
document . body . removeChild ( a ) ;
URL . revokeObjectURL ( url ) ;
}
function autoFit ( ws , rows ) {
if ( ! rows [ 0 ] ) return ;
ws [ '!cols' ] = rows [ 0 ] . map ( ( _ , ci ) => ( {
wch : Math . min ( 60 , Math . max ( 10 , ... rows . map ( r => String ( r [ ci ] ? ? '' ) . length ) ) )
} ) ) ;
}
function toXLSX ( rows , sheetName , filename ) {
const ws = XLSX . utils . aoa _to _sheet ( rows ) ;
autoFit ( ws , rows ) ;
const wb = XLSX . utils . book _new ( ) ;
XLSX . utils . book _append _sheet ( wb , ws , sheetName ) ;
XLSX . writeFile ( wb , filename ) ;
}
function toMultiXLSX ( sheets , filename ) {
const wb = XLSX . utils . book _new ( ) ;
sheets . forEach ( ( { name , rows } ) => {
const ws = XLSX . utils . aoa _to _sheet ( rows ) ;
autoFit ( ws , rows ) ;
XLSX . utils . book _append _sheet ( wb , ws , String ( name || 'Unknown' ) . slice ( 0 , 31 ) ) ;
} ) ;
XLSX . writeFile ( wb , filename ) ;
}
function toCSV ( rows , filename ) {
const csv = rows . map ( row =>
row . map ( cell => {
const s = String ( cell ? ? '' ) ;
return ( s . includes ( ',' ) || s . includes ( '"' ) || s . includes ( '\n' ) )
? ` " ${ s . replace ( /"/g , '""' ) } " ` : s ;
} ) . join ( ',' )
) . join ( '\r\n' ) ;
triggerDownload ( new Blob ( [ '\uFEFF' + csv ] , { type : 'text/csv;charset=utf-8;' } ) , filename ) ;
}
// ---------------------------------------------------------------------------
// Finding column definitions
// ---------------------------------------------------------------------------
const FINDING _HEADERS = [
'Finding ID' , 'Title' , 'Severity Score' , 'Severity Group' ,
'Host' , 'IP Address' , 'DNS' , 'Due Date' , 'SLA Status' ,
'Business Unit' , 'FP# ID' , 'FP# State' , 'Last Found' , 'CVEs' , 'Notes' ,
] ;
function findingRow ( f ) {
return [
f . id ,
f . title ,
f . severity != null ? Number ( f . severity ) . toFixed ( 2 ) : '' ,
f . vrrGroup ? ? '' ,
f . overrides ? . hostName ? ? f . hostName ? ? '' ,
f . ipAddress ? ? '' ,
f . overrides ? . dns ? ? f . dns ? ? '' ,
f . dueDate ? ? '' ,
f . slaStatus ? ? '' ,
f . buOwnership ? ? '' ,
f . workflow ? . id ? ? '' ,
f . workflow ? . state ? ? '' ,
f . lastFoundOn ? ? '' ,
( f . cves || [ ] ) . join ( ', ' ) ,
f . note ? ? '' ,
] ;
}
// ---------------------------------------------------------------------------
// API fetchers
// ---------------------------------------------------------------------------
async function fetchFindings ( ) {
const res = await fetch ( ` ${ API _BASE } /ivanti/findings ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` Ivanti findings returned ${ res . status } ` ) ;
const data = await res . json ( ) ;
return data . findings || [ ] ;
}
async function fetchCVEs ( status ) {
const url = status ? ` ${ API _BASE } /cves?status= ${ encodeURIComponent ( status ) } ` : ` ${ API _BASE } /cves ` ;
const res = await fetch ( url , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` CVE list returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchArcher ( ) {
const res = await fetch ( ` ${ API _BASE } /archer-tickets ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` Archer tickets returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchCompliance ( ) {
const res = await fetch ( ` ${ API _BASE } /cves/compliance ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` Compliance data returned ${ res . status } ` ) ;
return res . json ( ) ;
}
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
function ExportCard ( { color , colorRgb , icon : Icon , title , description , children } ) {
return (
2026-03-11 11:47:03 -06:00
< div style = { {
2026-03-18 11:39:26 -06:00
background : 'linear-gradient(135deg, rgba(15,26,46,0.95) 0%, rgba(10,22,40,0.9) 100%)' ,
border : ` 1px solid rgba( ${ colorRgb } ,0.2) ` ,
borderLeft : ` 3px solid ${ color } ` ,
borderRadius : '0.5rem' ,
padding : '1.5rem' ,
boxShadow : '0 4px 16px rgba(0,0,0,0.4)' ,
display : 'flex' ,
flexDirection : 'column' ,
gap : '1rem' ,
2026-03-11 11:47:03 -06:00
} } >
2026-03-18 11:39:26 -06:00
< div style = { { display : 'flex' , alignItems : 'center' , gap : '0.625rem' } } >
< Icon style = { { width : '18px' , height : '18px' , color , flexShrink : 0 } } / >
< h3 style = { {
fontFamily : 'monospace' , fontSize : '0.9rem' , fontWeight : '600' ,
color , textTransform : 'uppercase' , letterSpacing : '0.1em' ,
textShadow : ` 0 0 12px rgba( ${ colorRgb } ,0.4) ` , margin : 0 ,
} } >
{ title }
< / h 3 >
< / d i v >
< p style = { { fontFamily : 'monospace' , fontSize : '0.72rem' , color : '#475569' , margin : 0 , lineHeight : 1.6 } } >
{ description }
< / p >
< div style = { { borderTop : '1px solid rgba(255,255,255,0.06)' , paddingTop : '1rem' } } >
{ children }
< / d i v >
< / d i v >
) ;
}
function ExportBtn ( { label , exportKey , loading , color , colorRgb , onClick , disabled } ) {
const isLoading = loading === exportKey ;
return (
< button
onClick = { onClick }
disabled = { ! ! loading || disabled }
style = { {
display : 'flex' , alignItems : 'center' , gap : '0.375rem' ,
padding : '0.45rem 0.875rem' ,
background : ` rgba( ${ colorRgb } ,0.08) ` ,
border : ` 1px solid rgba( ${ colorRgb } ,0.25) ` ,
borderRadius : '0.375rem' ,
color : isLoading ? '#64748B' : color ,
cursor : ( ! ! loading || disabled ) ? 'not-allowed' : 'pointer' ,
opacity : ( ! ! loading && ! isLoading ) ? 0.45 : 1 ,
fontFamily : 'monospace' , fontSize : '0.72rem' , fontWeight : '600' ,
letterSpacing : '0.05em' ,
transition : 'opacity 0.15s, color 0.15s' ,
whiteSpace : 'nowrap' ,
} }
>
{ isLoading
? < Loader style = { { width : '12px' , height : '12px' , animation : 'spin 1s linear infinite' , flexShrink : 0 } } / >
: < Download style = { { width : '12px' , height : '12px' , flexShrink : 0 } } / >
}
{ label }
< / b u t t o n >
) ;
}
function Toggle ( { label , checked , onChange , color , colorRgb } ) {
return (
< label style = { { display : 'flex' , alignItems : 'center' , gap : '0.5rem' , cursor : 'pointer' , userSelect : 'none' } } >
< div
onClick = { ( ) => onChange ( ! checked ) }
style = { {
width : '32px' , height : '18px' , borderRadius : '9px' ,
background : checked ? color : 'rgba(255,255,255,0.1)' ,
border : ` 1px solid rgba( ${ colorRgb } ,0.4) ` ,
position : 'relative' , transition : 'background 0.2s' ,
cursor : 'pointer' , flexShrink : 0 ,
} }
>
< div style = { {
position : 'absolute' , top : '2px' ,
left : checked ? '14px' : '2px' ,
width : '12px' , height : '12px' , borderRadius : '50%' ,
background : '#E2E8F0' ,
transition : 'left 0.2s' ,
} } / >
< / d i v >
< span style = { { fontFamily : 'monospace' , fontSize : '0.7rem' , color : '#64748B' } } > { label } < / s p a n >
< / l a b e l >
) ;
}
// ---------------------------------------------------------------------------
// Main page
// ---------------------------------------------------------------------------
export default function ExportsPage ( ) {
const [ loading , setLoading ] = useState ( null ) ;
const [ error , setError ] = useState ( null ) ;
const [ cveStatus , setCveStatus ] = useState ( '' ) ;
const [ missingOnly , setMissingOnly ] = useState ( false ) ;
const run = useCallback ( async ( key , fn ) => {
setLoading ( key ) ;
setError ( null ) ;
try {
await fn ( ) ;
} catch ( e ) {
console . error ( '[Export]' , e ) ;
setError ( e . message || 'Export failed — check console for details' ) ;
} finally {
setLoading ( null ) ;
}
} , [ ] ) ;
// ---- Card 1: Ivanti Findings ----
const exportFullFindings = ( ) => run ( 'ivanti-full' , async ( ) => {
const findings = await fetchFindings ( ) ;
toXLSX (
[ FINDING _HEADERS , ... findings . map ( findingRow ) ] ,
'All Findings' ,
` findings-full- ${ dateStr ( ) } .xlsx ` ,
) ;
} ) ;
const exportPending = ( ) => run ( 'ivanti-pending' , async ( ) => {
const findings = await fetchFindings ( ) ;
const rows = findings . filter ( f => classifyFinding ( f ) === 'pending' ) . map ( findingRow ) ;
toXLSX ( [ FINDING _HEADERS , ... rows ] , 'Pending Action' , ` findings-pending- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportOverdue = ( ) => run ( 'ivanti-overdue' , async ( ) => {
const findings = await fetchFindings ( ) ;
const today = dateStr ( ) ;
const rows = findings . filter ( f => {
if ( ! f . dueDate && ! ( f . slaStatus || '' ) . toLowerCase ( ) . includes ( 'overdue' ) ) return false ;
return f . dueDate < today || ( f . slaStatus || '' ) . toUpperCase ( ) === 'OVERDUE' ;
} ) . map ( findingRow ) ;
toXLSX ( [ FINDING _HEADERS , ... rows ] , 'Overdue' , ` findings-overdue- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportByBU = ( ) => run ( 'ivanti-bu' , async ( ) => {
const findings = await fetchFindings ( ) ;
const groups = { } ;
findings . forEach ( f => {
const bu = f . buOwnership || 'Unknown' ;
if ( ! groups [ bu ] ) groups [ bu ] = [ ] ;
groups [ bu ] . push ( f ) ;
} ) ;
const sheets = Object . entries ( groups )
. sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
. map ( ( [ name , rows ] ) => ( { name , rows : [ FINDING _HEADERS , ... rows . map ( findingRow ) ] } ) ) ;
if ( sheets . length === 0 ) sheets . push ( { name : 'No Data' , rows : [ FINDING _HEADERS ] } ) ;
toMultiXLSX ( sheets , ` findings-by-bu- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
// ---- Card 2: FP Workflow Summary ----
const exportFPSummary = ( ) => run ( 'fp-summary' , async ( ) => {
const findings = await fetchFindings ( ) ;
const fpMap = { } ;
findings . forEach ( f => {
if ( ! f . workflow ? . id ) return ;
const id = f . workflow . id ;
if ( ! fpMap [ id ] ) fpMap [ id ] = { id , state : f . workflow . state || '' , count : 0 , hosts : new Set ( ) , bus : new Set ( ) , cves : new Set ( ) } ;
fpMap [ id ] . count ++ ;
const host = f . overrides ? . hostName ? ? f . hostName ;
if ( host ) fpMap [ id ] . hosts . add ( host ) ;
if ( f . buOwnership ) fpMap [ id ] . bus . add ( f . buOwnership ) ;
( f . cves || [ ] ) . forEach ( c => fpMap [ id ] . cves . add ( c ) ) ;
} ) ;
const headers = [ 'FP# ID' , 'State' , 'Finding Count' , 'Hosts' , 'Business Units' , 'CVEs' ] ;
const rows = Object . values ( fpMap )
. sort ( ( a , b ) => a . id . localeCompare ( b . id ) )
. map ( e => [ e . id , e . state , e . count , [ ... e . hosts ] . join ( ', ' ) , [ ... e . bus ] . join ( ', ' ) , [ ... e . cves ] . join ( ', ' ) ] ) ;
toXLSX ( [ headers , ... rows ] , 'FP Workflows' , ` fp-workflow-summary- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
// ---- Card 3: CVE Database ----
const exportCVEs = ( fmt ) => run ( ` cves- ${ fmt } ` , async ( ) => {
const data = await fetchCVEs ( cveStatus ) ;
const headers = [ 'CVE ID' , 'Vendor' , 'Severity' , 'Status' , 'Published Date' , 'Description' , 'Documents' ] ;
const rows = data . map ( c => [ c . cve _id , c . vendor , c . severity , c . status , c . published _date ? ? '' , c . description ? ? '' , c . document _count ? ? 0 ] ) ;
if ( fmt === 'csv' ) {
toCSV ( [ headers , ... rows ] , ` cve-database- ${ dateStr ( ) } .csv ` ) ;
} else {
toXLSX ( [ headers , ... rows ] , 'CVEs' , ` cve-database- ${ dateStr ( ) } .xlsx ` ) ;
}
} ) ;
// ---- Card 4: Archer Tickets ----
const exportArcher = ( ) => run ( 'archer' , async ( ) => {
const data = await fetchArcher ( ) ;
const headers = [ 'EXC Number' , 'Status' , 'CVE ID' , 'Vendor' , 'Archer URL' , 'Created' ] ;
const rows = data . map ( t => [ t . exc _number , t . status , t . cve _id ? ? '' , t . vendor ? ? '' , t . archer _url ? ? '' , t . created _at ? ? '' ] ) ;
toXLSX ( [ headers , ... rows ] , 'Archer Tickets' , ` archer-tickets- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
// ---- Card 5: Compliance Report ----
const exportCompliance = ( ) => run ( 'compliance' , async ( ) => {
const data = await fetchCompliance ( ) ;
const filtered = missingOnly ? data . filter ( r => r . compliance _status !== 'Complete' ) : data ;
const headers = [ 'CVE ID' , 'Vendor' , 'Severity' , 'Status' , 'Total Docs' , 'Advisory Docs' , 'Email Docs' , 'Screenshot Docs' , 'Compliance Status' ] ;
const rows = filtered . map ( r => [ r . cve _id , r . vendor , r . severity , r . status , r . total _documents , r . advisory _count , r . email _count , r . screenshot _count , r . compliance _status ] ) ;
toXLSX ( [ headers , ... rows ] , 'Compliance' , ` compliance-report- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
// ---- Render ----
return (
< div style = { { padding : '1.5rem' , display : 'flex' , flexDirection : 'column' , gap : '1.5rem' } } >
{ /* Page header */ }
< div style = { { display : 'flex' , alignItems : 'center' , gap : '0.625rem' } } >
< Download style = { { width : '20px' , height : '20px' , color : '#8B5CF6' } } / >
< h2 style = { { fontFamily : 'monospace' , fontSize : '1rem' , fontWeight : '600' , color : '#8B5CF6' , textTransform : 'uppercase' , letterSpacing : '0.1em' , textShadow : '0 0 12px rgba(139,92,246,0.4)' , margin : 0 } } >
Exports
< / h 2 >
< / d i v >
{ /* Error banner */ }
{ error && (
< div style = { {
display : 'flex' , alignItems : 'center' , gap : '0.625rem' ,
padding : '0.75rem 1rem' ,
background : 'rgba(239,68,68,0.08)' , border : '1px solid rgba(239,68,68,0.3)' ,
borderRadius : '0.375rem' ,
} } >
< AlertCircle style = { { width : '14px' , height : '14px' , color : '#EF4444' , flexShrink : 0 } } / >
< span style = { { fontFamily : 'monospace' , fontSize : '0.75rem' , color : '#EF4444' , flex : 1 } } > { error } < / s p a n >
< button onClick = { ( ) => setError ( null ) } style = { { background : 'none' , border : 'none' , cursor : 'pointer' , color : '#EF4444' , padding : 0 } } >
< X style = { { width : '14px' , height : '14px' } } / >
< / b u t t o n >
< / d i v >
) }
{ /* Card grid */ }
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(auto-fill, minmax(420px, 1fr))' , gap : '1.5rem' } } >
{ /* ── Card 1: Ivanti Findings ── */ }
< ExportCard
color = "#F59E0B" colorRgb = "245,158,11"
icon = { BarChart2 }
title = "Ivanti Host Findings"
description = "Export host findings from the local cache. Four report types: full dump, findings with no action taken, overdue SLA, and a per-business-unit multi-sheet workbook."
>
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : '0.5rem' } } >
< ExportBtn label = "Full Dump" exportKey = "ivanti-full" loading = { loading } color = "#F59E0B" colorRgb = "245,158,11" onClick = { exportFullFindings } / >
< ExportBtn label = "Pending Action" exportKey = "ivanti-pending" loading = { loading } color = "#F59E0B" colorRgb = "245,158,11" onClick = { exportPending } / >
< ExportBtn label = "Overdue SLA" exportKey = "ivanti-overdue" loading = { loading } color = "#F59E0B" colorRgb = "245,158,11" onClick = { exportOverdue } / >
< ExportBtn label = "By Business Unit" exportKey = "ivanti-bu" loading = { loading } color = "#F59E0B" colorRgb = "245,158,11" onClick = { exportByBU } / >
< / d i v >
< p style = { { fontFamily : 'monospace' , fontSize : '0.65rem' , color : '#334155' , margin : '0.75rem 0 0' , lineHeight : 1.5 } } >
"By Business Unit" creates one sheet per BU in a single workbook .
< / p >
< / E x p o r t C a r d >
{ /* ── Card 2: FP Workflow Summary ── */ }
< ExportCard
color = "#0EA5E9" colorRgb = "14,165,233"
icon = { FileText }
title = "FP Workflow Summary"
description = "One row per unique FP# ticket ID. Shows state, how many findings belong to that ticket, which hosts are affected, and which CVEs are involved. Use this for status meetings."
>
< ExportBtn label = "Export FP Summary (.xlsx)" exportKey = "fp-summary" loading = { loading } color = "#0EA5E9" colorRgb = "14,165,233" onClick = { exportFPSummary } / >
< / E x p o r t C a r d >
{ /* ── Card 3: CVE Database ── */ }
< ExportCard
color = "#22C55E" colorRgb = "34,197,94"
icon = { Shield }
title = "CVE Database"
description = "Export the full CVE registry. Optionally filter by status to produce a focused remediation backlog. Includes document count per entry."
>
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.75rem' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : '0.5rem' } } >
< span style = { { fontFamily : 'monospace' , fontSize : '0.68rem' , color : '#64748B' , textTransform : 'uppercase' , letterSpacing : '0.06em' , whiteSpace : 'nowrap' } } > Status < / s p a n >
< select
value = { cveStatus }
onChange = { e => setCveStatus ( e . target . value ) }
disabled = { ! ! loading }
style = { {
background : 'rgba(34,197,94,0.06)' , border : '1px solid rgba(34,197,94,0.2)' ,
borderRadius : '0.25rem' , color : '#CBD5E1' , padding : '0.25rem 0.5rem' ,
fontFamily : 'monospace' , fontSize : '0.72rem' , cursor : 'pointer' , outline : 'none' ,
} }
>
< option value = "" > All Statuses < / o p t i o n >
< option value = "Open" > Open < / o p t i o n >
< option value = "In Progress" > In Progress < / o p t i o n >
< option value = "Addressed" > Addressed < / o p t i o n >
< option value = "Resolved" > Resolved < / o p t i o n >
< / s e l e c t >
< / d i v >
< div style = { { display : 'flex' , gap : '0.5rem' } } >
< ExportBtn label = "Export CSV" exportKey = "cves-csv" loading = { loading } color = "#22C55E" colorRgb = "34,197,94" onClick = { ( ) => exportCVEs ( 'csv' ) } / >
< ExportBtn label = "Export .xlsx" exportKey = "cves-xlsx" loading = { loading } color = "#22C55E" colorRgb = "34,197,94" onClick = { ( ) => exportCVEs ( 'xlsx' ) } / >
< / d i v >
< / d i v >
< / E x p o r t C a r d >
{ /* ── Card 4: Archer Tickets ── */ }
< ExportCard
color = "#F97316" colorRgb = "249,115,22"
icon = { Tag }
title = "Archer Risk Acceptance Tickets"
description = "Export all Archer EXC exception tickets with their linked CVE IDs, vendors, statuses, and Archer URLs. Useful for risk acceptance reporting and audits."
>
< ExportBtn label = "Export Archer Tickets (.xlsx)" exportKey = "archer" loading = { loading } color = "#F97316" colorRgb = "249,115,22" onClick = { exportArcher } / >
< / E x p o r t C a r d >
{ /* ── Card 5: Compliance Report ── */ }
< ExportCard
color = "#EF4444" colorRgb = "239,68,68"
icon = { CheckCircle }
title = "Document Compliance Report"
description = "Shows document coverage per CVE/vendor pair. A row is marked Complete when an advisory document has been uploaded; otherwise Missing Required Docs. Filter to missing-only to generate a gap list."
>
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.75rem' } } >
< Toggle
label = "Missing required docs only"
checked = { missingOnly }
onChange = { setMissingOnly }
color = "#EF4444"
colorRgb = "239,68,68"
/ >
< ExportBtn label = "Export Compliance Report (.xlsx)" exportKey = "compliance" loading = { loading } color = "#EF4444" colorRgb = "239,68,68" onClick = { exportCompliance } / >
< / d i v >
< / E x p o r t C a r d >
< / d i v >
2026-03-11 11:47:03 -06:00
< / d i v >
2026-03-18 11:39:26 -06:00
) ;
2026-03-11 11:47:03 -06:00
}