// ============================================================================ // Imara — live data loader. // When window.IMARA_LIVE is set, this fetches the real control plane (REST // /api/* only) and overwrites window.MS_DATA in place with real data shaped // to the contract the pages consume. // // window.__IMARA_BOOT__ - the first load; the app's render (end of // app.jsx) awaits it so MS_DATA is populated // before any component constructs. // window.ImaraLive.loadAll({initial:false}) - the same loaders, re-run by // the app every 30 s without a page reload. // // Honesty rules: // - If the first load fails, EVERY mock dataset is cleared so nothing // fabricated can render; the app shows a banner. // - If one slice fails on the first load it is empty (never mock) and the // failure is listed in window.IMARA_LOAD_ERRORS. On a refresh a failed // slice keeps its last real value and is listed the same way. // - Host metrics the agent did not report are null (shown as "—"). // In mock mode this file is a no-op. // ============================================================================ (function () { const API = () => window.ImaraAPI; // Per-intent derived data, keyed by id:version (a version bump means a new // body). const derived = {}; function clearAll(D) { D.SERVERS = []; D.byId = {}; D.INTENTS = []; D.ALERTS = []; D.GROUPS = []; D.AUDIT = []; D.APPLICATIONS = []; D.FLOW_TS = []; D.TOPO_EDGES = []; D.TOP_HOSTS_1H = null; D.APP_NAMES = {}; D.fleetCounts = { total: 0, live: 0, stale: 0, silent: 0 }; } // Attach the per-server push rows, the approved and stable texts and the // real approvers_count to an intent row. // opts.fromItem = the row came from GET /api/intents/{id}, whose // approvers_count is authoritative (the list endpoint always reports 0). async function enrichIntent(i, opts = {}) { if (!i) return i; const key = i.id + ":" + i.version; const c = derived[key] || (derived[key] = {}); const jobs = []; if (opts.fromItem) { c.approvers = i.approvers_count || 0; } else if (i.approval_state === "requested") { jobs.push(API().getIntent(i.id) .then((full) => { c.approvers = (full && full.approvers_count) || 0; }) .catch(() => {})); } // Rule state and the "asked for" side of incoming protection (terms.js): // the per-server push rows of an approved rule, and the texts of the // versions servers run when they differ from the current text: the // approved version under a newer draft, and the stable version the other // servers keep during a trial (both come from the version history). const av = Number(i.approved_version || 0), sv = Number(i.stable_version || 0); // Read again when the rule itself changed (a trial that finished, a // push that failed), not only every 15 s: otherwise the panel shows the // servers of the trial as the only ones for up to a refresh. const tKey = [av, i.canary_state || "", i.status || "", i.updated_at || ""].join("|"); if (av > 0 && (!c.tAt || c.tKey !== tKey || Date.now() - c.tAt > 15000)) { jobs.push(API().intentTargets(i.id) .then((t) => { c.targets = Array.isArray(t) ? t : null; c.tAt = Date.now(); c.tKey = tKey; }) .catch(() => { c.targets = null; c.tAt = Date.now(); c.tKey = tKey; })); } // Kept with their version number and reused only for that version: the // approved or stable version can move while the current one does not. // A text that could not be read is tried again (at most every 15 s), so // one failed read does not stay "could not be loaded". const trial = i.canary_state === "canarying" || i.canary_state === "baking"; const have = (slot, v) => slot && slot.v === v && (slot.body != null || Date.now() - slot.at < 15000); const needApproved = av > 0 && Number(i.version) !== av && !have(c.approved, av); const needStable = trial && sv > 0 && sv !== av && sv !== Number(i.version) && !have(c.stable, sv); if (needApproved || needStable) { jobs.push(Promise.all([needApproved ? API().versionText(i.id, av) : null, needStable ? API().versionText(i.id, sv) : null]) .then(([a, st]) => { if (needApproved) c.approved = { v: av, body: a, at: Date.now() }; if (needStable) c.stable = { v: sv, body: st, at: Date.now() }; }) .catch(() => { if (needApproved) c.approved = { v: av, body: null, at: Date.now() }; if (needStable) c.stable = { v: sv, body: null, at: Date.now() }; })); } await Promise.all(jobs); return { ...i, targets: c.targets || null, approved_body: c.approved && c.approved.v === av ? c.approved.body : null, stable_body: trial && c.stable && c.stable.v === sv ? c.stable.body : null, target_selector: i.target_selector || null, approvers_count: c.approvers != null ? c.approvers : (i.approvers_count || 0), required_approvers: i.required_approvers || 1, canary_state: i.canary_state || "none", }; } function buildServers(srv, NOW) { const freshOf = (ls) => { const age = (NOW - new Date(ls).getTime()) / 1000; return age < 60 ? "live" : age < 300 ? "stale" : "silent"; }; const ageSecOf = (ls) => Math.max(0, Math.round((NOW - new Date(ls).getTime()) / 1000)); return srv.map((s) => { const L = s.labels || {}; // Labels as the server carries them; nothing is invented for missing // ones (the traffic map puts such servers under OTHER). const role = L.role || L.tier || "", tier = L.tier || "", app = L.app || ""; // Real cpu_pct/mem_pct only when the agent has reported them // (mem_total_mb > 0 proves a real sample). Otherwise null: the UI shows // "—". Never a synthesized number. const hasReal = (s.mem_total_mb || 0) > 0; return { ...s, labels: L, fresh: freshOf(s.last_seen), ageSec: ageSecOf(s.last_seen), ip: L._ip || "—", role, tier, app, cpu: hasReal && typeof s.cpu_pct === "number" ? s.cpu_pct : null, mem: hasReal && typeof s.mem_pct === "number" ? s.mem_pct : null, }; }); } // flows -> server graph (terms.js edgesFromFlows): one edge per source, // destination and service port, with the client addresses the destination // recorded. The policy estimate per connection is worked out when shown // (terms.js edgesWithVerdicts), from the latest evidence. function buildEdges(flowRows, SERVERS, byId) { return window.ImaraTerms.edgesFromFlows(flowRows, SERVERS, byId); } // Each server's own firewall report, re-read every 3 minutes (well inside // the 15 minutes after which it counts as stale), and at once after a // change made from this console (invalidateFirewalls). The detail endpoint // is heavy, so it is not read on every 30-second refresh. A failed read is // kept as an error (shown as "unknown"), never as "no protection". const fwCache = {}; const fwInFlight = new Set(); const FW_TTL_MS = 3 * 60 * 1000; // ids: only those servers' reports (after a change to one rule), else all. function invalidateFirewalls(ids) { (Array.isArray(ids) ? ids : Object.keys(fwCache)).forEach((k) => { if (fwCache[k]) fwCache[k].fetchedAt = 0; }); } async function loadFirewalls(SERVERS) { const now = Date.now(); // A report already being read (by an earlier refresh still running on a // large fleet) is not read twice. const todo = SERVERS.filter((s) => !fwInFlight.has(s.agent_id) && (!fwCache[s.agent_id] || now - fwCache[s.agent_id].fetchedAt > FW_TTL_MS)); let next = 0; async function worker() { while (next < todo.length) { const s = todo[next++]; if (fwInFlight.has(s.agent_id) || (fwCache[s.agent_id] && Date.now() - fwCache[s.agent_id].fetchedAt <= FW_TTL_MS)) continue; fwInFlight.add(s.agent_id); try { const fw = await API().serverFirewall(s.agent_id); fwCache[s.agent_id] = fw ? { ...fw, error: "", fetchedAt: Date.now() } : null; } catch (e) { fwCache[s.agent_id] = { at: null, rules: [], error: (e && e.message) || "failed", fetchedAt: Date.now() }; } finally { fwInFlight.delete(s.agent_id); } } } await Promise.all(Array.from({ length: Math.min(6, todo.length) }, worker)); SERVERS.forEach((s) => { s.firewall = fwCache[s.agent_id] || null; }); } // One full load. Throws only when the fleet itself (GET /api/servers) // cannot be read; any other failing slice is recorded in `errors`. async function loadAll(opts = {}) { const initial = !!opts.initial; const D = window.MS_DATA; // "Now" on the Imara server's clock (see api.js), refreshed by the first // request of this load. const rest0 = await API().get("/api/servers"); const NOW = Date.now() + (Number(window.IMARA_CLOCK_SKEW_MS) || 0); D.CLOCK_SKEW_MS = Number(window.IMARA_CLOCK_SKEW_MS) || 0; const errors = []; const note = (what, e) => errors.push(`${what}: ${(e && e.message) || e}`); const prev = (k, empty) => (initial || D[k] === undefined || D[k] === null) ? empty : D[k]; const get = (p) => API().get(p); // failed: the lists this load could not read (an earlier copy, or an // empty one on the first load, stands in for them). const failed = new Set(); const slice = async (what, key, empty, fn) => { try { return await fn(); } catch (e) { note(what, e); failed.add(key); return prev(key, empty); } }; const next = {}; // ---- 1. servers (required, read above) ---- const srv = Array.isArray(rest0 && rest0.servers) ? rest0.servers : (Array.isArray(rest0) ? rest0 : []); const SERVERS = buildServers(srv, NOW); window.ImaraTerms.configureHostnames(SERVERS); const byId = {}; SERVERS.forEach((s) => { byId[s.agent_id] = s; }); // App filter = the fleet's real `app` labels. const liveApps = {}; SERVERS.forEach((s) => { if (s.app) liveApps[s.app] = String(s.app).toUpperCase(); }); Object.assign(next, { SERVERS, byId, APP_NAMES: liveApps }); // Each server's own firewall report, fetched alongside everything below. const firewalls = loadFirewalls(SERVERS).catch((e) => note("firewall reports", e)); // ---- 2..5 in parallel: overview, groups, alerts, audit, applications, intents ---- const auditLimit = Math.min(500, Math.max(100, opts.auditLimit || 100)); const [ov, GROUPS, ALERTS, AUDIT, APPLICATIONS, rawIntents] = await Promise.all([ get("/api/stats/overview").catch((e) => { note("overview", e); return null; }), slice("groups", "GROUPS", [], () => API().listGroups()), slice("alerts", "ALERTS", [], async () => { const res = await API().listAlertsFull(); next.ALERT_OPEN_COUNTS = res.openCounts; next.ALERT_OPEN_LOADED = res.openLoaded; next.ALERT_ACK_PARTIAL = !!res.ackPartial; return res.alerts.map((a) => ({ ...a, details: a.details || {} })); }), slice("audit log", "AUDIT", [], () => API().listAudit({ limit: auditLimit, offset: 0 })), slice("applications", "APPLICATIONS", [], async () => (await API().listApplications()).map((a) => ({ id: a.id, name: a.name, description: a.description, env: a.env || "", required_approvers: a.required_approvers || 1, tiers: (a.tiers || []).map((t) => ({ name: t.name, group_ids: t.group_ids || [] })), }))), API().listIntents().catch((e) => { note("rules", e); return null; }), ]); Object.assign(next, { GROUPS, ALERTS, AUDIT, APPLICATIONS }); if (ov && ov.servers) { next.fleetCounts = { total: ov.servers.total, live: ov.servers.live, stale: ov.servers.stale, silent: ov.servers.silent }; next.TOP_HOSTS_1H = Array.isArray(ov.top_hosts_1h) ? ov.top_hosts_1h : null; } else { const by = (f) => SERVERS.filter((s) => s.fresh === f).length; next.fleetCounts = { total: SERVERS.length, live: by("live"), stale: by("stale"), silent: by("silent") }; next.TOP_HOSTS_1H = prev("TOP_HOSTS_1H", null); } // Intents + compiled preview + push rows + real approvers_count. next.INTENTS = rawIntents ? await Promise.all(rawIntents.map((i) => enrichIntent(i))) : prev("INTENTS", []); window.ImaraTerms.configureRuleTitles(next.INTENTS); // Rules or groups missing from this load: every protection // note, estimate and confirmation then says it cannot tell. next.POLICY_INCOMPLETE = !rawIntents || failed.has("GROUPS"); // Which lists this load could not read, so empty screens can say "could // not be loaded" instead of "none yet". next.LOAD_FAILED = { INTENTS: !rawIntents, GROUPS: failed.has("GROUPS"), ALERTS: failed.has("ALERTS"), AUDIT: failed.has("AUDIT"), APPLICATIONS: failed.has("APPLICATIONS") }; // ---- 6. flows -> FLOW_TS (connections per minute, last hour) + TOPO_EDGES (derived host graph) ---- const [ts, fr] = await Promise.all([ get("/api/flows/timeseries").catch((e) => { note("connection counts", e); return null; }), get("/api/flows?limit=5000").catch((e) => { note("recorded connections", e); return null; }), ]); // The last bucket is the current minute, still filling up: left out, or // the line would always drop at "now". next.FLOW_TS = ts ? (ts.buckets || []).slice(0, -1).map((b) => ({ t: b.bucket_start, count: b.count })) : prev("FLOW_TS", []); const edges = fr ? buildEdges(fr.flows || [], SERVERS, byId) : null; // A server that confirmed a change after the report the console has is // read again, so a finished rollout does not look failed for minutes. const catchUp = async () => { const latestApply = {}; (next.INTENTS || []).forEach((i) => (Array.isArray(i.targets) ? i.targets : []).forEach((t) => { if (t.status === "applied" && t.applied_at && (!latestApply[t.agent_id] || t.applied_at > latestApply[t.agent_id])) latestApply[t.agent_id] = t.applied_at; })); const behind = SERVERS.filter((s) => { const c = fwCache[s.agent_id]; return c && latestApply[s.agent_id] && (!c.at || new Date(latestApply[s.agent_id]) > new Date(c.at)) && Date.now() - c.fetchedAt > 20000; }); if (behind.length) { behind.forEach((s) => { fwCache[s.agent_id].fetchedAt = 0; }); await loadFirewalls(behind).catch(() => {}); } }; // A large fleet is shown at once, with the reports the console already // has (others read as "unknown"); each report fills in as it arrives // (FW_VERSION tells the screens to work their estimates out again). const progressive = SERVERS.length > 200; if (!progressive) { await firewalls; await catchUp(); next.FW_LOADING = false; } else { SERVERS.forEach((s) => { s.firewall = fwCache[s.agent_id] || null; }); next.FW_LOADING = true; firewalls.then(catchUp).then(() => { const DD = window.MS_DATA; if (DD.SERVERS !== SERVERS) return; // a newer load replaced these servers (and says when its reports are in) DD.FW_LOADING = false; DD.FW_VERSION = (DD.FW_VERSION || 0) + 1; if (window.ImaraLive && window.ImaraLive.onFirewalls) window.ImaraLive.onFirewalls(); }).catch(() => {}); } // Connections between servers; what Imara does with each one is worked // out when shown (terms.js edgesWithVerdicts), from the latest evidence. next.TOPO_EDGES = edges || prev("TOPO_EDGES", []); // What the connection sample covers: the newest rows, fleet-wide. if (fr) { const rows = fr.flows || []; const oldest = rows.reduce((m, f) => (f.received_at && (!m || f.received_at < m) ? f.received_at : m), ""); next.FLOWS_COUNT = rows.length; next.FLOWS_SINCE = oldest || null; } else { next.FLOWS_COUNT = prev("FLOWS_COUNT", 0); next.FLOWS_SINCE = prev("FLOWS_SINCE", null); } // Commit in one step so a half-finished refresh never shows mixed state. Object.assign(D, next); D.LOADED_AT = NOW; window.IMARA_LOAD_ERRORS = errors; window.IMARA_HEALTH = { ok: true, reason: "Imara server reachable", at: NOW }; window.IMARA_BOOT_FAILED = false; if (errors.length) console.warn("[imara] some live data failed to load:", errors); return { at: NOW, errors }; } window.ImaraLive = { loadAll, enrichIntent, invalidateFirewalls, clearAll: () => clearAll(window.MS_DATA) }; window.__IMARA_BOOT__ = (async function bootLive() { if (!window.IMARA_LIVE) return; // mock mode — leave MS_DATA untouched const D = window.MS_DATA; // Login-required-for-everything: check the session before fetching any // data. If auth is enabled and we're not signed in, flag it and skip the // fetch so the app shows a full-page login instead of a failure banner. // Nothing from the mock dataset may render behind the login either. try { const me = await fetch("/api/me", { cache: "no-store", credentials: "same-origin" }).then((r) => r.json()); window.IMARA_ME = me; if (me && me.auth_enabled && !me.authenticated) { window.IMARA_LOGIN_REQUIRED = true; clearAll(D); return; } } catch (e) { /* /api/me unreachable — fall through, the fetch errors handle it */ } try { const r = await loadAll({ initial: true }); console.info(`[imara] live data loaded — ${D.SERVERS.length} hosts, ${D.INTENTS.length} intents, ${D.ALERTS.length} alerts, ${D.TOPO_EDGES.length} edges, ${D.GROUPS.length} groups${r.errors.length ? ` (${r.errors.length} slice(s) failed)` : ""}`); } catch (err) { // The fleet could not be read. Do NOT keep the seeded mock dataset as if // it were real: clear every dataset, flag the failure so the app shows a // banner and the health dot goes red. console.error("[imara] live bootstrap FAILED — control plane unreachable:", err); window.IMARA_HEALTH = { ok: false, reason: (err && err.message) || "Imara server unreachable", at: Date.now() }; window.IMARA_BOOT_FAILED = true; window.IMARA_BOOT_ERROR = (err && err.message) || "Imara server unreachable"; if (err && err.authRequired) window.IMARA_LOGIN_REQUIRED = true; clearAll(D); } })(); })();