feat(reporting): store and display IP address on CARD queue items

Adds ip_address column to ivanti_todo_queue so CARD entries carry the
host IP needed to locate the asset in CARD.

- Migration: ALTER TABLE ADD COLUMN ip_address TEXT (safe to re-run)
- Backend: accepts ip_address in POST body, stores up to 64 chars
- Frontend: captures finding.ipAddress when adding to queue; CARD items
  in the queue panel show the IP in green instead of the CVE list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 15:01:32 -06:00
parent 6bf6371e51
commit 89b1f57ef4
4 changed files with 58 additions and 16 deletions

View File

@@ -0,0 +1,25 @@
// Migration: Add ip_address column to ivanti_todo_queue
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const dbPath = path.join(__dirname, '..', 'cve_database.db');
const db = new sqlite3.Database(dbPath);
console.log('Starting add_todo_queue_ip_address migration...');
db.run(
'ALTER TABLE ivanti_todo_queue ADD COLUMN ip_address TEXT',
(err) => {
if (err) {
// Column may already exist if migration was run before
if (err.message.includes('duplicate column name')) {
console.log('✓ ip_address column already exists, skipping');
} else {
console.error('Error adding column:', err);
}
} else {
console.log('✓ ip_address column added');
}
db.close(() => console.log('Migration complete!'));
}
);

View File

@@ -37,7 +37,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
// POST /api/ivanti/todo-queue // POST /api/ivanti/todo-queue
// Add a finding to the queue // Add a finding to the queue
router.post('/', requireAuth(db), (req, res) => { router.post('/', requireAuth(db), (req, res) => {
const { finding_id, finding_title, cves, vendor, workflow_type } = req.body; const { finding_id, finding_title, cves, ip_address, vendor, workflow_type } = req.body;
if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) { if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) {
return res.status(400).json({ error: 'finding_id is required.' }); return res.status(400).json({ error: 'finding_id is required.' });
@@ -55,15 +55,16 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim(); const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim();
const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null; const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null;
const ipVal = ip_address && typeof ip_address === 'string' ? ip_address.trim().slice(0, 64) : null;
const title = finding_title && typeof finding_title === 'string' const title = finding_title && typeof finding_title === 'string'
? finding_title.slice(0, 500) ? finding_title.slice(0, 500)
: null; : null;
db.run( db.run(
`INSERT INTO ivanti_todo_queue `INSERT INTO ivanti_todo_queue
(user_id, finding_id, finding_title, cves_json, vendor, workflow_type) (user_id, finding_id, finding_title, cves_json, ip_address, vendor, workflow_type)
VALUES (?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?)`,
[req.user.id, finding_id.trim(), title, cvesJson, vendorVal, workflow_type], [req.user.id, finding_id.trim(), title, cvesJson, ipVal, vendorVal, workflow_type],
function (err) { function (err) {
if (err) { if (err) {
console.error('Error adding to queue:', err); console.error('Error adding to queue:', err);

View File

@@ -138,6 +138,7 @@ const db = new sqlite3.Database('./cve_database.db', (err) => {
finding_id TEXT NOT NULL, finding_id TEXT NOT NULL,
finding_title TEXT, finding_title TEXT,
cves_json TEXT, cves_json TEXT,
ip_address TEXT,
vendor TEXT NOT NULL, vendor TEXT NOT NULL,
workflow_type TEXT NOT NULL CHECK(workflow_type IN ('FP', 'Archer', 'CARD')), workflow_type TEXT NOT NULL CHECK(workflow_type IN ('FP', 'Archer', 'CARD')),
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'complete')), status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'complete')),

View File

@@ -1361,6 +1361,7 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
const cveDisplay = cves.length > 0 const cveDisplay = cves.length > 0
? cves.slice(0, 3).join(', ') + (cves.length > 3 ? ` +${cves.length - 3}` : '') ? cves.slice(0, 3).join(', ') + (cves.length > 3 ? ` +${cves.length - 3}` : '')
: '—'; : '—';
const isCardItem = item.workflow_type === 'CARD';
return ( return (
<div <div
key={item.id} key={item.id}
@@ -1393,16 +1394,29 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
}} title={item.finding_id}> }} title={item.finding_id}>
{item.finding_id} {item.finding_id}
</div> </div>
{cves.length > 0 && ( {isCardItem ? (
<div style={{ item.ip_address && (
fontFamily: 'monospace', fontSize: '0.62rem', <div style={{
color: done ? '#334155' : '#64748B', fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
textDecoration: done ? 'line-through' : 'none', color: done ? '#334155' : '#10B981',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textDecoration: done ? 'line-through' : 'none',
marginTop: '1px', marginTop: '2px',
}} title={cves.join(', ')}> }}>
{cveDisplay} {item.ip_address}
</div> </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: '1px',
}} title={cves.join(', ')}>
{cveDisplay}
</div>
)
)} )}
</div> </div>
@@ -1673,8 +1687,9 @@ export default function ReportingPage({ filterDate, filterEXC }) {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
finding_id: finding.id, finding_id: finding.id,
finding_title: finding.title || null, finding_title: finding.title || null,
cves: finding.cves || [], cves: finding.cves || [],
ip_address: finding.ipAddress || null,
vendor: queueForm.vendor.trim(), vendor: queueForm.vendor.trim(),
workflow_type: queueForm.workflowType, workflow_type: queueForm.workflowType,
}), }),