2026-03-18 11:39:26 -06:00
import React , { useState , useCallback } from 'react' ;
import * as XLSX from 'xlsx' ;
2026-04-28 14:22:19 +00:00
import { Download , Loader , AlertCircle , BarChart2 , FileText , Shield , Tag , CheckCircle , X } from 'lucide-react' ;
2026-04-07 09:52:26 -06:00
import { useAuth } from '../../contexts/AuthContext' ;
2026-04-23 22:18:23 +00:00
import AtlasIcon from '../AtlasIcon' ;
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
// ---------------------------------------------------------------------------
2026-05-05 11:04:53 -06:00
async function fetchFindings ( teamsParam ) {
const url = teamsParam
? ` ${ API _BASE } /ivanti/findings?teams= ${ encodeURIComponent ( teamsParam ) } `
: ` ${ API _BASE } /ivanti/findings ` ;
const res = await fetch ( url , { credentials : 'include' } ) ;
2026-03-18 11:39:26 -06:00
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 ( ) ;
}
2026-04-23 22:18:23 +00:00
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 ( ) ;
}
Add Jira Tickets, CCP Metrics, and Remediation Status export cards
New export cards on the Exports page:
- Jira Tickets: All tickets, open/active only, by-CVE multi-sheet
- CCP Compliance Metrics: Current snapshot, non-compliant devices,
trend history, full multi-sheet report
- Remediation Status: Cross-domain report combining CVEs, Jira tickets,
Archer exceptions, and Ivanti findings into a per-CVE progress view
2026-05-22 14:15:06 -06:00
async function fetchJiraTickets ( ) {
const res = await fetch ( ` ${ API _BASE } /jira-tickets ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` Jira tickets returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchCCPStats ( ) {
const res = await fetch ( ` ${ API _BASE } /compliance/vcl-multi/stats ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` CCP stats returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchCCPVerticals ( ) {
const res = await fetch ( ` ${ API _BASE } /compliance/vcl-multi/verticals ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` CCP verticals returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchCCPMetrics ( ) {
const res = await fetch ( ` ${ API _BASE } /compliance/vcl-multi/metrics ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` CCP metrics returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchCCPTrend ( ) {
const res = await fetch ( ` ${ API _BASE } /compliance/vcl-multi/trend ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` CCP trend returned ${ res . status } ` ) ;
return res . json ( ) ;
}
async function fetchCCPVerticalMetrics ( code ) {
const res = await fetch ( ` ${ API _BASE } /compliance/vcl-multi/vertical/ ${ encodeURIComponent ( code ) } /metrics ` , { credentials : 'include' } ) ;
if ( ! res . ok ) throw new Error ( ` CCP vertical metrics returned ${ res . status } ` ) ;
return res . json ( ) ;
}
2026-05-05 11:04:53 -06:00
async function fetchAtlasAndFindings ( teamsParam ) {
const [ atlasRows , findings ] = await Promise . all ( [ fetchAtlasStatus ( ) , fetchFindings ( teamsParam ) ] ) ;
2026-04-23 22:18:23 +00:00
// 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 } ;
}
2026-03-18 11:39:26 -06:00
// ---------------------------------------------------------------------------
// 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 ( ) {
2026-05-05 11:04:53 -06:00
const { canExport , getActiveTeamsParam } = useAuth ( ) ;
const teamsParam = getActiveTeamsParam ( ) ;
2026-03-18 11:39:26 -06:00
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 ( ) => {
2026-05-05 11:04:53 -06:00
const findings = await fetchFindings ( teamsParam ) ;
const scopeLabel = teamsParam || 'ALL' ;
2026-03-18 11:39:26 -06:00
toXLSX (
[ FINDING _HEADERS , ... findings . map ( findingRow ) ] ,
'All Findings' ,
2026-05-05 11:04:53 -06:00
` findings-full- ${ scopeLabel } - ${ dateStr ( ) } .xlsx ` ,
2026-03-18 11:39:26 -06:00
) ;
} ) ;
const exportPending = ( ) => run ( 'ivanti-pending' , async ( ) => {
2026-05-05 11:04:53 -06:00
const findings = await fetchFindings ( teamsParam ) ;
const scopeLabel = teamsParam || 'ALL' ;
2026-03-18 11:39:26 -06:00
const rows = findings . filter ( f => classifyFinding ( f ) === 'pending' ) . map ( findingRow ) ;
2026-05-05 11:04:53 -06:00
toXLSX ( [ FINDING _HEADERS , ... rows ] , 'Pending Action' , ` findings-pending- ${ scopeLabel } - ${ dateStr ( ) } .xlsx ` ) ;
2026-03-18 11:39:26 -06:00
} ) ;
const exportOverdue = ( ) => run ( 'ivanti-overdue' , async ( ) => {
2026-05-05 11:04:53 -06:00
const findings = await fetchFindings ( teamsParam ) ;
const scopeLabel = teamsParam || 'ALL' ;
2026-03-18 11:39:26 -06:00
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 ) ;
2026-05-05 11:04:53 -06:00
toXLSX ( [ FINDING _HEADERS , ... rows ] , 'Overdue' , ` findings-overdue- ${ scopeLabel } - ${ dateStr ( ) } .xlsx ` ) ;
2026-03-18 11:39:26 -06:00
} ) ;
const exportByBU = ( ) => run ( 'ivanti-bu' , async ( ) => {
2026-05-05 11:04:53 -06:00
const findings = await fetchFindings ( teamsParam ) ;
2026-03-18 11:39:26 -06:00
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 ( ) => {
2026-05-05 11:04:53 -06:00
const findings = await fetchFindings ( teamsParam ) ;
2026-03-18 11:39:26 -06:00
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 ` ) ;
} ) ;
2026-04-23 22:18:23 +00:00
// ---- 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 ( ) => {
2026-05-05 11:04:53 -06:00
const { atlasRows , hostMap } = await fetchAtlasAndFindings ( teamsParam ) ;
2026-04-23 22:18:23 +00:00
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 ( ) => {
2026-05-05 11:04:53 -06:00
const { atlasRows , hostMap } = await fetchAtlasAndFindings ( teamsParam ) ;
2026-04-23 22:18:23 +00:00
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 ( ) => {
2026-05-05 11:04:53 -06:00
const { atlasRows , hostMap } = await fetchAtlasAndFindings ( teamsParam ) ;
2026-04-23 22:18:23 +00:00
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 ` ) ;
} ) ;
Add Jira Tickets, CCP Metrics, and Remediation Status export cards
New export cards on the Exports page:
- Jira Tickets: All tickets, open/active only, by-CVE multi-sheet
- CCP Compliance Metrics: Current snapshot, non-compliant devices,
trend history, full multi-sheet report
- Remediation Status: Cross-domain report combining CVEs, Jira tickets,
Archer exceptions, and Ivanti findings into a per-CVE progress view
2026-05-22 14:15:06 -06:00
// ---- Card 7: Jira Tickets ----
const exportJiraAll = ( ) => run ( 'jira-all' , async ( ) => {
const tickets = await fetchJiraTickets ( ) ;
const headers = [ 'Ticket Key' , 'CVE' , 'Vendor' , 'Summary' , 'Status' , 'Source' , 'URL' , 'Last Synced' , 'Created' ] ;
const rows = tickets . map ( t => [
t . ticket _key , t . cve _id , t . vendor || '' , t . summary || '' , t . status || 'Open' ,
t . source _context || 'cve' , t . url || '' ,
t . last _synced _at ? new Date ( t . last _synced _at ) . toLocaleDateString ( ) : 'Never' ,
t . created _at ? new Date ( t . created _at ) . toLocaleDateString ( ) : '' ,
] ) ;
toXLSX ( [ headers , ... rows ] , 'All Tickets' , ` jira-tickets-all- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportJiraOpen = ( ) => run ( 'jira-open' , async ( ) => {
const tickets = await fetchJiraTickets ( ) ;
const closedStatuses = [ 'closed' , 'done' , 'resolved' , 'complete' , 'completed' , 'cancelled' , 'canceled' , "won't do" , 'declined' ] ;
const open = tickets . filter ( t => {
const lower = ( t . status || '' ) . toLowerCase ( ) ;
return ! closedStatuses . some ( s => lower . includes ( s ) ) ;
} ) ;
const headers = [ 'Ticket Key' , 'CVE' , 'Vendor' , 'Summary' , 'Status' , 'Source' , 'URL' , 'Last Synced' , 'Created' ] ;
const rows = open . map ( t => [
t . ticket _key , t . cve _id , t . vendor || '' , t . summary || '' , t . status || 'Open' ,
t . source _context || 'cve' , t . url || '' ,
t . last _synced _at ? new Date ( t . last _synced _at ) . toLocaleDateString ( ) : 'Never' ,
t . created _at ? new Date ( t . created _at ) . toLocaleDateString ( ) : '' ,
] ) ;
toXLSX ( [ headers , ... rows ] , 'Open Tickets' , ` jira-tickets-open- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportJiraByCVE = ( ) => run ( 'jira-by-cve' , async ( ) => {
const tickets = await fetchJiraTickets ( ) ;
const groups = { } ;
tickets . forEach ( t => {
const key = t . cve _id || 'No CVE' ;
if ( ! groups [ key ] ) groups [ key ] = [ ] ;
groups [ key ] . push ( t ) ;
} ) ;
const headers = [ 'Ticket Key' , 'Vendor' , 'Summary' , 'Status' , 'Source' , 'URL' , 'Last Synced' ] ;
const sheets = Object . entries ( groups )
. sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
. map ( ( [ cve , tix ] ) => ( {
name : cve . slice ( 0 , 31 ) ,
rows : [ headers , ... tix . map ( t => [
t . ticket _key , t . vendor || '' , t . summary || '' , t . status || 'Open' ,
t . source _context || 'cve' , t . url || '' ,
t . last _synced _at ? new Date ( t . last _synced _at ) . toLocaleDateString ( ) : 'Never' ,
] ) ] ,
} ) ) ;
if ( sheets . length === 0 ) sheets . push ( { name : 'No Data' , rows : [ headers ] } ) ;
toMultiXLSX ( sheets , ` jira-tickets-by-cve- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
// ---- Card 8: CCP Metrics ----
const exportCCPSnapshot = ( ) => run ( 'ccp-snapshot' , async ( ) => {
const stats = await fetchCCPStats ( ) ;
const verticals = stats . verticals || [ ] ;
const headers = [ 'Vertical' , 'Total Devices' , 'Non-Compliant' , 'Compliance %' , 'Failing Metrics' , 'Report Date' ] ;
const rows = verticals . map ( v => [
v . vertical || v . code || '' ,
v . total _devices ? ? v . totalDevices ? ? '' ,
v . non _compliant _devices ? ? v . nonCompliantDevices ? ? '' ,
v . compliance _pct != null ? ` ${ Number ( v . compliance _pct ) . toFixed ( 1 ) } % ` : ( v . compliancePct != null ? ` ${ Number ( v . compliancePct ) . toFixed ( 1 ) } % ` : '' ) ,
v . failing _metrics ? ? v . failingMetrics ? ? '' ,
v . report _date ? ? v . reportDate ? ? '' ,
] ) ;
toXLSX ( [ headers , ... rows ] , 'CCP Snapshot' , ` ccp-compliance-snapshot- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportCCPNonCompliant = ( ) => run ( 'ccp-noncompliant' , async ( ) => {
const verticals = await fetchCCPVerticals ( ) ;
const allRows = [ ] ;
for ( const v of verticals ) {
const code = v . code || v . vertical ;
if ( ! code ) continue ;
try {
const metrics = await fetchCCPVerticalMetrics ( code ) ;
const metricList = metrics . metrics || metrics || [ ] ;
metricList . forEach ( m => {
const devices = m . devices || [ ] ;
devices . forEach ( d => {
allRows . push ( [
code , m . metric _id || m . metricId || '' , m . metric _desc || m . metricDesc || '' ,
d . hostname || '' , d . ip _address || d . ipAddress || '' , d . device _type || d . deviceType || '' ,
d . team || '' ,
] ) ;
} ) ;
} ) ;
} catch ( e ) {
// Skip verticals that fail
}
}
const headers = [ 'Vertical' , 'Metric ID' , 'Metric Description' , 'Hostname' , 'IP Address' , 'Device Type' , 'Team' ] ;
toXLSX ( [ headers , ... allRows ] , 'Non-Compliant Devices' , ` ccp-non-compliant-devices- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportCCPTrend = ( ) => run ( 'ccp-trend' , async ( ) => {
const trend = await fetchCCPTrend ( ) ;
const snapshots = trend . snapshots || trend || [ ] ;
const headers = [ 'Date' , 'Vertical' , 'Total Devices' , 'Non-Compliant' , 'Compliance %' ] ;
const rows = snapshots . flatMap ( s => {
const date = s . report _date || s . reportDate || s . date || '' ;
const verts = s . verticals || [ s ] ;
return verts . map ( v => [
date ,
v . vertical || v . code || '' ,
v . total _devices ? ? v . totalDevices ? ? '' ,
v . non _compliant _devices ? ? v . nonCompliantDevices ? ? '' ,
v . compliance _pct != null ? ` ${ Number ( v . compliance _pct ) . toFixed ( 1 ) } % ` : '' ,
] ) ;
} ) ;
toXLSX ( [ headers , ... rows ] , 'Trend' , ` ccp-compliance-trend- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
const exportCCPFull = ( ) => run ( 'ccp-full' , async ( ) => {
const [ stats , trend ] = await Promise . all ( [ fetchCCPStats ( ) , fetchCCPTrend ( ) ] ) ;
const verticals = stats . verticals || [ ] ;
const snapshots = trend . snapshots || trend || [ ] ;
// Sheet 1: Summary
const summaryHeaders = [ 'Vertical' , 'Total Devices' , 'Non-Compliant' , 'Compliance %' , 'Failing Metrics' , 'Report Date' ] ;
const summaryRows = verticals . map ( v => [
v . vertical || v . code || '' ,
v . total _devices ? ? v . totalDevices ? ? '' ,
v . non _compliant _devices ? ? v . nonCompliantDevices ? ? '' ,
v . compliance _pct != null ? ` ${ Number ( v . compliance _pct ) . toFixed ( 1 ) } % ` : '' ,
v . failing _metrics ? ? v . failingMetrics ? ? '' ,
v . report _date ? ? v . reportDate ? ? '' ,
] ) ;
// Sheet 2: Trend
const trendHeaders = [ 'Date' , 'Vertical' , 'Total Devices' , 'Non-Compliant' , 'Compliance %' ] ;
const trendRows = snapshots . flatMap ( s => {
const date = s . report _date || s . reportDate || s . date || '' ;
const verts = s . verticals || [ s ] ;
return verts . map ( v => [
date , v . vertical || v . code || '' ,
v . total _devices ? ? v . totalDevices ? ? '' ,
v . non _compliant _devices ? ? v . nonCompliantDevices ? ? '' ,
v . compliance _pct != null ? ` ${ Number ( v . compliance _pct ) . toFixed ( 1 ) } % ` : '' ,
] ) ;
} ) ;
toMultiXLSX ( [
{ name : 'Summary' , rows : [ summaryHeaders , ... summaryRows ] } ,
{ name : 'Trend' , rows : [ trendHeaders , ... trendRows ] } ,
] , ` ccp-full-report- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
// ---- Card 9: Remediation Status (Cross-Domain) ----
const exportRemediationStatus = ( ) => run ( 'remediation' , async ( ) => {
const [ cves , tickets , archer , findings ] = await Promise . all ( [
fetchCVEs ( '' ) ,
fetchJiraTickets ( ) ,
fetchArcher ( ) ,
fetchFindings ( teamsParam ) ,
] ) ;
// Build lookup maps
const ticketsByCVE = { } ;
tickets . forEach ( t => {
const key = ` ${ t . cve _id } | ${ t . vendor || '' } ` ;
if ( ! ticketsByCVE [ key ] ) ticketsByCVE [ key ] = [ ] ;
ticketsByCVE [ key ] . push ( t ) ;
} ) ;
const archerByCVE = { } ;
archer . forEach ( a => {
const key = ` ${ a . cve _id } | ${ a . vendor || '' } ` ;
if ( ! archerByCVE [ key ] ) archerByCVE [ key ] = [ ] ;
archerByCVE [ key ] . push ( a ) ;
} ) ;
const findingsByCVE = { } ;
findings . forEach ( f => {
( f . cves || [ ] ) . forEach ( cve => {
if ( ! findingsByCVE [ cve ] ) findingsByCVE [ cve ] = [ ] ;
findingsByCVE [ cve ] . push ( f ) ;
} ) ;
} ) ;
const headers = [
'CVE ID' , 'Vendor' , 'Severity' , 'CVE Status' ,
'Jira Tickets' , 'Jira Statuses' ,
'Archer EXC#' , 'Archer Status' ,
'Ivanti Findings' , 'Overdue Findings' ,
'Overall Progress' ,
] ;
const rows = cves . map ( c => {
const key = ` ${ c . cve _id } | ${ c . vendor } ` ;
const cveTickets = ticketsByCVE [ key ] || [ ] ;
const cveArcher = archerByCVE [ key ] || [ ] ;
const cveFindings = findingsByCVE [ c . cve _id ] || [ ] ;
const today = dateStr ( ) ;
const overdueCount = cveFindings . filter ( f => f . dueDate && f . dueDate < today ) . length ;
// Determine overall progress
let progress = 'Not Started' ;
if ( cveTickets . length > 0 || cveArcher . length > 0 ) {
const closedKeywords = [ 'closed' , 'done' , 'resolved' , 'complete' , 'completed' ] ;
const allTicketsClosed = cveTickets . length > 0 && cveTickets . every ( t => closedKeywords . some ( s => ( t . status || '' ) . toLowerCase ( ) . includes ( s ) ) ) ;
const allArcherAccepted = cveArcher . length > 0 && cveArcher . every ( a => a . status === 'Accepted' ) ;
if ( allTicketsClosed && ( cveArcher . length === 0 || allArcherAccepted ) ) {
progress = 'Complete' ;
} else {
progress = 'In Progress' ;
}
}
return [
c . cve _id , c . vendor , c . severity , c . status ,
cveTickets . map ( t => t . ticket _key ) . join ( ', ' ) ,
cveTickets . map ( t => ` ${ t . ticket _key } : ${ t . status || 'Open' } ` ) . join ( '; ' ) ,
cveArcher . map ( a => a . exc _number ) . join ( ', ' ) ,
cveArcher . map ( a => ` ${ a . exc _number } : ${ a . status } ` ) . join ( '; ' ) ,
cveFindings . length ,
overdueCount ,
progress ,
] ;
} ) ;
toXLSX ( [ headers , ... rows ] , 'Remediation Status' , ` remediation-status- ${ dateStr ( ) } .xlsx ` ) ;
} ) ;
2026-03-18 11:39:26 -06:00
// ---- Render ----
2026-04-07 09:52:26 -06:00
if ( ! canExport ( ) ) {
return (
< div style = { { textAlign : 'center' , padding : '4rem 1rem' , color : '#94A3B8' } } >
< Shield style = { { width : '48px' , height : '48px' , margin : '0 auto 1rem' , opacity : 0.5 } } / >
< p style = { { fontFamily : 'monospace' , fontSize : '0.9rem' } } > You do not have permission to export data . < / p >
< / d i v >
) ;
}
2026-03-18 11:39:26 -06:00
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 >
2026-04-23 22:18:23 +00:00
{ /* ── 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 } / >
< / d i v >
< div style = { { marginTop : '0.5rem' } } >
< ExportBtn label = "Full Report (multi-sheet)" exportKey = "atlas-full" loading = { loading } color = "#A855F7" colorRgb = "168,85,247" onClick = { exportAtlasFull } / >
< / d i v >
< 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 >
< / E x p o r t C a r d >
Add Jira Tickets, CCP Metrics, and Remediation Status export cards
New export cards on the Exports page:
- Jira Tickets: All tickets, open/active only, by-CVE multi-sheet
- CCP Compliance Metrics: Current snapshot, non-compliant devices,
trend history, full multi-sheet report
- Remediation Status: Cross-domain report combining CVEs, Jira tickets,
Archer exceptions, and Ivanti findings into a per-CVE progress view
2026-05-22 14:15:06 -06:00
{ /* ── Card 7: Jira Tickets ── */ }
< ExportCard
color = "#7DD3FC" colorRgb = "125,211,252"
icon = { FileText }
title = "Jira Tickets"
description = "Export Jira ticket tracking data. Full list, open/active only, or a multi-sheet workbook grouped by CVE for remediation status meetings."
>
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : '0.5rem' } } >
< ExportBtn label = "All Tickets" exportKey = "jira-all" loading = { loading } color = "#7DD3FC" colorRgb = "125,211,252" onClick = { exportJiraAll } / >
< ExportBtn label = "Open/Active Only" exportKey = "jira-open" loading = { loading } color = "#7DD3FC" colorRgb = "125,211,252" onClick = { exportJiraOpen } / >
< / d i v >
< div style = { { marginTop : '0.5rem' } } >
< ExportBtn label = "By CVE (multi-sheet)" exportKey = "jira-by-cve" loading = { loading } color = "#7DD3FC" colorRgb = "125,211,252" onClick = { exportJiraByCVE } / >
< / d i v >
< / E x p o r t C a r d >
{ /* ── Card 8: CCP Metrics ── */ }
< ExportCard
color = "#14B8A6" colorRgb = "20,184,166"
icon = { BarChart2 }
title = "CCP Compliance Metrics"
description = "Export cross-vertical compliance posture data. Current snapshot, non-compliant device list, historical trend, or a combined multi-sheet report."
>
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : '0.5rem' } } >
< ExportBtn label = "Current Snapshot" exportKey = "ccp-snapshot" loading = { loading } color = "#14B8A6" colorRgb = "20,184,166" onClick = { exportCCPSnapshot } / >
< ExportBtn label = "Non-Compliant Devices" exportKey = "ccp-noncompliant" loading = { loading } color = "#14B8A6" colorRgb = "20,184,166" onClick = { exportCCPNonCompliant } / >
< ExportBtn label = "Trend History" exportKey = "ccp-trend" loading = { loading } color = "#14B8A6" colorRgb = "20,184,166" onClick = { exportCCPTrend } / >
< ExportBtn label = "Full Report (multi-sheet)" exportKey = "ccp-full" loading = { loading } color = "#14B8A6" colorRgb = "20,184,166" onClick = { exportCCPFull } / >
< / d i v >
< p style = { { fontFamily : 'monospace' , fontSize : '0.65rem' , color : '#334155' , margin : '0.75rem 0 0' , lineHeight : 1.5 } } >
"Non-Compliant Devices" fetches per - metric device lists for all verticals — may take a moment .
< / p >
< / E x p o r t C a r d >
{ /* ── Card 9: Remediation Status (Cross-Domain) ── */ }
< ExportCard
color = "#EC4899" colorRgb = "236,72,153"
icon = { Shield }
title = "Remediation Status Report"
description = "Cross-domain view combining CVE entries, linked Jira tickets, Archer exceptions, and Ivanti findings into a single per-CVE/vendor row. Shows overall progress (Not Started, In Progress, Complete) based on ticket and exception statuses."
>
< ExportBtn label = "Export Remediation Status (.xlsx)" exportKey = "remediation" loading = { loading } color = "#EC4899" colorRgb = "236,72,153" onClick = { exportRemediationStatus } / >
< p style = { { fontFamily : 'monospace' , fontSize : '0.65rem' , color : '#334155' , margin : '0.75rem 0 0' , lineHeight : 1.5 } } >
Pulls from CVE database , Jira tickets , Archer tickets , and Ivanti findings cache . Best for leadership status updates .
< / p >
< / E x p o r t C a r d >
2026-03-18 11:39:26 -06:00
< / 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
}