Address PR comments

This commit is contained in:
Carlos Monastyrski
2025-11-07 02:09:26 -03:00
parent bb6b1940d1
commit b37a9f4168
19 changed files with 327 additions and 264 deletions
@@ -6,7 +6,7 @@ export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.PkiAlertsV2))) { if (!(await knex.schema.hasTable(TableName.PkiAlertsV2))) {
await knex.schema.createTable(TableName.PkiAlertsV2, (t) => { await knex.schema.createTable(TableName.PkiAlertsV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("slug").notNullable(); t.string("name").notNullable();
t.text("description").nullable(); t.text("description").nullable();
t.string("eventType").notNullable(); t.string("eventType").notNullable();
t.string("alertBefore").nullable(); t.string("alertBefore").nullable();
@@ -17,9 +17,7 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.index("projectId"); t.index("projectId");
t.index("eventType"); t.unique(["name", "projectId"]);
t.index("enabled");
t.unique(["slug", "projectId"]);
}); });
} }
@@ -44,7 +42,7 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("alertId").notNullable(); t.uuid("alertId").notNullable();
t.timestamp("triggeredAt").defaultTo(knex.fn.now()); t.timestamp("triggeredAt").defaultTo(knex.fn.now());
t.boolean("notificationSent").defaultTo(false); t.boolean("hasNotificationSent").defaultTo(false);
t.text("notificationError").nullable(); t.text("notificationError").nullable();
t.timestamps(true, true, true); t.timestamps(true, true, true);
+1 -1
View File
@@ -11,7 +11,7 @@ export const PkiAlertHistorySchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
alertId: z.string().uuid(), alertId: z.string().uuid(),
triggeredAt: z.date().nullable().optional(), triggeredAt: z.date().nullable().optional(),
notificationSent: z.boolean().default(false).nullable().optional(), hasNotificationSent: z.boolean().default(false).nullable().optional(),
notificationError: z.string().nullable().optional(), notificationError: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()
+1 -1
View File
@@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models";
export const PkiAlertsV2Schema = z.object({ export const PkiAlertsV2Schema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
slug: z.string(), name: z.string(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
eventType: z.string(), eventType: z.string(),
alertBefore: z.string().nullable().optional(), alertBefore: z.string().nullable().optional(),
+111 -102
View File
@@ -1,12 +1,14 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { import {
CreatePkiAlertV2Schema, CreatePkiAlertV2Schema,
createSecureAlertBeforeValidator, createSecureAlertBeforeValidator,
PkiAlertChannelType,
PkiAlertEventType, PkiAlertEventType,
PkiFilterRuleSchema, PkiFilterRuleSchema,
UpdatePkiAlertV2Schema UpdatePkiAlertV2Schema
@@ -22,32 +24,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Create a new PKI alert", description: "Create a new PKI alert",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
body: z.object({ body: CreatePkiAlertV2Schema.extend({
projectId: z.string().uuid().describe("Project ID"), projectId: z.string().uuid().describe("Project ID")
...CreatePkiAlertV2Schema.shape
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid(), alert: z.object({
slug: z.string(), id: z.string().uuid(),
description: z.string().nullable(), name: z.string(),
eventType: z.nativeEnum(PkiAlertEventType), description: z.string().nullable(),
alertBefore: z.string(), eventType: z.nativeEnum(PkiAlertEventType),
filters: z.array(z.any()), alertBefore: z.string(),
enabled: z.boolean(), filters: z.array(PkiFilterRuleSchema),
channels: z.array( enabled: z.boolean(),
z.object({ projectId: z.string().uuid(),
id: z.string().uuid(), channels: z.array(
channelType: z.string(), z.object({
config: z.record(z.any()), id: z.string().uuid(),
enabled: z.boolean(), channelType: z.nativeEnum(PkiAlertChannelType),
createdAt: z.date(), config: z.record(z.any()),
updatedAt: z.date() enabled: z.boolean(),
}) createdAt: z.date(),
), updatedAt: z.date()
createdAt: z.date(), })
updatedAt: z.date() ),
createdAt: z.date(),
updatedAt: z.date()
})
}) })
} }
}, },
@@ -67,14 +71,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
type: EventType.CREATE_PKI_ALERT, type: EventType.CREATE_PKI_ALERT,
metadata: { metadata: {
pkiAlertId: alert.id, pkiAlertId: alert.id,
name: alert.slug, name: alert.name,
eventType: alert.eventType, eventType: alert.eventType,
alertBefore: alert.alertBefore alertBefore: alert.alertBefore
} }
} }
}); });
return alert; return { alert };
} }
}); });
@@ -87,7 +91,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "List PKI alerts for a project", description: "List PKI alerts for a project",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
querystring: z.object({ querystring: z.object({
projectId: z.string().uuid(), projectId: z.string().uuid(),
search: z.string().optional(), search: z.string().optional(),
@@ -101,16 +105,16 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
alerts: z.array( alerts: z.array(
z.object({ z.object({
id: z.string().uuid(), id: z.string().uuid(),
slug: z.string(), name: z.string(),
description: z.string().nullable(), description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType), eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(), alertBefore: z.string(),
filters: z.array(z.any()), filters: z.array(PkiFilterRuleSchema),
enabled: z.boolean(), enabled: z.boolean(),
channels: z.array( channels: z.array(
z.object({ z.object({
id: z.string().uuid(), id: z.string().uuid(),
channelType: z.string(), channelType: z.nativeEnum(PkiAlertChannelType),
config: z.record(z.any()), config: z.record(z.any()),
enabled: z.boolean(), enabled: z.boolean(),
createdAt: z.date(), createdAt: z.date(),
@@ -147,31 +151,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Get a PKI alert by ID", description: "Get a PKI alert by ID",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
params: z.object({ params: z.object({
alertId: z.string().uuid().describe("Alert ID") alertId: z.string().uuid().describe("Alert ID")
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid(), alert: z.object({
slug: z.string(), id: z.string().uuid(),
description: z.string().nullable(), name: z.string(),
eventType: z.nativeEnum(PkiAlertEventType), description: z.string().nullable(),
alertBefore: z.string(), eventType: z.nativeEnum(PkiAlertEventType),
filters: z.array(z.any()), alertBefore: z.string(),
enabled: z.boolean(), filters: z.array(PkiFilterRuleSchema),
channels: z.array( enabled: z.boolean(),
z.object({ projectId: z.string().uuid(),
id: z.string().uuid(), channels: z.array(
channelType: z.string(), z.object({
config: z.record(z.any()), id: z.string().uuid(),
enabled: z.boolean(), channelType: z.nativeEnum(PkiAlertChannelType),
createdAt: z.date(), config: z.record(z.any()),
updatedAt: z.date() enabled: z.boolean(),
}) createdAt: z.date(),
), updatedAt: z.date()
createdAt: z.date(), })
updatedAt: z.date() ),
createdAt: z.date(),
updatedAt: z.date()
})
}) })
} }
}, },
@@ -195,7 +202,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
} }
}); });
return alert; return { alert };
} }
}); });
@@ -208,32 +215,35 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Update a PKI alert", description: "Update a PKI alert",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
params: z.object({ params: z.object({
alertId: z.string().uuid().describe("Alert ID") alertId: z.string().uuid().describe("Alert ID")
}), }),
body: UpdatePkiAlertV2Schema, body: UpdatePkiAlertV2Schema,
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid(), alert: z.object({
slug: z.string(), id: z.string().uuid(),
description: z.string().nullable(), name: z.string(),
eventType: z.nativeEnum(PkiAlertEventType), description: z.string().nullable(),
alertBefore: z.string(), eventType: z.nativeEnum(PkiAlertEventType),
filters: z.array(z.any()), alertBefore: z.string(),
enabled: z.boolean(), filters: z.array(PkiFilterRuleSchema),
channels: z.array( enabled: z.boolean(),
z.object({ projectId: z.string().uuid(),
id: z.string().uuid(), channels: z.array(
channelType: z.string(), z.object({
config: z.record(z.any()), id: z.string().uuid(),
enabled: z.boolean(), channelType: z.nativeEnum(PkiAlertChannelType),
createdAt: z.date(), config: z.record(z.any()),
updatedAt: z.date() enabled: z.boolean(),
}) createdAt: z.date(),
), updatedAt: z.date()
createdAt: z.date(), })
updatedAt: z.date() ),
createdAt: z.date(),
updatedAt: z.date()
})
}) })
} }
}, },
@@ -254,14 +264,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
type: EventType.UPDATE_PKI_ALERT, type: EventType.UPDATE_PKI_ALERT,
metadata: { metadata: {
pkiAlertId: alert.id, pkiAlertId: alert.id,
name: alert.slug, name: alert.name,
eventType: alert.eventType, eventType: alert.eventType,
alertBefore: alert.alertBefore alertBefore: alert.alertBefore
} }
} }
}); });
return alert; return { alert };
} }
}); });
@@ -274,31 +284,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Delete a PKI alert", description: "Delete a PKI alert",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
params: z.object({ params: z.object({
alertId: z.string().uuid().describe("Alert ID") alertId: z.string().uuid().describe("Alert ID")
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid(), alert: z.object({
slug: z.string(), id: z.string().uuid(),
description: z.string().nullable(), name: z.string(),
eventType: z.nativeEnum(PkiAlertEventType), description: z.string().nullable(),
alertBefore: z.string(), eventType: z.nativeEnum(PkiAlertEventType),
filters: z.array(z.any()), alertBefore: z.string(),
enabled: z.boolean(), filters: z.array(PkiFilterRuleSchema),
channels: z.array( enabled: z.boolean(),
z.object({ projectId: z.string().uuid(),
id: z.string().uuid(), channels: z.array(
channelType: z.string(), z.object({
config: z.record(z.any()), id: z.string().uuid(),
enabled: z.boolean(), channelType: z.nativeEnum(PkiAlertChannelType),
createdAt: z.date(), config: z.record(z.any()),
updatedAt: z.date() enabled: z.boolean(),
}) createdAt: z.date(),
), updatedAt: z.date()
createdAt: z.date(), })
updatedAt: z.date() ),
createdAt: z.date(),
updatedAt: z.date()
})
}) })
} }
}, },
@@ -322,7 +335,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
} }
}); });
return alert; return { alert };
} }
}); });
@@ -335,7 +348,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "List certificates that match an alert's filter rules", description: "List certificates that match an alert's filter rules",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
params: z.object({ params: z.object({
alertId: z.string().uuid().describe("Alert ID") alertId: z.string().uuid().describe("Alert ID")
}), }),
@@ -358,9 +371,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
status: z.string() status: z.string()
}) })
), ),
total: z.number(), total: z.number()
limit: z.number(),
offset: z.number()
}) })
} }
}, },
@@ -382,12 +393,12 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/preview/certificates", url: "/preview/certificates",
config: { config: {
rateLimit: readLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Preview certificates that would match the given filter rules", description: "Preview certificates that would match the given filter rules",
tags: ["PKI Alerts"], tags: [ApiDocsTags.PkiAlerting],
body: z.object({ body: z.object({
projectId: z.string().uuid().describe("Project ID"), projectId: z.string().uuid().describe("Project ID"),
filters: z.array(PkiFilterRuleSchema), filters: z.array(PkiFilterRuleSchema),
@@ -413,9 +424,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
status: z.string() status: z.string()
}) })
), ),
total: z.number(), total: z.number()
limit: z.number(),
offset: z.number()
}) })
} }
}, },
@@ -12,7 +12,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
alertId: string, alertId: string,
certificateIds: string[], certificateIds: string[],
options?: { options?: {
notificationSent?: boolean; hasNotificationSent?: boolean;
notificationError?: string; notificationError?: string;
} }
): Promise<TPkiAlertHistory> => { ): Promise<TPkiAlertHistory> => {
@@ -21,7 +21,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
const historyRecords = await tx(TableName.PkiAlertHistory) const historyRecords = await tx(TableName.PkiAlertHistory)
.insert({ .insert({
alertId, alertId,
notificationSent: options?.notificationSent || false, hasNotificationSent: options?.hasNotificationSent || false,
notificationError: options?.notificationError notificationError: options?.notificationError
}) })
.returning("*"); .returning("*");
@@ -91,7 +91,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
.from(`${TableName.PkiAlertHistory} as hist`) .from(`${TableName.PkiAlertHistory} as hist`)
.join(`${TableName.PkiAlertHistoryCertificate} as cert`, "hist.id", "cert.alertHistoryId") .join(`${TableName.PkiAlertHistoryCertificate} as cert`, "hist.id", "cert.alertHistoryId")
.where("hist.alertId", alertId) .where("hist.alertId", alertId)
.where("hist.notificationSent", true) .where("hist.hasNotificationSent", true)
.where("hist.triggeredAt", ">=", cutoffDate) .where("hist.triggeredAt", ">=", cutoffDate)
.whereIn("cert.certificateId", certificateIds)) as Array<{ certificateId: string }>; .whereIn("cert.certificateId", certificateIds)) as Array<{ certificateId: string }>;
@@ -102,7 +102,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
} }
}; };
const findByProjectId = async ( const findByProjectIdWithCount = async (
projectId: string, projectId: string,
filters?: { filters?: {
search?: string; search?: string;
@@ -112,15 +112,32 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
offset?: number; offset?: number;
}, },
tx?: Knex tx?: Knex
): Promise<TAlertWithChannels[]> => { ): Promise<{ alerts: TAlertWithChannels[]; total: number }> => {
try { try {
let countQuery = (tx || db.replicaNode())
.count("* as count")
.from(TableName.PkiAlertsV2)
.where(`${TableName.PkiAlertsV2}.projectId`, projectId);
if (filters?.search) {
countQuery = countQuery.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
}
if (filters?.eventType) {
countQuery = countQuery.where(`${TableName.PkiAlertsV2}.eventType`, filters.eventType);
}
if (filters?.enabled !== undefined) {
countQuery = countQuery.where(`${TableName.PkiAlertsV2}.enabled`, filters.enabled);
}
let alertQuery = (tx || db.replicaNode()) let alertQuery = (tx || db.replicaNode())
.select(selectAllTableCols(TableName.PkiAlertsV2)) .select(selectAllTableCols(TableName.PkiAlertsV2))
.from(TableName.PkiAlertsV2) .from(TableName.PkiAlertsV2)
.where(`${TableName.PkiAlertsV2}.projectId`, projectId); .where(`${TableName.PkiAlertsV2}.projectId`, projectId);
if (filters?.search) { if (filters?.search) {
alertQuery = alertQuery.whereILike(`${TableName.PkiAlertsV2}.slug`, `%${sanitizeLikeInput(filters.search)}%`); alertQuery = alertQuery.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
} }
if (filters?.eventType) { if (filters?.eventType) {
@@ -141,13 +158,11 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
alertQuery = alertQuery.offset(filters.offset); alertQuery = alertQuery.offset(filters.offset);
} }
const alerts = (await alertQuery) as TPkiAlertsV2[]; const [countResult, alerts] = await Promise.all([countQuery, alertQuery]);
if (alerts.length === 0) { const total = parseInt((countResult[0] as { count: string }).count, 10);
return [];
}
const alertIds = alerts.map((alert) => alert.id); const alertIds = (alerts as TPkiAlertsV2[]).map((alert) => alert.id);
const channels = (await (tx || db.replicaNode()) const channels = (await (tx || db.replicaNode())
.select(selectAllTableCols(TableName.PkiAlertChannels)) .select(selectAllTableCols(TableName.PkiAlertChannels))
.from(TableName.PkiAlertChannels) .from(TableName.PkiAlertChannels)
@@ -164,17 +179,32 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
{} as Record<string, TChannelResult[]> {} as Record<string, TChannelResult[]>
); );
const result: TAlertWithChannels[] = alerts.map((alert) => ({ const alertsWithChannels: TAlertWithChannels[] = (alerts as TPkiAlertsV2[]).map((alert) => ({
...alert, ...alert,
channels: channelsByAlertId[alert.id] || [] channels: channelsByAlertId[alert.id] || []
})); }));
return result; return { alerts: alertsWithChannels, total };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindByProjectId" }); throw new DatabaseError({ error, name: "FindByProjectIdWithCount" });
} }
}; };
const findByProjectId = async (
projectId: string,
filters?: {
search?: string;
eventType?: string;
enabled?: boolean;
limit?: number;
offset?: number;
},
tx?: Knex
): Promise<TAlertWithChannels[]> => {
const result = await findByProjectIdWithCount(projectId, filters, tx);
return result.alerts;
};
const countByProjectId = async ( const countByProjectId = async (
projectId: string, projectId: string,
filters?: { filters?: {
@@ -191,7 +221,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
.where(`${TableName.PkiAlertsV2}.projectId`, projectId); .where(`${TableName.PkiAlertsV2}.projectId`, projectId);
if (filters?.search) { if (filters?.search) {
query = query.whereILike(`${TableName.PkiAlertsV2}.slug`, `%${sanitizeLikeInput(filters.search)}%`); query = query.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
} }
if (filters?.eventType) { if (filters?.eventType) {
@@ -257,7 +287,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
let caCountQuery = (tx || db.replicaNode()) let caCountQuery = (tx || db.replicaNode())
.count("* as count") .count("* as count")
.from(TableName.CertificateAuthority) .from(TableName.CertificateAuthority)
.leftJoin( .innerJoin(
`${TableName.InternalCertificateAuthority} as ica`, `${TableName.InternalCertificateAuthority} as ica`,
`${TableName.CertificateAuthority}.id`, `${TableName.CertificateAuthority}.id`,
`ica.caId` `ica.caId`
@@ -268,16 +298,16 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.alertBefore) { if (options?.alertBefore) {
if (options.showFutureMatches) { if (options.showFutureMatches) {
caCountQuery = caCountQuery caCountQuery = caCountQuery
.whereRaw(`ica."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`ica."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`ica."notAfter" > NOW()`); .whereRaw(`ica."notAfter" > NOW()`);
} else if (options.showCurrentMatches) { } else if (options.showCurrentMatches) {
caCountQuery = caCountQuery caCountQuery = caCountQuery
.whereRaw(`ica."notAfter" > NOW()`) .whereRaw(`ica."notAfter" > NOW()`)
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`); .whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
} else { } else {
caCountQuery = caCountQuery caCountQuery = caCountQuery
.whereRaw(`ica."notAfter" > NOW()`) .whereRaw(`ica."notAfter" > NOW()`)
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`); .whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
} }
} }
@@ -290,23 +320,23 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.showPreview) { if (options?.showPreview) {
certCountQuery = certCountQuery certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options?.alertBefore) { } else if (options?.alertBefore) {
if (options.showFutureMatches) { if (options.showFutureMatches) {
certCountQuery = certCountQuery certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options.showCurrentMatches) { } else if (options.showCurrentMatches) {
certCountQuery = certCountQuery certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} else { } else {
certCountQuery = certCountQuery certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} }
} }
@@ -322,7 +352,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
`${TableName.PkiAlertHistoryCertificate}.alertHistoryId` `${TableName.PkiAlertHistoryCertificate}.alertHistoryId`
) )
.where(`${TableName.PkiAlertHistory}.alertId`, options.alertId) .where(`${TableName.PkiAlertHistory}.alertId`, options.alertId)
.whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}"."id"`) .whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}".id`)
); );
} }
@@ -346,7 +376,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
]; ];
if (needsProfileJoin) { if (needsProfileJoin) {
selectColumns.push("profile.slug as profileName"); selectColumns.push("profile.name as profileName");
} }
let certificateQuery = (tx || db.replicaNode()).select(selectColumns).from(TableName.Certificate); let certificateQuery = (tx || db.replicaNode()).select(selectColumns).from(TableName.Certificate);
@@ -355,23 +385,23 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.showPreview) { if (options?.showPreview) {
certificateQuery = certificateQuery certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options?.alertBefore) { } else if (options?.alertBefore) {
if (options.showFutureMatches) { if (options.showFutureMatches) {
certificateQuery = certificateQuery certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options.showCurrentMatches) { } else if (options.showCurrentMatches) {
certificateQuery = certificateQuery certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} else { } else {
certificateQuery = certificateQuery certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`) .whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked"); .whereNot(`${TableName.Certificate}.status`, "revoked");
} }
} }
@@ -387,7 +417,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
`${TableName.PkiAlertHistoryCertificate}.alertHistoryId` `${TableName.PkiAlertHistoryCertificate}.alertHistoryId`
) )
.where(`${TableName.PkiAlertHistory}.alertId`, options.alertId) .where(`${TableName.PkiAlertHistory}.alertId`, options.alertId)
.whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}"."id"`) .whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}".id`)
); );
} }
@@ -447,7 +477,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
`ica.notAfter` `ica.notAfter`
) )
.from(TableName.CertificateAuthority) .from(TableName.CertificateAuthority)
.leftJoin( .innerJoin(
`${TableName.InternalCertificateAuthority} as ica`, `${TableName.InternalCertificateAuthority} as ica`,
`${TableName.CertificateAuthority}.id`, `${TableName.CertificateAuthority}.id`,
`ica.caId` `ica.caId`
@@ -458,12 +488,12 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.alertBefore) { if (options?.alertBefore) {
if (options.showFutureMatches) { if (options.showFutureMatches) {
caQuery = caQuery caQuery = caQuery
.whereRaw(`ica."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`) .whereRaw(`ica."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`ica."notAfter" > NOW()`); .whereRaw(`ica."notAfter" > NOW()`);
} else { } else {
caQuery = caQuery caQuery = caQuery
.whereRaw(`ica."notAfter" > NOW()`) .whereRaw(`ica."notAfter" > NOW()`)
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`); .whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
} }
} }
@@ -518,6 +548,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
findById, findById,
findByIdWithChannels, findByIdWithChannels,
findByProjectId, findByProjectId,
findByProjectIdWithCount,
countByProjectId, countByProjectId,
getDistinctProjectIds, getDistinctProjectIds,
findMatchingCertificates findMatchingCertificates
@@ -7,6 +7,13 @@ import { logger } from "@app/lib/logger";
import { PkiFilterField, PkiFilterOperator, TPkiFilterRule } from "./pki-alert-v2-types"; import { PkiFilterField, PkiFilterOperator, TPkiFilterRule } from "./pki-alert-v2-types";
export const sanitizeLikeInput = (input: string): string => { export const sanitizeLikeInput = (input: string): string => {
const allowedCharsRegex = new RE2("^[a-zA-Z0-9\\s\\-_\\.@\\*]+$");
if (!allowedCharsRegex.test(input)) {
throw new Error(
"Invalid characters in input. Only alphanumeric characters, spaces, hyphens, underscores, dots, @ and * are allowed."
);
}
const backslashRegex = new RE2("\\\\", "g"); const backslashRegex = new RE2("\\\\", "g");
const percentRegex = new RE2("%", "g"); const percentRegex = new RE2("%", "g");
const underscoreRegex = new RE2("_", "g"); const underscoreRegex = new RE2("_", "g");
@@ -169,25 +176,32 @@ const applySanFilter = (query: Knex.QueryBuilder, filter: TPkiFilterRule): Knex.
if (Array.isArray(value)) { if (Array.isArray(value)) {
return query.where((builder) => { return query.where((builder) => {
value.forEach((v, index) => { value.forEach((v, index) => {
const condition = `${columnName}::text ILIKE ?`; const sanitizedValue = `%"${String(v)}"%`;
if (index === 0) { if (index === 0) {
void builder.whereRaw(condition, [`%"${String(v)}"%`]); void builder.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
} else { } else {
void builder.orWhereRaw(condition, [`%"${String(v)}"%`]); void builder.orWhereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
} }
}); });
}); });
} }
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%"${String(value)}"%`]); {
const sanitizedValue = `%"${String(value)}"%`;
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
}
case PkiFilterOperator.CONTAINS: case PkiFilterOperator.CONTAINS:
return applySanFilter(query, { ...filter, operator: PkiFilterOperator.MATCHES }); return applySanFilter(query, { ...filter, operator: PkiFilterOperator.MATCHES });
case PkiFilterOperator.STARTS_WITH: case PkiFilterOperator.STARTS_WITH: {
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%"${String(value)}%`]); const startsWithValue = `%"${String(value)}%`;
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, startsWithValue]);
}
case PkiFilterOperator.ENDS_WITH: case PkiFilterOperator.ENDS_WITH: {
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%${String(value)}"%`]); const endsWithValue = `%${String(value)}"%`;
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, endsWithValue]);
}
default: default:
logger.warn(`Unsupported operator for SAN: ${String(filter.operator)}`); logger.warn(`Unsupported operator for SAN: ${String(filter.operator)}`);
@@ -244,7 +258,7 @@ export const applyCaFilters = (
filters: TPkiFilterRule[], filters: TPkiFilterRule[],
projectId: string projectId: string
): Knex.QueryBuilder => { ): Knex.QueryBuilder => {
let filteredQuery = query.where(`${TableName.CertificateAuthority}.projectId`, projectId); let filteredQuery = query.where(`${TableName.CertificateAuthority}.projectId`, projectId).whereNotNull("ica.caId"); // Only include CAs that have internal CA data
filters.forEach((filter) => { filters.forEach((filter) => {
switch (filter.field) { switch (filter.field) {
@@ -63,7 +63,7 @@ export const pkiAlertV2QueueServiceFactory = ({
const evaluateAlert = async ( const evaluateAlert = async (
alert: { alert: {
id: string; id: string;
slug: string; name: string;
eventType: string; eventType: string;
alertBefore: string; alertBefore: string;
filters: TPkiFilterRule[]; filters: TPkiFilterRule[];
@@ -135,7 +135,7 @@ export const pkiAlertV2QueueServiceFactory = ({
for (const alert of alerts) { for (const alert of alerts) {
const typedAlert = alert as { const typedAlert = alert as {
id: string; id: string;
slug: string; name: string;
eventType: string; eventType: string;
alertBefore: string; alertBefore: string;
filters: TPkiFilterRule[]; filters: TPkiFilterRule[];
@@ -147,13 +147,13 @@ export const pkiAlertV2QueueServiceFactory = ({
await pkiAlertV2Service.sendAlertNotifications(typedAlert.id, certificateIds); await pkiAlertV2Service.sendAlertNotifications(typedAlert.id, certificateIds);
notificationsSent += 1; notificationsSent += 1;
logger.info( logger.info(
`Sent notification for alert ${typedAlert.id} (${typedAlert.slug}) with ${certificateIds.length} certificates` `Sent notification for alert ${typedAlert.id} (${typedAlert.name}) with ${certificateIds.length} certificates`
); );
} }
alertsProcessed += 1; alertsProcessed += 1;
} catch (error) { } catch (error) {
logger.error(error, `Failed to process alert ${typedAlert.id} (${typedAlert.slug})`); logger.error(error, `Failed to process alert ${typedAlert.id} (${typedAlert.name})`);
} }
} }
@@ -39,8 +39,10 @@ type TPkiAlertV2ServiceFactoryDep = {
| "updateById" | "updateById"
| "deleteById" | "deleteById"
| "findByProjectId" | "findByProjectId"
| "findByProjectIdWithCount"
| "countByProjectId" | "countByProjectId"
| "findMatchingCertificates" | "findMatchingCertificates"
| "transaction"
>; >;
pkiAlertChannelDAL: Pick<TPkiAlertChannelDALFactory, "create" | "findByAlertId" | "deleteByAlertId" | "insertMany">; pkiAlertChannelDAL: Pick<TPkiAlertChannelDALFactory, "create" | "findByAlertId" | "deleteByAlertId" | "insertMany">;
pkiAlertHistoryDAL: Pick<TPkiAlertHistoryDALFactory, "createWithCertificates" | "findByAlertId">; pkiAlertHistoryDAL: Pick<TPkiAlertHistoryDALFactory, "createWithCertificates" | "findByAlertId">;
@@ -59,7 +61,7 @@ export const pkiAlertV2ServiceFactory = ({
}: TPkiAlertV2ServiceFactoryDep) => { }: TPkiAlertV2ServiceFactoryDep) => {
type TAlertWithChannels = { type TAlertWithChannels = {
id: string; id: string;
slug: string; name: string;
description: string; description: string;
eventType: string; eventType: string;
alertBefore: string; alertBefore: string;
@@ -81,7 +83,7 @@ export const pkiAlertV2ServiceFactory = ({
const formatAlertResponse = (alert: TAlertWithChannels): TAlertV2Response => { const formatAlertResponse = (alert: TAlertWithChannels): TAlertV2Response => {
return { return {
id: alert.id, id: alert.id,
slug: alert.slug, name: alert.name,
description: alert.description, description: alert.description,
eventType: alert.eventType as PkiAlertEventType, eventType: alert.eventType as PkiAlertEventType,
alertBefore: alert.alertBefore, alertBefore: alert.alertBefore,
@@ -103,7 +105,7 @@ export const pkiAlertV2ServiceFactory = ({
const createAlert = async ({ const createAlert = async ({
projectId, projectId,
slug, name,
description, description,
eventType, eventType,
alertBefore, alertBefore,
@@ -132,31 +134,36 @@ export const pkiAlertV2ServiceFactory = ({
throw new BadRequestError({ message: "Invalid alertBefore format. Use format like '30d', '1w', '3m', '1y'" }); throw new BadRequestError({ message: "Invalid alertBefore format. Use format like '30d', '1w', '3m', '1y'" });
} }
const alert = await pkiAlertV2DAL.create({ return pkiAlertV2DAL.transaction(async (tx) => {
projectId, const alert = await pkiAlertV2DAL.create(
slug, {
description, projectId,
eventType, name,
alertBefore, description,
filters, eventType,
enabled alertBefore,
filters,
enabled
},
tx
);
const channelInserts = channels.map((channel) => ({
alertId: alert.id,
channelType: channel.channelType,
config: channel.config,
enabled: channel.enabled
}));
await pkiAlertChannelDAL.insertMany(channelInserts, tx);
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alert.id, tx);
if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve created alert" });
}
return formatAlertResponse(completeAlert as TAlertWithChannels);
}); });
const channelInserts = channels.map((channel) => ({
alertId: alert.id,
channelType: channel.channelType,
config: channel.config,
enabled: channel.enabled
}));
await pkiAlertChannelDAL.insertMany(channelInserts);
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alert.id);
if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve created alert" });
}
return formatAlertResponse(completeAlert as TAlertWithChannels);
}; };
const getAlertById = async ({ const getAlertById = async ({
@@ -208,10 +215,7 @@ export const pkiAlertV2ServiceFactory = ({
const filters = { search, eventType, enabled, limit, offset }; const filters = { search, eventType, enabled, limit, offset };
const [alerts, total] = await Promise.all([ const { alerts, total } = await pkiAlertV2DAL.findByProjectIdWithCount(projectId, filters);
pkiAlertV2DAL.findByProjectId(projectId, filters),
pkiAlertV2DAL.countByProjectId(projectId, { search, eventType, enabled })
]);
return { return {
alerts: alerts.map((alert) => formatAlertResponse(alert as TAlertWithChannels)), alerts: alerts.map((alert) => formatAlertResponse(alert as TAlertWithChannels)),
@@ -221,7 +225,7 @@ export const pkiAlertV2ServiceFactory = ({
const updateAlert = async ({ const updateAlert = async ({
alertId, alertId,
slug, name,
description, description,
eventType, eventType,
alertBefore, alertBefore,
@@ -256,41 +260,43 @@ export const pkiAlertV2ServiceFactory = ({
} }
const updateData: { const updateData: {
slug?: string; name?: string;
description?: string; description?: string;
eventType?: PkiAlertEventType; eventType?: PkiAlertEventType;
alertBefore?: string; alertBefore?: string;
filters?: TPkiFilterRule[]; filters?: TPkiFilterRule[];
enabled?: boolean; enabled?: boolean;
} = {}; } = {};
if (slug !== undefined) updateData.slug = slug; if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description; if (description !== undefined) updateData.description = description;
if (eventType !== undefined) updateData.eventType = eventType; if (eventType !== undefined) updateData.eventType = eventType;
if (alertBefore !== undefined) updateData.alertBefore = alertBefore; if (alertBefore !== undefined) updateData.alertBefore = alertBefore;
if (filters !== undefined) updateData.filters = filters; if (filters !== undefined) updateData.filters = filters;
if (enabled !== undefined) updateData.enabled = enabled; if (enabled !== undefined) updateData.enabled = enabled;
alert = await pkiAlertV2DAL.updateById(alertId, updateData); return pkiAlertV2DAL.transaction(async (tx) => {
alert = await pkiAlertV2DAL.updateById(alertId, updateData, tx);
if (channels) { if (channels) {
await pkiAlertChannelDAL.deleteByAlertId(alertId); await pkiAlertChannelDAL.deleteByAlertId(alertId, tx);
const channelInserts = channels.map((channel) => ({ const channelInserts = channels.map((channel) => ({
alertId, alertId,
channelType: channel.channelType, channelType: channel.channelType,
config: channel.config, config: channel.config,
enabled: channel.enabled enabled: channel.enabled
})); }));
await pkiAlertChannelDAL.insertMany(channelInserts); await pkiAlertChannelDAL.insertMany(channelInserts, tx);
} }
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alertId); const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alertId, tx);
if (!completeAlert) { if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve updated alert" }); throw new NotFoundError({ message: "Failed to retrieve updated alert" });
} }
return formatAlertResponse(completeAlert as TAlertWithChannels); return formatAlertResponse(completeAlert as TAlertWithChannels);
});
}; };
const deleteAlert = async ({ const deleteAlert = async ({
@@ -365,9 +371,7 @@ export const pkiAlertV2ServiceFactory = ({
return { return {
certificates: result.certificates, certificates: result.certificates,
total: result.total, total: result.total
limit,
offset
}; };
}; };
@@ -415,9 +419,7 @@ export const pkiAlertV2ServiceFactory = ({
return { return {
certificates: result.certificates, certificates: result.certificates,
total: result.total, total: result.total
limit,
offset
}; };
}; };
@@ -445,7 +447,7 @@ export const pkiAlertV2ServiceFactory = ({
if (matchingCertificates.length === 0) return; if (matchingCertificates.length === 0) return;
let notificationSent = false; let hasNotificationSent = false;
let notificationError: string | undefined; let notificationError: string | undefined;
try { try {
@@ -455,7 +457,7 @@ export const pkiAlertV2ServiceFactory = ({
); );
const alertBeforeDays = parseTimeToDays((alert as { alertBefore: string }).alertBefore); const alertBeforeDays = parseTimeToDays((alert as { alertBefore: string }).alertBefore);
const alertName = (alert as { slug: string }).slug; const alertName = (alert as { name: string }).name;
const emailPromises = emailChannels.map((channel) => { const emailPromises = emailChannels.map((channel) => {
const config = channel.config as TEmailChannelConfig; const config = channel.config as TEmailChannelConfig;
@@ -480,14 +482,14 @@ export const pkiAlertV2ServiceFactory = ({
await Promise.all(emailPromises); await Promise.all(emailPromises);
notificationSent = true; hasNotificationSent = true;
} catch (error) { } catch (error) {
notificationError = error instanceof Error ? error.message : "Unknown error occurred"; notificationError = error instanceof Error ? error.message : "Unknown error occurred";
logger.error(error, `Failed to send notifications for alert ${alertId}`); logger.error(error, `Failed to send notifications for alert ${alertId}`);
} }
await pkiAlertHistoryDAL.createWithCertificates(alertId, certificateIds, { await pkiAlertHistoryDAL.createWithCertificates(alertId, certificateIds, {
notificationSent, hasNotificationSent,
notificationError notificationError
}); });
}; };
@@ -3,12 +3,18 @@ import { z } from "zod";
import { TGenericPermission } from "@app/lib/types"; import { TGenericPermission } from "@app/lib/types";
const createSecureSlugValidator = () => { const createSecureNameValidator = () => {
const slugRegex = new RE2("^[a-z0-9]+(?:-[a-z0-9]+)*$"); // Validates name format: lowercase alphanumeric characters with optional hyphens
return (value: string) => slugRegex.test(value); // Pattern: starts and ends with alphanumeric, allows hyphens between segments
// Examples: "my-alert", "alert1", "test-alert-2"
const nameRegex = new RE2("^[a-z0-9]+(?:-[a-z0-9]+)*$");
return (value: string) => nameRegex.test(value);
}; };
export const createSecureAlertBeforeValidator = () => { export const createSecureAlertBeforeValidator = () => {
// Validates alertBefore duration format: number followed by time unit
// Pattern: one or more digits followed by d(days), w(weeks), m(months), or y(years)
// Examples: "30d", "2w", "6m", "1y"
const alertBeforeRegex = new RE2("^\\d+[dwmy]$"); const alertBeforeRegex = new RE2("^\\d+[dwmy]$");
return (value: string) => { return (value: string) => {
if (value.length > 32) return false; if (value.length > 32) return false;
@@ -98,11 +104,11 @@ export const CreateChannelSchema = z.object({
export type TCreateChannel = z.infer<typeof CreateChannelSchema>; export type TCreateChannel = z.infer<typeof CreateChannelSchema>;
export const CreatePkiAlertV2Schema = z.object({ export const CreatePkiAlertV2Schema = z.object({
slug: z name: z
.string() .string()
.min(1) .min(1)
.max(255) .max(255)
.refine(createSecureSlugValidator(), "Must be a valid slug (lowercase, numbers, hyphens only)"), .refine(createSecureNameValidator(), "Must be a valid name (lowercase, numbers, hyphens only)"),
description: z.string().max(1000).optional(), description: z.string().max(1000).optional(),
eventType: z.nativeEnum(PkiAlertEventType), eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string().refine(createSecureAlertBeforeValidator(), "Must be in format like '30d', '1w', '3m', '1y'"), alertBefore: z.string().refine(createSecureAlertBeforeValidator(), "Must be in format like '30d', '1w', '3m', '1y'"),
@@ -169,7 +175,7 @@ export type TCertificatePreview = {
export type TAlertV2Response = { export type TAlertV2Response = {
id: string; id: string;
slug: string; name: string;
description: string | null; description: string | null;
eventType: PkiAlertEventType; eventType: PkiAlertEventType;
alertBefore: string; alertBefore: string;
@@ -196,6 +202,4 @@ export type TListAlertsV2Response = {
export type TListMatchingCertificatesResponse = { export type TListMatchingCertificatesResponse = {
certificates: TCertificatePreview[]; certificates: TCertificatePreview[];
total: number; total: number;
limit: number;
offset: number;
}; };
@@ -10,8 +10,11 @@ export const useCreatePkiAlertV2 = () => {
return useMutation<TPkiAlertV2, unknown, TCreatePkiAlertV2>({ return useMutation<TPkiAlertV2, unknown, TCreatePkiAlertV2>({
mutationFn: async (data) => { mutationFn: async (data) => {
const { data: response } = await apiRequest.post<TPkiAlertV2>("/api/v2/pki/alerts", data); const { data: response } = await apiRequest.post<{ alert: TPkiAlertV2 }>(
return response; "/api/v2/pki/alerts",
data
);
return response.alert;
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -26,11 +29,11 @@ export const useUpdatePkiAlertV2 = () => {
return useMutation<TPkiAlertV2, unknown, TUpdatePkiAlertV2>({ return useMutation<TPkiAlertV2, unknown, TUpdatePkiAlertV2>({
mutationFn: async ({ alertId, ...data }) => { mutationFn: async ({ alertId, ...data }) => {
const { data: response } = await apiRequest.patch<TPkiAlertV2>( const { data: response } = await apiRequest.patch<{ alert: TPkiAlertV2 }>(
`/api/v2/pki/alerts/${alertId}`, `/api/v2/pki/alerts/${alertId}`,
data data
); );
return response; return response.alert;
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -48,8 +51,10 @@ export const useDeletePkiAlertV2 = () => {
return useMutation<TPkiAlertV2, unknown, TDeletePkiAlertV2>({ return useMutation<TPkiAlertV2, unknown, TDeletePkiAlertV2>({
mutationFn: async ({ alertId }) => { mutationFn: async ({ alertId }) => {
const { data } = await apiRequest.delete<TPkiAlertV2>(`/api/v2/pki/alerts/${alertId}`); const { data } = await apiRequest.delete<{ alert: TPkiAlertV2 }>(
return data; `/api/v2/pki/alerts/${alertId}`
);
return data.alert;
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -31,8 +31,8 @@ const fetchPkiAlertsV2 = async (params: TGetPkiAlertsV2): Promise<TGetPkiAlertsV
}; };
const fetchPkiAlertV2ById = async ({ alertId }: TGetPkiAlertV2ById): Promise<TPkiAlertV2> => { const fetchPkiAlertV2ById = async ({ alertId }: TGetPkiAlertV2ById): Promise<TPkiAlertV2> => {
const { data } = await apiRequest.get<TPkiAlertV2>(`/api/v2/pki/alerts/${alertId}`); const { data } = await apiRequest.get<{ alert: TPkiAlertV2 }>(`/api/v2/pki/alerts/${alertId}`);
return data; return data.alert;
}; };
const fetchPkiAlertV2MatchingCertificates = async ( const fetchPkiAlertV2MatchingCertificates = async (
+5 -5
View File
@@ -51,7 +51,7 @@ export interface TPkiAlertChannelV2 {
export interface TPkiAlertV2 { export interface TPkiAlertV2 {
id: string; id: string;
projectId: string; projectId: string;
slug: string; name: string;
description?: string; description?: string;
eventType: PkiAlertEventTypeV2; eventType: PkiAlertEventTypeV2;
alertBefore?: string; alertBefore?: string;
@@ -94,7 +94,7 @@ export interface TGetPkiAlertV2ById {
export interface TCreatePkiAlertV2 { export interface TCreatePkiAlertV2 {
projectId: string; projectId: string;
slug: string; name: string;
description?: string; description?: string;
eventType: PkiAlertEventTypeV2; eventType: PkiAlertEventTypeV2;
alertBefore?: string; alertBefore?: string;
@@ -105,7 +105,7 @@ export interface TCreatePkiAlertV2 {
export interface TUpdatePkiAlertV2 { export interface TUpdatePkiAlertV2 {
alertId: string; alertId: string;
slug?: string; name?: string;
description?: string; description?: string;
eventType?: PkiAlertEventTypeV2; eventType?: PkiAlertEventTypeV2;
alertBefore?: string; alertBefore?: string;
@@ -172,11 +172,11 @@ export const pkiAlertChannelV2Schema = z.object({
export const createPkiAlertV2Schema = z.object({ export const createPkiAlertV2Schema = z.object({
projectId: z.string().uuid(), projectId: z.string().uuid(),
slug: z name: z
.string() .string()
.min(1) .min(1)
.max(255) .max(255)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be a valid slug (lowercase, numbers, hyphens only)"), .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be a valid name (lowercase, numbers, hyphens only)"),
description: z.string().max(1000).optional(), description: z.string().max(1000).optional(),
eventType: z.nativeEnum(PkiAlertEventTypeV2), eventType: z.nativeEnum(PkiAlertEventTypeV2),
alertBefore: z alertBefore: z
@@ -45,7 +45,7 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
const [deleteModal, setDeleteModal] = useState<{ const [deleteModal, setDeleteModal] = useState<{
isOpen: boolean; isOpen: boolean;
alertId?: string; alertId?: string;
slug?: string; name?: string;
}>({ }>({
isOpen: false isOpen: false
}); });
@@ -113,7 +113,7 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
setDeleteModal({ setDeleteModal({
isOpen: true, isOpen: true,
alertId: alert.id, alertId: alert.id,
slug: alert.slug name: alert.name
}) })
} }
/> />
@@ -199,8 +199,8 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
<DeleteActionModal <DeleteActionModal
isOpen={deleteModal.isOpen} isOpen={deleteModal.isOpen}
deleteKey="delete" deleteKey="delete"
title={`Delete PKI Alert "${deleteModal.slug}"`} title={`Delete PKI Alert "${deleteModal.name}"`}
onChange={(isOpen) => setDeleteModal({ isOpen, alertId: undefined, slug: undefined })} onChange={(isOpen) => setDeleteModal({ isOpen, alertId: undefined, name: undefined })}
onDeleteApproved={handleDeleteAlert} onDeleteApproved={handleDeleteAlert}
/> />
</div> </div>
@@ -162,7 +162,7 @@ export const CreatePkiAlertV2FormSteps = () => {
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Alert Type" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Alert Type" isError={Boolean(error)} errorText={error?.message}>
<Select <Select
defaultValue={field.value} value={field.value}
onValueChange={(value) => field.onChange(value)} onValueChange={(value) => field.onChange(value)}
className="w-full" className="w-full"
> >
@@ -185,9 +185,9 @@ export const CreatePkiAlertV2FormSteps = () => {
<Controller <Controller
control={control} control={control}
name="slug" name="name"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Alert Slug" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Alert Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="e.g., prod-cert-expiring-soon" /> <Input {...field} placeholder="e.g., prod-cert-expiring-soon" />
</FormControl> </FormControl>
)} )}
@@ -490,12 +490,12 @@ export const CreatePkiAlertV2FormSteps = () => {
<div className="space-y-4"> <div className="space-y-4">
<FormControl label="Email Recipients"> <FormControl label="Email Recipients">
<TextArea <TextArea
defaultValue={ value={
Array.isArray((watchedChannels?.[0]?.config as any)?.recipients) Array.isArray((watchedChannels?.[0]?.config as any)?.recipients)
? (watchedChannels[0].config as any).recipients.join(", ") ? (watchedChannels[0].config as any).recipients.join(", ")
: "" : ""
} }
onBlur={(e) => { onChange={(e) => {
const emailList = e.target.value const emailList = e.target.value
.split(",") .split(",")
.map((email) => email.trim()) .map((email) => email.trim())
@@ -522,7 +522,7 @@ export const CreatePkiAlertV2FormSteps = () => {
<span className="text-sm text-mineshaft-300">Basic Information</span> <span className="text-sm text-mineshaft-300">Basic Information</span>
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2"> <div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Slug">{watch("slug") || "Not specified"}</GenericFieldLabel> <GenericFieldLabel label="Name">{watch("name") || "Not specified"}</GenericFieldLabel>
<GenericFieldLabel label="Event Type"> <GenericFieldLabel label="Event Type">
{formatEventType(watchedEventType)} {formatEventType(watchedEventType)}
</GenericFieldLabel> </GenericFieldLabel>
@@ -37,7 +37,7 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TFormData)[] }[] =
{ {
name: "Details", name: "Details",
key: "basicInfo", key: "basicInfo",
fields: ["slug", "description", "alertBefore"] fields: ["name", "description", "alertBefore"]
}, },
{ name: "Filters", key: "filterRules", fields: ["filters"] }, { name: "Filters", key: "filterRules", fields: ["filters"] },
{ name: "Preview", key: "preview", fields: [] }, { name: "Preview", key: "preview", fields: [] },
@@ -61,7 +61,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
resolver: zodResolver(isEditing ? updatePkiAlertV2Schema : createPkiAlertV2Schema), resolver: zodResolver(isEditing ? updatePkiAlertV2Schema : createPkiAlertV2Schema),
defaultValues: { defaultValues: {
projectId: currentProject?.id || "", projectId: currentProject?.id || "",
slug: "", name: "",
description: "", description: "",
eventType: PkiAlertEventTypeV2.EXPIRATION, eventType: PkiAlertEventTypeV2.EXPIRATION,
alertBefore: "30d", alertBefore: "30d",
@@ -93,7 +93,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
if (editingAlert && isEditing) { if (editingAlert && isEditing) {
reset({ reset({
projectId: currentProject?.id || "", projectId: currentProject?.id || "",
slug: editingAlert.slug, name: editingAlert.name,
description: editingAlert.description || "", description: editingAlert.description || "",
eventType: editingAlert.eventType, eventType: editingAlert.eventType,
alertBefore: editingAlert.alertBefore || "30d", alertBefore: editingAlert.alertBefore || "30d",
@@ -121,7 +121,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
} else if (!isEditing) { } else if (!isEditing) {
reset({ reset({
projectId: currentProject?.id || "", projectId: currentProject?.id || "",
slug: "", name: "",
description: "", description: "",
eventType: PkiAlertEventTypeV2.EXPIRATION, eventType: PkiAlertEventTypeV2.EXPIRATION,
alertBefore: "30d", alertBefore: "30d",
@@ -87,7 +87,7 @@ export const PkiAlertV2Row = ({ alert, onView, onEdit, onDelete }: Props) => {
<Tr> <Tr>
<Td> <Td>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="font-medium text-gray-200">{alert.slug}</div> <div className="font-medium text-gray-200">{alert.name}</div>
{alert.description && ( {alert.description && (
<Tooltip content={alert.description}> <Tooltip content={alert.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" /> <FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
@@ -60,7 +60,7 @@ export const UpdatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props)
useEffect(() => { useEffect(() => {
if (alert) { if (alert) {
reset({ reset({
slug: alert.slug, name: alert.name,
description: alert.description || "", description: alert.description || "",
eventType: alert.eventType, eventType: alert.eventType,
alertBefore: alert.alertBefore || "", alertBefore: alert.alertBefore || "",
@@ -190,7 +190,7 @@ export const UpdatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props)
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Controller <Controller
control={control} control={control}
name="slug" name="name"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Alert Name" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Alert Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="certificate-expiration-alert" /> <Input {...field} placeholder="certificate-expiration-alert" />
@@ -136,7 +136,7 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
<ModalContent title="Certificate Alert Details" className="max-w-6xl"> <ModalContent title="Certificate Alert Details" className="max-w-6xl">
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-200">{alert.slug}</h2> <h2 className="text-xl font-semibold text-gray-200">{alert.name}</h2>
<Badge variant={alert.enabled ? "success" : "neutral"}> <Badge variant={alert.enabled ? "success" : "neutral"}>
{alert.enabled ? "Enabled" : "Disabled"} {alert.enabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
@@ -176,11 +176,11 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-lg font-medium text-gray-200">Filter Rules</h3> <h3 className="text-lg font-medium text-gray-200">Filter Rules</h3>
{alert.filters.length === 0 ? ( {(alert.filters || []).length === 0 ? (
<p className="text-gray-400">No filter rules configured</p> <p className="text-gray-400">No filter rules configured</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{alert.filters.map((filter) => { {(alert.filters || []).map((filter) => {
const formatFilterText = () => { const formatFilterText = () => {
const field = filter.field.replace(/_/g, " "); const field = filter.field.replace(/_/g, " ");
const operator = filter.operator.replace(/_/g, " "); const operator = filter.operator.replace(/_/g, " ");
@@ -207,14 +207,14 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-lg font-medium text-gray-200">Notification Recipients</h3> <h3 className="text-lg font-medium text-gray-200">Notification Recipients</h3>
{alert.channels.some( {(alert.channels || []).some(
(channel) => (channel) =>
channel.channelType === "email" && (channel.config as any)?.recipients channel.channelType === "email" && (channel.config as any)?.recipients
) ? ( ) ? (
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{alert.channels {(alert.channels || [])
.filter( .filter(
(channel) => (channel) =>
channel.channelType === "email" && channel.channelType === "email" &&