feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
// routes/ivantiTodoQueue.js
|
|
|
|
|
|
const express = require('express');
|
2026-04-07 09:52:26 -06:00
|
|
|
|
const { requireGroup } = require('../middleware/auth');
|
2026-04-09 09:49:40 -06:00
|
|
|
|
const logAudit = require('../helpers/auditLog');
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
|
2026-03-26 14:46:59 -06:00
|
|
|
|
const VALID_WORKFLOW_TYPES = ['FP', 'Archer', 'CARD'];
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
const VALID_STATUSES = ['pending', 'complete'];
|
|
|
|
|
|
|
|
|
|
|
|
function isValidVendor(vendor) {
|
2026-04-07 10:23:10 -06:00
|
|
|
|
if (typeof vendor !== 'string') return false;
|
|
|
|
|
|
const trimmed = vendor.trim();
|
|
|
|
|
|
return trimmed.length > 0 && trimmed.length <= 200;
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createIvantiTodoQueueRouter(db, requireAuth) {
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
|
2026-04-09 09:49:40 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* GET /api/ivanti/todo-queue
|
|
|
|
|
|
*
|
|
|
|
|
|
* Fetch the current user's queue items, ordered by vendor then created_at.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Array<Object>} 200 - Array of queue items, each with:
|
|
|
|
|
|
* id, user_id, finding_id, finding_title, cves_json, ip_address,
|
|
|
|
|
|
* vendor, workflow_type, status, created_at, updated_at, cves (parsed array)
|
|
|
|
|
|
* @returns {Object} 500 - { error: string } on database error
|
|
|
|
|
|
*/
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
router.get('/', requireAuth(db), (req, res) => {
|
|
|
|
|
|
db.all(
|
|
|
|
|
|
`SELECT * FROM ivanti_todo_queue
|
|
|
|
|
|
WHERE user_id = ?
|
|
|
|
|
|
ORDER BY vendor ASC, created_at ASC`,
|
|
|
|
|
|
[req.user.id],
|
|
|
|
|
|
(err, rows) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error('Error fetching todo queue:', err);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
// Parse cves_json back to array for each row
|
|
|
|
|
|
const parsed = rows.map((r) => ({
|
|
|
|
|
|
...r,
|
|
|
|
|
|
cves: r.cves_json ? JSON.parse(r.cves_json) : [],
|
|
|
|
|
|
}));
|
|
|
|
|
|
res.json(parsed);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-09 09:49:40 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* POST /api/ivanti/todo-queue/batch
|
|
|
|
|
|
*
|
|
|
|
|
|
* Add multiple findings to the current user's queue in a single transaction.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @body {Object[]} findings - Required array of 1–200 finding objects
|
|
|
|
|
|
* @body {string} findings[].finding_id - Required, non-empty finding identifier
|
|
|
|
|
|
* @body {string} [findings[].finding_title] - Optional finding title (max 500 chars)
|
|
|
|
|
|
* @body {string[]} [findings[].cves] - Optional array of CVE identifiers
|
|
|
|
|
|
* @body {string} [findings[].ip_address] - Optional IP address (max 64 chars)
|
|
|
|
|
|
* @body {string} workflow_type - One of 'FP', 'Archer', 'CARD'
|
|
|
|
|
|
* @body {string} vendor - Required for FP/Archer (max 200 chars); optional for CARD
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} 201 - { items: Array<Object> } array of created queue items,
|
|
|
|
|
|
* each with: id, user_id, finding_id, finding_title, cves_json, ip_address,
|
|
|
|
|
|
* vendor, workflow_type, status, created_at, updated_at, cves (parsed array)
|
|
|
|
|
|
* @returns {Object} 400 - { error: string } on validation failure
|
|
|
|
|
|
* @returns {Object} 500 - { error: string } on database/transaction error (all inserts rolled back)
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post('/batch', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
|
|
|
|
|
const { findings, workflow_type, vendor } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// --- Validation ---
|
|
|
|
|
|
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, or CARD.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (workflow_type !== 'CARD') {
|
|
|
|
|
|
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 = workflow_type === 'CARD' ? '' : vendor.trim();
|
|
|
|
|
|
const userId = req.user.id;
|
|
|
|
|
|
|
|
|
|
|
|
// --- Transactional batch insert ---
|
|
|
|
|
|
// Prepare all row values upfront
|
|
|
|
|
|
const rows = findings.map((f) => {
|
|
|
|
|
|
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;
|
|
|
|
|
|
return [userId, findingId, title, cvesJson, ipVal, vendorVal, workflow_type];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const insertedIds = [];
|
|
|
|
|
|
let insertError = null;
|
|
|
|
|
|
let remaining = rows.length;
|
|
|
|
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
|
|
db.run('BEGIN TRANSACTION');
|
|
|
|
|
|
|
|
|
|
|
|
rows.forEach((params) => {
|
|
|
|
|
|
db.run(
|
|
|
|
|
|
`INSERT INTO ivanti_todo_queue
|
|
|
|
|
|
(user_id, finding_id, finding_title, cves_json, ip_address, vendor, workflow_type)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
|
params,
|
|
|
|
|
|
function (err) {
|
|
|
|
|
|
if (err && !insertError) {
|
|
|
|
|
|
insertError = err;
|
|
|
|
|
|
} else if (!err) {
|
|
|
|
|
|
insertedIds.push(this.lastID);
|
|
|
|
|
|
}
|
|
|
|
|
|
remaining--;
|
|
|
|
|
|
|
|
|
|
|
|
// After all insert callbacks have fired, commit or rollback
|
|
|
|
|
|
if (remaining === 0) {
|
|
|
|
|
|
if (insertError) {
|
|
|
|
|
|
db.run('ROLLBACK', () => {
|
|
|
|
|
|
console.error('Batch insert error:', insertError);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
db.run('COMMIT', (commitErr) => {
|
|
|
|
|
|
if (commitErr) {
|
|
|
|
|
|
console.error('Batch commit error:', commitErr);
|
|
|
|
|
|
db.run('ROLLBACK', () => {});
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch all inserted rows
|
|
|
|
|
|
const placeholders = insertedIds.map(() => '?').join(',');
|
|
|
|
|
|
db.all(
|
|
|
|
|
|
`SELECT * FROM ivanti_todo_queue WHERE id IN (${placeholders})`,
|
|
|
|
|
|
insertedIds,
|
|
|
|
|
|
(fetchErr, fetchedRows) => {
|
|
|
|
|
|
if (fetchErr) {
|
|
|
|
|
|
console.error('Error fetching inserted batch rows:', fetchErr);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const items = (fetchedRows || []).map((r) => ({
|
|
|
|
|
|
...r,
|
|
|
|
|
|
cves: r.cves_json ? JSON.parse(r.cves_json) : [],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// Audit log (fire-and-forget)
|
|
|
|
|
|
logAudit(db, {
|
|
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* POST /api/ivanti/todo-queue
|
|
|
|
|
|
*
|
|
|
|
|
|
* Add a single finding to the current user's queue.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @body {string} finding_id - Required, non-empty finding identifier
|
|
|
|
|
|
* @body {string} [finding_title] - Optional finding title (max 500 chars)
|
|
|
|
|
|
* @body {string[]} [cves] - Optional array of CVE identifiers
|
|
|
|
|
|
* @body {string} [ip_address] - Optional IP address (max 64 chars)
|
|
|
|
|
|
* @body {string} vendor - Required for FP/Archer (max 200 chars); optional for CARD
|
|
|
|
|
|
* @body {string} workflow_type - One of 'FP', 'Archer', 'CARD'
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} 201 - Created queue item with parsed cves array:
|
|
|
|
|
|
* id, user_id, finding_id, finding_title, cves_json, ip_address,
|
|
|
|
|
|
* vendor, workflow_type, status, created_at, updated_at, cves
|
|
|
|
|
|
* @returns {Object} 400 - { error: string } on validation failure
|
|
|
|
|
|
* @returns {Object} 500 - { error: string } on database error
|
|
|
|
|
|
*/
|
2026-04-07 09:52:26 -06:00
|
|
|
|
router.post('/', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
2026-03-26 15:01:32 -06:00
|
|
|
|
const { finding_id, finding_title, cves, ip_address, vendor, workflow_type } = req.body;
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
|
|
|
|
|
|
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)) {
|
2026-03-26 14:52:06 -06:00
|
|
|
|
return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
// Vendor is required for FP and Archer, optional for CARD
|
|
|
|
|
|
if (workflow_type !== 'CARD' && !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.' });
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 14:52:06 -06:00
|
|
|
|
const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim();
|
|
|
|
|
|
const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null;
|
2026-03-26 15:01:32 -06:00
|
|
|
|
const ipVal = ip_address && typeof ip_address === 'string' ? ip_address.trim().slice(0, 64) : null;
|
2026-03-26 14:52:06 -06:00
|
|
|
|
const title = finding_title && typeof finding_title === 'string'
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
? finding_title.slice(0, 500)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
db.run(
|
|
|
|
|
|
`INSERT INTO ivanti_todo_queue
|
2026-03-26 15:01:32 -06:00
|
|
|
|
(user_id, finding_id, finding_title, cves_json, ip_address, vendor, workflow_type)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
|
[req.user.id, finding_id.trim(), title, cvesJson, ipVal, vendorVal, workflow_type],
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
function (err) {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error('Error adding to queue:', err);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
db.get(
|
|
|
|
|
|
'SELECT * FROM ivanti_todo_queue WHERE id = ?',
|
|
|
|
|
|
[this.lastID],
|
|
|
|
|
|
(err2, row) => {
|
|
|
|
|
|
if (err2 || !row) {
|
|
|
|
|
|
return res.status(201).json({ id: this.lastID, message: 'Added to queue.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
res.status(201).json({ ...row, cves: row.cves_json ? JSON.parse(row.cves_json) : [] });
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-09 09:49:40 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* PUT /api/ivanti/todo-queue/:id
|
|
|
|
|
|
*
|
|
|
|
|
|
* Update vendor, workflow_type, or status on a queue item — scoped to current user.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} id - Queue item ID (URL parameter)
|
|
|
|
|
|
* @body {string} [vendor] - New vendor string (max 200 chars)
|
|
|
|
|
|
* @body {string} [workflow_type] - One of 'FP', 'Archer', 'CARD'
|
|
|
|
|
|
* @body {string} [status] - One of 'pending', 'complete'
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} 200 - Updated queue item with parsed cves array:
|
|
|
|
|
|
* id, user_id, finding_id, finding_title, cves_json, ip_address,
|
|
|
|
|
|
* vendor, workflow_type, status, created_at, updated_at, cves
|
|
|
|
|
|
* @returns {Object} 400 - { error: string } on validation failure or no fields to update
|
|
|
|
|
|
* @returns {Object} 404 - { error: string } if item not found for current user
|
|
|
|
|
|
* @returns {Object} 500 - { error: string } on database error
|
|
|
|
|
|
*/
|
2026-04-07 09:52:26 -06:00
|
|
|
|
router.put('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
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 or Archer.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (status !== undefined && !VALID_STATUSES.includes(status)) {
|
|
|
|
|
|
return res.status(400).json({ error: 'status must be pending or complete.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
db.get(
|
|
|
|
|
|
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ?',
|
|
|
|
|
|
[id, req.user.id],
|
|
|
|
|
|
(err, existing) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!existing) {
|
|
|
|
|
|
return res.status(404).json({ error: 'Queue item not found.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updates = [];
|
|
|
|
|
|
const params = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (vendor !== undefined) {
|
|
|
|
|
|
updates.push('vendor = ?');
|
|
|
|
|
|
params.push(vendor.trim());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (workflow_type !== undefined) {
|
|
|
|
|
|
updates.push('workflow_type = ?');
|
|
|
|
|
|
params.push(workflow_type);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (status !== undefined) {
|
|
|
|
|
|
updates.push('status = ?');
|
|
|
|
|
|
params.push(status);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (updates.length === 0) {
|
|
|
|
|
|
return res.status(400).json({ error: 'No fields to update.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updates.push('updated_at = CURRENT_TIMESTAMP');
|
|
|
|
|
|
params.push(id, req.user.id);
|
|
|
|
|
|
|
|
|
|
|
|
db.run(
|
|
|
|
|
|
`UPDATE ivanti_todo_queue SET ${updates.join(', ')} WHERE id = ? AND user_id = ?`,
|
|
|
|
|
|
params,
|
|
|
|
|
|
function (err2) {
|
|
|
|
|
|
if (err2) {
|
|
|
|
|
|
console.error(err2);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
db.get(
|
|
|
|
|
|
'SELECT * FROM ivanti_todo_queue WHERE id = ?',
|
|
|
|
|
|
[id],
|
|
|
|
|
|
(err3, row) => {
|
|
|
|
|
|
if (err3 || !row) {
|
|
|
|
|
|
return res.json({ message: 'Queue item updated.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
res.json({ ...row, cves: row.cves_json ? JSON.parse(row.cves_json) : [] });
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-09 09:49:40 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* DELETE /api/ivanti/todo-queue/completed
|
|
|
|
|
|
*
|
|
|
|
|
|
* Bulk-delete all completed items for the current user.
|
|
|
|
|
|
* IMPORTANT: This route must be registered BEFORE DELETE /:id.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} 200 - { message: string, deleted: number }
|
|
|
|
|
|
* @returns {Object} 500 - { error: string } on database error
|
|
|
|
|
|
*/
|
2026-04-07 09:52:26 -06:00
|
|
|
|
router.delete('/completed', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
db.run(
|
|
|
|
|
|
"DELETE FROM ivanti_todo_queue WHERE user_id = ? AND status = 'complete'",
|
|
|
|
|
|
[req.user.id],
|
|
|
|
|
|
function (err) {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error('Error clearing completed queue items:', err);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
res.json({ message: 'Completed items cleared.', deleted: this.changes });
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-09 09:49:40 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* DELETE /api/ivanti/todo-queue/:id
|
|
|
|
|
|
*
|
|
|
|
|
|
* Delete a single queue item — scoped to current user.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} id - Queue item ID (URL parameter)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} 200 - { message: string }
|
|
|
|
|
|
* @returns {Object} 404 - { error: string } if item not found for current user
|
|
|
|
|
|
* @returns {Object} 500 - { error: string } on database error
|
|
|
|
|
|
*/
|
2026-04-07 09:52:26 -06:00
|
|
|
|
router.delete('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
|
|
|
|
|
|
db.get(
|
|
|
|
|
|
'SELECT id FROM ivanti_todo_queue WHERE id = ? AND user_id = ?',
|
|
|
|
|
|
[id, req.user.id],
|
|
|
|
|
|
(err, row) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!row) {
|
|
|
|
|
|
return res.status(404).json({ error: 'Queue item not found.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
db.run(
|
|
|
|
|
|
'DELETE FROM ivanti_todo_queue WHERE id = ? AND user_id = ?',
|
|
|
|
|
|
[id, req.user.id],
|
|
|
|
|
|
function (err2) {
|
|
|
|
|
|
if (err2) {
|
|
|
|
|
|
console.error(err2);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
res.json({ message: 'Queue item deleted.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return router;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = createIvantiTodoQueueRouter;
|