Files
homarr/packages/api/src/router/widgets/wazuh.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

231 lines
9.4 KiB
TypeScript

import { z } from "zod/v4";
import { removeTrailingSlash } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { WidgetKind } from "@homarr/definitions";
import { mockWidgetData, toWazuhPublicError } from "@homarr/integrations";
import type { WazuhPublicError } from "@homarr/integrations";
import {
wazuhCveOrders,
wazuhFimEvents,
wazuhTimelineIntervals,
wazuhTimeRanges,
wazuhTopListKinds,
} from "@homarr/integrations/types";
import {
getWazuhTimelineRequestHandler,
wazuhAgentsRequestHandler,
wazuhAlertsRequestHandler,
wazuhAuthFailuresRequestHandler,
wazuhFimRequestHandler,
wazuhSummaryRequestHandler,
wazuhTopListRequestHandler,
wazuhVulnerabilitiesRequestHandler,
} from "@homarr/request-handler/wazuh";
import { createOneWidgetIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const logger = createLogger({ module: "wazuhRouter" });
type WazuhResult<TData> =
| { data: TData; updatedAt: Date; error: null; dashboardUrl: string | null }
| { data: null; updatedAt: null; error: WazuhPublicError; dashboardUrl: string | null };
interface WazuhContextIntegration {
id: string;
kind: string;
url: string;
externalUrl: string | null;
decryptedSecrets: { kind: string; value: string }[];
}
/**
* Base URL for deep links into the Wazuh dashboard: the optional "Dashboard URL" field, otherwise the linked app
* (unless the app just points at the API or indexer URL that the integration talks to).
*/
const getDashboardUrl = (integration: WazuhContextIntegration): string | null => {
const configured = integration.decryptedSecrets.find((secret) => secret.kind === "wazuhDashboardUrl")?.value;
const candidate = configured?.trim() ? configured : integration.externalUrl;
if (!candidate) return null;
if (!configured && removeTrailingSlash(candidate) === removeTrailingSlash(integration.url)) return null;
try {
const url = new URL(candidate);
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
return removeTrailingSlash(url.toString());
} catch {
return null;
}
};
/**
* Wazuh widgets render friendly messages for auth, TLS and reachability problems, so upstream failures are
* returned as a safe reason instead of a generic tRPC error.
*/
const settleAsync = async <TData>(
operation: string,
integration: WazuhContextIntegration,
getDataAsync: () => Promise<{ data: TData; timestamp: Date }>,
): Promise<WazuhResult<TData>> => {
const dashboardUrl = getDashboardUrl(integration);
try {
const { data, timestamp } = await getDataAsync();
return { data, updatedAt: timestamp, error: null, dashboardUrl };
} catch (error) {
const publicError = toWazuhPublicError(error);
logger.warn("Wazuh widget request failed", { operation, integrationId: integration.id, ...publicError });
return { data: null, updatedAt: null, error: publicError, dashboardUrl };
}
};
const mockResult = <TData>(data: TData): WazuhResult<TData> => ({
data,
updatedAt: new Date(mockWidgetData.timestamp),
error: null,
dashboardUrl: null,
});
const integrationIdHint = "REQUIRED: integrationId (single Wazuh integration ID from integration_all).";
const indexerHint = "Requires indexer credentials on the integration.";
const wazuhProcedure = (widgetKind: WidgetKind & `wazuh${string}`, description: string) =>
publicProcedure
.meta({ mcp: { enabled: true, description: `${description} ${integrationIdHint}` } })
.concat(createOneWidgetIntegrationMiddleware("query", widgetKind));
const timeRange = z.enum(wazuhTimeRanges);
export const wazuhRouter = createTRPCRouter({
getAgents: wazuhProcedure(
"wazuhAgents",
"Get Wazuh agent counts and the full agent list (status, IP, OS, version, last keepalive, groups).",
).query(async ({ ctx }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhAgents);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getAgents", integration, () =>
wazuhAgentsRequestHandler.handler(integration, {}).getDataAsync(),
);
}),
getAlerts: wazuhProcedure("wazuhAlerts", `Get recent Wazuh alerts at or above a rule level. ${indexerHint}`)
.input(
z.object({
minLevel: z.number().int().min(0).max(16).default(7),
limit: z.number().int().min(1).max(100).default(20),
hours: z.number().int().min(1).max(720).default(24),
}),
)
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhAlerts);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getAlerts", integration, () =>
wazuhAlertsRequestHandler
.handler(integration, { minLevel: input.minLevel, limit: input.limit, hours: input.hours })
.getDataAsync(),
);
}),
getSummary: wazuhProcedure(
"wazuhSummary",
"Get Wazuh alert counts by severity for a time range compared with the previous period, plus manager version and indexer health.",
)
.input(z.object({ range: timeRange.default("24h") }))
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhSummary);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getSummary", integration, () =>
wazuhSummaryRequestHandler.handler(integration, { range: input.range }).getDataAsync(),
);
}),
getTimeline: wazuhProcedure(
"wazuhTimeline",
`Get a histogram of Wazuh alerts over time split by severity (low, medium, high, critical). ${indexerHint}`,
)
.input(
z.object({
range: timeRange.default("24h"),
interval: z.enum(wazuhTimelineIntervals).default("auto"),
minLevel: z.number().int().min(0).max(16).default(0),
}),
)
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhTimeline);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getTimeline", integration, () =>
getWazuhTimelineRequestHandler(input.range).handler(integration, input).getDataAsync(),
);
}),
getTopList: wazuhProcedure(
"wazuhTopList",
`Get the top Wazuh rules, agents, source IPs, MITRE ATT&CK tactics or techniques by alert count. ${indexerHint}`,
)
.input(
z.object({
kind: z.enum(wazuhTopListKinds).default("rules"),
range: timeRange.default("24h"),
limit: z.number().int().min(1).max(50).default(8),
minLevel: z.number().int().min(0).max(16).default(0),
}),
)
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult({ ...mockWidgetData.wazuhTopList, kind: input.kind });
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getTopList", integration, () =>
wazuhTopListRequestHandler.handler(integration, input).getDataAsync(),
);
}),
getVulnerabilities: wazuhProcedure(
"wazuhVulnerabilities",
`Get Wazuh vulnerability counts by severity, the top CVEs and the most vulnerable agents. ${indexerHint}`,
)
.input(
z.object({
cveLimit: z.number().int().min(1).max(50).default(8),
agentLimit: z.number().int().min(1).max(50).default(5),
cveOrder: z.enum(wazuhCveOrders).default("score"),
minSeverity: z.enum(["all", "medium", "high", "critical"]).default("all"),
}),
)
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhVulnerabilities);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getVulnerabilities", integration, () =>
wazuhVulnerabilitiesRequestHandler.handler(integration, input).getDataAsync(),
);
}),
getFim: wazuhProcedure(
"wazuhFim",
`Get recent Wazuh file integrity monitoring (syscheck) events with counts per event type. ${indexerHint}`,
)
.input(
z.object({
range: timeRange.default("24h"),
event: z.enum(wazuhFimEvents).default("all"),
limit: z.number().int().min(1).max(100).default(20),
includeRegistry: z.boolean().default(true),
}),
)
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhFim);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getFim", integration, () =>
wazuhFimRequestHandler.handler(integration, input).getDataAsync(),
);
}),
getAuthFailures: wazuhProcedure(
"wazuhAuthFailures",
`Get failed-login and brute-force alert counts per agent with hourly series and top source IPs. ${indexerHint}`,
)
.input(
z.object({
hours: z.number().int().min(1).max(168).default(24),
limit: z.number().int().min(1).max(25).default(6),
}),
)
.query(async ({ ctx, input }) => {
if (ctx.integration.kind === "mock") return mockResult(mockWidgetData.wazuhAuthFailures);
const integration = { ...ctx.integration, kind: "wazuh" as const };
return await settleAsync("getAuthFailures", integration, () =>
wazuhAuthFailuresRequestHandler.handler(integration, input).getDataAsync(),
);
}),
});