Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// 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 || []); });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 17:30:06 +00:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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 === 1) {
|
|
|
|
|
result.hostsWithPlans++;
|
|
|
|
|
} else {
|
|
|
|
|
result.hostsWithoutPlans++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let plans;
|
|
|
|
|
try {
|
|
|
|
|
plans = JSON.parse(row.plans_json);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Invalid JSON — skip plan details for this row
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Router factory
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function createAtlasRouter(db, requireAuth) {
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
2026-04-24 17:30:06 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// 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) => {
|
|
|
|
|
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 has_action_plan, plans_json FROM atlas_action_plans_cache`
|
|
|
|
|
);
|
|
|
|
|
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.' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// GET /status
|
|
|
|
|
// Return all cached Atlas rows for badge rendering.
|
|
|
|
|
// Auth: any authenticated user
|
2026-04-24 17:30:06 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
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
|
2026-04-24 17:30:06 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
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
|
2026-04-24 17:30:06 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
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
|
2026-04-24 17:30:06 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
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
|
2026-04-24 17:30:06 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
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
|
2026-04-24 17:30:06 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-24 22:07:55 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// 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) => {
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
return router;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = createAtlasRouter;
|
2026-04-24 17:30:06 +00:00
|
|
|
module.exports.aggregateAtlasMetrics = aggregateAtlasMetrics;
|