// Atlas InfoSec Action Plans Routes // Proxies CRUD operations to the Atlas API and maintains a local cache // for fast badge rendering on the ReportingPage. const express = require('express'); const pool = require('../db'); const { requireAuth, requireGroup } = require('../middleware/auth'); const logAudit = require('../helpers/auditLog'); const { isConfigured, atlasGet, atlasPut, atlasPatch, atlasPost } = require('../helpers/atlasApi'); const fs = require('fs'); const path = require('path'); const VALID_PLAN_TYPES = ['decommission', 'remediation', 'false_positive', 'risk_acceptance', 'scan_exclusion']; const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; // Diagnostic log helper function syncLog(msg) { const line = `${new Date().toISOString()} ${msg}\n`; try { fs.appendFileSync(path.join(__dirname, '..', 'atlas-sync-debug.log'), line); } catch (_) { /* ignore */ } console.log(msg); } // --------------------------------------------------------------------------- // Pure aggregation function — exported for testability // --------------------------------------------------------------------------- function aggregateAtlasMetrics(rows) { const result = { totalHosts: rows.length, hostsWithPlans: 0, hostsWithoutPlans: 0, plansByType: {}, plansByStatus: {}, totalPlans: 0 }; for (const row of rows) { if (row.has_action_plan === true || row.has_action_plan === 1) { result.hostsWithPlans++; } else { result.hostsWithoutPlans++; } let plans; try { plans = JSON.parse(row.plans_json); } catch (e) { continue; } if (!Array.isArray(plans)) continue; for (const plan of plans) { result.totalPlans++; if (plan.plan_type) { result.plansByType[plan.plan_type] = (result.plansByType[plan.plan_type] || 0) + 1; } if (plan.status) { result.plansByStatus[plan.status] = (result.plansByStatus[plan.status] || 0) + 1; } } } return result; } // --------------------------------------------------------------------------- // Router factory // --------------------------------------------------------------------------- function createAtlasRouter() { const router = express.Router(); /** * GET /metrics * * Returns aggregated Atlas action plan metrics from the local cache. * Accepts optional `teams` query parameter to scope metrics to hosts * belonging to specific BUs (via JOIN on ivanti_findings). * * @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG') * @returns {Object} 200 - { totalHosts, hostsWithPlans, hostsWithoutPlans, plansByType, plansByStatus, totalPlans } * @returns {Object} 503 - { error } when Atlas API is not configured * @returns {Object} 500 - { error } on database failure */ router.get('/metrics', requireAuth(), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } try { const teamsParam = req.query.teams; let rows; if (teamsParam) { const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean); if (teams.length > 0) { const patterns = teams.map(t => `%${t}%`); const result = await pool.query( `SELECT a.has_action_plan, a.plans_json FROM atlas_action_plans_cache a INNER JOIN ( SELECT DISTINCT host_id FROM ivanti_findings WHERE bu_ownership ILIKE ANY($1::text[]) ) f ON a.host_id = f.host_id`, [patterns] ); rows = result.rows; } else { const result = await pool.query( `SELECT has_action_plan, plans_json FROM atlas_action_plans_cache` ); rows = result.rows; } } else { const result = await pool.query( `SELECT has_action_plan, plans_json FROM atlas_action_plans_cache` ); rows = result.rows; } const metrics = aggregateAtlasMetrics(rows); res.json(metrics); } catch (err) { console.error('[Atlas] Error fetching metrics:', err.message); res.status(500).json({ error: 'Failed to fetch Atlas metrics.' }); } }); /** * GET /status * * Returns atlas_action_plans_cache contents for status display. * Accepts optional `teams` query parameter to scope results to hosts * belonging to specific BUs (via JOIN on ivanti_findings). * * @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG') * @returns {Array} 200 - Array of { host_id, has_action_plan, plan_count, plans_json, synced_at } * @returns {Object} 503 - { error } when Atlas API is not configured * @returns {Object} 500 - { error } on database failure */ router.get('/status', requireAuth(), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } try { const teamsParam = req.query.teams; let rows; if (teamsParam) { const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean); if (teams.length > 0) { const patterns = teams.map(t => `%${t}%`); const result = await pool.query( `SELECT a.host_id, a.has_action_plan, a.plan_count, a.plans_json, a.synced_at FROM atlas_action_plans_cache a INNER JOIN ( SELECT DISTINCT host_id FROM ivanti_findings WHERE bu_ownership ILIKE ANY($1::text[]) ) f ON a.host_id = f.host_id`, [patterns] ); rows = result.rows; } else { const result = await pool.query( `SELECT host_id, has_action_plan, plan_count, plans_json, synced_at FROM atlas_action_plans_cache` ); rows = result.rows; } } else { const result = await pool.query( `SELECT host_id, has_action_plan, plan_count, plans_json, synced_at FROM atlas_action_plans_cache` ); rows = result.rows; } res.json(rows); } catch (err) { console.error('[Atlas] Error fetching status:', err.message); res.status(500).json({ error: 'Failed to fetch Atlas status.' }); } }); /** * POST /sync * * Syncs action plan data from Atlas for all hosts found in ivanti_findings. * Fetches plans per host in batches of 5 and upserts into the local cache. * Requires Admin or Standard_User group. * * @returns {Object} 200 - { synced, withPlans, failed } * @returns {Object} 503 - { error } when Atlas API is not configured * @returns {Object} 500 - { error } on unexpected failure */ router.post('/sync', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } try { // Scope sync to the user's active teams if provided, otherwise sync only // findings from managed BUs (IVANTI_MANAGED_BUS) to avoid polluting cache // with "no plan" entries for BUs not covered by Atlas. const teamsParam = req.query.teams || req.body.teams || ''; const managedBUs = (process.env.IVANTI_MANAGED_BUS || 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM') .split(',').map(b => b.trim()).filter(Boolean); let findingsRows; if (teamsParam) { const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean); if (teams.length > 0) { const patterns = teams.map(t => `%${t}%`); const result = await pool.query( `SELECT DISTINCT host_id FROM ivanti_findings WHERE host_id IS NOT NULL AND host_id > 0 AND bu_ownership ILIKE ANY($1::text[])`, [patterns] ); findingsRows = result.rows; } else { // No valid teams — fall back to managed BUs const patterns = managedBUs.map(b => `%${b}%`); const result = await pool.query( `SELECT DISTINCT host_id FROM ivanti_findings WHERE host_id IS NOT NULL AND host_id > 0 AND bu_ownership ILIKE ANY($1::text[])`, [patterns] ); findingsRows = result.rows; } } else { // No teams specified — default to managed BUs only const patterns = managedBUs.map(b => `%${b}%`); const result = await pool.query( `SELECT DISTINCT host_id FROM ivanti_findings WHERE host_id IS NOT NULL AND host_id > 0 AND bu_ownership ILIKE ANY($1::text[])`, [patterns] ); findingsRows = result.rows; } const hostIds = findingsRows.map(r => r.host_id); if (hostIds.length === 0) { return res.json({ synced: 0, withPlans: 0, failed: 0 }); } let synced = 0; let withPlans = 0; let failed = 0; const BATCH_SIZE = 5; for (let i = 0; i < hostIds.length; i += BATCH_SIZE) { const batch = hostIds.slice(i, i + BATCH_SIZE); const results = await Promise.allSettled( batch.map(async (hostId) => { const result = await atlasGet('/hosts/' + hostId + '/action-plans'); return { hostId, result }; }) ); for (const settled of results) { if (settled.status === 'rejected') { failed++; console.warn('[Atlas Sync] Request failed for host:', settled.reason?.message || settled.reason); continue; } const { hostId, result } = settled.value; if (result.status >= 200 && result.status < 300) { let allPlans = []; let activePlans = []; try { const parsed = JSON.parse(result.body); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { activePlans = Array.isArray(parsed.active) ? parsed.active : []; const inactive = Array.isArray(parsed.inactive) ? parsed.inactive : []; allPlans = [...activePlans, ...inactive]; } else if (Array.isArray(parsed)) { allPlans = parsed; activePlans = parsed; } } catch (e) { allPlans = []; activePlans = []; } const planCount = activePlans.length; const hasActionPlan = planCount > 0; try { if (!hasActionPlan) { const { rows: existingRows } = await pool.query( `SELECT has_action_plan, plans_json, synced_at FROM atlas_action_plans_cache WHERE host_id = $1`, [hostId] ); const existing = existingRows[0]; if (existing && existing.has_action_plan === true) { let existingPlans = []; try { existingPlans = JSON.parse(existing.plans_json || '[]'); } catch (_) {} const hasBulkStub = existingPlans.some(p => p.source === 'bulk-create'); if (hasBulkStub) { const ageMs = Date.now() - new Date(existing.synced_at).getTime(); const TEN_MINUTES = 10 * 60 * 1000; if (ageMs < TEN_MINUTES) { synced++; withPlans++; continue; } } } } await pool.query( `INSERT INTO atlas_action_plans_cache (host_id, has_action_plan, plan_count, plans_json, synced_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT(host_id) DO UPDATE SET has_action_plan = EXCLUDED.has_action_plan, plan_count = EXCLUDED.plan_count, plans_json = EXCLUDED.plans_json, synced_at = EXCLUDED.synced_at`, [hostId, hasActionPlan, planCount, JSON.stringify(allPlans)] ); } catch (dbErr) { console.error('[Atlas Sync] DB upsert failed for host', hostId, ':', dbErr.message); } synced++; if (hasActionPlan) withPlans++; } else { failed++; console.warn(`[Atlas Sync] Non-2xx response for host ${hostId}: status ${result.status}`); } } } logAudit({ userId: req.user.id, username: req.user.username, action: 'ATLAS_SYNC', entityType: 'atlas_action_plans', entityId: null, details: { synced, withPlans, failed, totalHosts: hostIds.length }, ipAddress: req.ip }); res.json({ synced, withPlans, failed }); } catch (err) { console.error('[Atlas Sync] Unexpected error:', err.message); res.status(500).json({ error: 'Atlas sync failed: ' + err.message }); } }); /** * GET /hosts/:hostId/action-plans * * Proxies a request to Atlas to retrieve action plans for a specific host. * * @param {number} req.params.hostId - Positive integer host identifier * @returns {Object} 2xx - Action plans response from Atlas API * @returns {Object} 400 - { error } when hostId is invalid * @returns {Object} 502 - { error } when Atlas API is unreachable * @returns {Object} 503 - { error } when Atlas API is not configured */ router.get('/hosts/:hostId/action-plans', requireAuth(), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } const hostId = parseInt(req.params.hostId, 10); if (!Number.isInteger(hostId) || hostId <= 0) { return res.status(400).json({ error: 'hostId must be a positive integer' }); } try { const result = await atlasGet('/hosts/' + hostId + '/action-plans'); if (result.status >= 200 && result.status < 300) { let body; try { body = JSON.parse(result.body); } catch (e) { body = result.body; } res.status(result.status).json(body); } else { let errorBody; try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; } res.status(result.status).json(errorBody); } } catch (err) { console.error('[Atlas] GET action-plans failed for host', hostId, ':', err.message); res.status(502).json({ error: 'Failed to reach Atlas API: ' + err.message }); } }); /** * PUT /hosts/:hostId/action-plans * * Creates a new action plan for a host via the Atlas API. * Requires Admin or Standard_User group. * * @param {number} req.params.hostId - Positive integer host identifier * @param {Object} req.body * @param {string} req.body.plan_type - One of: decommission, remediation, false_positive, risk_acceptance, scan_exclusion * @param {string} req.body.commit_date - Date in YYYY-MM-DD format * @returns {Object} 2xx - Created plan response from Atlas API * @returns {Object} 400 - { error } when hostId, plan_type, or commit_date is invalid * @returns {Object} 502 - { error } when Atlas API is unreachable * @returns {Object} 503 - { error } when Atlas API is not configured */ router.put('/hosts/:hostId/action-plans', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } const hostId = parseInt(req.params.hostId, 10); if (!Number.isInteger(hostId) || hostId <= 0) { return res.status(400).json({ error: 'hostId must be a positive integer' }); } const { plan_type, commit_date } = req.body || {}; if (!plan_type || !VALID_PLAN_TYPES.includes(plan_type)) { return res.status(400).json({ error: 'plan_type must be one of: ' + VALID_PLAN_TYPES.join(', ') }); } if (!commit_date || !DATE_PATTERN.test(commit_date)) { return res.status(400).json({ error: 'commit_date must be a valid YYYY-MM-DD date string' }); } try { const result = await atlasPut('/hosts/' + hostId + '/action-plans', req.body); logAudit({ userId: req.user.id, username: req.user.username, action: 'ATLAS_CREATE_PLAN', entityType: 'atlas_action_plan', entityId: String(hostId), details: { hostId, plan_type, commit_date }, ipAddress: req.ip }); if (result.status >= 200 && result.status < 300) { let body; try { body = JSON.parse(result.body); } catch (e) { body = result.body; } res.status(result.status).json(body); } else { let errorBody; try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; } res.status(result.status).json(errorBody); } } catch (err) { console.error('[Atlas] PUT action-plans failed for host', hostId, ':', err.message); res.status(502).json({ error: 'Failed to reach Atlas API: ' + err.message }); } }); /** * PATCH /hosts/:hostId/action-plans * * Updates an existing action plan for a host via the Atlas API. * Requires Admin or Standard_User group. * * @param {number} req.params.hostId - Positive integer host identifier * @param {Object} req.body * @param {string} req.body.action_plan_id - Non-empty string identifying the plan to update * @param {Object} req.body.updates - Object containing fields to update * @returns {Object} 2xx - Updated plan response from Atlas API * @returns {Object} 400 - { error } when hostId, action_plan_id, or updates is invalid * @returns {Object} 502 - { error } when Atlas API is unreachable * @returns {Object} 503 - { error } when Atlas API is not configured */ router.patch('/hosts/:hostId/action-plans', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } const hostId = parseInt(req.params.hostId, 10); if (!Number.isInteger(hostId) || hostId <= 0) { return res.status(400).json({ error: 'hostId must be a positive integer' }); } const { action_plan_id, updates } = req.body || {}; if (!action_plan_id || typeof action_plan_id !== 'string' || action_plan_id.trim() === '') { return res.status(400).json({ error: 'action_plan_id is required and must be a non-empty string' }); } if (!updates || typeof updates !== 'object' || Array.isArray(updates)) { return res.status(400).json({ error: 'updates is required and must be an object' }); } try { const result = await atlasPatch('/hosts/' + hostId + '/action-plans', req.body); logAudit({ userId: req.user.id, username: req.user.username, action: 'ATLAS_UPDATE_PLAN', entityType: 'atlas_action_plan', entityId: String(hostId), details: { hostId, action_plan_id }, ipAddress: req.ip }); if (result.status >= 200 && result.status < 300) { let body; try { body = JSON.parse(result.body); } catch (e) { body = result.body; } res.status(result.status).json(body); } else { let errorBody; try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; } res.status(result.status).json(errorBody); } } catch (err) { console.error('[Atlas] PATCH action-plans failed for host', hostId, ':', err.message); res.status(502).json({ error: 'Failed to reach Atlas API: ' + err.message }); } }); /** * POST /hosts/bulk-action-plans * * Creates action plans for multiple hosts in a single request via the Atlas API. * Optimistically updates the local cache with stub plans after a successful response. * Requires Admin or Standard_User group. * * @param {Object} req.body * @param {number[]} req.body.host_ids - Non-empty array of positive integer host identifiers * @param {string} req.body.plan_type - One of: decommission, remediation, false_positive, risk_acceptance, scan_exclusion * @param {string} req.body.commit_date - Date in YYYY-MM-DD format * @returns {Object} 2xx - Bulk creation response from Atlas API * @returns {Object} 400 - { error } when host_ids, plan_type, or commit_date is invalid * @returns {Object} 502 - { error } when Atlas API is unreachable * @returns {Object} 503 - { error } when Atlas API is not configured */ router.post('/hosts/bulk-action-plans', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } const { host_ids, plan_type, commit_date } = req.body || {}; if (!Array.isArray(host_ids) || host_ids.length === 0) { return res.status(400).json({ error: 'host_ids must be a non-empty array of positive integers' }); } for (const id of host_ids) { if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: 'host_ids must be a non-empty array of positive integers' }); } } if (!plan_type || !VALID_PLAN_TYPES.includes(plan_type)) { return res.status(400).json({ error: 'plan_type must be one of: ' + VALID_PLAN_TYPES.join(', ') }); } if (!commit_date || !DATE_PATTERN.test(commit_date)) { return res.status(400).json({ error: 'commit_date must be a valid YYYY-MM-DD date string' }); } try { const result = await atlasPost('/hosts/create-bulk-action-plans', req.body); if (result.status >= 200 && result.status < 300) { let body; try { body = JSON.parse(result.body); } catch (e) { body = result.body; } // Optimistically update local cache for (const hid of host_ids) { try { const { rows: existingRows } = await pool.query( `SELECT plan_count, plans_json FROM atlas_action_plans_cache WHERE host_id = $1`, [hid] ); const existing = existingRows[0]; let existingPlans = []; if (existing && existing.plans_json) { try { existingPlans = JSON.parse(existing.plans_json); } catch (_) {} } const stubPlan = { plan_type, commit_date, source: 'bulk-create', created_at: new Date().toISOString() }; const updatedPlans = [...existingPlans, stubPlan]; const newCount = updatedPlans.length; await pool.query( `INSERT INTO atlas_action_plans_cache (host_id, has_action_plan, plan_count, plans_json, synced_at) VALUES ($1, true, $2, $3, NOW()) ON CONFLICT(host_id) DO UPDATE SET has_action_plan = true, plan_count = EXCLUDED.plan_count, plans_json = EXCLUDED.plans_json, synced_at = EXCLUDED.synced_at`, [hid, newCount, JSON.stringify(updatedPlans)] ); } catch (cacheErr) { console.error('[Atlas] Cache update failed for host', hid, ':', cacheErr.message); } } logAudit({ userId: req.user.id, username: req.user.username, action: 'ATLAS_BULK_CREATE_PLANS', entityType: 'atlas_action_plan', entityId: null, details: { host_ids, plan_type, commit_date, count: host_ids.length }, ipAddress: req.ip }); res.status(result.status).json(body); } else { let errorBody; try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; } res.status(result.status).json(errorBody); } } catch (err) { console.error('[Atlas] POST bulk-action-plans failed:', err.message); res.status(502).json({ error: 'Failed to reach Atlas API: ' + err.message }); } }); /** * POST /hosts/:hostId/refresh-cache * * Triggers Atlas to refresh its Ivanti data cache, then updates the local * action plans cache for the specified host. Useful when action plan creation * fails due to stale finding IDs. * Requires Admin or Standard_User group. * * @param {number} req.params.hostId - Positive integer host identifier * @returns {Object} 200 - { success, message } on successful cache refresh * @returns {Object} 400 - { error } when hostId is invalid * @returns {Object} 502 - { error } when Atlas API is unreachable * @returns {Object} 503 - { error } when Atlas API is not configured */ router.post('/hosts/:hostId/refresh-cache', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } const hostId = parseInt(req.params.hostId, 10); if (!Number.isInteger(hostId) || hostId <= 0) { return res.status(400).json({ error: 'hostId must be a positive integer' }); } try { const result = await atlasPost('/cache/refresh-ivanti', {}, { timeout: 30000 }); if (result.status >= 200 && result.status < 300) { // Also refresh our local action plans cache for this host const plansResult = await atlasGet('/hosts/' + hostId + '/action-plans'); if (plansResult.status >= 200 && plansResult.status < 300) { let allPlans = []; let activePlans = []; try { const parsed = JSON.parse(plansResult.body); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { activePlans = Array.isArray(parsed.active) ? parsed.active : []; const inactive = Array.isArray(parsed.inactive) ? parsed.inactive : []; allPlans = [...activePlans, ...inactive]; } else if (Array.isArray(parsed)) { allPlans = parsed; activePlans = parsed; } } catch (_) {} const planCount = activePlans.length; const hasActionPlan = planCount > 0; await pool.query( `INSERT INTO atlas_action_plans_cache (host_id, has_action_plan, plan_count, plans_json, synced_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT(host_id) DO UPDATE SET has_action_plan = EXCLUDED.has_action_plan, plan_count = EXCLUDED.plan_count, plans_json = EXCLUDED.plans_json, synced_at = EXCLUDED.synced_at`, [hostId, hasActionPlan, planCount, JSON.stringify(allPlans)] ); } res.json({ success: true, message: 'Atlas cache refreshed for host ' + hostId }); } else { let errorBody; try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; } res.status(result.status).json(errorBody); } } catch (err) { console.error('[Atlas] POST refresh-cache failed for host', hostId, ':', err.message); res.status(502).json({ error: 'Failed to reach Atlas API: ' + err.message }); } }); /** * POST /hosts/vulnerabilities * * Fetches Ivanti vulnerability data for the specified hosts from Atlas. * * @param {Object} req.body * @param {number[]} req.body.host_ids - Non-empty array of positive integer host identifiers * @returns {Object} 2xx - Vulnerability data response from Atlas API * @returns {Object} 400 - { error } when host_ids is invalid * @returns {Object} 502 - { error } when Atlas API is unreachable * @returns {Object} 503 - { error } when Atlas API is not configured */ router.post('/hosts/vulnerabilities', requireAuth(), async (req, res) => { if (!isConfigured) { return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' }); } const { host_ids } = req.body || {}; if (!Array.isArray(host_ids) || host_ids.length === 0) { return res.status(400).json({ error: 'host_ids must be a non-empty array of positive integers' }); } for (const id of host_ids) { if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: 'host_ids must be a non-empty array of positive integers' }); } } try { const result = await atlasPost('/ivanti-vulnerabilities-by-host', { host_ids }, { timeout: 30000 }); if (result.status >= 200 && result.status < 300) { let body; try { body = JSON.parse(result.body); } catch (e) { body = result.body; } res.status(result.status).json(body); } else { let errorBody; try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; } res.status(result.status).json(errorBody); } } catch (err) { console.error('[Atlas] POST hosts/vulnerabilities failed:', err.message); res.status(502).json({ error: 'Failed to reach Atlas API: ' + err.message }); } }); return router; } module.exports = createAtlasRouter; module.exports.aggregateAtlasMetrics = aggregateAtlasMetrics;