Add Group by Host toggle to Ivanti findings table
Client-side grouping that collapses duplicate assets (same hostname + IP) with multiple finding IDs into expandable host rows. Hosts with only one finding remain as normal flat rows. - Toggle button in toolbar switches between flat and grouped views - Group header rows preserve column alignment (severity, host, IP in proper columns) - Expanded sub-rows show full finding details with all interactions intact - Selection, queue, hide, and workflow actions all work in both modes - Groups sorted by highest severity; expand/collapse all controls included
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, ChevronRight, ChevronsUpDown, Settings2, GripVertical, Eye, EyeOff, Filter, Download, RotateCcw, Trash2, X, ListTodo, Upload, FileText, Check, AlertTriangle, CornerUpRight, Edit3, Square, CheckSquare, MinusSquare, Search, Database, Plus, FileSpreadsheet } from 'lucide-react';
|
import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, ChevronRight, ChevronsUpDown, Settings2, GripVertical, Eye, EyeOff, Filter, Download, RotateCcw, Trash2, X, ListTodo, Upload, FileText, Check, AlertTriangle, CornerUpRight, Edit3, Square, CheckSquare, MinusSquare, Search, Database, Plus, FileSpreadsheet, Layers } from 'lucide-react';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import IvantiCountsChart from './IvantiCountsChart';
|
import IvantiCountsChart from './IvantiCountsChart';
|
||||||
@@ -5852,6 +5852,10 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
const [cardConfigured, setCardConfigured] = useState(false);
|
const [cardConfigured, setCardConfigured] = useState(false);
|
||||||
const [cardTeams, setCardTeams] = useState([]);
|
const [cardTeams, setCardTeams] = useState([]);
|
||||||
|
|
||||||
|
// Group-by-host toggle state
|
||||||
|
const [groupByHost, setGroupByHost] = useState(false);
|
||||||
|
const [expandedHosts, setExpandedHosts] = useState(new Set());
|
||||||
|
|
||||||
const updateColumns = useCallback((newOrder) => {
|
const updateColumns = useCallback((newOrder) => {
|
||||||
setColumnOrder(newOrder);
|
setColumnOrder(newOrder);
|
||||||
saveColumnOrder(newOrder);
|
saveColumnOrder(newOrder);
|
||||||
@@ -6165,6 +6169,67 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
return sort.dir === 'asc' ? cmp : -cmp;
|
return sort.dir === 'asc' ? cmp : -cmp;
|
||||||
}), [filtered, sort]);
|
}), [filtered, sort]);
|
||||||
|
|
||||||
|
// Grouped view — aggregate findings by hostName + ipAddress
|
||||||
|
const groupedByHost = useMemo(() => {
|
||||||
|
if (!groupByHost) return { groups: [], singles: [] };
|
||||||
|
const map = new Map();
|
||||||
|
sorted.forEach(f => {
|
||||||
|
const hostKey = `${(f.overrides?.hostName || f.hostName || '').toLowerCase()}||${(f.ipAddress || '').toLowerCase()}`;
|
||||||
|
if (!map.has(hostKey)) {
|
||||||
|
map.set(hostKey, {
|
||||||
|
hostKey,
|
||||||
|
hostName: f.overrides?.hostName || f.hostName || '',
|
||||||
|
ipAddress: f.ipAddress || '',
|
||||||
|
findings: [],
|
||||||
|
highestSeverity: 0,
|
||||||
|
highestVrrGroup: '',
|
||||||
|
cveSet: new Set(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const group = map.get(hostKey);
|
||||||
|
group.findings.push(f);
|
||||||
|
if (f.severity > group.highestSeverity) {
|
||||||
|
group.highestSeverity = f.severity;
|
||||||
|
group.highestVrrGroup = f.vrrGroup || '';
|
||||||
|
}
|
||||||
|
(f.cves || []).forEach(c => group.cveSet.add(c));
|
||||||
|
});
|
||||||
|
// Separate: groups with 2+ findings vs singles that stay flat
|
||||||
|
const groups = [];
|
||||||
|
const singles = [];
|
||||||
|
for (const g of map.values()) {
|
||||||
|
if (g.findings.length > 1) groups.push(g);
|
||||||
|
else singles.push(g.findings[0]);
|
||||||
|
}
|
||||||
|
groups.sort((a, b) => b.highestSeverity - a.highestSeverity);
|
||||||
|
return { groups, singles };
|
||||||
|
}, [sorted, groupByHost]);
|
||||||
|
|
||||||
|
// Combined render order for grouped mode: grouped hosts first, then singles
|
||||||
|
const groupedRenderList = useMemo(() => {
|
||||||
|
if (!groupByHost) return [];
|
||||||
|
const list = [];
|
||||||
|
groupedByHost.groups.forEach(g => list.push({ type: 'group', group: g }));
|
||||||
|
groupedByHost.singles.forEach(f => list.push({ type: 'single', finding: f }));
|
||||||
|
return list;
|
||||||
|
}, [groupByHost, groupedByHost]);
|
||||||
|
|
||||||
|
const toggleHostExpand = useCallback((hostKey) => {
|
||||||
|
setExpandedHosts(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(hostKey)) next.delete(hostKey); else next.add(hostKey);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const expandAllHosts = useCallback(() => {
|
||||||
|
setExpandedHosts(new Set(groupedByHost.groups.map(g => g.hostKey)));
|
||||||
|
}, [groupedByHost]);
|
||||||
|
|
||||||
|
const collapseAllHosts = useCallback(() => {
|
||||||
|
setExpandedHosts(new Set());
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Select/deselect all visible rows
|
// Select/deselect all visible rows
|
||||||
const toggleSelectAll = useCallback(() => {
|
const toggleSelectAll = useCallback(() => {
|
||||||
const allVisibleIds = sorted.map(f => String(f.id));
|
const allVisibleIds = sorted.map(f => String(f.id));
|
||||||
@@ -6908,6 +6973,24 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setGroupByHost(g => !g); setExpandedHosts(new Set()); }}
|
||||||
|
title={groupByHost ? 'Switch to flat view' : 'Group findings by host'}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '0.375rem',
|
||||||
|
padding: '0.375rem 0.75rem',
|
||||||
|
background: groupByHost ? 'rgba(139,92,246,0.15)' : 'rgba(139,92,246,0.06)',
|
||||||
|
border: `1px solid rgba(139,92,246,${groupByHost ? '0.5' : '0.2'})`,
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
color: groupByHost ? '#A78BFA' : '#7C3AED',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layers style={{ width: '13px', height: '13px' }} />
|
||||||
|
{groupByHost ? 'Grouped' : 'Group'}
|
||||||
|
</button>
|
||||||
<ColumnManager columnOrder={columnOrder} onChange={updateColumns} />
|
<ColumnManager columnOrder={columnOrder} onChange={updateColumns} />
|
||||||
<RowVisibilityManager hiddenRowIds={hiddenRowIds} findings={findings} onRestore={restoreRow} onRestoreAll={restoreAllRows} />
|
<RowVisibilityManager hiddenRowIds={hiddenRowIds} findings={findings} onRestore={restoreRow} onRestoreAll={restoreAllRows} />
|
||||||
<button
|
<button
|
||||||
@@ -7136,6 +7219,178 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{groupByHost ? (
|
||||||
|
/* ---- Grouped-by-host view ---- */
|
||||||
|
<>
|
||||||
|
{groupedRenderList.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={visibleCols.length + 3} style={{ padding: '0.4rem 0.75rem', background: 'rgba(139,92,246,0.04)', borderBottom: '1px solid rgba(139,92,246,0.15)' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
|
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', color: '#A78BFA', fontWeight: '600' }}>
|
||||||
|
{groupedByHost.groups.length} grouped host{groupedByHost.groups.length !== 1 ? 's' : ''} · {groupedByHost.singles.length} single{groupedByHost.singles.length !== 1 ? 's' : ''} · {sorted.length} total
|
||||||
|
</span>
|
||||||
|
<button onClick={expandAllHosts} style={{ background: 'none', border: 'none', color: '#64748B', fontFamily: 'monospace', fontSize: '0.62rem', cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>expand all</button>
|
||||||
|
<button onClick={collapseAllHosts} style={{ background: 'none', border: 'none', color: '#64748B', fontFamily: 'monospace', fontSize: '0.62rem', cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>collapse all</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{groupedRenderList.map((item, itemIdx) => {
|
||||||
|
if (item.type === 'single') {
|
||||||
|
// Render single-finding hosts as normal flat rows
|
||||||
|
const finding = item.finding;
|
||||||
|
const isSelected = selectedIds.has(finding.id);
|
||||||
|
const rowBg = isSelected ? 'rgba(14,165,233,0.12)' : (itemIdx % 2 === 0 ? 'rgba(15,26,46,0.4)' : 'rgba(10,18,32,0.4)');
|
||||||
|
const queued = isQueued(finding.id);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={finding.id}
|
||||||
|
style={{ borderBottom: '1px solid rgba(255,255,255,0.04)', background: rowBg }}
|
||||||
|
onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'rgba(14,165,233,0.05)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = rowBg; }}
|
||||||
|
>
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px', cursor: 'pointer' }} onClick={() => toggleRowSelection(finding.id)}>
|
||||||
|
{selectedRowIds.has(String(finding.id))
|
||||||
|
? <CheckSquare style={{ width: '13px', height: '13px', color: '#4fc3f7' }} />
|
||||||
|
: <Square style={{ width: '13px', height: '13px', color: '#8892a2' }} />
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px' }}>
|
||||||
|
<button onClick={() => hideRow(finding.id)} title="Hide this row" style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }} onMouseEnter={(e) => { e.currentTarget.querySelector('svg').style.color = '#4fc3f7'; }} onMouseLeave={(e) => { e.currentTarget.querySelector('svg').style.color = '#8892a2'; }}>
|
||||||
|
<EyeOff style={{ width: '13px', height: '13px', color: '#8892a2' }} />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px' }} onClick={() => { if (queued) return; setSelectedIds((prev) => { const next = new Set(prev); if (next.has(finding.id)) next.delete(finding.id); else next.add(finding.id); return next; }); setLastClickedId(finding.id); }}>
|
||||||
|
<input type="checkbox" readOnly checked={queued || isSelected} style={{ accentColor: queued ? '#10B981' : '#0EA5E9', width: '13px', height: '13px', cursor: queued ? 'default' : 'pointer', pointerEvents: 'none' }} />
|
||||||
|
</td>
|
||||||
|
{visibleCols.map((col) => (
|
||||||
|
<TableCell key={col.key} colKey={col.key} finding={finding} canWrite={canWrite()} onCveMouseEnter={handleCveMouseEnter} onCveMouseLeave={handleCveMouseLeave} fpSubmissions={fpSubmissions} onEditSubmission={handleEditSubmission} atlasStatusMap={atlasStatusMap} onAtlasBadgeClick={(hostId) => { setAtlasSelectedHostId(hostId); setAtlasSelectedHostName(finding.hostName || finding.ipAddress || ''); setAtlasSelectedFindingId(finding.id || null); setAtlasPanelOpen(true); }} />
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Render grouped host header + expandable sub-rows
|
||||||
|
const group = item.group;
|
||||||
|
const isExpanded = expandedHosts.has(group.hostKey);
|
||||||
|
const sc = severityColor(group.highestVrrGroup);
|
||||||
|
return (
|
||||||
|
<React.Fragment key={group.hostKey}>
|
||||||
|
{/* Host group header — uses same columns as regular rows */}
|
||||||
|
<tr
|
||||||
|
onClick={() => toggleHostExpand(group.hostKey)}
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid rgba(139,92,246,0.15)',
|
||||||
|
background: isExpanded ? 'rgba(139,92,246,0.06)' : 'rgba(15,26,46,0.5)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(139,92,246,0.08)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = isExpanded ? 'rgba(139,92,246,0.06)' : 'rgba(15,26,46,0.5)'; }}
|
||||||
|
>
|
||||||
|
{/* Expand/collapse icon in first fixed column */}
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px' }}>
|
||||||
|
{isExpanded
|
||||||
|
? <ChevronDown style={{ width: '13px', height: '13px', color: '#A78BFA' }} />
|
||||||
|
: <ChevronRight style={{ width: '13px', height: '13px', color: '#A78BFA' }} />
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
{/* Empty cells for hide + checkbox columns */}
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', width: '36px' }} />
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', width: '36px' }} />
|
||||||
|
{/* Render each column cell — show host-level summary data in the matching column positions */}
|
||||||
|
{visibleCols.map((col) => {
|
||||||
|
switch (col.key) {
|
||||||
|
case 'findingId':
|
||||||
|
return (
|
||||||
|
<td key={col.key} style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.35rem', padding: '0.15rem 0.5rem', borderRadius: '0.25rem', background: 'rgba(139,92,246,0.12)', border: '1px solid rgba(139,92,246,0.3)', fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '700', color: '#A78BFA' }}>
|
||||||
|
{group.findings.length} findings
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
case 'severity':
|
||||||
|
return (
|
||||||
|
<td key={col.key} style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.3rem', padding: '0.2rem 0.45rem', borderRadius: '0.25rem', background: sc.bg, border: `1px solid ${sc.border}40`, fontFamily: 'monospace', fontWeight: '700', color: sc.text, fontSize: '0.72rem' }}>
|
||||||
|
{group.highestSeverity.toFixed(2)}
|
||||||
|
<span style={{ fontSize: '0.6rem', opacity: 0.75 }}>{group.highestVrrGroup}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
case 'hostName':
|
||||||
|
return (
|
||||||
|
<td key={col.key} style={{ padding: '0.45rem 0.75rem' }}>
|
||||||
|
<span style={{ color: '#E2E8F0', fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600' }}>
|
||||||
|
{group.hostName || '—'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
case 'ipAddress':
|
||||||
|
return (
|
||||||
|
<td key={col.key} style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
|
||||||
|
<span style={{ color: '#0EA5E9', fontFamily: 'monospace', fontSize: '0.72rem' }}>
|
||||||
|
{group.ipAddress || '—'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
case 'cves':
|
||||||
|
return (
|
||||||
|
<td key={col.key} style={{ padding: '0.45rem 0.75rem' }}>
|
||||||
|
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#64748B' }}>
|
||||||
|
{group.cveSet.size} CVE{group.cveSet.size !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <td key={col.key} style={{ padding: '0.45rem 0.75rem' }} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
{/* Expanded sub-rows — individual findings */}
|
||||||
|
{isExpanded && group.findings.map((finding, idx) => {
|
||||||
|
const isSelected = selectedIds.has(finding.id);
|
||||||
|
const rowBg = isSelected ? 'rgba(14,165,233,0.12)' : (idx % 2 === 0 ? 'rgba(20,30,50,0.5)' : 'rgba(15,24,42,0.5)');
|
||||||
|
const queued = isQueued(finding.id);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={finding.id}
|
||||||
|
style={{ borderBottom: '1px solid rgba(255,255,255,0.03)', background: rowBg, borderLeft: '3px solid rgba(139,92,246,0.25)' }}
|
||||||
|
onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'rgba(14,165,233,0.05)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = rowBg; }}
|
||||||
|
>
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px', cursor: 'pointer' }} onClick={() => toggleRowSelection(finding.id)}>
|
||||||
|
{selectedRowIds.has(String(finding.id))
|
||||||
|
? <CheckSquare style={{ width: '13px', height: '13px', color: '#4fc3f7' }} />
|
||||||
|
: <Square style={{ width: '13px', height: '13px', color: '#8892a2' }} />
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px' }}>
|
||||||
|
<button onClick={() => hideRow(finding.id)} title="Hide this row" style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }} onMouseEnter={(e) => { e.currentTarget.querySelector('svg').style.color = '#4fc3f7'; }} onMouseLeave={(e) => { e.currentTarget.querySelector('svg').style.color = '#8892a2'; }}>
|
||||||
|
<EyeOff style={{ width: '13px', height: '13px', color: '#8892a2' }} />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'center', width: '36px' }} onClick={() => { if (queued) return; setSelectedIds((prev) => { const next = new Set(prev); if (next.has(finding.id)) next.delete(finding.id); else next.add(finding.id); return next; }); setLastClickedId(finding.id); }}>
|
||||||
|
<input type="checkbox" readOnly checked={queued || isSelected} style={{ accentColor: queued ? '#10B981' : '#0EA5E9', width: '13px', height: '13px', cursor: queued ? 'default' : 'pointer', pointerEvents: 'none' }} />
|
||||||
|
</td>
|
||||||
|
{visibleCols.map((col) => (
|
||||||
|
<TableCell key={col.key} colKey={col.key} finding={finding} canWrite={canWrite()} onCveMouseEnter={handleCveMouseEnter} onCveMouseLeave={handleCveMouseLeave} fpSubmissions={fpSubmissions} onEditSubmission={handleEditSubmission} atlasStatusMap={atlasStatusMap} onAtlasBadgeClick={(hostId) => { setAtlasSelectedHostId(hostId); setAtlasSelectedHostName(finding.hostName || finding.ipAddress || ''); setAtlasSelectedFindingId(finding.id || null); setAtlasPanelOpen(true); }} />
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{groupedRenderList.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={visibleCols.length + 3} style={{ textAlign: 'center', padding: '2rem', color: '#475569', fontFamily: 'monospace', fontSize: '0.75rem' }}>
|
||||||
|
{activeFilterCount > 0 ? 'No findings match the current filters' : 'No findings found'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* ---- Flat view (default) ---- */
|
||||||
|
<>
|
||||||
{sorted.map((finding, idx) => {
|
{sorted.map((finding, idx) => {
|
||||||
const isSelected = selectedIds.has(finding.id);
|
const isSelected = selectedIds.has(finding.id);
|
||||||
const rowBg = isSelected ? 'rgba(14,165,233,0.12)' : (idx % 2 === 0 ? 'rgba(15,26,46,0.4)' : 'rgba(10,18,32,0.4)');
|
const rowBg = isSelected ? 'rgba(14,165,233,0.12)' : (idx % 2 === 0 ? 'rgba(15,26,46,0.4)' : 'rgba(10,18,32,0.4)');
|
||||||
@@ -7230,6 +7485,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user