Add DECOM workflow type, auto-note/hide on decom, show CVEs on CARD queue items, auto-run migrations in pipeline
- Add DECOM to queue workflow types (red badge, inventory-style display) - When findings are added as DECOM, auto-set note to 'DECOM' and hide row - Hidden rows are excluded from donut charts (removes from pending count) - Show CVEs on CARD/GRANITE/DECOM queue items (was previously omitted) - Add backend/migrations/run-all.js for CI/CD auto-migration execution - Pipeline now runs migrations before service restart on both staging and prod - Add add_decom_workflow_type.js migration (updates CHECK constraint)
This commit is contained in:
@@ -1387,7 +1387,7 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, [onCancel]);
|
||||
|
||||
const isCard = queueForm.workflowType === 'CARD' || queueForm.workflowType === 'GRANITE';
|
||||
const isCard = queueForm.workflowType === 'CARD' || queueForm.workflowType === 'GRANITE' || queueForm.workflowType === 'DECOM';
|
||||
const canSubmit = isCard || queueForm.vendor.trim().length > 0;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
@@ -1457,6 +1457,7 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
|
||||
{ key: 'Archer', col: '#0EA5E9', rgb: '14,165,233' },
|
||||
{ key: 'CARD', col: '#10B981', rgb: '16,185,129' },
|
||||
{ key: 'GRANITE', col: '#A1887F', rgb: '161,136,127' },
|
||||
{ key: 'DECOM', col: '#EF4444', rgb: '239,68,68' },
|
||||
].map(({ key, col, rgb }) => {
|
||||
const active = queueForm.workflowType === key;
|
||||
return (
|
||||
@@ -1683,12 +1684,13 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
const wfColor = item.workflow_type === 'FP' ? { col: '#F59E0B', rgb: '245,158,11' }
|
||||
: item.workflow_type === 'Archer' ? { col: '#0EA5E9', rgb: '14,165,233' }
|
||||
: item.workflow_type === 'GRANITE' ? { col: '#A1887F', rgb: '161,136,127' }
|
||||
: item.workflow_type === 'DECOM' ? { col: '#EF4444', rgb: '239,68,68' }
|
||||
: { col: '#10B981', rgb: '16,185,129' };
|
||||
const cves = item.cves || [];
|
||||
const cveDisplay = cves.length > 0
|
||||
? cves.slice(0, 3).join(', ') + (cves.length > 3 ? ` +${cves.length - 3}` : '')
|
||||
: '—';
|
||||
const isInventoryItem = item.workflow_type === 'CARD' || item.workflow_type === 'GRANITE';
|
||||
const isInventoryItem = item.workflow_type === 'CARD' || item.workflow_type === 'GRANITE' || item.workflow_type === 'DECOM';
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
@@ -1752,6 +1754,17 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
{item.ip_address}
|
||||
</div>
|
||||
)}
|
||||
{cves.length > 0 && (
|
||||
<div style={{
|
||||
fontFamily: 'monospace', fontSize: '0.62rem',
|
||||
color: done ? '#334155' : '#64748B',
|
||||
textDecoration: done ? 'line-through' : 'none',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
marginTop: '2px',
|
||||
}} title={cves.join(', ')}>
|
||||
{cveDisplay}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -2112,12 +2125,13 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
return null;
|
||||
};
|
||||
|
||||
// Inventory items (CARD + GRANITE) are their own top section; everything else groups by vendor
|
||||
// Inventory items (CARD + GRANITE + DECOM) are their own top section; everything else groups by vendor
|
||||
const grouped = useMemo(() => {
|
||||
const inventoryItems = items.filter((i) => i.workflow_type === 'CARD' || i.workflow_type === 'GRANITE');
|
||||
const inventoryItems = items.filter((i) => i.workflow_type === 'CARD' || i.workflow_type === 'GRANITE' || i.workflow_type === 'DECOM');
|
||||
const cardItems = inventoryItems.filter((i) => i.workflow_type === 'CARD');
|
||||
const graniteItems = inventoryItems.filter((i) => i.workflow_type === 'GRANITE');
|
||||
const otherItems = items.filter((i) => i.workflow_type !== 'CARD' && i.workflow_type !== 'GRANITE');
|
||||
const decomItems = inventoryItems.filter((i) => i.workflow_type === 'DECOM');
|
||||
const otherItems = items.filter((i) => i.workflow_type !== 'CARD' && i.workflow_type !== 'GRANITE' && i.workflow_type !== 'DECOM');
|
||||
|
||||
const map = {};
|
||||
otherItems.forEach((item) => {
|
||||
@@ -2130,7 +2144,7 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
}));
|
||||
|
||||
return inventoryItems.length > 0
|
||||
? [{ key: '__INVENTORY__', label: 'Inventory', cardItems, graniteItems, items: inventoryItems, isInventory: true }, ...vendorGroups]
|
||||
? [{ key: '__INVENTORY__', label: 'Inventory', cardItems, graniteItems, decomItems, items: inventoryItems, isInventory: true }, ...vendorGroups]
|
||||
: vendorGroups;
|
||||
}, [items]);
|
||||
|
||||
@@ -2204,7 +2218,7 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
Check a row in the findings table to add it.
|
||||
</span>
|
||||
</div>
|
||||
) : grouped.map(({ key, label, items: groupItems, isInventory, cardItems, graniteItems }) => (
|
||||
) : grouped.map(({ key, label, items: groupItems, isInventory, cardItems, graniteItems, decomItems }) => (
|
||||
<div key={key} style={{ marginBottom: '1.25rem' }}>
|
||||
{/* Group header */}
|
||||
<div style={{
|
||||
@@ -2220,7 +2234,7 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Items — Inventory section renders CARD then GRANITE with optional sub-divider */}
|
||||
{/* Items — Inventory section renders CARD then GRANITE then DECOM with optional sub-dividers */}
|
||||
{isInventory ? (
|
||||
<>
|
||||
{cardItems.map((item) => (
|
||||
@@ -2237,6 +2251,14 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
}} />
|
||||
)}
|
||||
{graniteItems.map((item) => renderQueueItem(item, { done: item.status === 'complete', selectedIds, toggleSelect, onUpdate, onDelete, setRedirectItem, canWrite }))}
|
||||
{(cardItems.length > 0 || graniteItems.length > 0) && decomItems.length > 0 && (
|
||||
<div style={{
|
||||
height: '1px',
|
||||
background: 'rgba(239,68,68,0.18)',
|
||||
margin: '0.5rem 0.625rem',
|
||||
}} />
|
||||
)}
|
||||
{decomItems.map((item) => renderQueueItem(item, { done: item.status === 'complete', selectedIds, toggleSelect, onUpdate, onDelete, setRedirectItem, canWrite }))}
|
||||
</>
|
||||
) : (
|
||||
groupItems.map((item) => renderQueueItem(item, { done: item.status === 'complete', selectedIds, toggleSelect, onUpdate, onDelete, setRedirectItem, canWrite }))
|
||||
@@ -4086,7 +4108,7 @@ function FpEditModal({ open, onClose, submission, queueItems, onSuccess }) {
|
||||
// SelectionToolbar — batch action bar for multi-selected findings
|
||||
// ---------------------------------------------------------------------------
|
||||
function SelectionToolbar({ count, workflowType, vendor, submitting, error, onWorkflowChange, onVendorChange, onSubmit, onClear }) {
|
||||
const isCard = workflowType === 'CARD' || workflowType === 'GRANITE';
|
||||
const isCard = workflowType === 'CARD' || workflowType === 'GRANITE' || workflowType === 'DECOM';
|
||||
const canSubmit = !submitting && (isCard || vendor.trim().length > 0);
|
||||
|
||||
return (
|
||||
@@ -4122,6 +4144,7 @@ function SelectionToolbar({ count, workflowType, vendor, submitting, error, onWo
|
||||
{ type: 'Archer', color: '#0EA5E9', rgb: '14,165,233' },
|
||||
{ type: 'CARD', color: '#10B981', rgb: '16,185,129' },
|
||||
{ type: 'GRANITE', color: '#A1887F', rgb: '161,136,127' },
|
||||
{ type: 'DECOM', color: '#EF4444', rgb: '239,68,68' },
|
||||
].map(({ type, color, rgb }) => {
|
||||
const active = workflowType === type;
|
||||
return (
|
||||
@@ -5426,13 +5449,30 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
setQueueItems((prev) => [...prev, data].sort((a, b) =>
|
||||
a.vendor.localeCompare(b.vendor) || a.id - b.id
|
||||
));
|
||||
|
||||
// DECOM: auto-set note and auto-hide the finding
|
||||
if (queueForm.workflowType === 'DECOM') {
|
||||
// Set note to DECOM
|
||||
fetch(`${API_BASE}/ivanti/findings/${finding.id}/note`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ note: 'DECOM' }),
|
||||
}).catch(() => {});
|
||||
// Update local findings state
|
||||
setFindings(prev => prev.map(f =>
|
||||
f.id === finding.id ? { ...f, note: 'DECOM' } : f
|
||||
));
|
||||
// Auto-hide the row
|
||||
hideRow(finding.id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error adding to queue:', e);
|
||||
}
|
||||
setAddPopover(null);
|
||||
setQueueForm({ vendor: '', workflowType: 'FP' });
|
||||
}, [addPopover, queueForm]);
|
||||
}, [addPopover, queueForm, hideRow]);
|
||||
|
||||
// Prune selection when filters change — keep only IDs still in filtered set
|
||||
useEffect(() => {
|
||||
@@ -5479,7 +5519,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
body: JSON.stringify({
|
||||
findings: findingsPayload,
|
||||
workflow_type: batchWorkflowType,
|
||||
vendor: batchWorkflowType === 'CARD' ? '' : batchVendor.trim(),
|
||||
vendor: batchWorkflowType === 'CARD' || batchWorkflowType === 'GRANITE' || batchWorkflowType === 'DECOM' ? '' : batchVendor.trim(),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -5487,6 +5527,32 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
setQueueItems((prev) => [...prev, ...(data.items || [])].sort((a, b) =>
|
||||
(a.vendor || '').localeCompare(b.vendor || '') || a.id - b.id
|
||||
));
|
||||
|
||||
// DECOM: auto-set note and auto-hide all selected findings
|
||||
if (batchWorkflowType === 'DECOM') {
|
||||
const ids = [...selectedIds];
|
||||
// Set notes to DECOM in parallel (fire-and-forget)
|
||||
ids.forEach(id => {
|
||||
fetch(`${API_BASE}/ivanti/findings/${id}/note`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ note: 'DECOM' }),
|
||||
}).catch(() => {});
|
||||
});
|
||||
// Update local findings state
|
||||
setFindings(prev => prev.map(f =>
|
||||
ids.includes(f.id) ? { ...f, note: 'DECOM' } : f
|
||||
));
|
||||
// Auto-hide all
|
||||
setHiddenRowIds(prev => {
|
||||
const next = new Set(prev);
|
||||
ids.forEach(id => next.add(String(id)));
|
||||
saveHiddenRows(next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedIds(new Set());
|
||||
setBatchWorkflowType('FP');
|
||||
setBatchVendor('');
|
||||
|
||||
Reference in New Issue
Block a user