mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
Merge pull request #3314 from akhilmhdh/fix/ua-optimization
feat: patched up regex issues
This commit is contained in:
@@ -16,7 +16,7 @@ const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Dat
|
|||||||
const startDateStr = formatPartitionDate(startDate);
|
const startDateStr = formatPartitionDate(startDate);
|
||||||
const endDateStr = formatPartitionDate(endDate);
|
const endDateStr = formatPartitionDate(endDate);
|
||||||
|
|
||||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
|
const partitionName = `${TableName.AuditLog}_${startDateStr.replaceAll("-", "")}_${endDateStr.replaceAll("-", "")}`;
|
||||||
|
|
||||||
await knex.schema.raw(
|
await knex.schema.raw(
|
||||||
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
|
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
// for CSRs sent in PEM, we leave them as is
|
// for CSRs sent in PEM, we leave them as is
|
||||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
csrBody = csrBody.replaceAll("\n", "").replaceAll(" ", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
done(null, csrBody);
|
done(null, csrBody);
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (ldapConfig.groupSearchBase) {
|
if (ldapConfig.groupSearchBase) {
|
||||||
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||||
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
|
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
|
||||||
.replace(/{{\.Username}}/g, user.uid)
|
.replaceAll("{{.Username}}", user.uid)
|
||||||
.replace(/{{\.UserDN}}/g, user.dn);
|
.replaceAll("{{.UserDN}}", user.dn);
|
||||||
|
|
||||||
if (!isValidLdapFilter(groupSearchFilter)) {
|
if (!isValidLdapFilter(groupSearchFilter)) {
|
||||||
throw new Error("Generated LDAP search filter is invalid.");
|
throw new Error("Generated LDAP search filter is invalid.");
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
}: TCreateAuditLogStreamDTO) => {
|
}: TCreateAuditLogStreamDTO) => {
|
||||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.auditLogStreams) {
|
if (!plan.auditLogStreams) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -62,9 +61,8 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
if (appCfg.isCloud) {
|
const appCfg = getConfig();
|
||||||
blockLocalAndPrivateIpAddresses(url);
|
if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||||
}
|
|
||||||
|
|
||||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||||
@@ -135,9 +133,8 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
const { orgId } = logStream;
|
const { orgId } = logStream;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
|
if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||||
|
|
||||||
// testing connection first
|
// testing connection first
|
||||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
@@ -67,9 +68,7 @@ export const certificateEstServiceFactory = ({
|
|||||||
|
|
||||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||||
|
|
||||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
|
||||||
)?.[0];
|
|
||||||
|
|
||||||
if (!leafCertificate) {
|
if (!leafCertificate) {
|
||||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||||
@@ -88,10 +87,7 @@ export const certificateEstServiceFactory = ({
|
|||||||
const verifiedChains = await Promise.all(
|
const verifiedChains = await Promise.all(
|
||||||
caCertChains.map((chain) => {
|
caCertChains.map((chain) => {
|
||||||
const caCert = new x509.X509Certificate(chain.certificate);
|
const caCert = new x509.X509Certificate(chain.certificate);
|
||||||
const caChain =
|
const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
|
||||||
chain.certificateChain
|
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
|
||||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
|
||||||
|
|
||||||
return isCertChainValid([cert, caCert, ...caChain]);
|
return isCertChainValid([cert, caCert, ...caChain]);
|
||||||
})
|
})
|
||||||
@@ -172,19 +168,15 @@ export const certificateEstServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!estConfig.disableBootstrapCertValidation) {
|
if (!estConfig.disableBootstrapCertValidation) {
|
||||||
const caCerts = estConfig.caChain
|
const caCerts = extractX509CertFromChain(estConfig.caChain)?.map((cert) => {
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
return new x509.X509Certificate(cert);
|
||||||
?.map((cert) => {
|
});
|
||||||
return new x509.X509Certificate(cert);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!caCerts) {
|
if (!caCerts) {
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
|
||||||
)?.[0];
|
|
||||||
|
|
||||||
if (!leafCertificate) {
|
if (!leafCertificate) {
|
||||||
throw new BadRequestError({ message: "Missing client certificate" });
|
throw new BadRequestError({ message: "Missing client certificate" });
|
||||||
@@ -250,13 +242,7 @@ export const certificateEstServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificates = caCertChain
|
const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
|
||||||
?.map((cert) => new x509.X509Certificate(cert));
|
|
||||||
|
|
||||||
if (!certificates) {
|
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const caCertificate = new x509.X509Certificate(caCert);
|
const caCertificate = new x509.X509Certificate(caCert);
|
||||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
|||||||
password
|
password
|
||||||
});
|
});
|
||||||
|
|
||||||
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||||
|
|
||||||
for await (const query of queries) {
|
for await (const query of queries) {
|
||||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||||
@@ -116,7 +116,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
|||||||
username
|
username
|
||||||
});
|
});
|
||||||
|
|
||||||
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||||
|
|
||||||
const client = await $getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
const masterClient = await $getClient(providerInputs, true);
|
const masterClient = await $getClient(providerInputs, true);
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import crypto, { KeyObject } from "crypto";
|
|||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
import { isValidIp } from "@app/lib/ip";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
@@ -665,7 +666,7 @@ export const kmipServiceFactory = ({
|
|||||||
.split(",")
|
.split(",")
|
||||||
.map((name) => name.trim())
|
.map((name) => name.trim())
|
||||||
.map((altName) => {
|
.map((altName) => {
|
||||||
if (isValidHostname(altName)) {
|
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||||
return {
|
return {
|
||||||
type: "dns",
|
type: "dns",
|
||||||
value: altName
|
value: altName
|
||||||
|
|||||||
@@ -97,12 +97,14 @@ export const searchGroups = async (
|
|||||||
|
|
||||||
res.on("searchEntry", (entry) => {
|
res.on("searchEntry", (entry) => {
|
||||||
const dn = entry.dn.toString();
|
const dn = entry.dn.toString();
|
||||||
const regex = /cn=([^,]+)/;
|
const cnStartIndex = dn.indexOf("cn=");
|
||||||
const match = dn.match(regex);
|
|
||||||
// parse the cn from the dn
|
|
||||||
const cn = (match && match[1]) as string;
|
|
||||||
|
|
||||||
groups.push({ dn, cn });
|
if (cnStartIndex !== -1) {
|
||||||
|
const valueStartIndex = cnStartIndex + 3;
|
||||||
|
const commaIndex = dn.indexOf(",", valueStartIndex);
|
||||||
|
const cn = dn.substring(valueStartIndex, commaIndex === -1 ? undefined : commaIndex);
|
||||||
|
groups.push({ dn, cn });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
res.on("error", (error) => {
|
res.on("error", (error) => {
|
||||||
ldapClient.unbind();
|
ldapClient.unbind();
|
||||||
|
|||||||
@@ -29,15 +29,9 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
|
|||||||
attributeName = "name";
|
attributeName = "name";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
return { [attributeName]: parsedValue.replaceAll('"', "") };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function extractScimValueFromPath(path: string): string | null {
|
|
||||||
const regex = /members\[value eq "([^"]+)"\]/;
|
|
||||||
const match = path.match(regex);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildScimUser = ({
|
export const buildScimUser = ({
|
||||||
orgMembershipId,
|
orgMembershipId,
|
||||||
username,
|
username,
|
||||||
|
|||||||
+29
-2
@@ -14,16 +14,43 @@ import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns
|
|||||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||||
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
||||||
|
|
||||||
const REGEX = /\${([^}]+)}/g;
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
const replaceTemplateVariables = (str: string, getValue: (key: string) => unknown) => {
|
||||||
|
// Use array to collect pieces and join at the end (more efficient for large strings)
|
||||||
|
const parts: string[] = [];
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
while (pos < str.length) {
|
||||||
|
const start = str.indexOf("${", pos);
|
||||||
|
if (start === -1) {
|
||||||
|
parts.push(str.slice(pos));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push(str.slice(pos, start));
|
||||||
|
const end = str.indexOf("}", start + 2);
|
||||||
|
|
||||||
|
if (end === -1) {
|
||||||
|
parts.push(str.slice(start));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const varName = str.slice(start + 2, end);
|
||||||
|
parts.push(String(getValue(varName)));
|
||||||
|
pos = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("");
|
||||||
|
};
|
||||||
|
|
||||||
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
if (typeof data === "number") return data;
|
if (typeof data === "number") return data;
|
||||||
|
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return data.replace(REGEX, (_a, b) => getValue(b) as string);
|
return replaceTemplateVariables(data, getValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data === "object" && Array.isArray(data)) {
|
if (typeof data === "object" && Array.isArray(data)) {
|
||||||
|
|||||||
@@ -8,7 +8,18 @@ type GetFullFolderPath = {
|
|||||||
|
|
||||||
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
|
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
|
||||||
// Helper function to remove duplicate slashes
|
// Helper function to remove duplicate slashes
|
||||||
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
|
const removeDuplicateSlashes = (path: string) => {
|
||||||
|
const chars = [];
|
||||||
|
let lastWasSlash = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length; i += 1) {
|
||||||
|
const char = path[i];
|
||||||
|
if (char !== "/" || !lastWasSlash) chars.push(char);
|
||||||
|
lastWasSlash = char === "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return chars.join("");
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch all folders at once based on environment ID to avoid multiple queries
|
// Fetch all folders at once based on environment ID to avoid multiple queries
|
||||||
const folders = await folderDAL.find({ envId });
|
const folders = await folderDAL.find({ envId });
|
||||||
|
|||||||
+26
-6
@@ -1,14 +1,34 @@
|
|||||||
|
import { isIP } from "net";
|
||||||
|
|
||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
|
||||||
// Validates usernames or wildcard (*)
|
// Validates usernames or wildcard (*)
|
||||||
export const isValidUserPattern = (value: string): boolean => {
|
export const isValidUserPattern = (value: string): boolean => {
|
||||||
// Matches valid Linux usernames or a wildcard (*)
|
// Length check before regex to prevent ReDoS
|
||||||
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
if (typeof value !== "string") return false;
|
||||||
|
if (value.length > 32) return false; // Maximum Linux username length
|
||||||
|
if (value === "*") return true; // Handle wildcard separately
|
||||||
|
|
||||||
|
// Simpler, more specific pattern for usernames
|
||||||
|
const userRegex = /^[a-z_][a-z0-9_-]*$/i;
|
||||||
return userRegex.test(value);
|
return userRegex.test(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validates hostnames, wildcard domains, or IP addresses
|
// Validates hostnames, wildcard domains, or IP addresses
|
||||||
export const isValidHostPattern = (value: string): boolean => {
|
export const isValidHostPattern = (value: string): boolean => {
|
||||||
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
// Input validation
|
||||||
const hostRegex =
|
if (typeof value !== "string") return false;
|
||||||
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
|
||||||
return hostRegex.test(value);
|
// Length check
|
||||||
|
if (value.length > 255) return false;
|
||||||
|
|
||||||
|
// Handle the wildcard case separately
|
||||||
|
if (value === "*") return true;
|
||||||
|
|
||||||
|
// Check for IP addresses using Node.js built-in functions
|
||||||
|
if (isIP(value)) return true;
|
||||||
|
|
||||||
|
return isFQDN(value, {
|
||||||
|
allow_wildcard: true
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { promisify } from "util";
|
|||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-type
|
|||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
const EXEC_TIMEOUT_MS = 10000; // 10 seconds
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
export const createSshCertSerialNumber = () => {
|
export const createSshCertSerialNumber = () => {
|
||||||
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||||
@@ -64,7 +66,9 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
|||||||
// Generate the SSH key pair
|
// Generate the SSH key pair
|
||||||
// The "-N ''" sets an empty passphrase
|
// The "-N ''" sets an empty passphrase
|
||||||
// The keys are created in the temporary directory
|
// The keys are created in the temporary directory
|
||||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
|
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
||||||
|
timeout: EXEC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
|
||||||
// Read the generated keys
|
// Read the generated keys
|
||||||
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||||
@@ -87,7 +91,10 @@ export const getSshPublicKey = async (privateKey: string) => {
|
|||||||
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||||
|
|
||||||
// Run ssh-keygen to extract the public key
|
// Run ssh-keygen to extract the public key
|
||||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: EXEC_TIMEOUT_MS
|
||||||
|
});
|
||||||
return stdout.trim();
|
return stdout.trim();
|
||||||
} finally {
|
} finally {
|
||||||
// Ensure that files and the temporary directory are cleaned up
|
// Ensure that files and the temporary directory are cleaned up
|
||||||
@@ -143,7 +150,14 @@ export const validateSshCertificatePrincipals = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
||||||
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
|
if (
|
||||||
|
!characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Period,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Hyphen
|
||||||
|
])(sanitized)
|
||||||
|
) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
||||||
});
|
});
|
||||||
@@ -266,8 +280,8 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
|||||||
* that it only contains alphanumeric characters with no spaces.
|
* that it only contains alphanumeric characters with no spaces.
|
||||||
*/
|
*/
|
||||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||||
const regex = /^[A-Za-z0-9-]+$/;
|
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
||||||
if (!regex.test(keyId)) {
|
if (!regex(keyId)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message:
|
||||||
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
||||||
@@ -298,7 +312,7 @@ const validateSshPublicKey = async (publicKey: string) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
||||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
|
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile], { timeout: EXEC_TIMEOUT_MS });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to validate SSH public key format: could not be parsed."
|
message: "Failed to validate SSH public key format: could not be parsed."
|
||||||
@@ -363,7 +377,7 @@ export const createSshCert = async ({
|
|||||||
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||||
|
|
||||||
// Execute the signing process
|
// Execute the signing process
|
||||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8", timeout: EXEC_TIMEOUT_MS });
|
||||||
|
|
||||||
// Read the signed public key from the generated cert file
|
// Read the signed public key from the generated cert file
|
||||||
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
|
|||||||
nc += 1;
|
nc += 1;
|
||||||
const nonceCount = nc.toString(16).padStart(8, "0");
|
const nonceCount = nc.toString(16).padStart(8, "0");
|
||||||
const cnonce = crypto.randomBytes(24).toString("hex");
|
const cnonce = crypto.randomBytes(24).toString("hex");
|
||||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
|
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
|
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||||
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
||||||
const path = opts.url;
|
const path = opts.url;
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
// Credit: https://github.com/miguelmota/is-base64
|
type Base64Options = {
|
||||||
export const isBase64 = (
|
urlSafe?: boolean;
|
||||||
v: string,
|
padding?: boolean;
|
||||||
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
};
|
||||||
) => {
|
|
||||||
if (opts.allowEmpty === false && v === "") {
|
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||||
return false;
|
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
|
||||||
|
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
|
||||||
|
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
|
||||||
|
|
||||||
|
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
throw new TypeError("Expected a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
// Default padding to true unless urlSafe is true
|
||||||
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
const opts: Base64Options = {
|
||||||
|
urlSafe: false,
|
||||||
|
padding: options.urlSafe === undefined ? true : !options.urlSafe,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
if (opts.mimeRequired === true) {
|
if (str === "") return true;
|
||||||
regex = mimeRegex + regex;
|
|
||||||
} else if (opts.allowMime === true) {
|
let regex;
|
||||||
regex = `${mimeRegex}?${regex}`;
|
if (opts.urlSafe) {
|
||||||
|
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
|
||||||
|
} else {
|
||||||
|
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.paddingRequired === false) {
|
return (!opts.padding || str.length % 4 === 0) && regex.test(str);
|
||||||
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RegExp(`^${regex}$`, "gi").test(v);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBase64SizeInBytes = (base64String: string) => {
|
export const getBase64SizeInBytes = (base64String: string) => {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { extractX509CertFromChain } from "./extract-certificate";
|
||||||
|
|
||||||
|
describe("Extract Certificate Payload", () => {
|
||||||
|
test("Single chain", () => {
|
||||||
|
const payload = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
const result = extractX509CertFromChain(payload);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.length).toBe(1);
|
||||||
|
expect(result?.[0]).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multiple chain", () => {
|
||||||
|
const payload = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
const result = extractX509CertFromChain(payload);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.length).toBe(3);
|
||||||
|
expect(result).toEqual([
|
||||||
|
`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||||
|
-----END CERTIFICATE-----`,
|
||||||
|
`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
-----END CERTIFICATE-----`,
|
||||||
|
`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { BadRequestError } from "../errors";
|
||||||
|
|
||||||
|
export const extractX509CertFromChain = (certificateChain: string): string[] => {
|
||||||
|
if (!certificateChain) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate chain is empty or undefined"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificates: string[] = [];
|
||||||
|
let currentPosition = 0;
|
||||||
|
const chainLength = certificateChain.length;
|
||||||
|
|
||||||
|
while (currentPosition < chainLength) {
|
||||||
|
// Find the start of a certificate
|
||||||
|
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
||||||
|
const startIndex = certificateChain.indexOf(beginMarker, currentPosition);
|
||||||
|
|
||||||
|
if (startIndex === -1) {
|
||||||
|
break; // No more certificates found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the end of the certificate
|
||||||
|
const endMarker = "-----END CERTIFICATE-----";
|
||||||
|
const endIndex = certificateChain.indexOf(endMarker, startIndex);
|
||||||
|
|
||||||
|
if (endIndex === -1) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Malformed certificate chain: Found BEGIN marker without matching END marker"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the complete certificate including markers
|
||||||
|
const completeEndIndex = endIndex + endMarker.length;
|
||||||
|
const certificate = certificateChain.substring(startIndex, completeEndIndex);
|
||||||
|
|
||||||
|
// Add the extracted certificate to our results
|
||||||
|
certificates.push(certificate);
|
||||||
|
|
||||||
|
// Move position to after this certificate
|
||||||
|
currentPosition = completeEndIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificates.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No valid certificates found in the chain"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificates;
|
||||||
|
};
|
||||||
@@ -107,12 +107,6 @@ export const isValidIp = (ip: string) => {
|
|||||||
return net.isIPv4(ip) || net.isIPv6(ip);
|
return net.isIPv4(ip) || net.isIPv6(ip);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidHostname = (name: string) => {
|
|
||||||
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
|
||||||
|
|
||||||
return hostnameRegex.test(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TIp = {
|
export type TIp = {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
type: IPType;
|
type: IPType;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import { CharacterType, characterValidator } from "./validate-string";
|
||||||
|
|
||||||
// regex to allow only alphanumeric, dash, underscore
|
// regex to allow only alphanumeric, dash, underscore
|
||||||
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
|
export const isValidFolderName = characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.Underscore
|
||||||
|
]);
|
||||||
|
|
||||||
export const isValidSecretPath = (path: string) =>
|
export const isValidSecretPath = (path: string) =>
|
||||||
path
|
path
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { CharacterType, characterValidator } from "./validate-string";
|
||||||
|
|
||||||
|
describe("validate-string", () => {
|
||||||
|
test("Check alphabets", () => {
|
||||||
|
expect(characterValidator([CharacterType.Alphabets])("hello")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.Alphabets])("hello world")).toBeFalsy();
|
||||||
|
expect(characterValidator([CharacterType.Alphabets, CharacterType.Spaces])("hello world")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Check numbers", () => {
|
||||||
|
expect(characterValidator([CharacterType.Numbers])("1234567890")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890-")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Check special characters", () => {
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])("Hello-World")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Plus])("Hello+World")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello_World")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Colon])("Hello:World")).toBeTruthy();
|
||||||
|
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello World")).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
export enum CharacterType {
|
||||||
|
Alphabets = "alphabets",
|
||||||
|
Numbers = "numbers",
|
||||||
|
AlphaNumeric = "alpha-numeric",
|
||||||
|
Spaces = "spaces",
|
||||||
|
SpecialCharacters = "specialCharacters",
|
||||||
|
Punctuation = "punctuation",
|
||||||
|
Period = "period", // .
|
||||||
|
Underscore = "underscore", // _
|
||||||
|
Colon = "colon", // :
|
||||||
|
ForwardSlash = "forwardSlash", // /
|
||||||
|
Equals = "equals", // =
|
||||||
|
Plus = "plus", // +
|
||||||
|
Hyphen = "hyphen", // -
|
||||||
|
At = "at", // @
|
||||||
|
// Additional individual characters that might be useful
|
||||||
|
Asterisk = "asterisk", // *
|
||||||
|
Ampersand = "ampersand", // &
|
||||||
|
Question = "question", // ?
|
||||||
|
Hash = "hash", // #
|
||||||
|
Percent = "percent", // %
|
||||||
|
Dollar = "dollar", // $
|
||||||
|
Caret = "caret", // ^
|
||||||
|
Backtick = "backtick", // `
|
||||||
|
Pipe = "pipe", // |
|
||||||
|
Backslash = "backslash", // \
|
||||||
|
OpenParen = "openParen", // (
|
||||||
|
CloseParen = "closeParen", // )
|
||||||
|
OpenBracket = "openBracket", // [
|
||||||
|
CloseBracket = "closeBracket", // ]
|
||||||
|
OpenBrace = "openBrace", // {
|
||||||
|
CloseBrace = "closeBrace", // }
|
||||||
|
LessThan = "lessThan", // <
|
||||||
|
GreaterThan = "greaterThan", // >
|
||||||
|
SingleQuote = "singleQuote", // '
|
||||||
|
DoubleQuote = "doubleQuote", // "
|
||||||
|
Comma = "comma", // ,
|
||||||
|
Semicolon = "semicolon", // ;
|
||||||
|
Exclamation = "exclamation" // !
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a string contains only specific types of characters
|
||||||
|
*/
|
||||||
|
export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
||||||
|
// Create a regex pattern based on allowed character types
|
||||||
|
const patternMap: Record<CharacterType, string> = {
|
||||||
|
[CharacterType.Alphabets]: "a-zA-Z",
|
||||||
|
[CharacterType.Numbers]: "0-9",
|
||||||
|
[CharacterType.AlphaNumeric]: "a-zA-Z0-9",
|
||||||
|
[CharacterType.Spaces]: "\\s",
|
||||||
|
[CharacterType.SpecialCharacters]: "!@#$%^&*()_+\\-=\\[\\]{}|;:'\",.<>/?\\\\",
|
||||||
|
[CharacterType.Punctuation]: "\\.\\,\\;\\:\\!\\?",
|
||||||
|
[CharacterType.Colon]: "\\:",
|
||||||
|
[CharacterType.ForwardSlash]: "\\/",
|
||||||
|
[CharacterType.Underscore]: "_",
|
||||||
|
[CharacterType.Hyphen]: "\\-",
|
||||||
|
[CharacterType.Period]: "\\.",
|
||||||
|
[CharacterType.Equals]: "=",
|
||||||
|
[CharacterType.Plus]: "\\+",
|
||||||
|
[CharacterType.At]: "@",
|
||||||
|
[CharacterType.Asterisk]: "\\*",
|
||||||
|
[CharacterType.Ampersand]: "&",
|
||||||
|
[CharacterType.Question]: "\\?",
|
||||||
|
[CharacterType.Hash]: "#",
|
||||||
|
[CharacterType.Percent]: "%",
|
||||||
|
[CharacterType.Dollar]: "\\$",
|
||||||
|
[CharacterType.Caret]: "\\^",
|
||||||
|
[CharacterType.Backtick]: "`",
|
||||||
|
[CharacterType.Pipe]: "\\|",
|
||||||
|
[CharacterType.Backslash]: "\\\\",
|
||||||
|
[CharacterType.OpenParen]: "\\(",
|
||||||
|
[CharacterType.CloseParen]: "\\)",
|
||||||
|
[CharacterType.OpenBracket]: "\\[",
|
||||||
|
[CharacterType.CloseBracket]: "\\]",
|
||||||
|
[CharacterType.OpenBrace]: "\\{",
|
||||||
|
[CharacterType.CloseBrace]: "\\}",
|
||||||
|
[CharacterType.LessThan]: "<",
|
||||||
|
[CharacterType.GreaterThan]: ">",
|
||||||
|
[CharacterType.SingleQuote]: "'",
|
||||||
|
[CharacterType.DoubleQuote]: '\\"',
|
||||||
|
[CharacterType.Comma]: ",",
|
||||||
|
[CharacterType.Semicolon]: ";",
|
||||||
|
[CharacterType.Exclamation]: "!"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine patterns from allowed characters
|
||||||
|
const combinedPattern = allowedCharacters.map((char) => patternMap[char]).join("");
|
||||||
|
|
||||||
|
// Create a regex that matches only the allowed characters
|
||||||
|
const regex = new RegExp(`^[${combinedPattern}]+$`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if the input string contains only the allowed character types
|
||||||
|
* @param input String to validate
|
||||||
|
* @returns Boolean indicating if the string is valid
|
||||||
|
*/
|
||||||
|
return function validate(input: string): boolean {
|
||||||
|
return regex.test(input);
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { isFQDN } from "./validate-url";
|
||||||
|
|
||||||
|
describe("isFQDN", () => {
|
||||||
|
test("Non wildcard", () => {
|
||||||
|
expect(isFQDN("www.example.com")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wildcard", () => {
|
||||||
|
expect(isFQDN("*.example.com", { allow_wildcard: true })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wildcard FQDN fails on option allow_wildcard false", () => {
|
||||||
|
expect(isFQDN("*.example.com")).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,117 @@
|
|||||||
import { getConfig } from "../config/env";
|
import dns from "node:dns/promises";
|
||||||
|
|
||||||
|
import { isIPv4 } from "net";
|
||||||
|
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
|
import { isPrivateIp } from "../ip/ipRange";
|
||||||
|
|
||||||
export const blockLocalAndPrivateIpAddresses = (url: string) => {
|
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||||
const validUrl = new URL(url);
|
const validUrl = new URL(url);
|
||||||
const appCfg = getConfig();
|
const inputHostIps: string[] = [];
|
||||||
// on cloud local ips are not allowed
|
if (isIPv4(validUrl.host)) {
|
||||||
if (
|
inputHostIps.push(validUrl.host);
|
||||||
appCfg.isCloud &&
|
} else {
|
||||||
(validUrl.host === "host.docker.internal" ||
|
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
|
||||||
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||||
validUrl.host.match(/^192\.168\.\d+\.\d+/))
|
}
|
||||||
)
|
const resolvedIps = await dns.resolve4(validUrl.host);
|
||||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
inputHostIps.push(...resolvedIps);
|
||||||
|
}
|
||||||
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
throw new BadRequestError({ message: "Localhost not allowed" });
|
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||||
|
};
|
||||||
|
|
||||||
|
type FQDNOptions = {
|
||||||
|
require_tld?: boolean;
|
||||||
|
allow_underscores?: boolean;
|
||||||
|
allow_trailing_dot?: boolean;
|
||||||
|
allow_numeric_tld?: boolean;
|
||||||
|
allow_wildcard?: boolean;
|
||||||
|
ignore_max_length?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFqdnOptions: FQDNOptions = {
|
||||||
|
require_tld: true,
|
||||||
|
allow_underscores: false,
|
||||||
|
allow_trailing_dot: false,
|
||||||
|
allow_numeric_tld: false,
|
||||||
|
allow_wildcard: false,
|
||||||
|
ignore_max_length: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// credits: https://github.com/validatorjs/validator.js/blob/f5da7fb6ed59b94695e6fcb2e970c80029509919/src/lib/isFQDN.js#L13
|
||||||
|
export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => {
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
throw new TypeError("Expected a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default options
|
||||||
|
const opts: FQDNOptions = {
|
||||||
|
...defaultFqdnOptions,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
let testStr = str;
|
||||||
|
/* Remove the optional trailing dot before checking validity */
|
||||||
|
if (opts.allow_trailing_dot && str[str.length - 1] === ".") {
|
||||||
|
testStr = testStr.substring(0, str.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove the optional wildcard before checking validity */
|
||||||
|
if (opts.allow_wildcard === true && str.indexOf("*.") === 0) {
|
||||||
|
testStr = testStr.substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = testStr.split(".");
|
||||||
|
const tld = parts[parts.length - 1];
|
||||||
|
|
||||||
|
if (opts.require_tld) {
|
||||||
|
// disallow fqdns without tld
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!opts.allow_numeric_tld &&
|
||||||
|
!/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow spaces
|
||||||
|
if (/\s/.test(tld)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reject numeric TLDs
|
||||||
|
if (!opts.allow_numeric_tld && /^\d+$/.test(tld)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.every((part) => {
|
||||||
|
if (part.length > 63 && !opts.ignore_max_length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(part)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow full-width chars
|
||||||
|
if (/[\uff01-\uff5e]/.test(part)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow parts starting or ending with hyphen
|
||||||
|
if (/^-|-$/.test(part)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.allow_underscores && /_/.test(part)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
|
|
||||||
interface SlugSchemaInputs {
|
interface SlugSchemaInputs {
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
@@ -27,4 +29,13 @@ export const GenericResourceNameSchema = z
|
|||||||
.trim()
|
.trim()
|
||||||
.min(1, { message: "Name must be at least 1 character" })
|
.min(1, { message: "Name must be at least 1 character" })
|
||||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Spaces
|
||||||
|
])(val),
|
||||||
|
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { z } from "zod";
|
|||||||
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -58,7 +60,6 @@ import {
|
|||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TUpdateCaDTO
|
TUpdateCaDTO
|
||||||
} from "./certificate-authority-types";
|
} from "./certificate-authority-types";
|
||||||
import { hostnameRegex } from "./certificate-authority-validators";
|
|
||||||
|
|
||||||
type TCertificateAuthorityServiceFactoryDep = {
|
type TCertificateAuthorityServiceFactoryDep = {
|
||||||
certificateAuthorityDAL: Pick<
|
certificateAuthorityDAL: Pick<
|
||||||
@@ -1017,9 +1018,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||||
|
|
||||||
// validate imported certificate and certificate chain
|
// validate imported certificate and certificate chain
|
||||||
const certificates = certificateChain
|
const certificates = extractX509CertFromChain(certificateChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
|
||||||
?.map((cert) => new x509.X509Certificate(cert));
|
|
||||||
|
|
||||||
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
|
|
||||||
@@ -1325,7 +1324,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the altName is a valid hostname
|
// check if the altName is a valid hostname
|
||||||
if (hostnameRegex.test(altName)) {
|
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||||
return {
|
return {
|
||||||
type: "dns",
|
type: "dns",
|
||||||
value: altName
|
value: altName
|
||||||
@@ -1702,7 +1701,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the altName is a valid hostname
|
// check if the altName is a valid hostname
|
||||||
if (hostnameRegex.test(altName)) {
|
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||||
return {
|
return {
|
||||||
type: "dns",
|
type: "dns",
|
||||||
value: altName
|
value: altName
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { isValidIp } from "@app/lib/ip";
|
import { isValidIp } from "@app/lib/ip";
|
||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
|
||||||
const isValidDate = (dateString: string) => {
|
const isValidDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
@@ -9,7 +10,6 @@ const isValidDate = (dateString: string) => {
|
|||||||
|
|
||||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||||
|
|
||||||
export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
|
||||||
export const validateAltNamesField = z
|
export const validateAltNamesField = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -27,7 +27,7 @@ export const validateAltNamesField = z
|
|||||||
if (data === "") return true;
|
if (data === "") return true;
|
||||||
// Split and validate each alt name
|
// Split and validate each alt name
|
||||||
return data.split(", ").every((name) => {
|
return data.split(", ").every((name) => {
|
||||||
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const validateCertificateDetailsAgainstTemplate = (
|
|||||||
},
|
},
|
||||||
template: TCertificateTemplates
|
template: TCertificateTemplates
|
||||||
) => {
|
) => {
|
||||||
|
// these are validated in router using validateTemplateRegexField
|
||||||
const commonNameRegex = new RegExp(template.commonName);
|
const commonNameRegex = new RegExp(template.commonName);
|
||||||
if (!commonNameRegex.test(cert.commonName)) {
|
if (!commonNameRegex.test(cert.commonName)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
@@ -281,9 +282,7 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// validate CA chain
|
// validate CA chain
|
||||||
const certificates = caChain
|
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
|
||||||
?.map((cert) => new x509.X509Certificate(cert));
|
|
||||||
|
|
||||||
if (!certificates) {
|
if (!certificates) {
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
@@ -379,9 +378,7 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (caChain) {
|
if (caChain) {
|
||||||
const certificates = caChain
|
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
|
||||||
?.map((cert) => new x509.X509Certificate(cert));
|
|
||||||
|
|
||||||
if (!certificates) {
|
if (!certificates) {
|
||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import safe from "safe-regex";
|
import safe from "safe-regex";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
|
|
||||||
export const validateTemplateRegexField = z
|
export const validateTemplateRegexField = z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(100)
|
.max(100)
|
||||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
.refine(
|
||||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
(val) =>
|
||||||
})
|
characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Spaces, // (space)
|
||||||
|
CharacterType.Asterisk, // *
|
||||||
|
CharacterType.At, // @
|
||||||
|
CharacterType.Hyphen, // -
|
||||||
|
CharacterType.Period, // .
|
||||||
|
CharacterType.Backslash // \
|
||||||
|
])(val),
|
||||||
|
{
|
||||||
|
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||||
|
}
|
||||||
|
)
|
||||||
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||||
.refine((v) => safe(v), {
|
.refine((v) => safe(v), {
|
||||||
message: "Unsafe REGEX pattern"
|
message: "Unsafe REGEX pattern"
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
.some((principalArn) => {
|
.some((principalArn) => {
|
||||||
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
|
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
|
||||||
// considers exact matches + wildcard matches
|
// considers exact matches + wildcard matches
|
||||||
const regex = new RegExp(`^${principalArn.replace(/\*/g, ".*")}$`);
|
// heavily validated in router
|
||||||
|
const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`);
|
||||||
return regex.test(extractPrincipalArn(Arn));
|
return regex.test(extractPrincipalArn(Arn));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import safe from "safe-regex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const twelveDigitRegex = /^\d{12}$/;
|
const twelveDigitRegex = /^\d{12}$/;
|
||||||
|
// akhilmhdh: change this to a normal function later. Checked no redosable at the moment
|
||||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
|
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
|
||||||
|
|
||||||
export const validateAccountIds = z
|
export const validateAccountIds = z
|
||||||
@@ -42,7 +44,8 @@ export const validatePrincipalArns = z
|
|||||||
// Split the string by commas to check each supposed ARN
|
// Split the string by commas to check each supposed ARN
|
||||||
const arns = data.split(",");
|
const arns = data.split(",");
|
||||||
// Return true only if every item matches one of the allowed ARN formats
|
// Return true only if every item matches one of the allowed ARN formats
|
||||||
return arns.every((arn) => arnRegex.test(arn.trim()));
|
// and checks whether the provided regex is safe
|
||||||
|
return arns.map((el) => el.trim()).every((arn) => safe(`^${arn.replaceAll("*", ".*")}$`) && arnRegex.test(arn));
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
|
|||||||
@@ -584,7 +584,7 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
Object.keys(secrets).forEach((key) => {
|
Object.keys(secrets).forEach((key) => {
|
||||||
const hyphenatedKey = key.replace(/_/g, "-");
|
const hyphenatedKey = key.replaceAll("_", "-");
|
||||||
if (!(hyphenatedKey in res)) {
|
if (!(hyphenatedKey in res)) {
|
||||||
// case: secret has been created
|
// case: secret has been created
|
||||||
setSecrets.push({
|
setSecrets.push({
|
||||||
@@ -603,7 +603,7 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
const deleteSecrets: AzureKeyVaultSecret[] = [];
|
const deleteSecrets: AzureKeyVaultSecret[] = [];
|
||||||
|
|
||||||
Object.keys(res).forEach((key) => {
|
Object.keys(res).forEach((key) => {
|
||||||
const underscoredKey = key.replace(/-/g, "_");
|
const underscoredKey = key.replaceAll("-", "_");
|
||||||
if (!(underscoredKey in secrets)) {
|
if (!(underscoredKey in secrets)) {
|
||||||
deleteSecrets.push(res[key]);
|
deleteSecrets.push(res[key]);
|
||||||
}
|
}
|
||||||
@@ -617,7 +617,7 @@ const syncSecretsAzureKeyVault = async ({
|
|||||||
if (!integration.lastUsed) {
|
if (!integration.lastUsed) {
|
||||||
Object.keys(res).forEach((key) => {
|
Object.keys(res).forEach((key) => {
|
||||||
// first time using integration
|
// first time using integration
|
||||||
const underscoredKey = key.replace(/-/g, "_");
|
const underscoredKey = key.replaceAll("-", "_");
|
||||||
|
|
||||||
// -> apply initial sync behavior
|
// -> apply initial sync behavior
|
||||||
switch (metadata.initialSyncBehavior) {
|
switch (metadata.initialSyncBehavior) {
|
||||||
@@ -3578,7 +3578,7 @@ const syncSecretsTeamCity = async ({
|
|||||||
.filter((parameter) => !parameter.inherited)
|
.filter((parameter) => !parameter.inherited)
|
||||||
.reduce(
|
.reduce(
|
||||||
(obj, secret) => {
|
(obj, secret) => {
|
||||||
const secretName = secret.name.replace(/^env\./, "");
|
const secretName = secret.name.startsWith(".env") ? secret.name.slice(4) : secret.name;
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
[secretName]: secret.value
|
[secretName]: secret.value
|
||||||
@@ -3635,7 +3635,7 @@ const syncSecretsTeamCity = async ({
|
|||||||
)
|
)
|
||||||
).data.property.reduce(
|
).data.property.reduce(
|
||||||
(obj, secret) => {
|
(obj, secret) => {
|
||||||
const secretName = secret.name.replace(/^env\./, "");
|
const secretName = secret.name.startsWith("env.") ? secret.name.slice(4) : secret.name;
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
[secretName]: secret.value
|
[secretName]: secret.value
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ import { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
|||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { isValidSecretPath } from "@app/lib/validator";
|
import { isValidSecretPath } from "@app/lib/validator";
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
|
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
|
||||||
|
|
||||||
export const validateFolderName = (folderName: string) => {
|
export const validateFolderName = characterValidator([
|
||||||
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
CharacterType.AlphaNumeric,
|
||||||
return validNameRegex.test(folderName);
|
CharacterType.Hyphen,
|
||||||
};
|
CharacterType.Underscore
|
||||||
|
]);
|
||||||
|
|
||||||
const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => {
|
const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => {
|
||||||
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
||||||
|
|||||||
+49
-10
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSyncs } from "@app/lib/api-docs";
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +11,25 @@ import {
|
|||||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
const tagFieldCharacterValidator = characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Spaces,
|
||||||
|
CharacterType.Period,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Colon,
|
||||||
|
CharacterType.ForwardSlash,
|
||||||
|
CharacterType.Equals,
|
||||||
|
CharacterType.Plus,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.At
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pathCharacterValidator = characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Hyphen
|
||||||
|
]);
|
||||||
|
|
||||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||||
path: z
|
path: z
|
||||||
@@ -17,35 +37,54 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.min(1, "Parameter Store Path required")
|
.min(1, "Parameter Store Path required")
|
||||||
.max(2048, "Cannot exceed 2048 characters")
|
.max(2048, "Cannot exceed 2048 characters")
|
||||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format')
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
val.startsWith("/") &&
|
||||||
|
val.endsWith("/") &&
|
||||||
|
val
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean)
|
||||||
|
.every((el) => pathCharacterValidator(el)),
|
||||||
|
'Invalid path - must follow "/example/path/" format'
|
||||||
|
)
|
||||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||||
});
|
});
|
||||||
|
|
||||||
const AwsParameterStoreSyncOptionsSchema = z.object({
|
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||||
keyId: z
|
keyId: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
|
||||||
.min(1, "Invalid KMS Key ID")
|
.min(1, "Invalid KMS Key ID")
|
||||||
.max(256, "Invalid KMS Key ID")
|
.max(256, "Invalid KMS Key ID")
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Colon,
|
||||||
|
CharacterType.ForwardSlash,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Hyphen
|
||||||
|
])(val),
|
||||||
|
"Invalid KMS Key ID"
|
||||||
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||||
tags: z
|
tags: z
|
||||||
.object({
|
.object({
|
||||||
key: z
|
key: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
|
||||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
|
||||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
|
||||||
)
|
|
||||||
.min(1, "Resource tag key required")
|
.min(1, "Resource tag key required")
|
||||||
.max(128, "Resource tag key cannot exceed 128 characters"),
|
.max(128, "Resource tag key cannot exceed 128 characters")
|
||||||
|
.refine(
|
||||||
|
(val) => tagFieldCharacterValidator(val),
|
||||||
|
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
),
|
||||||
value: z
|
value: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
.refine(
|
||||||
|
(val) => tagFieldCharacterValidator(val),
|
||||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
)
|
)
|
||||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.max(50)
|
.max(50)
|
||||||
|
|||||||
+49
-14
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSyncs } from "@app/lib/api-docs";
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
@@ -24,12 +25,23 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
|||||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
|
||||||
secretName: z
|
secretName: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
|
||||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
|
||||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
|
||||||
)
|
|
||||||
.min(1, "Secret name is required")
|
.min(1, "Secret name is required")
|
||||||
.max(256, "Secret name cannot exceed 256 characters")
|
.max(256, "Secret name cannot exceed 256 characters")
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.ForwardSlash,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Plus,
|
||||||
|
CharacterType.Equals,
|
||||||
|
CharacterType.Period,
|
||||||
|
CharacterType.At,
|
||||||
|
CharacterType.Hyphen
|
||||||
|
])(val),
|
||||||
|
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||||
|
)
|
||||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName)
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName)
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
@@ -39,31 +51,54 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tagFieldCharacterValidator = characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Spaces,
|
||||||
|
CharacterType.Period,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Colon,
|
||||||
|
CharacterType.ForwardSlash,
|
||||||
|
CharacterType.Equals,
|
||||||
|
CharacterType.Plus,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.At
|
||||||
|
]);
|
||||||
|
|
||||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||||
keyId: z
|
keyId: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
|
||||||
.min(1, "Invalid KMS Key ID")
|
.min(1, "Invalid KMS Key ID")
|
||||||
.max(256, "Invalid KMS Key ID")
|
.max(256, "Invalid KMS Key ID")
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Colon,
|
||||||
|
CharacterType.ForwardSlash,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Hyphen
|
||||||
|
])(val),
|
||||||
|
"Invalid KMS Key ID"
|
||||||
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
||||||
tags: z
|
tags: z
|
||||||
.object({
|
.object({
|
||||||
key: z
|
key: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
|
||||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
|
||||||
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
|
||||||
)
|
|
||||||
.min(1, "Tag key required")
|
.min(1, "Tag key required")
|
||||||
.max(128, "Tag key cannot exceed 128 characters"),
|
.max(128, "Tag key cannot exceed 128 characters")
|
||||||
|
.refine(
|
||||||
|
(val) => tagFieldCharacterValidator(val),
|
||||||
|
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
),
|
||||||
value: z
|
value: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
|
||||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
|
||||||
"Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
|
||||||
)
|
|
||||||
.max(256, "Tag value cannot exceed 256 characters")
|
.max(256, "Tag value cannot exceed 256 characters")
|
||||||
|
.refine(
|
||||||
|
(val) => tagFieldCharacterValidator(val),
|
||||||
|
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.max(50)
|
.max(50)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
|||||||
const deleteSecrets: string[] = [];
|
const deleteSecrets: string[] = [];
|
||||||
|
|
||||||
Object.keys(secretMap).forEach((infisicalKey) => {
|
Object.keys(secretMap).forEach((infisicalKey) => {
|
||||||
const hyphenatedKey = infisicalKey.replace(/_/g, "-");
|
const hyphenatedKey = infisicalKey.replaceAll("_", "-");
|
||||||
if (!(hyphenatedKey in vaultSecrets)) {
|
if (!(hyphenatedKey in vaultSecrets)) {
|
||||||
// case: secret has been created
|
// case: secret has been created
|
||||||
setSecrets.push({
|
setSecrets.push({
|
||||||
@@ -117,7 +117,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
|||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(vaultSecrets).forEach((key) => {
|
Object.keys(vaultSecrets).forEach((key) => {
|
||||||
const underscoredKey = key.replace(/-/g, "_");
|
const underscoredKey = key.replaceAll("-", "_");
|
||||||
if (!(underscoredKey in secretMap)) {
|
if (!(underscoredKey in secretMap)) {
|
||||||
deleteSecrets.push(key);
|
deleteSecrets.push(key);
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
|||||||
);
|
);
|
||||||
|
|
||||||
for await (const [key] of Object.entries(vaultSecrets)) {
|
for await (const [key] of Object.entries(vaultSecrets)) {
|
||||||
const underscoredKey = key.replace(/-/g, "_");
|
const underscoredKey = key.replaceAll("-", "_");
|
||||||
|
|
||||||
if (underscoredKey in secretMap) {
|
if (underscoredKey in secretMap) {
|
||||||
if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) {
|
if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) {
|
||||||
@@ -237,7 +237,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
|||||||
|
|
||||||
Object.keys(vaultSecrets).forEach((key) => {
|
Object.keys(vaultSecrets).forEach((key) => {
|
||||||
if (!disabledAzureKeyVaultSecretKeys.includes(key)) {
|
if (!disabledAzureKeyVaultSecretKeys.includes(key)) {
|
||||||
const underscoredKey = key.replace(/-/g, "_");
|
const underscoredKey = key.replaceAll("-", "_");
|
||||||
secretMap[underscoredKey] = {
|
secretMap[underscoredKey] = {
|
||||||
value: vaultSecrets[key].value
|
value: vaultSecrets[key].value
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ export const recursivelyGetSecretPaths = async ({
|
|||||||
const formatMultiValueEnv = (val?: string) => {
|
const formatMultiValueEnv = (val?: string) => {
|
||||||
if (!val) return "";
|
if (!val) return "";
|
||||||
if (!val.match("\n")) return val;
|
if (!val.match("\n")) return val;
|
||||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
return `"${val.replaceAll("\n", "\\n")}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TSecretReferenceTraceNode = {
|
type TSecretReferenceTraceNode = {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export const recursivelyGetSecretPaths = ({
|
|||||||
const formatMultiValueEnv = (val?: string) => {
|
const formatMultiValueEnv = (val?: string) => {
|
||||||
if (!val) return "";
|
if (!val) return "";
|
||||||
if (!val.match("\n")) return val;
|
if (!val.match("\n")) return val;
|
||||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
return `"${val.replaceAll("\n", "\\n")}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TInterpolateSecretArg = {
|
type TInterpolateSecretArg = {
|
||||||
@@ -218,7 +218,7 @@ type TInterpolateSecretArg = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SECRET_REFERENCE_DEPTH = 5;
|
const MAX_SECRET_REFERENCE_DEPTH = 5;
|
||||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
||||||
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
||||||
const secretCache: Record<string, Record<string, string>> = {};
|
const secretCache: Record<string, Record<string, string>> = {};
|
||||||
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
||||||
|
|||||||
+1
-9
@@ -40,15 +40,7 @@ import {
|
|||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const validateTemplateRegexField = z
|
const validateTemplateRegexField = z.string().trim().min(1).max(100);
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(100)
|
|
||||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
|
||||||
message:
|
|
||||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
|
||||||
});
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
|
|||||||
@@ -26,21 +26,6 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
// Validates usernames or wildcard (*)
|
|
||||||
export const isValidUserPattern = (value: string): boolean => {
|
|
||||||
// Matches valid Linux usernames or a wildcard (*)
|
|
||||||
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
|
||||||
return userRegex.test(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validates hostnames, wildcard domains, or IP addresses
|
|
||||||
export const isValidHostPattern = (value: string): boolean => {
|
|
||||||
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
|
||||||
const hostRegex =
|
|
||||||
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
|
||||||
return hostRegex.test(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
sshCaId: z.string(),
|
sshCaId: z.string(),
|
||||||
@@ -69,28 +54,8 @@ const schema = z
|
|||||||
"Max TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
|
"Max TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
|
||||||
)
|
)
|
||||||
.default("30d"),
|
.default("30d"),
|
||||||
allowedUsers: z.string().refine(
|
allowedUsers: z.string(),
|
||||||
(val) => {
|
allowedHosts: z.string(),
|
||||||
const trimmed = val.trim();
|
|
||||||
if (trimmed === "") return true;
|
|
||||||
const users = trimmed.split(",").map((u) => u.trim());
|
|
||||||
return users.every(isValidUserPattern);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Invalid user pattern in allowedUsers"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
allowedHosts: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
const trimmed = val.trim();
|
|
||||||
if (trimmed === "") return true;
|
|
||||||
const users = trimmed.split(",").map((u) => u.trim());
|
|
||||||
return users.every(isValidHostPattern);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Invalid host pattern in allowedHosts"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
allowUserCertificates: z.boolean().optional().default(false),
|
allowUserCertificates: z.boolean().optional().default(false),
|
||||||
allowHostCertificates: z.boolean().optional().default(false),
|
allowHostCertificates: z.boolean().optional().default(false),
|
||||||
allowCustomKeyIds: z.boolean().optional().default(false)
|
allowCustomKeyIds: z.boolean().optional().default(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user