Files
cve-dashboard/backend/routes/ivantiWorkflows.js
Jordan Ramos 33927b150b feat(postgres): migrate all route files from SQLite to pg pool
- All 16 route files now import pool from ../db directly
- Removed db parameter from all factory functions
- All callbacks replaced with async/await pool.query()
- All ? placeholders converted to $1, $2... numbered params
- datetime('now') → NOW(), INSERT OR IGNORE → ON CONFLICT DO NOTHING
- LIKE → ILIKE for case-insensitive searches
- Error detection: err.code === '23505' for unique violations
- server.js no longer passes pool/db/requireAuth to route factories
- Only ivantiFindings.js still receives pool (pending task 8 rewrite)
2026-05-06 11:44:17 -06:00

200 lines
7.1 KiB
JavaScript

// Ivanti / RiskSense Workflow Routes
// Data is cached in PostgreSQL and refreshed on a daily schedule or on-demand.
const express = require('express');
const pool = require('../db');
const { requireAuth, requireGroup } = require('../middleware/auth');
const { ivantiPost } = require('../helpers/ivantiApi');
const SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
// ---------------------------------------------------------------------------
// Core sync — calls Ivanti API, stores result in PostgreSQL
// ---------------------------------------------------------------------------
async function syncWorkflows() {
const apiKey = process.env.IVANTI_API_KEY;
const clientId = process.env.IVANTI_CLIENT_ID || '1550';
const firstName = process.env.IVANTI_FIRST_NAME || '';
const lastName = process.env.IVANTI_LAST_NAME || '';
const skipTls = process.env.IVANTI_SKIP_TLS === 'true';
if (!apiKey) {
const errMsg = 'IVANTI_API_KEY not set in .env — skipping sync';
console.warn('[Ivanti]', errMsg);
await pool.query(
`UPDATE ivanti_sync_state SET sync_status='error', error_message=$1, synced_at=NOW() WHERE id=1`,
[errMsg]
);
return;
}
console.log('[Ivanti] Syncing workflows...');
const urlPath = `/client/${encodeURIComponent(clientId)}/workflowBatch/search`;
const body = {
filters: [
{
field: 'created_by_last_name',
exclusive: false,
operator: 'IN',
orWithPrevious: false,
implicitFilters: [],
value: lastName,
caseSensitive: false
},
{
field: 'created_by_first_name',
exclusive: false,
operator: 'IN',
orWithPrevious: false,
implicitFilters: [],
value: firstName,
caseSensitive: false
}
],
projection: 'internal',
sort: [{ field: 'created', direction: 'DESC' }],
page: 0,
size: 50
};
try {
const result = await ivantiPost(urlPath, body, apiKey, skipTls);
if (result.status === 401) {
throw new Error('Invalid or missing API key (401) — check IVANTI_API_KEY in .env');
}
if (result.status === 419) {
throw new Error('Insufficient privileges (419) — API key lacks workflow access');
}
if (result.status === 429) {
throw new Error('Rate limited (429) — will retry at next scheduled sync');
}
if (result.status !== 200) {
throw new Error(`Ivanti API returned unexpected status ${result.status}`);
}
const data = JSON.parse(result.body);
let total = 0;
let workflows = [];
if (data.page && typeof data.page.totalElements === 'number') {
total = data.page.totalElements;
workflows = data._embedded?.workflowBatches
|| data._embedded?.workflowBatch
|| [];
} else if (typeof data.total === 'number') {
total = data.total;
workflows = data.data || data.content || data.results || [];
} else if (typeof data.totalElements === 'number') {
total = data.totalElements;
workflows = data.content || data.data || [];
} else if (Array.isArray(data)) {
workflows = data;
total = data.length;
}
await pool.query(
`UPDATE ivanti_sync_state
SET total=$1, workflows_json=$2, synced_at=NOW(), sync_status='success', error_message=NULL
WHERE id=1`,
[total, JSON.stringify(workflows)]
);
console.log(`[Ivanti] Sync complete — ${total} workflows`);
} catch (err) {
const msg = err.message || 'Unknown error';
console.error('[Ivanti] Sync failed:', msg);
await pool.query(
`UPDATE ivanti_sync_state SET sync_status='error', error_message=$1, synced_at=NOW() WHERE id=1`,
[msg]
);
}
}
// ---------------------------------------------------------------------------
// Scheduler — runs sync immediately if >24h stale, then every 24h
// ---------------------------------------------------------------------------
async function scheduleSync() {
try {
const { rows } = await pool.query('SELECT synced_at FROM ivanti_sync_state WHERE id = 1');
const row = rows[0];
if (!row || !row.synced_at) {
syncWorkflows();
} else {
const lastSync = new Date(row.synced_at);
const hoursSince = (Date.now() - lastSync.getTime()) / (1000 * 60 * 60);
if (hoursSince >= 24) {
syncWorkflows();
} else {
const hoursUntil = (24 - hoursSince).toFixed(1);
console.log(`[Ivanti] Last sync ${hoursSince.toFixed(1)}h ago — next auto-sync in ${hoursUntil}h`);
}
}
} catch (err) {
console.error('[Ivanti] Schedule check failed:', err);
syncWorkflows();
}
setInterval(() => syncWorkflows(), SYNC_INTERVAL_MS);
}
// ---------------------------------------------------------------------------
// Helper — read current state from DB and return as JSON-ready object
// ---------------------------------------------------------------------------
async function readState() {
const { rows } = await pool.query(
'SELECT total, workflows_json, synced_at, sync_status, error_message FROM ivanti_sync_state WHERE id = 1'
);
const row = rows[0];
if (!row) return { total: 0, workflows: [], synced_at: null, sync_status: 'never', error_message: null };
let workflows = [];
try { workflows = JSON.parse(row.workflows_json || '[]'); } catch (_) { /* leave empty */ }
return {
total: row.total || 0,
workflows,
synced_at: row.synced_at,
sync_status: row.sync_status,
error_message: row.error_message
};
}
// ---------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------
function createIvantiWorkflowsRouter() {
const router = express.Router();
// Kick off scheduler (fire-and-forget on startup)
scheduleSync().catch((err) => console.error('[Ivanti] Init failed:', err));
// All routes require authentication
router.use(requireAuth());
// GET / — return cached data (fast, no external call)
router.get('/', async (req, res) => {
try {
res.json(await readState());
} catch {
res.status(500).json({ error: 'Database error reading sync state' });
}
});
// POST /sync — trigger an immediate sync, await completion, return fresh state
router.post('/sync', requireGroup('Admin', 'Standard_User'), async (req, res) => {
await syncWorkflows();
try {
res.json(await readState());
} catch {
res.status(500).json({ error: 'Sync ran but could not read updated state' });
}
});
return router;
}
module.exports = createIvantiWorkflowsRouter;