Add Atlas metrics reporting, security audit tracker, and spec documents
This commit is contained in:
@@ -31,16 +31,96 @@ function dbAll(db, sql, params = []) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Router factory
|
||||
// ---------------------------------------------------------------------------
|
||||
function createAtlasRouter(db, requireAuth) {
|
||||
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) => {
|
||||
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.' });
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 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) => {
|
||||
if (!isConfigured) {
|
||||
@@ -62,6 +142,12 @@ 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) => {
|
||||
if (!isConfigured) {
|
||||
@@ -187,6 +273,12 @@ 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) => {
|
||||
if (!isConfigured) {
|
||||
@@ -229,6 +321,16 @@ 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) => {
|
||||
if (!isConfigured) {
|
||||
@@ -290,6 +392,14 @@ 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) => {
|
||||
if (!isConfigured) {
|
||||
@@ -351,6 +461,15 @@ 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) => {
|
||||
if (!isConfigured) {
|
||||
@@ -407,3 +526,4 @@ function createAtlasRouter(db, requireAuth) {
|
||||
}
|
||||
|
||||
module.exports = createAtlasRouter;
|
||||
module.exports.aggregateAtlasMetrics = aggregateAtlasMetrics;
|
||||
|
||||
Reference in New Issue
Block a user