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:
25
backend/migrations/add_todo_queue_ip_address.js
Normal file
25
backend/migrations/add_todo_queue_ip_address.js
Normal 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!'));
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -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,7 +1394,19 @@ 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 ? (
|
||||||
|
item.ip_address && (
|
||||||
|
<div style={{
|
||||||
|
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
|
||||||
|
color: done ? '#334155' : '#10B981',
|
||||||
|
textDecoration: done ? 'line-through' : 'none',
|
||||||
|
marginTop: '2px',
|
||||||
|
}}>
|
||||||
|
{item.ip_address}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
cves.length > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontFamily: 'monospace', fontSize: '0.62rem',
|
fontFamily: 'monospace', fontSize: '0.62rem',
|
||||||
color: done ? '#334155' : '#64748B',
|
color: done ? '#334155' : '#64748B',
|
||||||
@@ -1403,6 +1416,7 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
|
|||||||
}} title={cves.join(', ')}>
|
}} title={cves.join(', ')}>
|
||||||
{cveDisplay}
|
{cveDisplay}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1675,6 +1689,7 @@ export default function ReportingPage({ filterDate, filterEXC }) {
|
|||||||
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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user