// Atlas InfoSec Action Plans Routes // Proxies CRUD operations to the Atlas API and maintains a local SQLite cache // for fast badge rendering on the ReportingPage. const express = require('express'); const { requireGroup } = require('../middleware/auth'); const logAudit = require('../helpers/auditLog'); const { isConfigured, atlasGet, atlasPut, atlasPatch, atlasPost } = require('../helpers/atlasApi'); const VALID_PLAN_TYPES = ['decommission', 'remediation', 'false_positive', 'risk_acceptance', 'scan_exclusion']; const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; // --------------------------------------------------------------------------- // DB helpers — promise wrappers for callback-based SQLite API // --------------------------------------------------------------------------- function dbRun(db, sql, params = []) { return new Promise((resolve, reject) => { db.run(sql, params, function (err) { if (err) reject(err); else resolve(this); }); }); } function dbGet(db, sql, params = []) { return new Promise((resolve, reject) => { db.get(sql, params, (err, row) => { if (err) reject(err); else resolve(row); }); }); } function dbAll(db, sql, params = []) { return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); } // --------------------------------------------------------------------------- // Router factory // --------------------------------------------------------------------------- function createAtlasRouter(db, requireAuth) { const router = express.Router(); // ----------------------------------------------------------------------- // GET /status // Return all cached Atlas rows for badge rendering. // Auth: any authenticated user // ----------------------------------------------------------------------- router.get('/status', requireAuth(db), 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 rows = await dbAll(db, `SELECT host_id, has_action_plan, plan_count, synced_at FROM atlas_action_plans_cache` ); 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 // Sync Atlas action plan data for all hosts found in the Ivanti cache. // Auth: Admin or Standard_User // ----------------------------------------------------------------------- router.post('/sync', requireAuth(db), 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 { // 1. Read Ivanti findings cache and extract unique non-null hostIds const cacheRow = await dbGet(db, `SELECT findings_json FROM ivanti_findings_cache WHERE id = 1`); if (!cacheRow || !cacheRow.findings_json) { return res.json({ synced: 0, withPlans: 0, failed: 0 }); } let findings; try { findings = JSON.parse(cacheRow.findings_json); } catch (parseErr) { return res.status(500).json({ error: 'Failed to parse Ivanti findings cache.' }); } const hostIdSet = new Set(); for (const f of findings) { if (f.hostId != null && typeof f.hostId === 'number' && f.hostId > 0) { hostIdSet.add(f.hostId); } } const hostIds = [...hostIdSet]; if (hostIds.length === 0) { return res.json({ synced: 0, withPlans: 0, failed: 0 }); } // 2. Process hosts in batches of 5 concurrent requests 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); // Atlas returns { active: [...], inactive: [...] } 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 = []; } // Badge counts only active plans — inactive are historical const planCount = activePlans.length; const hasActionPlan = planCount > 0 ? 1 : 0; try { await dbRun(db, `INSERT INTO atlas_action_plans_cache (host_id, has_action_plan, plan_count, plans_json, synced_at) VALUES (?, ?, ?, ?, datetime('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}`); } } } // 3. Log audit entry logAudit(db, { 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 // Proxy to Atlas API — returns live action plan data for a single host. // Auth: any authenticated user // ----------------------------------------------------------------------- router.get('/hosts/:hostId/action-plans', requireAuth(db), 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 { // Forward non-2xx Atlas responses to the client 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 // Create a new action plan for a host. // Auth: Admin or Standard_User // ----------------------------------------------------------------------- router.put('/hosts/:hostId/action-plans', requireAuth(db), 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(db, { 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 // Update an existing action plan for a host. // Auth: Admin or Standard_User // ----------------------------------------------------------------------- router.patch('/hosts/:hostId/action-plans', requireAuth(db), 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(db, { 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 // Create action plans for multiple hosts at once. // Auth: Admin or Standard_User // ----------------------------------------------------------------------- router.post('/hosts/bulk-action-plans', requireAuth(db), 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; } 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 }); } }); return router; } module.exports = createAtlasRouter;