Scope Atlas sync and metrics to active BU teams
Problem 1: Atlas sync was querying ALL host_ids from ivanti_findings regardless of BU, writing 'no plan' entries for ACCESS-OPS hosts that Atlas doesn't cover. Now the sync respects the user's active teams scope (passed via query param) and falls back to IVANTI_MANAGED_BUS when no scope is provided. Problem 2: Atlas /metrics and /status endpoints returned unscoped data from the full cache, so changing scope didn't update the Atlas Coverage donut or badge counts. Both endpoints now accept a teams query param and JOIN against ivanti_findings to scope results by BU. Frontend changes: - fetchAtlasStatus and fetchAtlasMetrics now pass teams param - Atlas sync button passes active teams to the sync endpoint - Scope change (adminScope) triggers Atlas data refresh Also purged 6,461 polluted cache entries for non-managed BU hosts.
This commit is contained in:
@@ -74,7 +74,10 @@ function createAtlasRouter() {
|
||||
* GET /metrics
|
||||
*
|
||||
* Returns aggregated Atlas action plan metrics from the local cache.
|
||||
* Accepts optional `teams` query parameter to scope metrics to hosts
|
||||
* belonging to specific BUs (via JOIN on ivanti_findings).
|
||||
*
|
||||
* @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG')
|
||||
* @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
|
||||
@@ -85,9 +88,36 @@ function createAtlasRouter() {
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT has_action_plan, plans_json FROM atlas_action_plans_cache`
|
||||
);
|
||||
const teamsParam = req.query.teams;
|
||||
let rows;
|
||||
|
||||
if (teamsParam) {
|
||||
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (teams.length > 0) {
|
||||
const patterns = teams.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT a.has_action_plan, a.plans_json
|
||||
FROM atlas_action_plans_cache a
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE bu_ownership ILIKE ANY($1::text[])
|
||||
) f ON a.host_id = f.host_id`,
|
||||
[patterns]
|
||||
);
|
||||
rows = result.rows;
|
||||
} else {
|
||||
const result = await pool.query(
|
||||
`SELECT has_action_plan, plans_json FROM atlas_action_plans_cache`
|
||||
);
|
||||
rows = result.rows;
|
||||
}
|
||||
} else {
|
||||
const result = await pool.query(
|
||||
`SELECT has_action_plan, plans_json FROM atlas_action_plans_cache`
|
||||
);
|
||||
rows = result.rows;
|
||||
}
|
||||
|
||||
const metrics = aggregateAtlasMetrics(rows);
|
||||
res.json(metrics);
|
||||
} catch (err) {
|
||||
@@ -99,8 +129,11 @@ function createAtlasRouter() {
|
||||
/**
|
||||
* GET /status
|
||||
*
|
||||
* Returns the full atlas_action_plans_cache table contents for status display.
|
||||
* Returns atlas_action_plans_cache contents for status display.
|
||||
* Accepts optional `teams` query parameter to scope results to hosts
|
||||
* belonging to specific BUs (via JOIN on ivanti_findings).
|
||||
*
|
||||
* @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG')
|
||||
* @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
|
||||
@@ -111,9 +144,36 @@ function createAtlasRouter() {
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT host_id, has_action_plan, plan_count, plans_json, synced_at FROM atlas_action_plans_cache`
|
||||
);
|
||||
const teamsParam = req.query.teams;
|
||||
let rows;
|
||||
|
||||
if (teamsParam) {
|
||||
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (teams.length > 0) {
|
||||
const patterns = teams.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT a.host_id, a.has_action_plan, a.plan_count, a.plans_json, a.synced_at
|
||||
FROM atlas_action_plans_cache a
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE bu_ownership ILIKE ANY($1::text[])
|
||||
) f ON a.host_id = f.host_id`,
|
||||
[patterns]
|
||||
);
|
||||
rows = result.rows;
|
||||
} else {
|
||||
const result = await pool.query(
|
||||
`SELECT host_id, has_action_plan, plan_count, plans_json, synced_at FROM atlas_action_plans_cache`
|
||||
);
|
||||
rows = result.rows;
|
||||
}
|
||||
} else {
|
||||
const result = await pool.query(
|
||||
`SELECT host_id, has_action_plan, plan_count, plans_json, synced_at FROM atlas_action_plans_cache`
|
||||
);
|
||||
rows = result.rows;
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error('[Atlas] Error fetching status:', err.message);
|
||||
@@ -138,10 +198,48 @@ function createAtlasRouter() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 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`
|
||||
);
|
||||
// Scope sync to the user's active teams if provided, otherwise sync only
|
||||
// findings from managed BUs (IVANTI_MANAGED_BUS) to avoid polluting cache
|
||||
// with "no plan" entries for BUs not covered by Atlas.
|
||||
const teamsParam = req.query.teams || req.body.teams || '';
|
||||
const managedBUs = (process.env.IVANTI_MANAGED_BUS || 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM')
|
||||
.split(',').map(b => b.trim()).filter(Boolean);
|
||||
|
||||
let findingsRows;
|
||||
if (teamsParam) {
|
||||
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (teams.length > 0) {
|
||||
const patterns = teams.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
findingsRows = result.rows;
|
||||
} else {
|
||||
// No valid teams — fall back to managed BUs
|
||||
const patterns = managedBUs.map(b => `%${b}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
findingsRows = result.rows;
|
||||
}
|
||||
} else {
|
||||
// No teams specified — default to managed BUs only
|
||||
const patterns = managedBUs.map(b => `%${b}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
findingsRows = result.rows;
|
||||
}
|
||||
|
||||
const hostIds = findingsRows.map(r => r.host_id);
|
||||
|
||||
if (hostIds.length === 0) {
|
||||
|
||||
@@ -6142,7 +6142,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
|
||||
const fetchAtlasStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/atlas/status`, { credentials: 'include' });
|
||||
const teamsParam = getActiveTeamsParam();
|
||||
const url = teamsParam
|
||||
? `${API_BASE}/atlas/status?teams=${encodeURIComponent(teamsParam)}`
|
||||
: `${API_BASE}/atlas/status`;
|
||||
const res = await fetch(url, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const map = new Map();
|
||||
@@ -6158,7 +6162,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
setAtlasMetricsLoading(true);
|
||||
setAtlasMetricsError(null);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/atlas/metrics`, { credentials: 'include' });
|
||||
const teamsParam = getActiveTeamsParam();
|
||||
const url = teamsParam
|
||||
? `${API_BASE}/atlas/metrics?teams=${encodeURIComponent(teamsParam)}`
|
||||
: `${API_BASE}/atlas/metrics`;
|
||||
const res = await fetch(url, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAtlasMetrics(data);
|
||||
@@ -6269,6 +6277,9 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
.catch(() => {});
|
||||
// Also refresh FP workflow counts for the new scope
|
||||
fetchFPWorkflowCounts();
|
||||
// Refresh Atlas data for the new scope
|
||||
fetchAtlasStatus();
|
||||
fetchAtlasMetrics();
|
||||
}, [adminScope]); // eslint-disable-line
|
||||
|
||||
// Set/clear a single column filter
|
||||
@@ -7185,7 +7196,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
setAtlasSyncing(true);
|
||||
setAtlasError(null);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/atlas/sync`, { method: 'POST', credentials: 'include' });
|
||||
const teamsParam = getActiveTeamsParam();
|
||||
const syncUrl = teamsParam
|
||||
? `${API_BASE}/atlas/sync?teams=${encodeURIComponent(teamsParam)}`
|
||||
: `${API_BASE}/atlas/sync`;
|
||||
const res = await fetch(syncUrl, { method: 'POST', credentials: 'include' });
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Atlas sync failed');
|
||||
|
||||
Reference in New Issue
Block a user