mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
Address PR comments
This commit is contained in:
@@ -6,7 +6,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.PkiAlertsV2))) {
|
||||
await knex.schema.createTable(TableName.PkiAlertsV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("slug").notNullable();
|
||||
t.string("name").notNullable();
|
||||
t.text("description").nullable();
|
||||
t.string("eventType").notNullable();
|
||||
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.index("projectId");
|
||||
t.index("eventType");
|
||||
t.index("enabled");
|
||||
t.unique(["slug", "projectId"]);
|
||||
t.unique(["name", "projectId"]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("alertId").notNullable();
|
||||
t.timestamp("triggeredAt").defaultTo(knex.fn.now());
|
||||
t.boolean("notificationSent").defaultTo(false);
|
||||
t.boolean("hasNotificationSent").defaultTo(false);
|
||||
t.text("notificationError").nullable();
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const PkiAlertHistorySchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
alertId: z.string().uuid(),
|
||||
triggeredAt: z.date().nullable().optional(),
|
||||
notificationSent: z.boolean().default(false).nullable().optional(),
|
||||
hasNotificationSent: z.boolean().default(false).nullable().optional(),
|
||||
notificationError: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const PkiAlertsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
eventType: z.string(),
|
||||
alertBefore: z.string().nullable().optional(),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
CreatePkiAlertV2Schema,
|
||||
createSecureAlertBeforeValidator,
|
||||
PkiAlertChannelType,
|
||||
PkiAlertEventType,
|
||||
PkiFilterRuleSchema,
|
||||
UpdatePkiAlertV2Schema
|
||||
@@ -22,32 +24,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create a new PKI alert",
|
||||
tags: ["PKI Alerts"],
|
||||
body: z.object({
|
||||
projectId: z.string().uuid().describe("Project ID"),
|
||||
...CreatePkiAlertV2Schema.shape
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
body: CreatePkiAlertV2Schema.extend({
|
||||
projectId: z.string().uuid().describe("Project ID")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(z.any()),
|
||||
enabled: z.boolean(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.string(),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
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,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id,
|
||||
name: alert.slug,
|
||||
name: alert.name,
|
||||
eventType: alert.eventType,
|
||||
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]),
|
||||
schema: {
|
||||
description: "List PKI alerts for a project",
|
||||
tags: ["PKI Alerts"],
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid(),
|
||||
search: z.string().optional(),
|
||||
@@ -101,16 +105,16 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
alerts: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(z.any()),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.string(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
@@ -147,31 +151,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get a PKI alert by ID",
|
||||
tags: ["PKI Alerts"],
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
params: z.object({
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(z.any()),
|
||||
enabled: z.boolean(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.string(),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
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]),
|
||||
schema: {
|
||||
description: "Update a PKI alert",
|
||||
tags: ["PKI Alerts"],
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
params: z.object({
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
body: UpdatePkiAlertV2Schema,
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(z.any()),
|
||||
enabled: z.boolean(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.string(),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
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,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id,
|
||||
name: alert.slug,
|
||||
name: alert.name,
|
||||
eventType: alert.eventType,
|
||||
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]),
|
||||
schema: {
|
||||
description: "Delete a PKI alert",
|
||||
tags: ["PKI Alerts"],
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
params: z.object({
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(z.any()),
|
||||
enabled: z.boolean(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.string(),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
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]),
|
||||
schema: {
|
||||
description: "List certificates that match an alert's filter rules",
|
||||
tags: ["PKI Alerts"],
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
params: z.object({
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
@@ -358,9 +371,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
status: z.string()
|
||||
})
|
||||
),
|
||||
total: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.number()
|
||||
total: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -382,12 +393,12 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/preview/certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Preview certificates that would match the given filter rules",
|
||||
tags: ["PKI Alerts"],
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
body: z.object({
|
||||
projectId: z.string().uuid().describe("Project ID"),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
@@ -413,9 +424,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
status: z.string()
|
||||
})
|
||||
),
|
||||
total: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.number()
|
||||
total: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
|
||||
alertId: string,
|
||||
certificateIds: string[],
|
||||
options?: {
|
||||
notificationSent?: boolean;
|
||||
hasNotificationSent?: boolean;
|
||||
notificationError?: string;
|
||||
}
|
||||
): Promise<TPkiAlertHistory> => {
|
||||
@@ -21,7 +21,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
|
||||
const historyRecords = await tx(TableName.PkiAlertHistory)
|
||||
.insert({
|
||||
alertId,
|
||||
notificationSent: options?.notificationSent || false,
|
||||
hasNotificationSent: options?.hasNotificationSent || false,
|
||||
notificationError: options?.notificationError
|
||||
})
|
||||
.returning("*");
|
||||
@@ -91,7 +91,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
|
||||
.from(`${TableName.PkiAlertHistory} as hist`)
|
||||
.join(`${TableName.PkiAlertHistoryCertificate} as cert`, "hist.id", "cert.alertHistoryId")
|
||||
.where("hist.alertId", alertId)
|
||||
.where("hist.notificationSent", true)
|
||||
.where("hist.hasNotificationSent", true)
|
||||
.where("hist.triggeredAt", ">=", cutoffDate)
|
||||
.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,
|
||||
filters?: {
|
||||
search?: string;
|
||||
@@ -112,15 +112,32 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
offset?: number;
|
||||
},
|
||||
tx?: Knex
|
||||
): Promise<TAlertWithChannels[]> => {
|
||||
): Promise<{ alerts: TAlertWithChannels[]; total: number }> => {
|
||||
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())
|
||||
.select(selectAllTableCols(TableName.PkiAlertsV2))
|
||||
.from(TableName.PkiAlertsV2)
|
||||
.where(`${TableName.PkiAlertsV2}.projectId`, projectId);
|
||||
|
||||
if (filters?.search) {
|
||||
alertQuery = alertQuery.whereILike(`${TableName.PkiAlertsV2}.slug`, `%${sanitizeLikeInput(filters.search)}%`);
|
||||
alertQuery = alertQuery.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
|
||||
}
|
||||
|
||||
if (filters?.eventType) {
|
||||
@@ -141,13 +158,11 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
alertQuery = alertQuery.offset(filters.offset);
|
||||
}
|
||||
|
||||
const alerts = (await alertQuery) as TPkiAlertsV2[];
|
||||
const [countResult, alerts] = await Promise.all([countQuery, alertQuery]);
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const total = parseInt((countResult[0] as { count: string }).count, 10);
|
||||
|
||||
const alertIds = alerts.map((alert) => alert.id);
|
||||
const alertIds = (alerts as TPkiAlertsV2[]).map((alert) => alert.id);
|
||||
const channels = (await (tx || db.replicaNode())
|
||||
.select(selectAllTableCols(TableName.PkiAlertChannels))
|
||||
.from(TableName.PkiAlertChannels)
|
||||
@@ -164,17 +179,32 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
{} as Record<string, TChannelResult[]>
|
||||
);
|
||||
|
||||
const result: TAlertWithChannels[] = alerts.map((alert) => ({
|
||||
const alertsWithChannels: TAlertWithChannels[] = (alerts as TPkiAlertsV2[]).map((alert) => ({
|
||||
...alert,
|
||||
channels: channelsByAlertId[alert.id] || []
|
||||
}));
|
||||
|
||||
return result;
|
||||
return { alerts: alertsWithChannels, total };
|
||||
} 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 (
|
||||
projectId: string,
|
||||
filters?: {
|
||||
@@ -191,7 +221,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
.where(`${TableName.PkiAlertsV2}.projectId`, projectId);
|
||||
|
||||
if (filters?.search) {
|
||||
query = query.whereILike(`${TableName.PkiAlertsV2}.slug`, `%${sanitizeLikeInput(filters.search)}%`);
|
||||
query = query.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
|
||||
}
|
||||
|
||||
if (filters?.eventType) {
|
||||
@@ -257,7 +287,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
let caCountQuery = (tx || db.replicaNode())
|
||||
.count("* as count")
|
||||
.from(TableName.CertificateAuthority)
|
||||
.leftJoin(
|
||||
.innerJoin(
|
||||
`${TableName.InternalCertificateAuthority} as ica`,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`ica.caId`
|
||||
@@ -268,16 +298,16 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
if (options?.alertBefore) {
|
||||
if (options.showFutureMatches) {
|
||||
caCountQuery = caCountQuery
|
||||
.whereRaw(`ica."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`ica."notAfter" > NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereRaw(`ica."notAfter" > NOW()`);
|
||||
} else if (options.showCurrentMatches) {
|
||||
caCountQuery = caCountQuery
|
||||
.whereRaw(`ica."notAfter" > NOW()`)
|
||||
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`);
|
||||
.whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
|
||||
} else {
|
||||
caCountQuery = caCountQuery
|
||||
.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) {
|
||||
certCountQuery = certCountQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
} else if (options?.alertBefore) {
|
||||
if (options.showFutureMatches) {
|
||||
certCountQuery = certCountQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
} else if (options.showCurrentMatches) {
|
||||
certCountQuery = certCountQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
} else {
|
||||
certCountQuery = certCountQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
}
|
||||
}
|
||||
@@ -322,7 +352,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
`${TableName.PkiAlertHistoryCertificate}.alertHistoryId`
|
||||
)
|
||||
.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) {
|
||||
selectColumns.push("profile.slug as profileName");
|
||||
selectColumns.push("profile.name as profileName");
|
||||
}
|
||||
|
||||
let certificateQuery = (tx || db.replicaNode()).select(selectColumns).from(TableName.Certificate);
|
||||
@@ -355,23 +385,23 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
if (options?.showPreview) {
|
||||
certificateQuery = certificateQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
} else if (options?.alertBefore) {
|
||||
if (options.showFutureMatches) {
|
||||
certificateQuery = certificateQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
} else if (options.showCurrentMatches) {
|
||||
certificateQuery = certificateQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
} else {
|
||||
certificateQuery = certificateQuery
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
|
||||
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
|
||||
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereNot(`${TableName.Certificate}.status`, "revoked");
|
||||
}
|
||||
}
|
||||
@@ -387,7 +417,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
`${TableName.PkiAlertHistoryCertificate}.alertHistoryId`
|
||||
)
|
||||
.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`
|
||||
)
|
||||
.from(TableName.CertificateAuthority)
|
||||
.leftJoin(
|
||||
.innerJoin(
|
||||
`${TableName.InternalCertificateAuthority} as ica`,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`ica.caId`
|
||||
@@ -458,12 +488,12 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
|
||||
if (options?.alertBefore) {
|
||||
if (options.showFutureMatches) {
|
||||
caQuery = caQuery
|
||||
.whereRaw(`ica."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
|
||||
.whereRaw(`ica."notAfter" > NOW() + ?::interval`, [options.alertBefore])
|
||||
.whereRaw(`ica."notAfter" > NOW()`);
|
||||
} else {
|
||||
caQuery = caQuery
|
||||
.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,
|
||||
findByIdWithChannels,
|
||||
findByProjectId,
|
||||
findByProjectIdWithCount,
|
||||
countByProjectId,
|
||||
getDistinctProjectIds,
|
||||
findMatchingCertificates
|
||||
|
||||
@@ -7,6 +7,13 @@ import { logger } from "@app/lib/logger";
|
||||
import { PkiFilterField, PkiFilterOperator, TPkiFilterRule } from "./pki-alert-v2-types";
|
||||
|
||||
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 percentRegex = new RE2("%", "g");
|
||||
const underscoreRegex = new RE2("_", "g");
|
||||
@@ -169,25 +176,32 @@ const applySanFilter = (query: Knex.QueryBuilder, filter: TPkiFilterRule): Knex.
|
||||
if (Array.isArray(value)) {
|
||||
return query.where((builder) => {
|
||||
value.forEach((v, index) => {
|
||||
const condition = `${columnName}::text ILIKE ?`;
|
||||
const sanitizedValue = `%"${String(v)}"%`;
|
||||
if (index === 0) {
|
||||
void builder.whereRaw(condition, [`%"${String(v)}"%`]);
|
||||
void builder.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
|
||||
} 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:
|
||||
return applySanFilter(query, { ...filter, operator: PkiFilterOperator.MATCHES });
|
||||
|
||||
case PkiFilterOperator.STARTS_WITH:
|
||||
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%"${String(value)}%`]);
|
||||
case PkiFilterOperator.STARTS_WITH: {
|
||||
const startsWithValue = `%"${String(value)}%`;
|
||||
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, startsWithValue]);
|
||||
}
|
||||
|
||||
case PkiFilterOperator.ENDS_WITH:
|
||||
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%${String(value)}"%`]);
|
||||
case PkiFilterOperator.ENDS_WITH: {
|
||||
const endsWithValue = `%${String(value)}"%`;
|
||||
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, endsWithValue]);
|
||||
}
|
||||
|
||||
default:
|
||||
logger.warn(`Unsupported operator for SAN: ${String(filter.operator)}`);
|
||||
@@ -244,7 +258,7 @@ export const applyCaFilters = (
|
||||
filters: TPkiFilterRule[],
|
||||
projectId: string
|
||||
): 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) => {
|
||||
switch (filter.field) {
|
||||
|
||||
@@ -63,7 +63,7 @@ export const pkiAlertV2QueueServiceFactory = ({
|
||||
const evaluateAlert = async (
|
||||
alert: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
eventType: string;
|
||||
alertBefore: string;
|
||||
filters: TPkiFilterRule[];
|
||||
@@ -135,7 +135,7 @@ export const pkiAlertV2QueueServiceFactory = ({
|
||||
for (const alert of alerts) {
|
||||
const typedAlert = alert as {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
eventType: string;
|
||||
alertBefore: string;
|
||||
filters: TPkiFilterRule[];
|
||||
@@ -147,13 +147,13 @@ export const pkiAlertV2QueueServiceFactory = ({
|
||||
await pkiAlertV2Service.sendAlertNotifications(typedAlert.id, certificateIds);
|
||||
notificationsSent += 1;
|
||||
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;
|
||||
} 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"
|
||||
| "deleteById"
|
||||
| "findByProjectId"
|
||||
| "findByProjectIdWithCount"
|
||||
| "countByProjectId"
|
||||
| "findMatchingCertificates"
|
||||
| "transaction"
|
||||
>;
|
||||
pkiAlertChannelDAL: Pick<TPkiAlertChannelDALFactory, "create" | "findByAlertId" | "deleteByAlertId" | "insertMany">;
|
||||
pkiAlertHistoryDAL: Pick<TPkiAlertHistoryDALFactory, "createWithCertificates" | "findByAlertId">;
|
||||
@@ -59,7 +61,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
}: TPkiAlertV2ServiceFactoryDep) => {
|
||||
type TAlertWithChannels = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
eventType: string;
|
||||
alertBefore: string;
|
||||
@@ -81,7 +83,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
const formatAlertResponse = (alert: TAlertWithChannels): TAlertV2Response => {
|
||||
return {
|
||||
id: alert.id,
|
||||
slug: alert.slug,
|
||||
name: alert.name,
|
||||
description: alert.description,
|
||||
eventType: alert.eventType as PkiAlertEventType,
|
||||
alertBefore: alert.alertBefore,
|
||||
@@ -103,7 +105,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
const createAlert = async ({
|
||||
projectId,
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
eventType,
|
||||
alertBefore,
|
||||
@@ -132,31 +134,36 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Invalid alertBefore format. Use format like '30d', '1w', '3m', '1y'" });
|
||||
}
|
||||
|
||||
const alert = await pkiAlertV2DAL.create({
|
||||
projectId,
|
||||
slug,
|
||||
description,
|
||||
eventType,
|
||||
alertBefore,
|
||||
filters,
|
||||
enabled
|
||||
return pkiAlertV2DAL.transaction(async (tx) => {
|
||||
const alert = await pkiAlertV2DAL.create(
|
||||
{
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
eventType,
|
||||
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 ({
|
||||
@@ -208,10 +215,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
const filters = { search, eventType, enabled, limit, offset };
|
||||
|
||||
const [alerts, total] = await Promise.all([
|
||||
pkiAlertV2DAL.findByProjectId(projectId, filters),
|
||||
pkiAlertV2DAL.countByProjectId(projectId, { search, eventType, enabled })
|
||||
]);
|
||||
const { alerts, total } = await pkiAlertV2DAL.findByProjectIdWithCount(projectId, filters);
|
||||
|
||||
return {
|
||||
alerts: alerts.map((alert) => formatAlertResponse(alert as TAlertWithChannels)),
|
||||
@@ -221,7 +225,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
const updateAlert = async ({
|
||||
alertId,
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
eventType,
|
||||
alertBefore,
|
||||
@@ -256,41 +260,43 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
}
|
||||
|
||||
const updateData: {
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
eventType?: PkiAlertEventType;
|
||||
alertBefore?: string;
|
||||
filters?: TPkiFilterRule[];
|
||||
enabled?: boolean;
|
||||
} = {};
|
||||
if (slug !== undefined) updateData.slug = slug;
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (eventType !== undefined) updateData.eventType = eventType;
|
||||
if (alertBefore !== undefined) updateData.alertBefore = alertBefore;
|
||||
if (filters !== undefined) updateData.filters = filters;
|
||||
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) {
|
||||
await pkiAlertChannelDAL.deleteByAlertId(alertId);
|
||||
if (channels) {
|
||||
await pkiAlertChannelDAL.deleteByAlertId(alertId, tx);
|
||||
|
||||
const channelInserts = channels.map((channel) => ({
|
||||
alertId,
|
||||
channelType: channel.channelType,
|
||||
config: channel.config,
|
||||
enabled: channel.enabled
|
||||
}));
|
||||
const channelInserts = channels.map((channel) => ({
|
||||
alertId,
|
||||
channelType: channel.channelType,
|
||||
config: channel.config,
|
||||
enabled: channel.enabled
|
||||
}));
|
||||
|
||||
await pkiAlertChannelDAL.insertMany(channelInserts);
|
||||
}
|
||||
await pkiAlertChannelDAL.insertMany(channelInserts, tx);
|
||||
}
|
||||
|
||||
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alertId);
|
||||
if (!completeAlert) {
|
||||
throw new NotFoundError({ message: "Failed to retrieve updated alert" });
|
||||
}
|
||||
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alertId, tx);
|
||||
if (!completeAlert) {
|
||||
throw new NotFoundError({ message: "Failed to retrieve updated alert" });
|
||||
}
|
||||
|
||||
return formatAlertResponse(completeAlert as TAlertWithChannels);
|
||||
return formatAlertResponse(completeAlert as TAlertWithChannels);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAlert = async ({
|
||||
@@ -365,9 +371,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
return {
|
||||
certificates: result.certificates,
|
||||
total: result.total,
|
||||
limit,
|
||||
offset
|
||||
total: result.total
|
||||
};
|
||||
};
|
||||
|
||||
@@ -415,9 +419,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
return {
|
||||
certificates: result.certificates,
|
||||
total: result.total,
|
||||
limit,
|
||||
offset
|
||||
total: result.total
|
||||
};
|
||||
};
|
||||
|
||||
@@ -445,7 +447,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
if (matchingCertificates.length === 0) return;
|
||||
|
||||
let notificationSent = false;
|
||||
let hasNotificationSent = false;
|
||||
let notificationError: string | undefined;
|
||||
|
||||
try {
|
||||
@@ -455,7 +457,7 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
);
|
||||
|
||||
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 config = channel.config as TEmailChannelConfig;
|
||||
@@ -480,14 +482,14 @@ export const pkiAlertV2ServiceFactory = ({
|
||||
|
||||
await Promise.all(emailPromises);
|
||||
|
||||
notificationSent = true;
|
||||
hasNotificationSent = true;
|
||||
} catch (error) {
|
||||
notificationError = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
logger.error(error, `Failed to send notifications for alert ${alertId}`);
|
||||
}
|
||||
|
||||
await pkiAlertHistoryDAL.createWithCertificates(alertId, certificateIds, {
|
||||
notificationSent,
|
||||
hasNotificationSent,
|
||||
notificationError
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,12 +3,18 @@ import { z } from "zod";
|
||||
|
||||
import { TGenericPermission } from "@app/lib/types";
|
||||
|
||||
const createSecureSlugValidator = () => {
|
||||
const slugRegex = new RE2("^[a-z0-9]+(?:-[a-z0-9]+)*$");
|
||||
return (value: string) => slugRegex.test(value);
|
||||
const createSecureNameValidator = () => {
|
||||
// Validates name format: lowercase alphanumeric characters with optional hyphens
|
||||
// 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 = () => {
|
||||
// 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]$");
|
||||
return (value: string) => {
|
||||
if (value.length > 32) return false;
|
||||
@@ -98,11 +104,11 @@ export const CreateChannelSchema = z.object({
|
||||
export type TCreateChannel = z.infer<typeof CreateChannelSchema>;
|
||||
|
||||
export const CreatePkiAlertV2Schema = z.object({
|
||||
slug: z
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.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(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string().refine(createSecureAlertBeforeValidator(), "Must be in format like '30d', '1w', '3m', '1y'"),
|
||||
@@ -169,7 +175,7 @@ export type TCertificatePreview = {
|
||||
|
||||
export type TAlertV2Response = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
eventType: PkiAlertEventType;
|
||||
alertBefore: string;
|
||||
@@ -196,6 +202,4 @@ export type TListAlertsV2Response = {
|
||||
export type TListMatchingCertificatesResponse = {
|
||||
certificates: TCertificatePreview[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
@@ -10,8 +10,11 @@ export const useCreatePkiAlertV2 = () => {
|
||||
|
||||
return useMutation<TPkiAlertV2, unknown, TCreatePkiAlertV2>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: response } = await apiRequest.post<TPkiAlertV2>("/api/v2/pki/alerts", data);
|
||||
return response;
|
||||
const { data: response } = await apiRequest.post<{ alert: TPkiAlertV2 }>(
|
||||
"/api/v2/pki/alerts",
|
||||
data
|
||||
);
|
||||
return response.alert;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -26,11 +29,11 @@ export const useUpdatePkiAlertV2 = () => {
|
||||
|
||||
return useMutation<TPkiAlertV2, unknown, TUpdatePkiAlertV2>({
|
||||
mutationFn: async ({ alertId, ...data }) => {
|
||||
const { data: response } = await apiRequest.patch<TPkiAlertV2>(
|
||||
const { data: response } = await apiRequest.patch<{ alert: TPkiAlertV2 }>(
|
||||
`/api/v2/pki/alerts/${alertId}`,
|
||||
data
|
||||
);
|
||||
return response;
|
||||
return response.alert;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -48,8 +51,10 @@ export const useDeletePkiAlertV2 = () => {
|
||||
|
||||
return useMutation<TPkiAlertV2, unknown, TDeletePkiAlertV2>({
|
||||
mutationFn: async ({ alertId }) => {
|
||||
const { data } = await apiRequest.delete<TPkiAlertV2>(`/api/v2/pki/alerts/${alertId}`);
|
||||
return data;
|
||||
const { data } = await apiRequest.delete<{ alert: TPkiAlertV2 }>(
|
||||
`/api/v2/pki/alerts/${alertId}`
|
||||
);
|
||||
return data.alert;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
|
||||
@@ -31,8 +31,8 @@ const fetchPkiAlertsV2 = async (params: TGetPkiAlertsV2): Promise<TGetPkiAlertsV
|
||||
};
|
||||
|
||||
const fetchPkiAlertV2ById = async ({ alertId }: TGetPkiAlertV2ById): Promise<TPkiAlertV2> => {
|
||||
const { data } = await apiRequest.get<TPkiAlertV2>(`/api/v2/pki/alerts/${alertId}`);
|
||||
return data;
|
||||
const { data } = await apiRequest.get<{ alert: TPkiAlertV2 }>(`/api/v2/pki/alerts/${alertId}`);
|
||||
return data.alert;
|
||||
};
|
||||
|
||||
const fetchPkiAlertV2MatchingCertificates = async (
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface TPkiAlertChannelV2 {
|
||||
export interface TPkiAlertV2 {
|
||||
id: string;
|
||||
projectId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
eventType: PkiAlertEventTypeV2;
|
||||
alertBefore?: string;
|
||||
@@ -94,7 +94,7 @@ export interface TGetPkiAlertV2ById {
|
||||
|
||||
export interface TCreatePkiAlertV2 {
|
||||
projectId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
eventType: PkiAlertEventTypeV2;
|
||||
alertBefore?: string;
|
||||
@@ -105,7 +105,7 @@ export interface TCreatePkiAlertV2 {
|
||||
|
||||
export interface TUpdatePkiAlertV2 {
|
||||
alertId: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
eventType?: PkiAlertEventTypeV2;
|
||||
alertBefore?: string;
|
||||
@@ -172,11 +172,11 @@ export const pkiAlertChannelV2Schema = z.object({
|
||||
|
||||
export const createPkiAlertV2Schema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
slug: z
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.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(),
|
||||
eventType: z.nativeEnum(PkiAlertEventTypeV2),
|
||||
alertBefore: z
|
||||
|
||||
@@ -45,7 +45,7 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
|
||||
const [deleteModal, setDeleteModal] = useState<{
|
||||
isOpen: boolean;
|
||||
alertId?: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
}>({
|
||||
isOpen: false
|
||||
});
|
||||
@@ -113,7 +113,7 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
|
||||
setDeleteModal({
|
||||
isOpen: true,
|
||||
alertId: alert.id,
|
||||
slug: alert.slug
|
||||
name: alert.name
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -199,8 +199,8 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
|
||||
<DeleteActionModal
|
||||
isOpen={deleteModal.isOpen}
|
||||
deleteKey="delete"
|
||||
title={`Delete PKI Alert "${deleteModal.slug}"`}
|
||||
onChange={(isOpen) => setDeleteModal({ isOpen, alertId: undefined, slug: undefined })}
|
||||
title={`Delete PKI Alert "${deleteModal.name}"`}
|
||||
onChange={(isOpen) => setDeleteModal({ isOpen, alertId: undefined, name: undefined })}
|
||||
onDeleteApproved={handleDeleteAlert}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ export const CreatePkiAlertV2FormSteps = () => {
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Alert Type" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
className="w-full"
|
||||
>
|
||||
@@ -185,9 +185,9 @@ export const CreatePkiAlertV2FormSteps = () => {
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
name="name"
|
||||
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" />
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -490,12 +490,12 @@ export const CreatePkiAlertV2FormSteps = () => {
|
||||
<div className="space-y-4">
|
||||
<FormControl label="Email Recipients">
|
||||
<TextArea
|
||||
defaultValue={
|
||||
value={
|
||||
Array.isArray((watchedChannels?.[0]?.config as any)?.recipients)
|
||||
? (watchedChannels[0].config as any).recipients.join(", ")
|
||||
: ""
|
||||
}
|
||||
onBlur={(e) => {
|
||||
onChange={(e) => {
|
||||
const emailList = e.target.value
|
||||
.split(",")
|
||||
.map((email) => email.trim())
|
||||
@@ -522,7 +522,7 @@ export const CreatePkiAlertV2FormSteps = () => {
|
||||
<span className="text-sm text-mineshaft-300">Basic Information</span>
|
||||
</div>
|
||||
<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">
|
||||
{formatEventType(watchedEventType)}
|
||||
</GenericFieldLabel>
|
||||
|
||||
@@ -37,7 +37,7 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TFormData)[] }[] =
|
||||
{
|
||||
name: "Details",
|
||||
key: "basicInfo",
|
||||
fields: ["slug", "description", "alertBefore"]
|
||||
fields: ["name", "description", "alertBefore"]
|
||||
},
|
||||
{ name: "Filters", key: "filterRules", fields: ["filters"] },
|
||||
{ name: "Preview", key: "preview", fields: [] },
|
||||
@@ -61,7 +61,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
|
||||
resolver: zodResolver(isEditing ? updatePkiAlertV2Schema : createPkiAlertV2Schema),
|
||||
defaultValues: {
|
||||
projectId: currentProject?.id || "",
|
||||
slug: "",
|
||||
name: "",
|
||||
description: "",
|
||||
eventType: PkiAlertEventTypeV2.EXPIRATION,
|
||||
alertBefore: "30d",
|
||||
@@ -93,7 +93,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
|
||||
if (editingAlert && isEditing) {
|
||||
reset({
|
||||
projectId: currentProject?.id || "",
|
||||
slug: editingAlert.slug,
|
||||
name: editingAlert.name,
|
||||
description: editingAlert.description || "",
|
||||
eventType: editingAlert.eventType,
|
||||
alertBefore: editingAlert.alertBefore || "30d",
|
||||
@@ -121,7 +121,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
|
||||
} else if (!isEditing) {
|
||||
reset({
|
||||
projectId: currentProject?.id || "",
|
||||
slug: "",
|
||||
name: "",
|
||||
description: "",
|
||||
eventType: PkiAlertEventTypeV2.EXPIRATION,
|
||||
alertBefore: "30d",
|
||||
|
||||
@@ -87,7 +87,7 @@ export const PkiAlertV2Row = ({ alert, onView, onEdit, onDelete }: Props) => {
|
||||
<Tr>
|
||||
<Td>
|
||||
<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 && (
|
||||
<Tooltip content={alert.description}>
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
|
||||
@@ -60,7 +60,7 @@ export const UpdatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props)
|
||||
useEffect(() => {
|
||||
if (alert) {
|
||||
reset({
|
||||
slug: alert.slug,
|
||||
name: alert.name,
|
||||
description: alert.description || "",
|
||||
eventType: alert.eventType,
|
||||
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">
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Alert Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<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">
|
||||
<div className="space-y-6">
|
||||
<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"}>
|
||||
{alert.enabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
@@ -176,11 +176,11 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
|
||||
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{alert.filters.map((filter) => {
|
||||
{(alert.filters || []).map((filter) => {
|
||||
const formatFilterText = () => {
|
||||
const field = filter.field.replace(/_/g, " ");
|
||||
const operator = filter.operator.replace(/_/g, " ");
|
||||
@@ -207,14 +207,14 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-medium text-gray-200">Notification Recipients</h3>
|
||||
{alert.channels.some(
|
||||
{(alert.channels || []).some(
|
||||
(channel) =>
|
||||
channel.channelType === "email" && (channel.config as any)?.recipients
|
||||
) ? (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{alert.channels
|
||||
{(alert.channels || [])
|
||||
.filter(
|
||||
(channel) =>
|
||||
channel.channelType === "email" &&
|
||||
|
||||
Reference in New Issue
Block a user