// ============================================================================ // Imara — drawers: ServerDrawer, AlertDrawer, IntentDrawer // ============================================================================ function KV({ k, v, mono = true }) { return (
{k} {v}
); } function DrawerHead({ icon, iconColor, eyebrow, title, sub, onClose, children, plain }) { return (
{eyebrow &&
{eyebrow}
}
{title}
{sub &&
{sub}
} {children}
); } // Rows show only what is unusual. The grey status that most rows of a list // share (at least two rows, and at least half of them) is said once above // the list instead of in every row: {text, count, has(status)}, or null. function sharedStatus(statuses) { const n = {}; statuses.forEach((s) => { if (s && s.tone === "muted") n[s.text] = (n[s.text] || 0) + 1; }); const text = Object.keys(n).filter((t) => n[t] >= 2 && n[t] * 2 >= statuses.length).sort((a, b) => n[b] - n[a])[0]; return text == null ? null : { text, count: n[text], has: (s) => !!s && s.tone === "muted" && s.text === text }; } // What the statuses of a list mean (a status may carry a help text), once, // behind a "?"; null when none does. function statusHelp(statuses) { const hs = Array.from(new Set(statuses.map((s) => s && s.help).filter(Boolean))); return hs.length ? {hs.map((h) =>

{h}

)}
: null; } // nft syntax highlight function highlightNft(src) { const KW = /\b(table|chain|type|hook|policy|ct|state|iif|tcp|udp|ip|dport|saddr|daddr|counter|accept|established|related|filter|priority)\b/g; return src.split("\n").map((line, i) => { const cm = line.indexOf("#"); const code = cm >= 0 ? line.slice(0, cm) : line; const comment = cm >= 0 ? line.slice(cm) : ""; const html = code .replace(/&/g, "&").replace(/$1') .replace(KW, '$1') .replace(/\b(\d{2,5})\b/g, '$1'); return (
{comment && {comment}}
); }); } // ---------- SERVER DRAWER ---------- // The server panel: whether Imara's incoming protection is in force (from // the server's own firewall report), which rules let connections in, its // addresses and listening programs, recent connections, and the raw // firewall under Technical details. function ServerDrawer({ server, onClose, toast, onAudit, onRefresh, onProtect, onProtectServer }) { const T = window.ImaraTerms; const [tab, setTab] = useState("summary"); const [allLabels, setAllLabels] = useState(false); // labels set at install, shown on request const [detail, setDetail] = useState(null); // real /api/servers/{id}/detail const [detailErr, setDetailErr] = useState(""); const [loading, setLoading] = useState(false); const [confirm, setConfirm] = useState(null); const [revoked, setRevoked] = useState(null); // result of the last disconnect in this panel // Trusted addresses: approved ones count for group membership and rule // targeting; waiting ones need an operator. Seeded from the fleet list, // then replaced by the fresh detail and by each action's server response // (never optimistic). const addrsOf = (s) => ({ approved: (s && s.approved_ips) || [], pending: (s && s.pending_ips) || [] }); const [addrs, setAddrs] = useState(() => addrsOf(server)); const shownId = useRef(null); useEffect(() => { // A caller can ask for another first tab (ServerDrawer.openTab), used once. setTab(ServerDrawer.openTab || "summary"); ServerDrawer.openTab = null; setDetail(null); setDetailErr(""); setRevoked(null); setAddrs(addrsOf(server)); shownId.current = server ? server.agent_id : null; if (server && window.IMARA_LIVE && window.ImaraAPI) { const id = server.agent_id; setLoading(true); // A late answer for another server is dropped. window.ImaraAPI.serverDetail(id) .then((d) => { if (shownId.current !== id) return; setDetail(d); if (d && d.server) setAddrs(addrsOf(d.server)); }) .catch((e) => { if (shownId.current !== id) return; setDetail(null); setDetailErr((e && e.message) || "failed"); }) .finally(() => { if (shownId.current === id) setLoading(false); }); } }, [server?.agent_id]); if (!server) return null; const D = window.MS_DATA; const ctx = { groups: D.GROUPS || [], servers: D.SERVERS, intents: D.INTENTS }; // POST /api/servers/{agent_id}/addresses/{approve|reject|remove}. Confirmed // first; the dialog shows the server's error and stays open on failure. const askAddresses = (action, list) => { const what = list.length === 1 ? list[0] : `${list.length} addresses`; const spec = { approve: { title: `Approve ${what} for ${server.hostname}?`, confirmLabel: "Approve", tone: "primary", body: `Approved addresses count as ${server.hostname}: rules for its groups cover them, as source and destination. Servers whose rules use this server are updated. Approve only addresses you expect on this server.`, }, reject: { title: `Reject ${what}?`, confirmLabel: "Reject", tone: "danger", body: "Removes the address from the pending list. It was never used in rules, so no server changes. If the server still has it at the next agent start, it returns as pending, with a new alert. Rules for this server's groups do not cover traffic from it.", }, remove: { title: `Remove ${what} from the approved addresses of ${server.hostname}?`, confirmLabel: "Remove", tone: "danger", body: `The address stops counting as ${server.hostname}: its groups drop it. Rules that matched this server only through it stop applying here, including any default deny they turn on. Affected servers are updated.`, }, }[action]; // Removing addresses: what it does to rules that use this server as a // source; refused when it would leave such a group with no usable // address (every update of those rules' servers would then fail). // Worked out from the addresses this panel shows now (they change with // each action, before the next refresh) and checked again when // confirming. const removalEffects = (DD) => { const me = { ...((DD.byId || {})[server.agent_id] || server), approved_ips: addrs.approved }; return T.addressRemovalEffects(me, list, { groups: DD.GROUPS || [], servers: (DD.SERVERS || []).map((x) => (x.agent_id === me.agent_id ? me : x)), byId: DD.byId || {}, intents: DD.INTENTS }); }; const eff = action === "remove" ? removalEffects(D) : null; const approvalEffects = (DD) => { const me = { ...((DD.byId || {})[server.agent_id] || server), approved_ips: addrs.approved }; return T.addressApprovalEffects(me, list, { groups: DD.GROUPS || [], servers: (DD.SERVERS || []).map((x) => (x.agent_id === me.agent_id ? me : x)), intents: DD.INTENTS }); }; const joinedEff = action === "approve" ? approvalEffects(D) : null; const approvalRefusal = (e) => !e ? "" : D.POLICY_INCOMPLETE ? "Some rules did not load: cannot confirm what approving the address does. Try again after the next refresh." : e.unknown.length ? `Cannot confirm what approving the address does to ${T.joinList(e.unknown)}, so it cannot be approved here. Try again after the next refresh.` : ""; if (joinedEff && approvalRefusal(joinedEff)) { setConfirm({ title: `${what.charAt(0).toUpperCase() + what.slice(1)} cannot be approved now`, confirmLabel: "Close", tone: "primary", body: approvalRefusal(joinedEff), run: async () => {} }); return; } const refusal = (e) => !e ? "" : D.POLICY_INCOMPLETE ? "Some rules did not load: cannot confirm what removing the address does. Try again after the next refresh." : e.unknown.length ? `Could not read ${T.joinList(e.unknown)}: cannot confirm what removing the address does.` : e.emptied.some((x) => !x.waiting) ? `Removing ${list.length === 1 ? "it" : "them"} leaves ${T.joinList(e.emptied.filter((x) => !x.waiting).map((x) => `${T.groupName(x.group)} (the ${x.side} of ${T.joinList(x.rules)})`))} with no approved IP address. The servers of those rules could then not be updated at all. Approve another address for ${server.hostname} first, or change those rules.` : ""; if (eff && refusal(eff)) { setConfirm({ title: `${what.charAt(0).toUpperCase() + what.slice(1)} cannot be removed now`, confirmLabel: "Close", tone: "primary", body: refusal(eff), run: async () => {} }); return; } setConfirm({ ...spec, body: ( {spec.body}
{list.join(", ")}
{eff && eff.rules.filter((x) => x.services.length).map((x, i) => (
{x.waiting ? `Once approved, ${T.ruleLabel(x.name)} (pending) would not allow ` : ""}{x.out ? `${x.waiting ? "c" : "C"}onnections from ${T.hostList(x.servers) || "its servers"} to ${list.length === 1 ? "this address" : "these addresses"}` : `${x.waiting ? "c" : "C"}onnections from ${list.length === 1 ? "this address" : "these addresses"} to ${T.hostList(x.servers) || "its servers"}`} on {T.servicesText(x.services)}{x.waiting ? "." : are no longer allowed by {x.name}. Blocked where {x.out ? "outbound default deny" : "default deny"} is on and no other rule allows them.}
))} {eff && eff.rules.some((x) => x.services.length && !x.out) &&
If this server is a jump host, check that you keep a way in.
} {eff && eff.rules.some((x) => x.blocks && !x.waiting) &&
They are also no longer always blocked on the ports {T.joinList(eff.rules.filter((x) => x.blocks && !x.waiting).map((x) => T.ruleLabel(x.name)))} {eff.rules.filter((x) => x.blocks && !x.waiting).length === 1 ? "blocks" : "block"}.
} {eff && eff.rules.some((x) => x.blocks && x.waiting) && (() => { const w = eff.rules.filter((x) => x.blocks && x.waiting); return
If {T.joinList(w.map((x) => T.ruleLabel(x.name)))} {w.length === 1 ? "is" : "are"} approved later, {w.length === 1 ? "its port block" : "their port blocks"} would not cover {list.length === 1 ? "this address" : "these addresses"}. Nothing is blocked by {w.length === 1 ? "it" : "them"} now.
; })()} {eff && (eff.leftOut || []).map((x, i) => (
{T.ruleLabel(x.name)} then no longer applies to {server.hostname} (no longer in its destination){x.asksIn ? ", so its default deny turns off here" : x.asksOut ? ", so its outbound default deny turns off here" : ""}.
))} {eff && (eff.joined || []).map((x, i) => (
{T.ruleLabel(x.name)} then applies to {server.hostname} ({addrs.approved.filter((a) => !list.includes(a)).length ? "in its destination" : "no approved address left, so its destination is no longer checked"}){x.asksIn ? ": default deny turns on here, and all other inbound traffic, including SSH, is blocked unless a rule allows it" : x.asksOut ? ": outbound default deny turns on here" : x.blocks.length ? `: ${T.servicesText(x.blocks)} is always blocked here` : ""}.
))} {joinedEff && joinedEff.joined.map((x, i) => (
{T.ruleLabel(x.name)} then applies to {server.hostname} (in its destination){x.asksIn ? ": default deny turns on here, and all other inbound traffic, including SSH, is blocked unless a rule allows it" : x.asksOut ? ": outbound default deny turns on here" : x.blocks.length ? `: ${T.servicesText(x.blocks)} is always blocked here` : ""}.
))} {eff && eff.emptied.filter((x) => x.waiting).map((x, i) => (
{T.groupName(x.group)}, the {x.side} of {T.joinList(x.rules)} (pending approval), is then left with no approved IP address, so that version cannot be approved until it has one again.
))} {joinedEff && joinedEff.rules.filter((x) => x.blocks.length).map((x, i) => (
{x.out ? `Connections from ${T.hostList(x.servers) || "its servers"} to ${list.length === 1 ? "this address" : "these addresses"}` : `Connections from ${list.length === 1 ? "this address" : "these addresses"} to ${T.hostList(x.servers) || "its servers"}`} on {T.servicesText(x.blocks)} {x.waiting ? `would always be blocked by ${T.ruleLabel(x.name)} once approved (it is pending; approving the address alone blocks nothing)` : `are then always blocked by ${T.ruleLabel(x.name)}, whatever other rules allow`}{!x.out && x.blocks.indexOf("tcp/22") >= 0 ? ". If this server is a jump host, check that you keep a way in" : ""}.
))} {joinedEff && joinedEff.rules.filter((x) => x.services.length).map((x, i) => (
{x.out ? `Connections from ${T.hostList(x.servers) || "its servers"} to ${list.length === 1 ? "this address" : "these addresses"}` : `Connections from ${list.length === 1 ? "this address" : "these addresses"} to ${T.hostList(x.servers) || "its servers"}`} on {T.servicesText(x.services)} {x.waiting ? `would be allowed by ${T.ruleLabel(x.name)} once approved (it is pending; approving the address alone allows nothing)` : `are then allowed by ${T.ruleLabel(x.name)}`}.
))} {joinedEff && joinedEff.revived.map((x, i) => (
{x.server} can then be updated again: updates stopped while {T.groupName(x.group)}, used by {T.ruleLabel(x.rule)}, had no approved IP address. It then gets every change approved since, at once.
))} {joinedEff && joinedEff.leftOut.map((x, i) => (
{T.ruleLabel(x.name)} then no longer applies to {server.hostname} (with an approved address, its destination is checked and does not include it){x.asksIn ? ", so its default deny turns off here" : x.asksOut ? ", so its outbound default deny turns off here" : ""}.
))}
), run: async () => { const id = server.agent_id; // Removing: read everything again and refuse if the effect changed. if ((eff || joinedEff) && window.IMARA_LIVE && window.ImaraLive) { await window.ImaraLive.loadAll({ initial: false }); const DD = window.MS_DATA; const fresh = (DD.byId || {})[id]; if (fresh && JSON.stringify((fresh.approved_ips || []).slice().sort()) !== JSON.stringify(addrs.approved.slice().sort())) { if (shownId.current === id) setAddrs(addrsOf(fresh)); throw new Error("This server's approved addresses changed while this dialog was open. The list is updated: check it and try again."); } if (eff) { const now = removalEffects(DD); if (refusal(now)) throw new Error(refusal(now)); if (JSON.stringify(now) !== JSON.stringify(eff)) throw new Error("The effect of removing the address changed while this dialog was open (a rule or server changed). Close it and check again."); } else { const now = approvalEffects(DD); if (approvalRefusal(now)) throw new Error(approvalRefusal(now)); if (JSON.stringify(now) !== JSON.stringify(joinedEff)) throw new Error("The effect of approving the address changed while this dialog was open (a rule or server changed). Close it and check again."); } } const r = await window.ImaraAPI.serverAddresses(id, action, list); if (shownId.current === id) setAddrs(addrsOf(r)); // Imara rebuilds the servers that depend on these addresses: read // their firewall reports again soon rather than in 3 minutes. if (action !== "reject") { T.noteServerChange([id]); if (window.ImaraLive && window.ImaraLive.invalidateFirewalls) window.ImaraLive.invalidateFirewalls(); if (onRefresh) { setTimeout(() => onRefresh(), 8000); setTimeout(() => { if (window.ImaraLive && window.ImaraLive.invalidateFirewalls) window.ImaraLive.invalidateFirewalls(); onRefresh(); }, 35000); } } const verb = action === "approve" ? "Approved" : action === "reject" ? "Rejected" : "Removed"; toast(`${verb} ${what}${r && r.recompile === "scheduled" ? ". Updating the affected servers." : ""}`, "ok"); if (onAudit) onAudit(); }, }); }; // POST /api/agents/{agent_id}/revoke -> {revoked: n}. Confirmed first. const askRevoke = () => setConfirm({ title: `Disconnect ${server.hostname} from Imara?`, confirmLabel: "Disconnect from Imara", tone: "danger", body: "Imara stops managing this server. Its certificates are revoked and its messages rejected. Its connection is refused once the revocation list reloads, usually within seconds. Its firewall rules stay in place, including default deny if on. To manage it again, enrol it with a new one-time token.", input: { label: "Reason", required: false, placeholder: "For example: server decommissioned" }, run: async (reason) => { const r = await window.ImaraAPI.revokeAgent(server.agent_id, reason); const n = r && typeof r.revoked === "number" ? r.revoked : null; setRevoked({ n, at: Date.now() }); toast(n == null ? "Disconnect accepted" : n === 0 ? "No active certificate found for this server" : `Disconnected: ${n} certificate${n === 1 ? "" : "s"} revoked`, n === 0 ? "warn" : "ok"); if (onAudit) onAudit(); }, }); // Agent-reported listening ports (process name and pid come off the box). const ports = (detail?.listening_ports || []).map((p) => ({ proto: p.protocol || "tcp", port: p.port, process: p.process_name || "", pid: p.pid, addr: p.address, exposed: p.exposed_external, })); // Recent connections captured by the agent's kernel probes. out = this // server connected to daddr:dport; in = a client at daddr connected to this // server's listening socket saddr:sport. The peer is daddr either way. const flows = (detail?.recent_flows || []).map((f) => ({ dir: f.direction || "out", peerName: f.daddr_name || f.daddr, peerIp: f.daddr, port: f.direction === "in" ? f.sport : f.dport, comm: f.comm, })); // The live firewall the server reported, every table. const rules = (detail?.firewall_rules || []).map((r, i) => ({ id: (r.rule_id || "r") + "-" + i, dir: r.direction === "inbound" ? "in" : r.direction === "outbound" ? "out" : (r.chain || ""), table: r.table, chain: r.chain, proto: r.protocol || "any", port: r.dst_port || "*", src: r.src_address || "", action: r.action, pkts: r.packets, })); // A raw nftables source in words. Imara compiles a rule's `from:` into a // named set @src_, so the set names the rule. function prettySource(src) { if (!src || src === "any" || src === "*") return { text: "any source", title: "no source condition", dim: true }; if (/^@(src|dst)6?_/.test(src)) { const pol = src.replace(/^@(src|dst)6?_/, ""); const it = (D.INTENTS || []).find((i) => (i.name || "").replace(/-/g, "_") === pol); const r = it ? T.parseRule(T.bodyOfVersion(it, T.expectedVersion(it, server.agent_id)) || it.body) : null; if (!r || !r.ok) return { text: `set ${src}`, title: "the rule for this set could not be read", dim: false }; return { text: T.peerText(r.from, ctx), title: `set ${src} from rule ${T.ruleLabel(it.name)}`, dim: false }; } if (/(^|[,\s])0\.0\.0\.0\/0/.test(src)) return { text: "any address", title: src, dim: false }; const ips = src.split(",").map((x) => x.trim()).filter(Boolean); if (ips.length > 2) return { text: `${ips[0]} and ${ips.length - 1} more`, title: ips.join(", "), dim: false }; return { text: src, title: src, dim: false }; } // Rules sent to this server, read at the version it should run. const mine = (D.INTENTS || []).filter((i) => T.targetsServer(i, server)).map((i) => { const want = T.expectedVersion(i, server.agent_id); const r = want ? T.parseRule(T.bodyOfVersion(i, want) || "") : null; const row = T.targetRow(i, server.agent_id); let status; // Withdrawn: Imara takes the whole rule off; if that failed here, it may // still be in force on this server. const gone = i.approval_state === "rolled_back" || (i.canary_state === "rolled_back" && ["draft", "requested", "rejected"].indexOf(i.approval_state) < 0); if (gone) status = row && row.status === "error" ? { tone: "bad", text: "rolled back, but removal failed on this server: it may still apply here", withdrawn: true } : { tone: "muted", text: "rolled back; removal not confirmed by this server yet", withdrawn: true }; else if (!want) status = { tone: "muted", text: Number(i.approved_version || 0) ? "not on this server yet (waiting for the trial)" : "not approved yet" }; else if (r && r.ok && T.hostSideIncludes(r, server, { groups: D.GROUPS || [], servers: D.SERVERS }) === false) status = { tone: "muted", text: "left out on this server", help: "Left out on this server: not in the rule's destination group by its approved addresses.", leftOut: true }; else if (row && row.status === "error") status = /does not target this host/.test(row.error_text || "") ? { tone: "bad", text: "left out: not in the rule's destination group by its approved addresses", leftOut: true } : { tone: "bad", text: "error: the last change failed on this server" }; else if (row && row.status === "applied" && Number(row.version) === want) status = { tone: "ok", text: `version ${want} confirmed` }; else if (row) status = { tone: "accent", text: `version ${want} not confirmed yet` }; else if (i.target_agent_id) status = { tone: i.status === "applied" ? "ok" : i.status === "error" ? "bad" : "accent", text: i.status === "applied" ? `version ${want} confirmed` : i.status === "error" ? "error" : `version ${want} not confirmed yet` }; else if (!Array.isArray(i.targets)) status = { tone: "warn", text: "target list could not be loaded" }; else status = { tone: "muted", text: "no record here", help: "No record here: a server enrolled after the rule was last sent gets it on registration, without a record." }; return { it: i, want, rule: r, status }; }); const today = T.serverToday(); const ended = mine.filter((m) => m.want && m.rule && m.rule.ok && m.rule.expires && m.rule.expires < today); const live = mine.filter((m) => !ended.includes(m)); const inRules = live.filter((m) => m.want && m.rule && m.rule.ok && m.rule.direction === "ingress"); const outRules = live.filter((m) => m.want && m.rule && m.rule.ok && m.rule.direction === "egress"); const unreadable = mine.filter((m) => m.want && (!m.rule || !m.rule.ok)); const withdrawnHere = mine.filter((m) => m.status.withdrawn); const notYet = mine.filter((m) => !m.want && !m.status.withdrawn); const p = T.protectionNow(server, D); const seenAgo = p.seen && p.seen.at ? agoLong(p.seen.at) : ""; const protectionText = { on: `On: inbound traffic that no rule allows is blocked. Seen in the firewall report ${seenAgo}.`, off: `Off: inbound traffic is not blocked by default, so inbound allow rules have no effect yet.${p.seen && p.seen.blocks ? ` ${T.plural(p.seen.blocks, "port block")} still ${p.seen.blocks === 1 ? "applies" : "apply"}.` : ""} Seen in the firewall report ${seenAgo}.`, waiting: p.waitingFor === "off" ? "Not confirmed yet: no rule turns it on any more, but the latest firewall report predates that change. The next report shows whether it is off." : "Not confirmed yet: requested, but the firewall report does not show it yet.", unknown: "Unknown: cannot confirm from this server's firewall report.", }[p.state] || "Unknown."; const toneColor = { ok: "var(--ok)", accent: "var(--accent)", warn: "var(--warn)", bad: "var(--bad)", muted: "var(--text-faint)" }; const labelChips = Object.entries(server.labels || {}).filter(([k]) => k.charAt(0) !== "_" || T.HOSTNAME_PARTS[k]); // Protection is requested for a group: the smallest usable group that // contains this server. const protectGroup = (() => { const gs = (D.GROUPS || []).filter((g) => !T.isEmptySelector(g.selector) && !T.isServerGroup(g) && T.membersOf(g.selector, [server]).length && T.groupMembers(g.selector, D.SERVERS).some(T.hasApprovedAddress)); gs.sort((a, b) => T.groupMembers(a.selector, D.SERVERS).length - T.groupMembers(b.selector, D.SERVERS).length); return gs[0] || null; })(); // One rule; a status its list says once above it is left out here. const ruleItem = (m, shared) => { const d = T.describeRule(m.rule, ctx); return (
{T.ruleTitle(m.it)}{!(shared && shared.has(m.status)) && {m.status.text}}
{d.text}
{d.lines.map((l, k) =>
{l}
)}
); }; // A list of rules (a server may have hundreds): the status most of them // share is said once above it; what the other statuses mean is behind // the "?" of its heading (help). const ruleList = (list, empty) => { const shared = sharedStatus(list.map((m) => m.status)); return { help: statusHelp(list.filter((m) => !(shared && shared.has(m.status))).map((m) => m.status)), body: (
{shared &&
{shared.count === list.length ? `All ${T.plural(list.length, "rule")}` : `${shared.count.toLocaleString("en-US")} of ${T.plural(list.length, "rule")}`}: {shared.text}.{statusHelp(list.filter((m) => shared.has(m.status)).map((m) => m.status))}
} {list.map((m) => ruleItem(m, shared))} {list.length === 0 && empty &&
{empty}
}
), }; }; // Built only for the Summary tab, the one that shows them. const inList = tab === "summary" ? ruleList(inRules, "No approved rule allows inbound traffic to this server.") : null; const outList = tab === "summary" ? ruleList(outRules) : null; // The addresses the server reported when its Imara agent last started. const reportedIps = String((server.labels || {})._ips || "").split(",").concat(String((server.labels || {})._ips6 || "").split(",")).map((x) => x.trim()).filter(Boolean); const notReportedNow = reportedIps.length ? addrs.approved.filter((a) => !reportedIps.includes(a)) : []; const reportedOnly = reportedIps.filter((a) => !addrs.approved.includes(a) && !addrs.pending.includes(a)); // Some names, then how many more: a list of rules on a server can be long. const someNames = (names, n = 12) => (names.length > n ? `${names.slice(0, n).join(", ")} and ${names.length - n} more` : T.joinList(names)); const tabs = [["summary", "Summary"], ["addresses", "Addresses and ports"], ["connections", `Connections (${flows.length})`], ["tech", "Technical details"]]; return ( { if (!confirm) onClose(); }} wide>
{T.freshnessText(server.fresh)} default deny: {T.protectionWord(p.state).toLowerCase()}
{tabs.map(([t, l]) =>
setTab(t)}>{l}
)}
{tab === "summary" && (
Default deny
{protectionText}
{p.notes.map((n, i) =>
{n}
)}
{p.contributors.length ? Requested by {T.joinList(p.contributors.map((c) => { // Only what is unusual: one that is not used or could not be read. const q = c.asks === false ? (c.ended ? `expired ${c.ended}` : "not used") : c.asks === "unknown" ? "could not be read" : ""; return q ? `${T.ruleTitle(c.name)} (${q})` : T.ruleTitle(c.name); }))}. : "No rule turns it on."}
{p.state !== "on" && onProtect && (() => { // For a whole group it is in, or for this server only (through // its one-server group). const wide = protectGroup && !T.isServerGroup(protectGroup) ? protectGroup : null; const alone = !!(onProtectServer && !T.serverNameIssue(server, D.SERVERS) && T.hasApprovedAddress(server)); return (
{alone && } {wide && } {!wide && !alone && {T.serverNameIssue(server, D.SERVERS) || "Rules can use this server once it has an approved IP address (Addresses and ports)."}} {wide && !alone && T.serverNameIssue(server, D.SERVERS) && Not offered for this server alone: {T.serverNameIssue(server, D.SERVERS)}}
); })()}
Inbound rules ({inRules.filter((m) => !m.status.leftOut).length}{inRules.some((m) => m.status.leftOut) ? `, plus ${inRules.filter((m) => m.status.leftOut).length} left out here` : ""}){inList.help}
{inList.body} {outRules.length > 0 && (
Outbound rules ({outRules.length}){outList.help}
{outList.body}
)} {(unreadable.length > 0 || notYet.length > 0 || ended.length > 0 || withdrawnHere.length > 0) && (
{unreadable.length > 0 &&
Could not read: {T.joinList(unreadable.map((m) => T.ruleTitle(m.it)))}.
} {notYet.length > 0 &&
Not on this server yet (pending approval or trial): {someNames(notYet.map((m) => T.ruleTitle(m.it)))}.
} {withdrawnHere.filter((m) => m.status.tone === "bad").map((m) =>
{T.ruleTitle(m.it)}: {m.status.text}.
)} {(() => { const w = withdrawnHere.filter((m) => m.status.tone !== "bad"); return w.length ?
Rolled back; this server has not confirmed {w.length === 1 ? "its" : "their"} removal: {T.joinList(w.map((m) => T.ruleTitle(m.it)))}.
: null; })()} {ended.length > 0 &&
Expired (Imara removes them): {someNames(ended.map((m) => `${T.ruleTitle(m.it)} (${m.rule.expires})`))}.
}
)} {(() => { // What the server says about itself first (site and role from // its name, network from its main address); labels set when // Imara was installed after them, quieter. const facts = T.serverFacts(server); const factKeys = facts.map(([k]) => k); const others = labelChips.filter(([k]) => !factKeys.includes(k)); return (
{T.namesFollowPattern() ? "Site, role and network" : "Labels"}Groups match servers on these. Site and role come from the hostname, network from the main address. Other labels were set at install.
{facts.map(([k, v]) => {T.labelText(k, v)})} {allLabels && others.map(([k, v]) => {T.labelText(k, v)})} {others.length > 0 && } {facts.length + others.length === 0 && No labels.}
); })()} {(() => { const inGroups = (D.GROUPS || []).filter((g) => !T.isEmptySelector(g.selector || {}) && T.groupMembers(g.selector, [server]).length); return (
In groups ({inGroups.length})
{inGroups.slice(0, 12).map((g) => {T.groupName(g.name)})} {inGroups.length > 12 && and {inGroups.length - 12} more} {inGroups.length === 0 && Not in any group yet.}
); })()}
)} {tab === "addresses" && (
IP addresses

Only approved addresses count for groups: as a source, this server is matched by them only. Addresses reported after first registration wait here for approval.

{notReportedNow.length > 0 &&

Not reported now: whoever holds this address is treated as this server. Remove it if it is no longer this server's.

} {reportedOnly.length > 0 &&

Reported, not approved: rejected or removed, but still reported. Rules for this server's groups do not cover traffic from it.

}
{addrs.pending.length > 0 && {addrs.pending.length} pending approval}
{addrs.pending.length > 1 && }
{addrs.pending.map((a) => ( ))} {addrs.approved.map((a) => ( ))} {reportedOnly.map((a) => ( ))} {addrs.approved.length === 0 && addrs.pending.length === 0 && ( )}
AddressState
{a} pending approval
{a} approved{notReportedNow.includes(a) &&
not reported now
}
{a} reported, not approved
{loading ? "Loading…" : "No addresses yet. The first ones the server reports are trusted."}
Listening ports {loading && · loading…}
{ports.map((pt, i) => { // Wildcard = every address; loopback = this server only; // anything else is reachable on that one address. const a = String(pt.addr || "").replace(/^\[|\]$/g, ""); const loop = /^127\./.test(a) || a === "::1"; return ( ); })} {ports.length === 0 && }
ServiceProcessListens on

"All addresses" or "address ...": reachable from the network on those addresses unless a firewall blocks it.

"This server only": loopback, reachable only from the server itself.

{T.serviceName(`${String(pt.proto).replace(/6$/, "")}/${pt.port}`)} {pt.process || "-"}{pt.pid ? ` (pid ${pt.pid})` : ""} {pt.exposed ? all addresses : loop ? this server only : {a ? `address ${a}` : "unknown address"}}
{loading ? "Loading…" : detailErr ? `Server details could not be loaded: ${detailErr}` : "No listening ports reported."}
)} {tab === "connections" && (
Latest TCP connections

Latest TCP connections recorded by the agent on this server ({flows.length} shown, newest first; older ones not listed).

DNS lookups and other UDP traffic are not recorded.

{flows.map((f, i) => ( ))} {flows.length === 0 && }
DirectionPeerServiceProcess
{f.dir === "out" ? "out" : "in"} {f.peerName} {T.serviceName(`tcp/${f.port}`)} {f.comm || "-"}
{loading ? "Loading…" : detailErr ? `Server details could not be loaded: ${detailErr}` : "No connections recorded recently."}
)} {tab === "tech" && (
Firewall rules on the server (nftables)

Imara's tables: imara_fw (rules) and imara_safety (keeps the server reachable by Imara). Other tables belong to other software.

A change to Imara's tables made outside Imara raises a "Firewall drift" alert. Automatic restore is an option on the Imara server.

all tables, as reported by the server {loading && "· loading…"}
{rules.map((r) => ( ))} {rules.length === 0 && }
TableChainMatchSourceActionPackets
{r.table} {r.chain} {r.proto !== "any" ? `${r.proto}/${r.port}` : r.port} {(() => { const ps = prettySource(r.src); return {ps.text}; })()} {r.action} {r.pkts != null ? fmt(r.pkts) : "-"}
Disconnect from Imara
Imara stops managing this server. Its firewall rules stay in place. {revoked &&
{revoked.n == null ? "Disconnect accepted" : `${revoked.n} certificate${revoked.n === 1 ? "" : "s"} revoked`} · {new Date(revoked.at).toLocaleTimeString()}
}
)}
confirm.run(text)} onClose={() => setConfirm(null)}>{confirm?.body}
); } // ---------- ALERT DRAWER ---------- // What each alert means and what to do, in plain words. const RUNBOOK = { agent_silent: "The agent has not reported for over 5 minutes. Its firewall rules stay in place; monitoring and rule changes resume when it reports again. Check that the server is up and reachable, and that the imara-agent service is running.", firewall_drift: "The firewall on this server changed outside Imara. A change in Imara's tables (imara_fw, imara_safety) needs investigation: find who or what made it. Imara restores its rules automatically if that option is on. A change in another program's table (for example fail2ban) is shown for completeness.", lateral_movement: "This server connected to far more of your servers than usual, including servers it never contacted before. Possible lateral movement: review the new connections.", beaconing: "This server connects to one external address at very regular intervals, typical of command-and-control (C2) check-ins. Identify the process making the connections.", dns_tunneling: "This server made many unusual DNS queries under one domain, typical of data tunnelled over DNS. Identify the process making them and check the domain.", service_disablement: "The Imara agent on this server was stopped. Its firewall rules stay in place. If the stop was not planned, treat it as tampering.", binary_hash_drift: "The Imara agent binary on this server changed. Expected after an upgrade; otherwise treat it as tampering.", kernel_module_unload: "A kernel module the Imara agent needs (such as nf_tables or nf_conntrack) was unloaded. Firewall rules may not be enforced correctly until it is loaded again.", auditd_tamper: "auditd recorded a write to the Imara agent's settings, certificates or keys (/etc/imara). Expected after a re-enrolment; otherwise treat it as tampering. This audit watch is optional.", host_address_change: "The server reported IP addresses that are not approved yet. Rules for its groups do not cover them until approved. Rules for any address or address ranges may still allow them. Open the server and approve them if expected (new NIC or new address); otherwise reject them and investigate. The alert closes when none are pending.", }; const IMARA_TABLES = ["imara_fw", "imara_safety"]; // Configured SIEM connectors from GET /api/siem/status. Fetched when an // alert opens; a successful answer is cached for the page, a failure is not // (so a later open retries instead of showing "none" forever). function useSiemConnectors(key) { const [c, setC] = useState(() => window.__imaraSiemConnectors || null); useEffect(() => { if (key == null || !window.ImaraAPI?.siemStatus) return; if (window.__imaraSiemConnectors) { setC(window.__imaraSiemConnectors); return; } let alive = true; window.ImaraAPI.siemStatus() .then((list) => { window.__imaraSiemConnectors = list || []; if (alive) setC(list || []); }) .catch(() => { if (alive) setC(null); }); return () => { alive = false; }; }, [key]); return c; } function AlertDrawer({ alert, onClose, onStatus, onOpenHost }) { const siem = useSiemConnectors(alert ? alert.id : null); const [tech, setTech] = useState(false); useEffect(() => { setTech(false); }, [alert ? alert.id : null]); if (!alert) return null; const T = window.ImaraTerms; const D = window.MS_DATA; const host = alert.server_id ? D.byId[alert.server_id] : null; const sevWord = alert.severity === "warn" ? "warning" : alert.severity; const stateWord = { open: "open", acknowledged: "acknowledged: muted until resolved", resolved: "resolved", false_positive: "false positive" }[alert.status] || alert.status; // One alert can hold several findings of its kind (the strongest first). const also = alert.details && Array.isArray(alert.details.also_flagged) ? alert.details.also_flagged : []; const changes = alert.kind === "firewall_drift" && Array.isArray(alert.details && alert.details.changes) ? alert.details.changes : []; // When every change outside Imara's tables is in one other table, that is // said once above the list instead of on each change. const tableOf = (c) => (c.rule && c.rule.table) || ""; const foreign = changes.filter((c) => !IMARA_TABLES.includes(tableOf(c))); const foreignTables = Array.from(new Set(foreign.map(tableOf))); const onceForeign = foreign.length > 1 && foreignTables.length === 1; return (
{sevWord} {stateWord} raised {agoLong(alert.created_at)} {host && onOpenHost && }
What this means

{RUNBOOK[alert.kind] || alert.title || "A check raised this alert."}

{alert.details && alert.details.auto_restored === false &&

Automatic restore failed: this server may be running without Imara's rules. Open the server and check its firewall report.

} {alert.details && alert.details.auto_restored === true &&

Imara restored its rules automatically. Find out who or what changed them.

} {(() => { const ev = (window.ALERT_EVIDENCE || {})[alert.kind] || []; const d = alert.details || {}; const chips = ev.filter(([k]) => d[k] !== undefined && d[k] !== null && d[k] !== "").map(([k, f]) => f(d[k])); return chips.length ?
{chips.map((c, i) => {c})}
: null; })()}
{also.length > 0 && (
Also in this alert ({also.length})
{also.slice(0, 20).map((x, k) =>
{x}
)} {also.length > 20 &&
and {also.length - 20} more
}
)} {changes.length > 0 && (
What changed
{onceForeign &&
{foreign.length === changes.length ? "Every change is" : "The other changes are"} in another program's table{foreignTables[0] ? ` (${foreignTables[0]})` : ""}, not Imara's.
}
{changes.map((c, i) => { const table = tableOf(c); const ours = IMARA_TABLES.includes(table); return (
{c.change === "added" ? "Added" : c.change === "removed" ? "Removed" : "Changed"}{c.at ? agoLong(c.at) : ""}
{(() => { const t = T.firewallRuleText(c.rule); return t.charAt(0).toUpperCase() + t.slice(1) + "."; })()}
{(ours || !onceForeign) &&
{ours ? `In Imara's own table ${table}.` : `In another program's table${table ? ` (${table})` : ""}, not Imara's.`}
}
); })}
)}
Timeline
{[["raised", alert.created_at, SEV[alert.severity]], alert.resolved_at && ["resolved", alert.resolved_at, "ok"]].filter(Boolean).map((e, i) => (
{e[0]} {agoLong(e[1])}
))}
{alert.severity === "critical" && siem && siem.length > 0 && (
Critical alerts are forwarded to your SIEM ({siem.join(", ")}) when raised.Best-effort delivery, not tracked here.
)} {alert.details && Object.keys(alert.details).length > 0 && (
{tech && (
{Object.entries(alert.details).map(([k, v]) => )}
)}
)} {(alert.status === "open" || alert.status === "acknowledged") && (
{alert.status === "open" && }
Acknowledging mutes new alerts of this kind for this server until it is resolved.{also.length ? ` Resolving also mutes the ${also.length} other finding${also.length === 1 ? "" : "s"} in this alert.` : ""}
)}
); } window.ServerDrawer = ServerDrawer; window.AlertDrawer = AlertDrawer; window.highlightNft = highlightNft; window.KV = KV; window.DrawerHead = DrawerHead; window.sharedStatus = sharedStatus; window.statusHelp = statusHelp;