Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging environment on dev box, and SSH-based production deploy to 71.85.90.6 - Add POST /api/health endpoint for pipeline verification - Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness - AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities, prefer qualys_id over active_host_findings_id, retry on failure - Add FeedbackModal component with bug report button in header and feature request in UserMenu, creates GitLab issues via /api/feedback - Fix all frontend test failures (ESM transforms, TextDecoder polyfill, fast-check resolution, App.test.js boilerplate replacement) - Fix root package.json test script to run jest - Add deploy/ directory with staging systemd service and setup script
This commit is contained in:
@@ -70,7 +70,15 @@ function aggregateAtlasMetrics(rows) {
|
||||
function createAtlasRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// GET /metrics
|
||||
/**
|
||||
* GET /metrics
|
||||
*
|
||||
* Returns aggregated Atlas action plan metrics from the local cache.
|
||||
*
|
||||
* @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.' });
|
||||
@@ -88,7 +96,15 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /status
|
||||
/**
|
||||
* GET /status
|
||||
*
|
||||
* Returns the full atlas_action_plans_cache table contents for status display.
|
||||
*
|
||||
* @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.' });
|
||||
@@ -105,7 +121,17 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /sync
|
||||
/**
|
||||
* 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.' });
|
||||
@@ -229,7 +255,17 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /hosts/:hostId/action-plans
|
||||
/**
|
||||
* 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.' });
|
||||
@@ -257,7 +293,21 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /hosts/:hostId/action-plans
|
||||
/**
|
||||
* 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.' });
|
||||
@@ -304,7 +354,21 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /hosts/:hostId/action-plans
|
||||
/**
|
||||
* 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.' });
|
||||
@@ -351,7 +415,22 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /hosts/bulk-action-plans
|
||||
/**
|
||||
* 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.' });
|
||||
@@ -435,7 +514,90 @@ function createAtlasRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /hosts/vulnerabilities
|
||||
/**
|
||||
* 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.' });
|
||||
|
||||
@@ -135,6 +135,11 @@ app.use('/uploads', express.static('uploads', {
|
||||
index: false
|
||||
}));
|
||||
|
||||
// Health check endpoint (public — used by CI/CD pipeline verification)
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Auth routes (public)
|
||||
app.use('/api/auth', createAuthRouter(logAudit));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user