Merge branch 'main' into feat/azureClientSecretsRotation

This commit is contained in:
carlosmonastyrski
2025-04-22 14:25:56 -03:00
27 changed files with 892 additions and 68 deletions
@@ -77,20 +77,22 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
const { credentials, app } = config;
const client = await getSqlConnectionClient({ app, credentials });
let client: Knex | undefined;
try {
client = await getSqlConnectionClient({ app, credentials });
await client.raw(`Select 1`);
return credentials;
} catch (error) {
throw new BadRequestError({
message:
(error as Error)?.message?.replaceAll(credentials.password, "********************") ??
"Unable to validate connection: verify credentials"
message: `Unable to validate connection: ${
(error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials"
}`
});
} finally {
await client.destroy();
await client?.destroy();
}
};
@@ -1,3 +1,5 @@
import RE2 from "re2";
import { TCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
@@ -12,7 +14,7 @@ export const validateCertificateDetailsAgainstTemplate = (
template: TCertificateTemplates
) => {
// these are validated in router using validateTemplateRegexField
const commonNameRegex = new RegExp(template.commonName);
const commonNameRegex = new RE2(template.commonName);
if (!commonNameRegex.test(cert.commonName)) {
throw new BadRequestError({
message: "Invalid common name based on template policy"
@@ -25,7 +27,7 @@ export const validateCertificateDetailsAgainstTemplate = (
});
}
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
const subjectAlternativeNameRegex = new RE2(template.subjectAlternativeName);
cert.altNames.forEach((altName) => {
if (!subjectAlternativeNameRegex.test(altName)) {
throw new BadRequestError({
@@ -2,6 +2,7 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import jwt from "jsonwebtoken";
import RE2 from "re2";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -67,6 +68,18 @@ const awsRegionFromHeader = (authorizationHeader: string): string | null => {
return null;
};
function isValidAwsRegion(region: (string | null)): boolean {
const validRegionPattern = new RE2('^[a-z0-9-]+$');
if (typeof region !== 'string' || region.length === 0 || region.length > 20) {
return false;
}
return validRegionPattern.test(region);
}
export const identityAwsAuthServiceFactory = ({
identityAccessTokenDAL,
identityAwsAuthDAL,
@@ -84,8 +97,12 @@ export const identityAwsAuthServiceFactory = ({
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString();
const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null;
if (!isValidAwsRegion(region)) {
throw new BadRequestError({message: "Invalid AWS region"});
}
const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint;
const {
@@ -125,7 +142,7 @@ export const identityAwsAuthServiceFactory = ({
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
// considers exact matches + wildcard matches
// heavily validated in router
const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`);
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
return regex.test(extractPrincipalArn(Arn));
});
@@ -1,9 +1,10 @@
import RE2 from "re2";
import safe from "safe-regex";
import { z } from "zod";
const twelveDigitRegex = /^\d{12}$/;
const twelveDigitRegex = new RE2(/^\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 = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/);
export const validateAccountIds = z
.string()
@@ -27,6 +27,7 @@ import { randomUUID } from "crypto";
import https from "https";
import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
import RE2 from "re2";
import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
@@ -4151,7 +4152,7 @@ const syncSecretsWindmill = async ({
);
// eslint-disable-next-line
const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
const pattern = new RE2("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
for await (const key of Object.keys(secrets)) {
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
if (!(key in res)) {
@@ -1,5 +1,7 @@
import path from "node:path";
import RE2 from "re2";
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@@ -13,9 +15,8 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
// akhilmhdh: JS regex with global save state in .test
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([a-zA-Z0-9-_.]+)}/;
const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`;
const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING);
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
@@ -36,7 +37,14 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3;
* // ]
*/
export const getAllSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
const references = [];
let match;
const regex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
// eslint-disable-next-line no-cond-assign
while ((match = regex.exec(maybeSecretReference)) !== null) {
references.push(match[1]);
}
const nestedReferences = references
.filter((el) => el.includes("."))
@@ -531,9 +539,17 @@ export const expandSecretReferencesFactory = ({
// eslint-disable-next-line no-continue
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
const refs = value?.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
const matchRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
const refs = [];
let match;
// eslint-disable-next-line no-cond-assign
while ((match = matchRegex.exec(value || "")) !== null) {
refs.push(match[0]);
}
if (refs.length > 0) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
@@ -592,7 +608,7 @@ export const expandSecretReferencesFactory = ({
trace
};
const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue);
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue);
if (dto.shouldStackTrace) {
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
trace?.children.push(stackTraceNode);
@@ -626,7 +642,7 @@ export const expandSecretReferencesFactory = ({
}) => {
if (!inputSecret.value) return inputSecret.value;
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value);
if (!shouldExpand) return inputSecret.value;
const { expandedValue } = await recursivelyExpandSecret(inputSecret);
+29 -7
View File
@@ -1,5 +1,6 @@
/* eslint-disable no-await-in-loop */
import path from "path";
import RE2 from "re2";
import {
ActionProjectType,
@@ -218,7 +219,9 @@ type TInterpolateSecretArg = {
};
const MAX_SECRET_REFERENCE_DEPTH = 5;
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`;
const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING);
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
@@ -273,9 +276,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
if (!value) return "";
if (depth > MAX_SECRET_REFERENCE_DEPTH) return "";
const refs = value.match(INTERPOLATION_SYNTAX_REG);
const refs = [];
let match;
const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
// eslint-disable-next-line no-cond-assign
while ((match = execRegex.exec(value)) !== null) {
refs.push(match[0]);
}
let expandedValue = value;
if (refs) {
if (refs.length > 0) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
@@ -284,7 +295,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
const [secretKey] = entities;
// eslint-disable-next-line
let referenceValue = await fetchSecret(environment, secretPath, secretKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
if (INTERPOLATION_TEST_REGEX.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment,
@@ -305,7 +316,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
// eslint-disable-next-line
let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
if (INTERPOLATION_TEST_REGEX.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment: secretReferenceEnvironment,
@@ -332,7 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
}) => {
if (!inputSecret.value) return inputSecret.value;
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value);
if (!shouldExpand) return inputSecret.value;
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
@@ -451,7 +462,18 @@ export const fnSecretBlindIndexCheckV2 = async ({
* // ]
*/
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
const matches = [];
let match;
const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
// eslint-disable-next-line no-cond-assign
while ((match = execRegex.exec(maybeSecretReference)) !== null) {
matches.push(match);
}
const references = matches.map((m) => m[1]);
return references
.filter((el) => el.includes("."))
.map((el) => {