Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/keystore/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const KeyStorePrefixes = {

CertDashboardStats: (projectId: string) => `cert-dashboard-stats:${projectId}` as const,
CertActivityTrend: (projectId: string, range: string) => `cert-activity-trend:${projectId}:${range}` as const,
CertPqcTrend: (projectId: string, range: string) => `cert-pqc-trend:${projectId}:${range}` as const,
RefreshTokenGrace: (sessionId: string) => `refresh-token-grace:${sessionId}` as const,
InsightsCache: (projectId: string, endpoint: string) => `insights-cache:${projectId}:${endpoint}` as const
};
Expand Down
45 changes: 45 additions & 0 deletions backend/src/server/routes/v1/project-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,51 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});

server.route({
method: "GET",
url: "/:projectId/certificates/pqc-trend",
config: {
rateLimit: readLimit
},
schema: {
hide: true,
operationId: "getCertificatePqcTrend",
tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate PQC adoption trend over time.",
params: z.object({
projectId: z.string().trim()
}),
querystring: z.object({
range: z.enum(["7d", "30d", "6m"]).optional().default("30d")
}),
response: {
200: z.object({
periods: z.array(
z.object({
period: z.string(),
pqc: z.number(),
nonPqc: z.number()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.project.getPqcTrend({
filter: {
projectId: req.params.projectId,
type: ProjectFilterType.ID
},
range: req.query.range,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
}
});

server.route({
method: "GET",
url: "/:projectId/pki-alerts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TProjectPermission } from "@app/lib/types";

import { TPermissionServiceFactory } from "../../ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "../../ee/services/permission/project-permission";
import { NON_PQC_KEY_ALGORITHMS, PQC_KEY_ALGORITHMS } from "../certificate/certificate-dal";
import { TCertificateInventoryViewDALFactory } from "./certificate-inventory-view-dal";
import {
TCreateInventoryViewDTO,
Expand All @@ -17,6 +18,7 @@ import {
type TSystemViewFilters = {
status?: string[];
notAfterTo?: string;
keyAlgorithm?: string[];
};

type TSystemView = {
Expand Down Expand Up @@ -61,6 +63,22 @@ const SYSTEM_VIEWS: TSystemView[] = [
columns: null,
isSystem: true,
createdByUserId: null
},
{
id: "system-pqc",
name: "Post-Quantum (PQC)",
filters: { keyAlgorithm: PQC_KEY_ALGORITHMS },
columns: null,
isSystem: true,
createdByUserId: null
},
{
id: "system-non-pqc",
name: "Classical (Non-PQC)",
filters: { keyAlgorithm: NON_PQC_KEY_ALGORITHMS },
columns: null,
isSystem: true,
createdByUserId: null
}
];

Expand Down
92 changes: 90 additions & 2 deletions backend/src/services/certificate/certificate-dal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ import { isUuidV4 } from "@app/lib/validator";
import { applyMetadataFilter } from "@app/services/resource-metadata/resource-metadata-fns";

import { keySizeToAlgorithms } from "./certificate-fns";
import { CertStatus } from "./certificate-types";
import { CertKeyAlgorithm, CertStatus } from "./certificate-types";

// Scoped to UI-surfaced algorithms; intentionally narrower than pqc-utils.PQC_ALGORITHMS.
export const PQC_KEY_ALGORITHMS: string[] = [
CertKeyAlgorithm.ML_DSA_44,
CertKeyAlgorithm.ML_DSA_65,
CertKeyAlgorithm.ML_DSA_87
];

export const NON_PQC_KEY_ALGORITHMS: string[] = [
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_3072,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384,
CertKeyAlgorithm.ECDSA_P521
];

export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;

Expand Down Expand Up @@ -918,6 +934,77 @@ export const certificateDALFactory = (db: TDbClient) => {
}
};

const getPqcTrend = async (projectId: string, daysBack: number) => {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
startDate.setHours(0, 0, 0, 0);
const now = new Date();

const useDaily = daysBack <= 30;
if (!useDaily) {
startDate.setDate(1);
}
const truncUnit = useDaily ? "day" : "month";
const dateFormat = useDaily ? "YYYY-MM-DD" : "YYYY-MM";

const rows = await db
.replicaNode()(TableName.Certificate)
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
.where(`${TableName.CertificateAuthority}.projectId`, projectId)
.where(`${TableName.Certificate}.notBefore`, ">=", startDate)
.where(`${TableName.Certificate}.notBefore`, "<=", now)
.whereIn(`${TableName.Certificate}.keyAlgorithm`, [...PQC_KEY_ALGORITHMS, ...NON_PQC_KEY_ALGORITHMS])
Comment thread
saifsmailbox98 marked this conversation as resolved.
.select(
db.raw(`to_char(date_trunc(?, "${TableName.Certificate}"."notBefore"), ?) as period`, [truncUnit, dateFormat])
)
.select(
db.raw(
`CASE WHEN "${TableName.Certificate}"."keyAlgorithm" IN (${PQC_KEY_ALGORITHMS.map(() => "?").join(",")}) THEN 'pqc' ELSE 'nonPqc' END as bucket`,
[...PQC_KEY_ALGORITHMS]
)
)
.select(db.raw("count(*)::int as count"))
.groupBy("period", "bucket")
.orderBy("period");

interface PeriodBucketCount {
period: string;
bucket: "pqc" | "nonPqc";
count: string;
}
const typed = rows as unknown as PeriodBucketCount[];
const pqcMap = new Map<string, number>();
const nonPqcMap = new Map<string, number>();
typed.forEach((r) => {
if (r.bucket === "pqc") pqcMap.set(r.period, Number(r.count));
else nonPqcMap.set(r.period, Number(r.count));
});

const periods: Array<{ period: string; pqc: number; nonPqc: number }> = [];
const cursor = new Date(startDate);
while (cursor <= now) {
const periodKey = useDaily
? `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}-${String(cursor.getDate()).padStart(2, "0")}`
: `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}`;
periods.push({
period: periodKey,
pqc: pqcMap.get(periodKey) ?? 0,
nonPqc: nonPqcMap.get(periodKey) ?? 0
});
if (useDaily) {
cursor.setDate(cursor.getDate() + 1);
} else {
cursor.setMonth(cursor.getMonth() + 1);
}
}

return { periods };
} catch (error) {
throw new DatabaseError({ error, name: "Get PQC trend" });
}
};

return {
...certificateOrm,
countCertificatesInProject,
Expand All @@ -932,6 +1019,7 @@ export const certificateDALFactory = (db: TDbClient) => {
findWithPrivateKeyInfo,
findWithFullDetails,
getDashboardStats,
getActivityTrend
getActivityTrend,
getPqcTrend
};
};
38 changes: 38 additions & 0 deletions backend/src/services/project/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ type TProjectServiceFactoryDep = {
| "countActiveCertificatesForSync"
| "getDashboardStats"
| "getActivityTrend"
| "getPqcTrend"
>;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
Expand Down Expand Up @@ -1403,6 +1404,42 @@ export const projectServiceFactory = ({
});
};

const getPqcTrend = async ({
filter,
range = "30d",
actorId,
actorOrgId,
actorAuthMethod,
actor
}: TGetActivityTrendDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
const projectId = project.id;

const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});

ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);

const rangeDaysMap: Record<string, number> = { "7d": 7, "30d": 30, "6m": 180 };
const daysBack = rangeDaysMap[range];

return withCache({
keyStore,
key: KeyStorePrefixes.CertPqcTrend(projectId, range),
ttlSeconds: DASHBOARD_CACHE_TTL,
fetcher: () => certificateDAL.getPqcTrend(projectId, daysBack)
});
};

/**
* Return list of (PKI) alerts configured for project
*/
Expand Down Expand Up @@ -2433,6 +2470,7 @@ export const projectServiceFactory = ({
listProjectCertificates,
getDashboardStats,
getActivityTrend,
getPqcTrend,
listProjectAlerts,
listProjectPkiCollections,
listProjectCertificateTemplates,
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/hooks/api/certificates/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
[CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
[CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384",
[CertKeyAlgorithm.ECDSA_P521]: "ECDSA P521",
[CertKeyAlgorithm.ML_DSA_44]: "ML-DSA-44",
[CertKeyAlgorithm.ML_DSA_65]: "ML-DSA-65",
[CertKeyAlgorithm.ML_DSA_87]: "ML-DSA-87"
Expand All @@ -45,6 +46,24 @@ export const certSignatureAlgorithmToNameMap: Record<string, string> = {
"ML-DSA-87": "ML-DSA-87"
};

export const PQC_KEY_ALGORITHMS = [
CertKeyAlgorithm.ML_DSA_44,
CertKeyAlgorithm.ML_DSA_65,
CertKeyAlgorithm.ML_DSA_87
] as const;
Comment thread
saifsmailbox98 marked this conversation as resolved.

export const NON_PQC_KEY_ALGORITHMS = [
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_3072,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384,
CertKeyAlgorithm.ECDSA_P521
] as const;

export const isPqcAlgorithm = (label: string): boolean =>
(PQC_KEY_ALGORITHMS as readonly string[]).includes(label);

export const certKeyAlgorithms = [
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_3072], value: CertKeyAlgorithm.RSA_3072 },
Comment thread
saifsmailbox98 marked this conversation as resolved.
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/api/certificates/enums.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum CertKeyAlgorithm {
RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1",
ECDSA_P384 = "EC_secp384r1",
ECDSA_P521 = "EC_secp521r1",
ML_DSA_44 = "ML-DSA-44",
ML_DSA_65 = "ML-DSA-65",
ML_DSA_87 = "ML-DSA-87"
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/hooks/api/certificates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
useGetCertDashboardStats,
useGetCertificateById,
useGetCertificateRequest,
useGetCertPqcTrend,
useListCertificateRequests
} from "./queries";
export type {
Expand All @@ -31,5 +32,7 @@ export type {
TExpirationBucket,
TListCertificateRequestsParams,
TListCertificateRequestsResponse,
TPqcTrendPoint,
TPqcTrendResponse,
TUpdateCertificateDTO
} from "./types";
22 changes: 20 additions & 2 deletions frontend/src/hooks/api/certificates/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
TCertificateRequestDetails,
TDashboardStats,
TListCertificateRequestsParams,
TListCertificateRequestsResponse
TListCertificateRequestsResponse,
TPqcTrendResponse
} from "./types";

export const certKeys = {
Expand Down Expand Up @@ -38,7 +39,9 @@ export const certKeys = {
],
getDashboardStats: (projectId: string) => ["cert-dashboard-stats", { projectId }] as const,
getActivityTrend: (projectId: string, range: string) =>
["cert-activity-trend", { projectId }, { range }] as const
["cert-activity-trend", { projectId }, { range }] as const,
getPqcTrend: (projectId: string, range: string) =>
["cert-pqc-trend", { projectId }, { range }] as const
};

export const useGetCert = (serialNumber: string) => {
Expand Down Expand Up @@ -175,3 +178,18 @@ export const useGetCertActivityTrend = (projectId: string, range = "6m") => {
staleTime: DASHBOARD_STALE_TIME
});
};

export const useGetCertPqcTrend = (projectId: string, range = "30d") => {
return useQuery({
queryKey: certKeys.getPqcTrend(projectId, range),
queryFn: async () => {
const { data } = await apiRequest.get<TPqcTrendResponse>(
`/api/v1/projects/${projectId}/certificates/pqc-trend`,
{ params: { range } }
);
return data;
},
enabled: Boolean(projectId),
staleTime: DASHBOARD_STALE_TIME
});
};
10 changes: 10 additions & 0 deletions frontend/src/hooks/api/certificates/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,13 @@ export type TActivityTrendPoint = {
export type TActivityTrendResponse = {
periods: TActivityTrendPoint[];
};

export type TPqcTrendPoint = {
period: string;
pqc: number;
nonPqc: number;
};

export type TPqcTrendResponse = {
periods: TPqcTrendPoint[];
};
Loading
Loading