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)
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
// Atlas InfoSec Action Plans Routes
|
||||
// Proxies CRUD operations to the Atlas API and maintains a local SQLite cache
|
||||
// Proxies CRUD operations to the Atlas API and maintains a local cache
|
||||
// for fast badge rendering on the ReportingPage.
|
||||
|
||||
const express = require('express');
|
||||
const { requireGroup } = require('../middleware/auth');
|
||||
const pool = require('../db');
|
||||
const { requireAuth, requireGroup } = require('../middleware/auth');
|
||||
const logAudit = require('../helpers/auditLog');
|
||||
const { isConfigured, atlasGet, atlasPut, atlasPatch, atlasPost } = require('../helpers/atlasApi');
|
||||
|
||||
@@ -13,34 +14,13 @@ 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 — writes to atlas-sync-debug.log in the backend folder
|
||||
// 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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 || []); });
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure aggregation function — exported for testability
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -55,7 +35,7 @@ function aggregateAtlasMetrics(rows) {
|
||||
};
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.has_action_plan === 1) {
|
||||
if (row.has_action_plan === true || row.has_action_plan === 1) {
|
||||
result.hostsWithPlans++;
|
||||
} else {
|
||||
result.hostsWithoutPlans++;
|
||||
@@ -65,7 +45,6 @@ function aggregateAtlasMetrics(rows) {
|
||||
try {
|
||||
plans = JSON.parse(row.plans_json);
|
||||
} catch (e) {
|
||||
// Invalid JSON — skip plan details for this row
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -73,11 +52,9 @@ function aggregateAtlasMetrics(rows) {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -90,28 +67,17 @@ function aggregateAtlasMetrics(rows) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Router factory
|
||||
// ---------------------------------------------------------------------------
|
||||
function createAtlasRouter(db, requireAuth) {
|
||||
function createAtlasRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GET /metrics
|
||||
// Return aggregated Atlas metrics for chart rendering.
|
||||
// Auth: any authenticated user
|
||||
//
|
||||
// Response 200:
|
||||
// { totalHosts: number, hostsWithPlans: number, hostsWithoutPlans: number,
|
||||
// plansByType: { [type: string]: number }, plansByStatus: { [status: string]: number },
|
||||
// totalPlans: number }
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 500: { error: string } — DB query failure
|
||||
// -----------------------------------------------------------------------
|
||||
router.get('/metrics', requireAuth(db), async (req, res) => {
|
||||
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 rows = await dbAll(db,
|
||||
const { rows } = await pool.query(
|
||||
`SELECT has_action_plan, plans_json FROM atlas_action_plans_cache`
|
||||
);
|
||||
const metrics = aggregateAtlasMetrics(rows);
|
||||
@@ -122,23 +88,14 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GET /status
|
||||
// Return all cached Atlas rows for badge rendering.
|
||||
// Auth: any authenticated user
|
||||
//
|
||||
// Response 200:
|
||||
// [ { host_id: number, has_action_plan: 0|1, plan_count: number, synced_at: string }, ... ]
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 500: { error: string } — DB query failure
|
||||
// -----------------------------------------------------------------------
|
||||
router.get('/status', requireAuth(db), async (req, res) => {
|
||||
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 rows = await dbAll(db,
|
||||
const { rows } = await pool.query(
|
||||
`SELECT host_id, has_action_plan, plan_count, plans_json, synced_at FROM atlas_action_plans_cache`
|
||||
);
|
||||
res.json(rows);
|
||||
@@ -148,49 +105,23 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// POST /sync
|
||||
// Sync Atlas action plan data for all hosts found in the Ivanti cache.
|
||||
// Auth: Admin or Standard_User
|
||||
//
|
||||
// Request body: none
|
||||
// Response 200:
|
||||
// { synced: number, withPlans: number, failed: number }
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 500: { error: string } — sync failure or Ivanti cache parse error
|
||||
// -----------------------------------------------------------------------
|
||||
router.post('/sync', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
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 {
|
||||
// 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];
|
||||
// Read Ivanti findings and extract unique non-null hostIds
|
||||
const { rows: findingsRows } = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings WHERE host_id IS NOT NULL AND host_id > 0`
|
||||
);
|
||||
const hostIds = findingsRows.map(r => r.host_id);
|
||||
|
||||
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;
|
||||
@@ -219,7 +150,6 @@ function createAtlasRouter(db, requireAuth) {
|
||||
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 : [];
|
||||
@@ -233,30 +163,24 @@ function createAtlasRouter(db, requireAuth) {
|
||||
activePlans = [];
|
||||
}
|
||||
|
||||
// Badge counts only active plans — inactive are historical
|
||||
const planCount = activePlans.length;
|
||||
const hasActionPlan = planCount > 0 ? 1 : 0;
|
||||
|
||||
console.log(`[Atlas Sync] Host ${hostId}: status=${result.status}, activePlans=${activePlans.length}, allPlans=${allPlans.length}, hasActionPlan=${hasActionPlan}`);
|
||||
const hasActionPlan = planCount > 0;
|
||||
|
||||
try {
|
||||
// If Atlas returns 0 plans but we have a recent optimistic
|
||||
// entry (from bulk creation within the last 10 minutes),
|
||||
// keep the optimistic value — Atlas's GET may lag behind.
|
||||
if (hasActionPlan === 0) {
|
||||
const existing = await dbGet(db,
|
||||
`SELECT has_action_plan, plans_json, synced_at FROM atlas_action_plans_cache WHERE host_id = ?`,
|
||||
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]
|
||||
);
|
||||
if (existing && existing.has_action_plan === 1) {
|
||||
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 + 'Z').getTime();
|
||||
const ageMs = Date.now() - new Date(existing.synced_at).getTime();
|
||||
const TEN_MINUTES = 10 * 60 * 1000;
|
||||
if (ageMs < TEN_MINUTES) {
|
||||
console.log(`[Atlas Sync] Host ${hostId}: keeping optimistic bulk-create entry (${Math.round(ageMs / 1000)}s old)`);
|
||||
synced++;
|
||||
withPlans++;
|
||||
continue;
|
||||
@@ -265,14 +189,14 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
}
|
||||
|
||||
await dbRun(db,
|
||||
await pool.query(
|
||||
`INSERT INTO atlas_action_plans_cache (host_id, has_action_plan, plan_count, plans_json, synced_at)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
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`,
|
||||
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) {
|
||||
@@ -283,13 +207,12 @@ function createAtlasRouter(db, requireAuth) {
|
||||
if (hasActionPlan) withPlans++;
|
||||
} else {
|
||||
failed++;
|
||||
console.warn(`[Atlas Sync] Non-2xx response for host ${hostId}: status ${result.status}, body=${result.body}`);
|
||||
console.warn(`[Atlas Sync] Non-2xx response for host ${hostId}: status ${result.status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Log audit entry
|
||||
logAudit(db, {
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'ATLAS_SYNC',
|
||||
@@ -306,18 +229,8 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GET /hosts/:hostId/action-plans
|
||||
// Proxy to Atlas API — returns live action plan data for a single host.
|
||||
// Auth: any authenticated user
|
||||
//
|
||||
// Params: hostId (positive integer)
|
||||
// Response 2xx: proxied Atlas response body (parsed JSON or raw)
|
||||
// Response 400: { error: string } — invalid hostId
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 502: { error: string } — Atlas API unreachable
|
||||
// -----------------------------------------------------------------------
|
||||
router.get('/hosts/:hostId/action-plans', requireAuth(db), async (req, res) => {
|
||||
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.' });
|
||||
}
|
||||
@@ -329,23 +242,13 @@ function createAtlasRouter(db, requireAuth) {
|
||||
|
||||
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;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; }
|
||||
res.status(result.status).json(errorBody);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -354,22 +257,8 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// PUT /hosts/:hostId/action-plans
|
||||
// Create a new action plan for a host.
|
||||
// Auth: Admin or Standard_User
|
||||
//
|
||||
// Params: hostId (positive integer)
|
||||
// Request body:
|
||||
// { plan_type: string (one of VALID_PLAN_TYPES), commit_date: string (YYYY-MM-DD),
|
||||
// qualys_id?: string, active_host_findings_id?: string,
|
||||
// jira_vnr?: string, archer_exc?: string }
|
||||
// Response 2xx: proxied Atlas response body
|
||||
// Response 400: { error: string } — invalid hostId, plan_type, or commit_date
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 502: { error: string } — Atlas API unreachable
|
||||
// -----------------------------------------------------------------------
|
||||
router.put('/hosts/:hostId/action-plans', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
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.' });
|
||||
}
|
||||
@@ -380,11 +269,9 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
@@ -392,7 +279,7 @@ function createAtlasRouter(db, requireAuth) {
|
||||
try {
|
||||
const result = await atlasPut('/hosts/' + hostId + '/action-plans', req.body);
|
||||
|
||||
logAudit(db, {
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'ATLAS_CREATE_PLAN',
|
||||
@@ -404,19 +291,11 @@ function createAtlasRouter(db, requireAuth) {
|
||||
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
body = result.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 };
|
||||
}
|
||||
try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; }
|
||||
res.status(result.status).json(errorBody);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -425,20 +304,8 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// PATCH /hosts/:hostId/action-plans
|
||||
// Update an existing action plan for a host.
|
||||
// Auth: Admin or Standard_User
|
||||
//
|
||||
// Params: hostId (positive integer)
|
||||
// Request body:
|
||||
// { action_plan_id: string (non-empty), updates: object (non-null, non-array) }
|
||||
// Response 2xx: proxied Atlas response body
|
||||
// Response 400: { error: string } — invalid hostId, action_plan_id, or updates
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 502: { error: string } — Atlas API unreachable
|
||||
// -----------------------------------------------------------------------
|
||||
router.patch('/hosts/:hostId/action-plans', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
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.' });
|
||||
}
|
||||
@@ -449,11 +316,9 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
@@ -461,7 +326,7 @@ function createAtlasRouter(db, requireAuth) {
|
||||
try {
|
||||
const result = await atlasPatch('/hosts/' + hostId + '/action-plans', req.body);
|
||||
|
||||
logAudit(db, {
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'ATLAS_UPDATE_PLAN',
|
||||
@@ -473,19 +338,11 @@ function createAtlasRouter(db, requireAuth) {
|
||||
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
body = result.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 };
|
||||
}
|
||||
try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; }
|
||||
res.status(result.status).json(errorBody);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -494,41 +351,24 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// POST /hosts/bulk-action-plans
|
||||
// Create action plans for multiple hosts at once.
|
||||
// Auth: Admin or Standard_User
|
||||
//
|
||||
// Request body:
|
||||
// { host_ids: number[] (non-empty, positive integers),
|
||||
// plan_type: string (one of VALID_PLAN_TYPES),
|
||||
// commit_date: string (YYYY-MM-DD) }
|
||||
// Response 2xx: proxied Atlas response body
|
||||
// Response 400: { error: string } — invalid host_ids, plan_type, or commit_date
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 502: { error: string } — Atlas API unreachable
|
||||
// -----------------------------------------------------------------------
|
||||
router.post('/hosts/bulk-action-plans', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
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' });
|
||||
}
|
||||
@@ -538,40 +378,34 @@ function createAtlasRouter(db, requireAuth) {
|
||||
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
body = result.body;
|
||||
}
|
||||
try { body = JSON.parse(result.body); } catch (e) { body = result.body; }
|
||||
|
||||
// Optimistically update local cache for all submitted hosts.
|
||||
// Atlas's individual GET endpoint may lag behind the bulk
|
||||
// creation, so we mark every host as having a plan now rather
|
||||
// than waiting for the next sync to discover it.
|
||||
// Optimistically update local cache
|
||||
for (const hid of host_ids) {
|
||||
try {
|
||||
const existing = await dbGet(db,
|
||||
`SELECT plan_count, plans_json FROM atlas_action_plans_cache WHERE host_id = ?`,
|
||||
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 (_) { /* ignore */ }
|
||||
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 dbRun(db,
|
||||
await pool.query(
|
||||
`INSERT INTO atlas_action_plans_cache (host_id, has_action_plan, plan_count, plans_json, synced_at)
|
||||
VALUES (?, 1, ?, ?, datetime('now'))
|
||||
VALUES ($1, true, $2, $3, NOW())
|
||||
ON CONFLICT(host_id) DO UPDATE SET
|
||||
has_action_plan = 1,
|
||||
plan_count = excluded.plan_count,
|
||||
plans_json = excluded.plans_json,
|
||||
synced_at = excluded.synced_at`,
|
||||
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) {
|
||||
@@ -579,7 +413,7 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
}
|
||||
|
||||
logAudit(db, {
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'ATLAS_BULK_CREATE_PLANS',
|
||||
@@ -592,11 +426,7 @@ function createAtlasRouter(db, requireAuth) {
|
||||
res.status(result.status).json(body);
|
||||
} else {
|
||||
let errorBody;
|
||||
try {
|
||||
errorBody = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
errorBody = { error: result.body };
|
||||
}
|
||||
try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; }
|
||||
res.status(result.status).json(errorBody);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -605,29 +435,16 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// POST /hosts/vulnerabilities
|
||||
// Fetch active Ivanti vulnerabilities for multiple hosts from Atlas.
|
||||
// Used by the bulk action plan modal to populate the qualys_id dropdown.
|
||||
// Auth: any authenticated user
|
||||
//
|
||||
// Request body: { host_ids: number[] }
|
||||
// Response 2xx: proxied Atlas response body
|
||||
// Response 400: { error: string } — invalid host_ids
|
||||
// Response 503: { error: string } — Atlas not configured
|
||||
// Response 502: { error: string } — Atlas API unreachable
|
||||
// -----------------------------------------------------------------------
|
||||
router.post('/hosts/vulnerabilities', requireAuth(db), async (req, res) => {
|
||||
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' });
|
||||
@@ -637,24 +454,13 @@ function createAtlasRouter(db, requireAuth) {
|
||||
try {
|
||||
const result = await atlasPost('/ivanti-vulnerabilities-by-host', { host_ids }, { timeout: 30000 });
|
||||
|
||||
console.log('[Atlas] POST /ivanti-vulnerabilities-by-host status:', result.status, 'body length:', result.body?.length);
|
||||
console.log('[Atlas] Response preview:', result.body?.substring(0, 500));
|
||||
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
body = result.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 };
|
||||
}
|
||||
try { errorBody = JSON.parse(result.body); } catch (e) { errorBody = { error: result.body }; }
|
||||
res.status(result.status).json(errorBody);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user