Files
cve-dashboard/backend/routes/ivantiTodoQueue.js

353 lines
14 KiB
JavaScript
Raw Normal View History

// routes/ivantiTodoQueue.js
const express = require('express');
const pool = require('../db');
const { requireAuth, requireGroup } = require('../middleware/auth');
const logAudit = require('../helpers/auditLog');
const VALID_WORKFLOW_TYPES = ['FP', 'Archer', 'CARD', 'GRANITE'];
const VALID_STATUSES = ['pending', 'complete'];
function isValidVendor(vendor) {
if (typeof vendor !== 'string') return false;
const trimmed = vendor.trim();
return trimmed.length > 0 && trimmed.length <= 200;
}
function createIvantiTodoQueueRouter() {
const router = express.Router();
// GET /api/ivanti/todo-queue
router.get('/', requireAuth(), async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT q.*
FROM ivanti_todo_queue q
WHERE q.user_id = $1
ORDER BY q.vendor ASC, q.created_at ASC`,
[req.user.id]
);
const parsed = rows.map((r) => ({
...r,
cves: r.cves_json ? JSON.parse(r.cves_json) : [],
}));
res.json(parsed);
} catch (err) {
console.error('Error fetching todo queue:', err);
res.status(500).json({ error: 'Internal server error.' });
}
});
// POST /api/ivanti/todo-queue/batch
router.post('/batch', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
const { findings, workflow_type, vendor } = req.body;
if (!Array.isArray(findings) || findings.length < 1 || findings.length > 200) {
return res.status(400).json({ error: 'findings array must contain 1-200 items.' });
}
for (let i = 0; i < findings.length; i++) {
const f = findings[i];
if (!f || typeof f.finding_id !== 'string' || f.finding_id.trim().length === 0) {
return res.status(400).json({ error: 'Each finding must have a non-empty finding_id string.' });
}
}
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (!['CARD', 'GRANITE'].includes(workflow_type)) {
if (!isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
}
}
if (vendor !== undefined && vendor !== '' && typeof vendor === 'string' && vendor.trim().length > 200) {
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
const vendorVal = ['CARD', 'GRANITE'].includes(workflow_type) ? '' : vendor.trim();
const userId = req.user.id;
const client = await pool.connect();
try {
await client.query('BEGIN');
const insertedIds = [];
for (const f of findings) {
const findingId = f.finding_id.trim();
const title = f.finding_title && typeof f.finding_title === 'string'
? f.finding_title.slice(0, 500) : null;
const cvesJson = Array.isArray(f.cves) ? JSON.stringify(f.cves) : null;
const ipVal = f.ip_address && typeof f.ip_address === 'string'
? f.ip_address.trim().slice(0, 64) : null;
const hostVal = f.hostname && typeof f.hostname === 'string'
? f.hostname.trim().slice(0, 255) : null;
const { rows } = await client.query(
`INSERT INTO ivanti_todo_queue
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id`,
[userId, findingId, title, cvesJson, ipVal, hostVal, vendorVal, workflow_type]
);
insertedIds.push(rows[0].id);
}
await client.query('COMMIT');
// Fetch all inserted rows
const { rows: fetchedRows } = await pool.query(
`SELECT * FROM ivanti_todo_queue WHERE id = ANY($1)`,
[insertedIds]
);
const items = fetchedRows.map((r) => ({
...r,
cves: r.cves_json ? JSON.parse(r.cves_json) : [],
}));
logAudit({
userId: req.user.id,
username: req.user.username,
action: 'batch_add_to_queue',
entityType: 'ivanti_todo_queue',
entityId: null,
details: {
count: insertedIds.length,
workflow_type: workflow_type,
finding_ids: findings.map((f) => f.finding_id.trim()),
},
ipAddress: req.ip,
});
return res.status(201).json({ items });
} catch (err) {
await client.query('ROLLBACK');
console.error('Batch insert error:', err);
return res.status(500).json({ error: 'Internal server error.' });
} finally {
client.release();
}
});
// POST /api/ivanti/todo-queue
router.post('/', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
const { finding_id, finding_title, cves, ip_address, hostname, vendor, workflow_type } = req.body;
if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) {
return res.status(400).json({ error: 'finding_id is required.' });
}
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (!['CARD', 'GRANITE'].includes(workflow_type) && !isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
}
if (vendor !== undefined && vendor !== '' && !isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
const vendorVal = ['CARD', 'GRANITE'].includes(workflow_type) ? '' : vendor.trim();
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 hostVal = hostname && typeof hostname === 'string' ? hostname.trim().slice(0, 255) : null;
const title = finding_title && typeof finding_title === 'string'
? finding_title.slice(0, 500) : null;
try {
const { rows } = await pool.query(
`INSERT INTO ivanti_todo_queue
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[req.user.id, finding_id.trim(), title, cvesJson, ipVal, hostVal, vendorVal, workflow_type]
);
const result = {
...rows[0],
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
};
res.status(201).json(result);
} catch (err) {
console.error('Error adding to queue:', err);
res.status(500).json({ error: 'Internal server error.' });
}
});
// PUT /api/ivanti/todo-queue/:id
router.put('/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
const { id } = req.params;
const { vendor, workflow_type, status } = req.body;
if (vendor !== undefined && !isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor must be a non-empty string (max 200 chars).' });
}
if (workflow_type !== undefined && !VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (status !== undefined && !VALID_STATUSES.includes(status)) {
return res.status(400).json({ error: 'status must be pending or complete.' });
}
try {
const { rows: existingRows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
[id, req.user.id]
);
if (!existingRows[0]) {
return res.status(404).json({ error: 'Queue item not found.' });
}
const updates = [];
const params = [];
let paramIndex = 1;
if (vendor !== undefined) {
updates.push(`vendor = $${paramIndex++}`);
params.push(vendor.trim());
}
if (workflow_type !== undefined) {
updates.push(`workflow_type = $${paramIndex++}`);
params.push(workflow_type);
}
if (status !== undefined) {
updates.push(`status = $${paramIndex++}`);
params.push(status);
}
if (updates.length === 0) {
return res.status(400).json({ error: 'No fields to update.' });
}
updates.push('updated_at = NOW()');
params.push(id, req.user.id);
await pool.query(
`UPDATE ivanti_todo_queue SET ${updates.join(', ')} WHERE id = $${paramIndex++} AND user_id = $${paramIndex}`,
params
);
const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1', [id]
);
const result = {
...rows[0],
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
};
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error.' });
}
});
// POST /api/ivanti/todo-queue/:id/redirect
router.post('/:id/redirect', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
const { id } = req.params;
const { workflow_type, vendor } = req.body;
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, or GRANITE.' });
}
if (!['CARD', 'GRANITE'].includes(workflow_type)) {
if (!isValidVendor(vendor)) {
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
}
}
if (vendor !== undefined && vendor !== '' && typeof vendor === 'string' && vendor.trim().length > 200) {
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
const vendorVal = ['CARD', 'GRANITE'].includes(workflow_type) ? '' : vendor.trim();
try {
const { rows: origRows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
[id, req.user.id]
);
const original = origRows[0];
if (!original) {
return res.status(404).json({ error: 'Queue item not found.' });
}
if (original.status !== 'complete') {
return res.status(400).json({ error: 'Only completed queue items can be redirected.' });
}
const { rows } = await pool.query(
`INSERT INTO ivanti_todo_queue
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[req.user.id, original.finding_id, original.finding_title, original.cves_json, original.ip_address, original.hostname, vendorVal, workflow_type]
);
logAudit({
userId: req.user.id,
username: req.user.username,
action: 'queue_item_redirected',
entityType: 'ivanti_todo_queue',
entityId: String(original.id),
details: {
original_workflow_type: original.workflow_type,
target_workflow_type: workflow_type,
new_item_id: rows[0].id,
vendor: vendorVal,
},
ipAddress: req.ip,
});
const result = {
...rows[0],
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
};
return res.status(201).json(result);
} catch (err) {
console.error('Error redirecting queue item:', err);
res.status(500).json({ error: 'Internal server error.' });
}
});
// DELETE /api/ivanti/todo-queue/completed
router.delete('/completed', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
try {
const result = await pool.query(
"DELETE FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'",
[req.user.id]
);
res.json({ message: 'Completed items cleared.', deleted: result.rowCount });
} catch (err) {
console.error('Error clearing completed queue items:', err);
res.status(500).json({ error: 'Internal server error.' });
}
});
// DELETE /api/ivanti/todo-queue/:id
router.delete('/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
const { id } = req.params;
try {
const { rows } = await pool.query(
'SELECT id FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
[id, req.user.id]
);
if (!rows[0]) {
return res.status(404).json({ error: 'Queue item not found.' });
}
await pool.query(
'DELETE FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
[id, req.user.id]
);
res.json({ message: 'Queue item deleted.' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error.' });
}
});
return router;
}
module.exports = createIvantiTodoQueueRouter;