mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 23:00:14 +00:00
Address PR comments
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user